install-rules.mjs 4.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172
  1. #!/usr/bin/env node
  2. /**
  3. * install-rules.mjs —— 把 fmode-code-lit 的前端规则装入目标项目的 ./.claude/rules/。
  4. *
  5. * 用法:
  6. * node install-rules.mjs [targetDir] [--force]
  7. * node install-rules.mjs [targetDir] --from <rulesRepoDir> # 指定规则仓根目录
  8. *
  9. * 规则来源优先级:--from > 技能同仓 ../../.claude/rules > 内置最小规则集。
  10. * 已存在文件默认不覆盖(--force 覆盖)。
  11. */
  12. import { mkdirSync, writeFileSync, existsSync, readdirSync, readFileSync, statSync, copyFileSync } from 'node:fs';
  13. import { join, resolve, dirname } from 'node:path';
  14. import { fileURLToPath } from 'node:url';
  15. const __dirname = dirname(fileURLToPath(import.meta.url));
  16. const args = process.argv.slice(2);
  17. const target = resolve(args.find((a) => !a.startsWith('--') && a !== undefined && !a.startsWith('-')) || '.');
  18. const force = args.includes('--force');
  19. const fromIdx = args.indexOf('--from');
  20. const fromDir = fromIdx > -1 ? resolve(args[fromIdx + 1]) : null;
  21. // 规则来源候选
  22. const candidates = [
  23. fromDir,
  24. resolve(__dirname, '../../../.claude/rules'), // 技能在 <repo>/skills/fmode-code-lit/scripts/
  25. resolve(__dirname, '../../.claude/rules'),
  26. ].filter(Boolean);
  27. const srcRules = candidates.find((d) => d && existsSync(d) && existsSync(join(d, 'architecture.md')));
  28. const destRules = join(target, '.claude', 'rules');
  29. mkdirSync(destRules, { recursive: true });
  30. let created = 0, skipped = 0;
  31. function copyTree(from, to) {
  32. for (const name of readdirSync(from)) {
  33. const s = join(from, name), d = join(to, name);
  34. const st = statSync(s);
  35. if (st.isDirectory()) { mkdirSync(d, { recursive: true }); copyTree(s, d); continue; }
  36. if (!name.endsWith('.md')) continue;
  37. if (existsSync(d) && !force) { skipped++; console.log(` skip ${d}`); continue; }
  38. copyFileSync(s, d); created++; console.log(` write ${d}`);
  39. }
  40. }
  41. if (srcRules) {
  42. console.log(`\n规则来源:${srcRules}`);
  43. copyTree(srcRules, destRules);
  44. } else {
  45. console.log('\n未找到规则仓,写入内置最小规则集。');
  46. const minimal = {
  47. 'lit-no-build.md': `# Lit 无构建前端 · 最小规则\n\n1. 源码即产物:不引入任何打包器。\n2. 依赖由 importmap 指向 FMODE Repose,禁止拷进项目。\n3. **禁用装饰器**(@customElement/@property/@state/@consume)——用 static properties + customElements.define + ContextConsumer。\n4. 分层:组件(渲染)→服务(业务/状态)→工具(纯函数);组件禁 fetch,服务禁 DOM。\n5. 相对导入必须带 .js 后缀。\n6. 全局状态用 @lit-labs/signals;组件本地状态用 static properties(state:true)。\n7. 子目录部署必须 <base href=\"/<project>/\">,且与 OBS 前缀、importmap 前缀三者同源。\n8. 导航一律走 utils/router.js(把应用内路径拼到 base 的 pathname 上);禁止 new URL(path, baseURI),子路径部署会丢前缀。\n9. 子路径部署禁用 @lit-labs/router(全局 <a> 拦截 + 未剥前缀 pathname 会污染路由)。\n10. CDN History 回退:不存在路径 200 返回 index.html,禁 302。\n`,
  48. };
  49. for (const [name, content] of Object.entries(minimal)) {
  50. const d = join(destRules, name);
  51. if (existsSync(d) && !force) { skipped++; continue; }
  52. writeFileSync(d, content); created++; console.log(` write ${d}`);
  53. }
  54. }
  55. // CLAUDE.md 片段(追加式,不覆盖已有正文)
  56. const claudeMd = join(target, 'CLAUDE.md');
  57. const snippet = `\n## 前端(Lit 原生 ESM 无构建)\n\n本项目前端遵循 \`.claude/rules/\` 下的 Lit 无构建规则:源码即产物、依赖走 importmap、\n分层(组件/服务/工具)、**禁用装饰器**、@lit-labs/signals 响应式、自写 utils/router.js History 路由(子路径部署禁用 @lit-labs/router)。\n详见 \`.claude/rules/architecture.md\` 与 \`.claude/rules/components.md\`。\n`;
  58. if (existsSync(claudeMd)) {
  59. const cur = readFileSync(claudeMd, 'utf8');
  60. if (!cur.includes('Lit 原生 ESM 无构建')) { writeFileSync(claudeMd, cur + snippet); console.log(` append ${claudeMd}`); }
  61. else console.log(` skip ${claudeMd}(已含片段)`);
  62. } else {
  63. writeFileSync(claudeMd, `# 项目规则\n${snippet}`); created++; console.log(` write ${claudeMd}`);
  64. }
  65. console.log(`\n✓ 完成:新建 ${created},跳过 ${skipped}。规则目录:${destRules}\n`);