mdrules 0.1.1 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (128) hide show
  1. package/README.md +196 -55
  2. package/dist/cli.js +855 -163
  3. package/dist/web/assets/{abnfDiagram-WEYOIA3C-DKgk2hIQ.js → abnfDiagram-WEYOIA3C-COoXc57p.js} +1 -1
  4. package/dist/web/assets/{arc-CCAU-51h.js → arc-CQ-mpttu.js} +1 -1
  5. package/dist/web/assets/{architecture-WOLXFQ4H-BcNYkHmW.js → architecture-WOLXFQ4H-Cl3-F9oV.js} +1 -1
  6. package/dist/web/assets/{architectureDiagram-ZHVYKT53-DgRlfGj2.js → architectureDiagram-ZHVYKT53-C1LOUHfq.js} +1 -1
  7. package/dist/web/assets/{blockDiagram-RJAE7QA3-DoBplOhb.js → blockDiagram-RJAE7QA3-YeQnD2ds.js} +1 -1
  8. package/dist/web/assets/{c4Diagram-XDPPPE54-CGKzgX6O.js → c4Diagram-XDPPPE54-XLmSlndV.js} +1 -1
  9. package/dist/web/assets/channel-Bu489uW7.js +1 -0
  10. package/dist/web/assets/{chunk-2E7WHVMD-DhQx5a0n.js → chunk-2E7WHVMD-HGXqy_LQ.js} +1 -1
  11. package/dist/web/assets/{chunk-4EMNKR2T-vHF11V5m.js → chunk-4EMNKR2T-DsQggzQE.js} +1 -1
  12. package/dist/web/assets/{chunk-55XCU76K-DgZ0btzr.js → chunk-55XCU76K-DY1xg8cl.js} +1 -1
  13. package/dist/web/assets/{chunk-7S6XSTFO-DoKaEIP0.js → chunk-7S6XSTFO-BuyzABLx.js} +1 -1
  14. package/dist/web/assets/{chunk-AQDTSKZB-DktDdZ_o.js → chunk-AQDTSKZB-CsDWfENX.js} +1 -1
  15. package/dist/web/assets/{chunk-DSNLSHJZ-C_3sVqJn.js → chunk-DSNLSHJZ-CuiVxF9g.js} +1 -1
  16. package/dist/web/assets/{chunk-HAGFOTED-DhCQIE99.js → chunk-HAGFOTED-CjjQypU8.js} +1 -1
  17. package/dist/web/assets/{chunk-HNE4Q36P-D_sbl6UM.js → chunk-HNE4Q36P-B0-YOuZI.js} +1 -1
  18. package/dist/web/assets/{chunk-NQOKRVIF-UuKYBp6F.js → chunk-NQOKRVIF-DMWnWh6r.js} +1 -1
  19. package/dist/web/assets/{chunk-UHW7PFBP-BkreF8rs.js → chunk-UHW7PFBP-D3E4ik9Y.js} +1 -1
  20. package/dist/web/assets/{chunk-VQ5W62QI-Cb7PeOKH.js → chunk-VQ5W62QI-CiPAEjx6.js} +1 -1
  21. package/dist/web/assets/{chunk-WQQL4CDK-GsIKInJ-.js → chunk-WQQL4CDK-StBWijFa.js} +1 -1
  22. package/dist/web/assets/{chunk-XXDRQBXY-Bbz14gys.js → chunk-XXDRQBXY-CINBf_Ay.js} +1 -1
  23. package/dist/web/assets/{chunk-YZO67HFG--IdJYNXK.js → chunk-YZO67HFG-BSkchBSd.js} +1 -1
  24. package/dist/web/assets/{chunk-ZAUX2LTX-DmenwPIM.js → chunk-ZAUX2LTX-CYfcgJpt.js} +1 -1
  25. package/dist/web/assets/{chunk-ZCHMQSOR-BZ2XP8BW.js → chunk-ZCHMQSOR-CwWl64AI.js} +1 -1
  26. package/dist/web/assets/{classDiagram-v2-4MYWR3UA-B_FJCXgY.js → classDiagram-v2-4MYWR3UA-Ce6JKsB3.js} +1 -1
  27. package/dist/web/assets/{cose-bilkent-JH36ORCC-DMBdq3mq.js → cose-bilkent-JH36ORCC-COClxHi_.js} +1 -1
  28. package/dist/web/assets/{cynefin-EF2NZ3EQ-BthcZtsD.js → cynefin-EF2NZ3EQ-BGZI2c8V.js} +1 -1
  29. package/dist/web/assets/{cynefinDiagram-WF24UDRP-Bva6JySb.js → cynefinDiagram-WF24UDRP-B24KUNeS.js} +1 -1
  30. package/dist/web/assets/{dagre-NK4GIZVX-BE6DKQYs.js → dagre-NK4GIZVX-DjCzI5LG.js} +1 -1
  31. package/dist/web/assets/{diagram-CBK3MWTC-DuDzgplS.js → diagram-CBK3MWTC-pa5xxV7I.js} +1 -1
  32. package/dist/web/assets/{diagram-I5XIIV2D-DU3jaZXW.js → diagram-I5XIIV2D-BQtih66c.js} +1 -1
  33. package/dist/web/assets/{diagram-ODIL2UCW-DYI3Rj6j.js → diagram-ODIL2UCW-CtodvdqO.js} +1 -1
  34. package/dist/web/assets/{diagram-PFBEJUXE-FD1CYRbT.js → diagram-PFBEJUXE-B-CnrB9d.js} +1 -1
  35. package/dist/web/assets/{diagram-RJA3I4VV-BsbAupu0.js → diagram-RJA3I4VV-Di8-srfd.js} +1 -1
  36. package/dist/web/assets/{diagram-TYUNFSNR-BMB0kMWv.js → diagram-TYUNFSNR-DT-cfj0M.js} +1 -1
  37. package/dist/web/assets/{dist-B5RhPiDb.js → dist-5RSr3i1a.js} +1 -1
  38. package/dist/web/assets/{dist-CugnsBBM.js → dist-B4Z2sV8l.js} +1 -1
  39. package/dist/web/assets/{dist-EDnzgrlE.js → dist-B7yJAZLB.js} +1 -1
  40. package/dist/web/assets/{dist-ABZ0bfMR.js → dist-BxXtIjV1.js} +1 -1
  41. package/dist/web/assets/{dist-1HFNA2Sd.js → dist-CA-ReuvA.js} +1 -1
  42. package/dist/web/assets/{dist-CIDoXcx0.js → dist-CLMm60_p.js} +1 -1
  43. package/dist/web/assets/{dist-D6VkQ_7h.js → dist-CPFvkcDl.js} +1 -1
  44. package/dist/web/assets/{dist-BoeMvgh-.js → dist-CZxj92ue.js} +1 -1
  45. package/dist/web/assets/{dist-CFaMxIWT.js → dist-DVG9d1NU.js} +1 -1
  46. package/dist/web/assets/{dist-BFFH-h4L.js → dist-DevJcR0Y.js} +1 -1
  47. package/dist/web/assets/{dist-Be0WQzsz.js → dist-JJNpWgR6.js} +1 -1
  48. package/dist/web/assets/{ebnfDiagram-V4P3TTPT-C5zEPRhM.js → ebnfDiagram-V4P3TTPT-5btoYlHi.js} +1 -1
  49. package/dist/web/assets/{elk-QONTBCTQ-BR5YBYTe.js → elk-QONTBCTQ-BiXiP5mn.js} +1 -1
  50. package/dist/web/assets/{erDiagram-KYQIGQAA-B-5pcNVz.js → erDiagram-KYQIGQAA-Csvahmwh.js} +1 -1
  51. package/dist/web/assets/{eventmodeling-K75KTNOO-DkMnS-Cz.js → eventmodeling-K75KTNOO-CTDk7azy.js} +1 -1
  52. package/dist/web/assets/flowDiagram-IUGLLWBU-CTlRlF-F.js +1 -0
  53. package/dist/web/assets/{ganttDiagram-R7GPNTJY-B21dBS-O.js → ganttDiagram-R7GPNTJY-Kr_UYAyo.js} +1 -1
  54. package/dist/web/assets/{gitGraph-VSP46ZUC-DalD3PH5.js → gitGraph-VSP46ZUC-DWAozJzq.js} +1 -1
  55. package/dist/web/assets/{gitGraphDiagram-PDYL7XHY-CWi1FEeW.js → gitGraphDiagram-PDYL7XHY-DyTW4WQG.js} +1 -1
  56. package/dist/web/assets/index-lmzHXuPr.css +1 -0
  57. package/dist/web/assets/index-qPUGRLvu.js +63 -0
  58. package/dist/web/assets/{info-OHQRW6UA-CByswqH6.js → info-OHQRW6UA-DiJj6mF5.js} +1 -1
  59. package/dist/web/assets/{infoDiagram-MVDRKAX5-DizZ8XVo.js → infoDiagram-MVDRKAX5-D02QrLp5.js} +1 -1
  60. package/dist/web/assets/{ishikawaDiagram-HPEHV2GE-DQoKMoDQ.js → ishikawaDiagram-HPEHV2GE-CXW4mRts.js} +1 -1
  61. package/dist/web/assets/{journeyDiagram-BOSHW7O7-DKj7UdLQ.js → journeyDiagram-BOSHW7O7-DwyTpaHZ.js} +1 -1
  62. package/dist/web/assets/{kanban-definition-FNSA4HC5-bTblh8-S.js → kanban-definition-FNSA4HC5-B8br7aDC.js} +1 -1
  63. package/dist/web/assets/{line-DWkG8aZQ.js → line-C7pCcPIA.js} +1 -1
  64. package/dist/web/assets/{linear-aVPxw88X.js → linear-Tw3CU3Fk.js} +1 -1
  65. package/dist/web/assets/{mermaid-parser.core-C-J7_HBm.js → mermaid-parser.core-DuxqJtRQ.js} +3 -3
  66. package/dist/web/assets/{mermaid.core-DpHqjC1s.js → mermaid.core-DsOnw7nE.js} +4 -4
  67. package/dist/web/assets/{mindmap-definition-6OY57UG3-DnifqP1p.js → mindmap-definition-6OY57UG3-Cx4RGd4t.js} +1 -1
  68. package/dist/web/assets/{packet-JDAUHWVQ-_ADKLP5T.js → packet-JDAUHWVQ-FgGACr61.js} +1 -1
  69. package/dist/web/assets/{pegDiagram-ECIUJNBX-_SU4-PwS.js → pegDiagram-ECIUJNBX-DbM0mpqP.js} +1 -1
  70. package/dist/web/assets/{pie-XZMESJXO-BfbJN8hk.js → pie-XZMESJXO-D3_sBOVu.js} +1 -1
  71. package/dist/web/assets/{pieDiagram-GNMPY74T-DQQKmEIp.js → pieDiagram-GNMPY74T-COgyAYuf.js} +1 -1
  72. package/dist/web/assets/{quadrantDiagram-D57K4MC3-BLtF0lyY.js → quadrantDiagram-D57K4MC3-CHA07iGU.js} +1 -1
  73. package/dist/web/assets/{radar-ABXABTNO-D7Tlesuz.js → radar-ABXABTNO-DecHbZ8y.js} +1 -1
  74. package/dist/web/assets/{railroad-I3PHUGI6-CA4jSH3I.js → railroad-I3PHUGI6-CX9gFbv6.js} +1 -1
  75. package/dist/web/assets/{railroad-abnf-LNEFI6M7-CumrL10E.js → railroad-abnf-LNEFI6M7-7ekNszOQ.js} +1 -1
  76. package/dist/web/assets/{railroad-ebnf-DWYD2UWJ-2GeHQm3G.js → railroad-ebnf-DWYD2UWJ-DdczO4iK.js} +1 -1
  77. package/dist/web/assets/{railroad-peg-JR7BVNR7-CGbW9a0w.js → railroad-peg-JR7BVNR7-tTF8C5h6.js} +1 -1
  78. package/dist/web/assets/{railroadDiagram-Y72NRBL7-BQKP5iZM.js → railroadDiagram-Y72NRBL7-7GM0UzXp.js} +1 -1
  79. package/dist/web/assets/{requirementDiagram-EQYSW5RA-Cn_K53gc.js → requirementDiagram-EQYSW5RA-BSC7dW7G.js} +1 -1
  80. package/dist/web/assets/{sankeyDiagram-SY24JHO5-BouDevNW.js → sankeyDiagram-SY24JHO5-zbVVy5A9.js} +1 -1
  81. package/dist/web/assets/{sequenceDiagram-BLEHNHQU-C0rguio1.js → sequenceDiagram-BLEHNHQU-DEe5_sJr.js} +1 -1
  82. package/dist/web/assets/{src-BJB85M8z.js → src-BRIqFVDQ.js} +1 -1
  83. package/dist/web/assets/{stateDiagram-v2-A6IESKWZ-BzgsnDJf.js → stateDiagram-v2-A6IESKWZ-D9iIEu7F.js} +1 -1
  84. package/dist/web/assets/{swimlanes-B3ENRKFQ-BAnQnO43.js → swimlanes-B3ENRKFQ-kSlZwPrl.js} +1 -1
  85. package/dist/web/assets/swimlanesDiagram-AYQRV4AK-DUYiDwoF.js +8 -0
  86. package/dist/web/assets/{timeline-definition-JXYKGLGB-i_V2SDud.js → timeline-definition-JXYKGLGB-D3FVcL1M.js} +1 -1
  87. package/dist/web/assets/{treeView-D4JQ5SDB-blg1ho_V.js → treeView-D4JQ5SDB-Dg-vnn_2.js} +1 -1
  88. package/dist/web/assets/{treemap-SAJKECNS-GWe0sLdw.js → treemap-SAJKECNS-SUcVM8SE.js} +1 -1
  89. package/dist/web/assets/{usecaseDiagram-WPOLBNNX-C2epnXsY.js → usecaseDiagram-WPOLBNNX-pg19th6w.js} +1 -1
  90. package/dist/web/assets/{vennDiagram-Q3Y3IDCW-CYguUs2o.js → vennDiagram-Q3Y3IDCW-C0bGBnZZ.js} +1 -1
  91. package/dist/web/assets/{wardley-7MLQ67FV-CA1BMgJ-.js → wardley-7MLQ67FV-DRUta4Q-.js} +1 -1
  92. package/dist/web/assets/{wardleyDiagram-IVAIDKWA-COJi2fLq.js → wardleyDiagram-IVAIDKWA-CafLMkAZ.js} +1 -1
  93. package/dist/web/assets/{xychartDiagram-4URNHDU4-BxweoZn5.js → xychartDiagram-4URNHDU4-CbBmnzba.js} +1 -1
  94. package/dist/web/index.html +2 -2
  95. package/package.json +1 -1
  96. package/templates/main.md +1 -11
  97. package/templates/method/area/docs/ARCHITECTURE.md +14 -0
  98. package/templates/method/area/docs/TASKS.md +18 -0
  99. package/templates/method/area/docs/tasks/.gitkeep +0 -0
  100. package/templates/method/area-main.md +9 -0
  101. package/templates/method/root/docs/README.md +69 -0
  102. package/templates/method/root/docs/TASKS.md +18 -0
  103. package/templates/method/root/docs/architecture/README.md +10 -0
  104. package/templates/method/root/docs/architecture/stack.md +28 -0
  105. package/templates/method/root/docs/phases/01-foundations.md +23 -0
  106. package/templates/method/root/docs/tasks/_TEMPLATE.md +28 -0
  107. package/templates/method/root/docs/tasks/{{prefix}}-001-set-up-the-project.md +30 -0
  108. package/templates/method/root/docs/vision.md +22 -0
  109. package/templates/method/skills/mdrules-close-session/SKILL.md +20 -0
  110. package/templates/method/skills/mdrules-new-task/SKILL.md +22 -0
  111. package/templates/proposals/SKILL.md +114 -0
  112. package/templates/sdd/openspec/openspec/README.md +89 -0
  113. package/templates/sdd/openspec/openspec/changes/archive/.gitkeep +0 -0
  114. package/templates/sdd/openspec/openspec/config.yaml +12 -0
  115. package/templates/sdd/openspec/openspec/specs/.gitkeep +0 -0
  116. package/templates/sdd/speckit/.specify/memory/constitution.md +30 -0
  117. package/templates/sdd/speckit/specs/README.md +82 -0
  118. package/templates/skill/SKILL.md +9 -10
  119. package/dist/web/assets/channel-BWmWik05.js +0 -1
  120. package/dist/web/assets/flowDiagram-IUGLLWBU-Dq34jEaq.js +0 -1
  121. package/dist/web/assets/index-DBrCTV1V.js +0 -58
  122. package/dist/web/assets/index-DxpaDO0r.css +0 -1
  123. package/dist/web/assets/swimlanesDiagram-AYQRV4AK-BLBJ1_iu.js +0 -8
  124. package/templates/docs/architecture.md +0 -21
  125. package/templates/docs/decisions.md +0 -11
  126. package/templates/docs/phases/README.md +0 -25
  127. package/templates/docs/rules.md +0 -11
  128. package/templates/docs/tasks.md +0 -15
package/dist/cli.js CHANGED
@@ -9,6 +9,8 @@ var LANGS = ["en", "es", "fr", "pt", "de", "it"];
9
9
  var LANG_NAMES = { en: "English", es: "Espa\xF1ol", fr: "Fran\xE7ais", pt: "Portugu\xEAs", de: "Deutsch", it: "Italiano" };
10
10
  var DEFAULT_SOURCE = "en";
11
11
  var isLang = (v) => LANGS.includes(v);
12
+ var isLangCode = (v) => typeof v === "string" && /^[a-z]{2,3}(-[a-z0-9]{2,8})?$/i.test(v);
13
+ var langName = (code, config) => isLang(code) ? LANG_NAMES[code] : config?.langNames?.[code] ?? code;
12
14
  var MAIN_DOCS = ["CLAUDE.md", "AGENTS.md"];
13
15
  var PKG_ROOT = (() => {
14
16
  let dir = path.dirname(fileURLToPath(import.meta.url));
@@ -24,7 +26,7 @@ function readConfig(root2) {
24
26
  try {
25
27
  const raw = JSON.parse(fs.readFileSync(configFile(root2), "utf8"));
26
28
  const langs = Array.isArray(raw.langs) ? raw.langs : raw.lang ? [raw.lang] : [];
27
- return { mainDoc: raw.mainDoc ?? null, langs: langs.filter(isLang), initializedAt: raw.initializedAt };
29
+ return { mainDoc: raw.mainDoc ?? null, langs: langs.filter(isLangCode), langNames: raw.langNames, workflow: raw.workflow, proposals: raw.proposals, initializedAt: raw.initializedAt };
28
30
  } catch {
29
31
  return null;
30
32
  }
@@ -38,7 +40,7 @@ function safeResolve(root2, rel) {
38
40
  if (abs !== root2 && !abs.startsWith(root2 + path.sep)) throw new Error(`Path outside project: ${rel}`);
39
41
  return abs;
40
42
  }
41
- var toPosix = (p2) => p2.split(path.sep).join("/");
43
+ var toPosix = (p3) => p3.split(path.sep).join("/");
42
44
  var DEFAULT_SETTINGS = { font: "inter", size: 15, lineHeight: 1.65, padding: 28, width: "full", wrap: true };
43
45
  var settingsFile = () => path.join(GLOBAL_DIR, "settings.json");
44
46
  function readSettings() {
@@ -97,10 +99,10 @@ function seedRules() {
97
99
  const src = path2.join(TEMPLATES_DIR, "rules");
98
100
  for (const f of fs2.readdirSync(src)) fs2.copyFileSync(path2.join(src, f), path2.join(rulesDir(), f));
99
101
  }
100
- function parseRule(id, text) {
101
- const m = text.match(/^---\n([\s\S]*?)\n---\n?([\s\S]*)$/);
102
- const meta = m ? YAML.parse(m[1]) ?? {} : {};
103
- return { id, title: meta.title ?? id, body: (m ? m[2] : text).trim() };
102
+ function parseRule(id, text3) {
103
+ const m = text3.match(/^---\n([\s\S]*?)\n---\n?([\s\S]*)$/);
104
+ const meta = m ? YAML.parse(m[1], { logLevel: "error" }) ?? {} : {};
105
+ return { id, title: meta.title ?? id, body: (m ? m[2] : text3).trim() };
104
106
  }
105
107
  function listRules() {
106
108
  seedRules();
@@ -109,31 +111,31 @@ function listRules() {
109
111
  function saveRule(rule) {
110
112
  seedRules();
111
113
  const id = rule.id || slugify(rule.title) || `rule-${Date.now()}`;
112
- const text = `---
114
+ const text3 = `---
113
115
  ${YAML.stringify({ title: rule.title }).trim()}
114
116
  ---
115
117
  ${rule.body.trim()}
116
118
  `;
117
- fs2.writeFileSync(path2.join(rulesDir(), `${id}.md`), text);
119
+ fs2.writeFileSync(path2.join(rulesDir(), `${id}.md`), text3);
118
120
  return { id, title: rule.title, body: rule.body.trim() };
119
121
  }
120
122
  function deleteRule(id) {
121
123
  fs2.rmSync(path2.join(rulesDir(), `${path2.basename(id)}.md`), { force: true });
122
124
  }
123
125
  function appliedRuleIds(docText) {
124
- const section = extractSection(docText);
125
- if (!section) return [];
126
- return [...section.matchAll(/<!-- rule:([\w-]+) -->/g)].map((m) => m[1]);
126
+ const section2 = extractSection(docText);
127
+ if (!section2) return [];
128
+ return [...section2.matchAll(/<!-- rule:([\w-]+) -->/g)].map((m) => m[1]);
127
129
  }
128
- function extractSection(text) {
129
- const s = text.indexOf(START);
130
- const e = text.indexOf(END);
131
- return s >= 0 && e > s ? text.slice(s, e + END.length) : null;
130
+ function extractSection(text3) {
131
+ const s = text3.indexOf(START);
132
+ const e = text3.indexOf(END);
133
+ return s >= 0 && e > s ? text3.slice(s, e + END.length) : null;
132
134
  }
133
135
  function writtenRules(docText) {
134
- const section = extractSection(docText) ?? "";
136
+ const section2 = extractSection(docText) ?? "";
135
137
  const out = {};
136
- for (const m of section.matchAll(/<!-- rule:([\w-]+) -->\n([\s\S]*?)(?=\n+<!-- (?:rule:|mdrules:rules:end))/g)) out[m[1]] = { id: m[1], title: m[1], body: m[2].trim() };
138
+ for (const m of section2.matchAll(/<!-- rule:([\w-]+) -->\n([\s\S]*?)(?=\n+<!-- (?:rule:|mdrules:rules:end))/g)) out[m[1]] = { id: m[1], title: m[1], body: m[2].trim() };
137
139
  return out;
138
140
  }
139
141
  function sourceLangOf(docText) {
@@ -165,17 +167,17 @@ ${r.body}`).join("\n\n");
165
167
  ].join("\n");
166
168
  }
167
169
  function applyRules(docText, rules, sourceLang = sourceLangOf(docText)) {
168
- const section = renderSection(rules, sourceLang);
170
+ const section2 = renderSection(rules, sourceLang);
169
171
  const existing = extractSection(docText);
170
- if (existing) return docText.replace(existing, section);
171
- return docText.replace(/\s*$/, "") + "\n\n" + section + "\n";
172
+ if (existing) return docText.replace(existing, section2);
173
+ return docText.replace(/\s*$/, "") + "\n\n" + section2 + "\n";
172
174
  }
173
175
  function applyRulesToFile(file, ids, sourceLang) {
174
- const text = fs2.existsSync(file) ? fs2.readFileSync(file, "utf8") : "";
176
+ const text3 = fs2.existsSync(file) ? fs2.readFileSync(file, "utf8") : "";
175
177
  const all = listRules();
176
- const written = writtenRules(text);
177
- const rules = (ids ?? appliedRuleIds(text)).map((id) => all.find((r) => r.id === id) ?? written[id]).filter((r) => !!r);
178
- fs2.writeFileSync(file, applyRules(text, rules, sourceLang ?? sourceLangOf(text)));
178
+ const written = writtenRules(text3);
179
+ const rules = (ids ?? appliedRuleIds(text3)).map((id) => all.find((r) => r.id === id) ?? written[id]).filter((r) => !!r);
180
+ fs2.writeFileSync(file, applyRules(text3, rules, sourceLang ?? sourceLangOf(text3)));
179
181
  }
180
182
 
181
183
  // src/core/scan.ts
@@ -233,7 +235,7 @@ function scanTree(root2) {
233
235
  if (gitIgnore.ignores(rel + "/")) continue;
234
236
  const ignored = parentIgnored || mdIgnore.ignores(rel + "/");
235
237
  const children = walk(abs, ignored);
236
- if (!children.length) continue;
238
+ if (!children.length && !isEmptyDir(abs)) continue;
237
239
  const project = MAIN_DOCS.some((d) => children.some((c) => c.kind === "file" && c.name === d));
238
240
  nodes.push({ name: e.name, path: rel, kind: "dir", project: project || void 0, ignored: ignored || void 0, children });
239
241
  } else if (e.isFile() && e.name.toLowerCase().endsWith(".md")) {
@@ -246,9 +248,16 @@ function scanTree(root2) {
246
248
  }
247
249
  return walk(root2, false);
248
250
  }
251
+ function isEmptyDir(abs) {
252
+ try {
253
+ return fs3.readdirSync(abs).length === 0;
254
+ } catch {
255
+ return false;
256
+ }
257
+ }
258
+ var collator = new Intl.Collator(void 0, { numeric: true, sensitivity: "base" });
249
259
  function compareNodes(a, b) {
250
- const rank = (n) => n.kind === "file" && MAIN_DOCS.includes(n.name) ? 0 : n.kind === "dir" ? 1 : 2;
251
- return rank(a) - rank(b) || a.name.localeCompare(b.name);
260
+ return (a.kind === "dir" ? 0 : 1) - (b.kind === "dir" ? 0 : 1) || collator.compare(a.name, b.name);
252
261
  }
253
262
  function flattenFiles(nodes, includeIgnored = false) {
254
263
  const out = [];
@@ -272,7 +281,7 @@ function findSubprojects(nodes) {
272
281
  // src/core/i18n.ts
273
282
  var stateFile = (root2) => path4.join(stateDir(root2), "state.json");
274
283
  var translationPath = (root2, lang, rel) => path4.join(stateDir(root2), "i18n", lang, rel);
275
- var hash = (text) => crypto.createHash("sha1").update(text).digest("hex").slice(0, 12);
284
+ var hash = (text3) => crypto.createHash("sha1").update(text3).digest("hex").slice(0, 12);
276
285
  var readText = (abs) => {
277
286
  try {
278
287
  return fs4.readFileSync(abs, "utf8");
@@ -318,8 +327,8 @@ function foreignFiles(root2, sourceLang) {
318
327
  const { keepLanguage: keepLanguage2 } = readState(root2);
319
328
  const out = [];
320
329
  for (const f of flattenFiles(scanTree(root2))) {
321
- const text = readText(path4.join(root2, f.path));
322
- const detected = text ? detectLang(text) : null;
330
+ const text3 = readText(path4.join(root2, f.path));
331
+ const detected = text3 ? detectLang(text3) : null;
323
332
  if (detected && detected !== sourceLang && keepLanguage2[f.path] !== detected) out.push({ path: f.path, detected });
324
333
  }
325
334
  return out;
@@ -331,6 +340,8 @@ function syncReport(root2) {
331
340
  return {
332
341
  sourceLang,
333
342
  langs,
343
+ /** Language names by code: translate into these (codes added by hand have no built-in name). */
344
+ names: Object.fromEntries([sourceLang, ...langs].map((l) => [l, langName(l, config)])),
334
345
  foreign: foreignFiles(root2, sourceLang),
335
346
  files: langs.flatMap((l) => allStatuses(root2, l))
336
347
  };
@@ -346,8 +357,8 @@ function markSynced(root2, lang, rel) {
346
357
  writeState(root2, state);
347
358
  }
348
359
  function keepLanguage(root2, rel) {
349
- const text = readText(path4.join(root2, rel));
350
- const detected = text ? detectLang(text) : null;
360
+ const text3 = readText(path4.join(root2, rel));
361
+ const detected = text3 ? detectLang(text3) : null;
351
362
  if (!detected) throw new Error(`Could not detect the language of ${rel}`);
352
363
  const state = readState(root2);
353
364
  state.keepLanguage[rel] = detected;
@@ -356,9 +367,9 @@ function keepLanguage(root2, rel) {
356
367
  }
357
368
 
358
369
  // src/server/index.ts
359
- import fs6 from "fs";
370
+ import fs10 from "fs";
360
371
  import http from "http";
361
- import path6 from "path";
372
+ import path10 from "path";
362
373
  import { watch } from "chokidar";
363
374
  import open from "open";
364
375
  import pc from "picocolors";
@@ -368,29 +379,35 @@ import { WebSocketServer } from "ws";
368
379
  import fs5 from "fs";
369
380
  import path5 from "path";
370
381
  import YAML2 from "yaml";
371
- function parseFrontmatter(text) {
372
- const m = text.match(/^---\r?\n([\s\S]*?)\r?\n---/);
382
+ function parseFrontmatter(text3) {
383
+ const m = text3.match(/^---\r?\n([\s\S]*?)\r?\n---/);
373
384
  if (!m) return {};
374
385
  try {
375
- return YAML2.parse(m[1]) ?? {};
386
+ return YAML2.parse(m[1], { logLevel: "error" }) ?? {};
376
387
  } catch {
377
388
  return {};
378
389
  }
379
390
  }
391
+ var asText = (v) => v instanceof Date ? v.toISOString().slice(0, 10) : v == null || typeof v === "object" ? null : String(v);
380
392
  function summarize(root2) {
381
- return flattenFiles(scanTree(root2)).map((f) => {
382
- const text = fs5.readFileSync(path5.join(root2, f.path), "utf8");
383
- const meta = parseFrontmatter(text);
384
- const total = text.match(/^\s*[-*+] \[[ xX]\]/gm)?.length ?? 0;
385
- const done = text.match(/^\s*[-*+] \[[xX]\]/gm)?.length ?? 0;
386
- const heading = text.match(/^#\s+(.+)$/m)?.[1];
393
+ return flattenFiles(scanTree(root2)).flatMap((f) => {
394
+ const text3 = fs5.readFileSync(path5.join(root2, f.path), "utf8");
395
+ const meta = parseFrontmatter(text3);
396
+ if (meta.type === "task" && f.name.startsWith("_")) return [];
397
+ const id = asText(meta.id);
398
+ const total = text3.match(/^\s*[-*+] \[[ xX]\]/gm)?.length ?? 0;
399
+ const done = text3.match(/^\s*[-*+] \[[xX]\]/gm)?.length ?? 0;
400
+ const heading2 = text3.match(/^#\s+(.+)$/m)?.[1];
401
+ const title = asText(meta.title) ?? (heading2 && id ? heading2.replace(new RegExp(`^${id.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\s*[\xB7:-]\\s*`), "") : heading2);
387
402
  return {
388
403
  path: f.path,
389
- title: meta.title ?? heading ?? f.name.replace(/\.md$/i, ""),
390
- type: meta.type ?? null,
391
- status: meta.status ?? null,
404
+ title: title ?? f.name.replace(/\.md$/i, ""),
405
+ type: asText(meta.type),
406
+ status: asText(meta.status),
392
407
  phase: typeof meta.phase === "number" ? meta.phase : null,
393
- updated: meta.updated ? String(meta.updated instanceof Date ? meta.updated.toISOString().slice(0, 10) : meta.updated) : null,
408
+ updated: asText(meta.updated),
409
+ id,
410
+ area: asText(meta.area),
394
411
  tasks: { done, total }
395
412
  };
396
413
  });
@@ -432,6 +449,414 @@ function gitDiff(root2, rel) {
432
449
  return git(root2, hasHead ? ["diff", "HEAD", "--", rel] : ["diff", "--cached", "--", rel]) ?? "";
433
450
  }
434
451
 
452
+ // src/core/proposals.ts
453
+ import { execFileSync as execFileSync2 } from "child_process";
454
+ import fs8 from "fs";
455
+ import path8 from "path";
456
+
457
+ // src/core/scaffold.ts
458
+ import fs6 from "fs";
459
+ import path6 from "path";
460
+ var today = () => (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
461
+ function fill(text3, vars) {
462
+ return text3.replace(/\{\{(\w+)\}\}/g, (_, k) => vars[k] ?? "");
463
+ }
464
+ function copyTree(src, dest, vars, created, root2) {
465
+ for (const e of fs6.readdirSync(src, { withFileTypes: true })) {
466
+ const from = path6.join(src, e.name);
467
+ const to = path6.join(dest, fill(e.name, vars));
468
+ if (e.isDirectory()) {
469
+ fs6.mkdirSync(to, { recursive: true });
470
+ copyTree(from, to, vars, created, root2);
471
+ } else if (!fs6.existsSync(to)) {
472
+ fs6.writeFileSync(to, fill(fs6.readFileSync(from, "utf8"), vars));
473
+ created.push(toPosix(path6.relative(root2, to)));
474
+ }
475
+ }
476
+ }
477
+ function createMainDoc(root2, name) {
478
+ const file = path6.join(root2, name);
479
+ if (fs6.existsSync(file)) return;
480
+ const text3 = fs6.readFileSync(path6.join(TEMPLATES_DIR, "main.md"), "utf8");
481
+ fs6.writeFileSync(file, fill(text3, { name: path6.basename(root2), date: today() }));
482
+ }
483
+ function installSkill(root2) {
484
+ const dir = path6.join(root2, ".claude", "skills", "mdrules");
485
+ fs6.mkdirSync(dir, { recursive: true });
486
+ fs6.copyFileSync(path6.join(TEMPLATES_DIR, "skill", "SKILL.md"), path6.join(dir, "SKILL.md"));
487
+ }
488
+ function ensureGitignore(root2) {
489
+ const file = path6.join(root2, ".gitignore");
490
+ const text3 = fs6.existsSync(file) ? fs6.readFileSync(file, "utf8") : "";
491
+ if (text3.split("\n").some((l) => [".mdrules", ".mdrules/", "/.mdrules", "/.mdrules/"].includes(l.trim()))) return;
492
+ fs6.writeFileSync(file, text3 + (text3 && !text3.endsWith("\n") ? "\n" : "") + ".mdrules/\n");
493
+ }
494
+ function hideInVsCode(root2) {
495
+ const file = path6.join(root2, ".vscode", "settings.json");
496
+ let settings = {};
497
+ if (fs6.existsSync(file)) {
498
+ try {
499
+ settings = JSON.parse(fs6.readFileSync(file, "utf8"));
500
+ } catch {
501
+ return false;
502
+ }
503
+ }
504
+ settings["files.exclude"] = { ...settings["files.exclude"] ?? {}, "**/.mdrules": true };
505
+ fs6.mkdirSync(path6.dirname(file), { recursive: true });
506
+ fs6.writeFileSync(file, JSON.stringify(settings, null, 2) + "\n");
507
+ return true;
508
+ }
509
+
510
+ // src/core/sdd.ts
511
+ import { spawnSync } from "child_process";
512
+ import fs7 from "fs";
513
+ import path7 from "path";
514
+ var SDD_AGENTS = {
515
+ claude: { label: "Claude Code", openspec: "claude", speckit: "claude" },
516
+ codex: { label: "Codex", openspec: "codex", speckit: "codex" },
517
+ copilot: { label: "GitHub Copilot", openspec: "github-copilot", speckit: "copilot" },
518
+ gemini: { label: "Gemini CLI", openspec: "gemini", speckit: "gemini" },
519
+ cursor: { label: "Cursor", openspec: "cursor", speckit: "cursor-agent" }
520
+ };
521
+ var OPENSPEC_PKG = "@fission-ai/openspec@latest";
522
+ var SPECKIT_SRC = "git+https://github.com/github/spec-kit.git";
523
+ function detectWorkflow(root2) {
524
+ if (fs7.existsSync(path7.join(root2, "openspec"))) return "openspec";
525
+ if (fs7.existsSync(path7.join(root2, ".specify"))) return "speckit";
526
+ return null;
527
+ }
528
+ var has = (cmd) => spawnSync(cmd, ["--version"], { stdio: "ignore", shell: process.platform === "win32" }).status === 0;
529
+ var hasUv = () => has("uvx");
530
+ function installCommand(workflow, agents) {
531
+ if (workflow === "openspec") {
532
+ const tools = agents.map((a) => SDD_AGENTS[a].openspec).join(",") || "none";
533
+ const runner = has("pnpm") ? ["pnpm", "dlx"] : has("npx") ? ["npx", "--yes"] : null;
534
+ return runner && [...runner, OPENSPEC_PKG, "init", "--tools", tools, "--no-animation", "--no-copilot-cloud"];
535
+ }
536
+ if (!hasUv()) return null;
537
+ return ["uvx", "--from", SPECKIT_SRC, "specify", "init", "--here", "--force", "--non-interactive", "--ignore-agent-tools", "--integration", SDD_AGENTS[agents[0] ?? "claude"].speckit];
538
+ }
539
+ function installWorkflow(root2, workflow, agents, official) {
540
+ const before = new Set(flattenFiles(scanTree(root2), true).map((f) => f.path));
541
+ const result = { official: false, created: [], ignored: [] };
542
+ const cmd = official ? installCommand(workflow, agents) : null;
543
+ if (cmd) {
544
+ const run = spawnSync(cmd[0], cmd.slice(1), { cwd: root2, encoding: "utf8", timeout: 3e5, shell: process.platform === "win32", env: { ...process.env, NO_COLOR: "1" } });
545
+ if (run.status === 0) result.official = true;
546
+ else result.error = (run.error?.message ?? `${run.stderr ?? ""}
547
+ ${run.stdout ?? ""}`).trim().split("\n").slice(-6).join("\n");
548
+ }
549
+ copyTree(path7.join(TEMPLATES_DIR, "sdd", workflow), root2, { date: today2() }, result.created, root2);
550
+ const dirs2 = /* @__PURE__ */ new Set();
551
+ for (const f of flattenFiles(scanTree(root2), true)) {
552
+ if (before.has(f.path) || !f.path.startsWith(".") || f.path.startsWith(".specify/memory/")) continue;
553
+ dirs2.add(path7.posix.dirname(f.path));
554
+ }
555
+ for (const dir of dirs2) setIgnored(root2, dir, "dir", true);
556
+ result.ignored = [...dirs2].sort();
557
+ return result;
558
+ }
559
+ var today2 = () => (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
560
+ var WORKFLOW_TEXT = {
561
+ openspec: [
562
+ "## Workflow",
563
+ "",
564
+ "This project uses [OpenSpec](https://github.com/Fission-AI/OpenSpec) (spec-driven development; guide: `openspec/README.md`):",
565
+ "",
566
+ "- `openspec/specs/<domain>/spec.md` describes how the system behaves today.",
567
+ "- Every change starts in `openspec/changes/<change-id>/` with `proposal.md`, `tasks.md`, an optional `design.md` and delta specs, before any code.",
568
+ "- When it is done, archive it so its delta specs merge into `openspec/specs/`.",
569
+ "- Use the `/opsx:*` commands (or the `openspec` CLI) when available."
570
+ ].join("\n"),
571
+ speckit: [
572
+ "## Workflow",
573
+ "",
574
+ "This project uses [GitHub Spec Kit](https://github.com/github/spec-kit) (spec-driven development; guide: `specs/README.md`):",
575
+ "",
576
+ "- Principles every change must respect: `.specify/memory/constitution.md`.",
577
+ "- One folder per feature in `specs/NNN-feature-name/`: `spec.md` (what and why), then `plan.md` (how), then `tasks.md`.",
578
+ "- Use the `/speckit-*` commands when available."
579
+ ].join("\n")
580
+ };
581
+ function applyWorkflowToFile(file, workflow, text3) {
582
+ writeManagedBlock(file, workflow === "none" ? null : text3 ?? (workflow === "mdrules" ? null : WORKFLOW_TEXT[workflow]));
583
+ }
584
+ function writeManagedBlock(file, body, top = false, name = "workflow") {
585
+ const START2 = `<!-- mdrules:${name}:start -->`;
586
+ const END2 = `<!-- mdrules:${name}:end -->`;
587
+ const text3 = fs7.readFileSync(file, "utf8");
588
+ const s = text3.indexOf(START2);
589
+ const e = text3.indexOf(END2);
590
+ const existing = s >= 0 && e > s ? text3.slice(s, e + END2.length) : null;
591
+ if (body === null) {
592
+ if (existing) fs7.writeFileSync(file, text3.replace(new RegExp(`\\n*${escapeRe(existing)}\\n?`), "\n"));
593
+ return;
594
+ }
595
+ const section2 = `${START2}
596
+ ${body}
597
+ ${END2}`;
598
+ if (existing) return fs7.writeFileSync(file, text3.replace(existing, section2));
599
+ const title = top ? /^# .*\n/m.exec(text3) : null;
600
+ if (title) return fs7.writeFileSync(file, text3.slice(0, title.index + title[0].length) + "\n" + section2 + "\n" + text3.slice(title.index + title[0].length));
601
+ fs7.writeFileSync(file, text3.replace(/\s*$/, "") + "\n\n" + section2 + "\n");
602
+ }
603
+ var escapeRe = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
604
+ var dirs = (abs) => {
605
+ try {
606
+ return fs7.readdirSync(abs, { withFileTypes: true }).filter((e) => e.isDirectory() && !e.name.startsWith(".")).map((e) => e.name).sort();
607
+ } catch {
608
+ return [];
609
+ }
610
+ };
611
+ var read = (abs) => {
612
+ try {
613
+ return fs7.readFileSync(abs, "utf8");
614
+ } catch {
615
+ return null;
616
+ }
617
+ };
618
+ var progress = (text3) => ({ done: text3?.match(/^\s*[-*+] \[[xX]\]/gm)?.length ?? 0, total: text3?.match(/^\s*[-*+] \[[ xX]\]/gm)?.length ?? 0 });
619
+ var heading = (text3) => text3?.match(/^#\s+(.+)$/m)?.[1].trim() ?? null;
620
+ var countSpecs = (abs) => {
621
+ try {
622
+ return fs7.readdirSync(abs, { recursive: true }).filter((f) => String(f).endsWith("spec.md")).length;
623
+ } catch {
624
+ return 0;
625
+ }
626
+ };
627
+ function summarizeSdd(root2) {
628
+ const out = {};
629
+ const os3 = path7.join(root2, "openspec");
630
+ if (fs7.existsSync(os3)) {
631
+ const rel = (...p3) => toPosix(path7.join("openspec", ...p3));
632
+ out.openspec = {
633
+ changes: dirs(path7.join(os3, "changes")).filter((id) => id !== "archive").map((id) => {
634
+ const dir = path7.join(os3, "changes", id);
635
+ const proposal = read(path7.join(dir, "proposal.md"));
636
+ const tasks = read(path7.join(dir, "tasks.md"));
637
+ return {
638
+ id,
639
+ // "Proposal: Add login" → "Add login"
640
+ title: heading(proposal)?.replace(/^(change\s+)?proposal:\s*/i, "") ?? id,
641
+ path: rel("changes", id, proposal !== null ? "proposal.md" : "tasks.md"),
642
+ artifacts: { proposal: proposal !== null, design: fs7.existsSync(path7.join(dir, "design.md")), tasks: tasks !== null, specs: countSpecs(path7.join(dir, "specs")) },
643
+ tasks: progress(tasks)
644
+ };
645
+ }),
646
+ specs: dirs(path7.join(os3, "specs")).filter((id) => fs7.existsSync(path7.join(os3, "specs", id, "spec.md"))).map((id) => ({ id, path: rel("specs", id, "spec.md") })),
647
+ archived: dirs(path7.join(os3, "changes", "archive")).reverse().slice(0, 12).map((id) => {
648
+ const file = ["proposal.md", "tasks.md"].find((f) => fs7.existsSync(path7.join(os3, "changes", "archive", id, f)));
649
+ return { id, path: file ? rel("changes", "archive", id, file) : "" };
650
+ })
651
+ };
652
+ }
653
+ if (fs7.existsSync(path7.join(root2, ".specify"))) {
654
+ const constitution = read(path7.join(root2, ".specify", "memory", "constitution.md"));
655
+ out.speckit = {
656
+ // The template is full of [PLACEHOLDERS] until /speckit-constitution fills it.
657
+ constitution: constitution === null ? null : { path: ".specify/memory/constitution.md", filled: !/\[[A-Z_0-9]+\]/.test(constitution) },
658
+ features: dirs(path7.join(root2, "specs")).map((id) => {
659
+ const dir = path7.join(root2, "specs", id);
660
+ const spec = read(path7.join(dir, "spec.md"));
661
+ const tasks = read(path7.join(dir, "tasks.md"));
662
+ return {
663
+ id,
664
+ // "Feature Specification: Login" → "Login"
665
+ title: heading(spec)?.replace(/^feature specification:\s*/i, "") ?? id,
666
+ path: toPosix(path7.join("specs", id, spec !== null ? "spec.md" : "plan.md")),
667
+ artifacts: { spec: spec !== null, plan: fs7.existsSync(path7.join(dir, "plan.md")), tasks: tasks !== null },
668
+ tasks: progress(tasks)
669
+ };
670
+ })
671
+ };
672
+ }
673
+ return out;
674
+ }
675
+ function sddPrefixes(summary) {
676
+ return [...summary.openspec ? ["openspec/"] : [], ...summary.speckit ? ["specs/", ".specify/"] : []];
677
+ }
678
+
679
+ // src/core/proposals.ts
680
+ var SKILL = path8.join(".claude", "skills", "mdrules-proposals");
681
+ function proposalsState(root2) {
682
+ return { active: fs8.existsSync(path8.join(root2, SKILL, "SKILL.md")), layout: readConfig(root2)?.proposals ?? "general" };
683
+ }
684
+ var WHERE = {
685
+ general: "`docs/proposals/NNN-short-name.md` (NNN: the next number in that folder)",
686
+ structured: "`docs/proposals/<topic>/NNN-short-name.md`, one folder per topic (e.g. `sidebar`, `auth`); in a monorepo, a proposal for a single area goes in `<area>/docs/proposals/<topic>/`. Reuse an existing topic folder; ask the user before creating a new one"
687
+ };
688
+ function section(layout) {
689
+ return [
690
+ "## Proposals",
691
+ "",
692
+ "When an answer would be long, has options or a design to validate, or the user asks for one, write a",
693
+ "temporary proposal the user answers in mdrules instead of a chat answer (skill: `mdrules-proposals`).",
694
+ "",
695
+ `- Where: ${WHERE[layout]}.`,
696
+ "- The user ticks options and comments (`<!-- comment by \u2026 -->`). When they say it is answered, reply to the",
697
+ " comments, update every doc that depends on the decision (tasks, phases, architecture\u2026), then delete the proposal."
698
+ ].join("\n");
699
+ }
700
+ function mainDocs(root2, mainDoc) {
701
+ const doc = mainDoc ?? MAIN_DOCS.find((d) => fs8.existsSync(path8.join(root2, d)));
702
+ return doc ? [path8.join(root2, doc)] : [];
703
+ }
704
+ function activateProposals(root2, layout, mainDoc) {
705
+ const dir = path8.join(root2, SKILL);
706
+ fs8.mkdirSync(dir, { recursive: true });
707
+ fs8.writeFileSync(path8.join(dir, "SKILL.md"), fill(fs8.readFileSync(path8.join(TEMPLATES_DIR, "proposals", "SKILL.md"), "utf8"), { where: WHERE[layout] }));
708
+ fs8.mkdirSync(path8.join(root2, "docs", "proposals"), { recursive: true });
709
+ for (const file of mainDocs(root2, mainDoc)) writeManagedBlock(file, section(layout), false, "proposals");
710
+ writeConfig(root2, { ...readConfig(root2) ?? { mainDoc, langs: [] }, proposals: layout });
711
+ }
712
+ function deactivateProposals(root2, mainDoc) {
713
+ fs8.rmSync(path8.join(root2, SKILL), { recursive: true, force: true });
714
+ for (const file of mainDocs(root2, mainDoc)) writeManagedBlock(file, null, false, "proposals");
715
+ }
716
+ function authorName(root2) {
717
+ try {
718
+ return execFileSync2("git", ["config", "user.name"], { cwd: root2, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim() || null;
719
+ } catch {
720
+ return null;
721
+ }
722
+ }
723
+
724
+ // src/core/skills.ts
725
+ import fs9 from "fs";
726
+ import os2 from "os";
727
+ import path9 from "path";
728
+ import YAML3 from "yaml";
729
+ var SOURCE_DIRS = {
730
+ claude: [".claude", "skills"],
731
+ // Shared folder read by Codex, Copilot and Gemini CLI, and where `npx skills` installs.
732
+ agents: [".agents", "skills"],
733
+ codex: [".codex", "skills"],
734
+ gemini: [".gemini", "skills"],
735
+ copilot: [".copilot", "skills"],
736
+ cursor: [".cursor", "skills"]
737
+ };
738
+ var PROJECT_TARGETS = { claude: ".claude/skills", agents: ".agents/skills" };
739
+ var sourceDir = (source) => path9.join(os2.homedir(), ...SOURCE_DIRS[source]);
740
+ var display = (p3) => p3.startsWith(os2.homedir() + path9.sep) ? "~" + p3.slice(os2.homedir().length) : p3;
741
+ function skillSources() {
742
+ return Object.keys(SOURCE_DIRS).map((id) => ({ id, path: display(sourceDir(id)), exists: fs9.existsSync(sourceDir(id)) }));
743
+ }
744
+ var tagsFile = () => path9.join(GLOBAL_DIR, "skills.json");
745
+ function readTags() {
746
+ try {
747
+ return JSON.parse(fs9.readFileSync(tagsFile(), "utf8")).tags ?? {};
748
+ } catch {
749
+ return {};
750
+ }
751
+ }
752
+ function setSkillTags(name, tags) {
753
+ const all = readTags();
754
+ const clean = [...new Set(tags.map((t) => t.trim().toLowerCase()).filter(Boolean))];
755
+ if (clean.length) all[name] = clean;
756
+ else delete all[name];
757
+ fs9.mkdirSync(GLOBAL_DIR, { recursive: true });
758
+ fs9.writeFileSync(tagsFile(), JSON.stringify({ tags: all }, null, 2) + "\n");
759
+ return clean;
760
+ }
761
+ function findSkills(dir, source, managed, depth = 0) {
762
+ let entries;
763
+ try {
764
+ entries = fs9.readdirSync(dir, { withFileTypes: true });
765
+ } catch {
766
+ return [];
767
+ }
768
+ return entries.flatMap((e) => {
769
+ const abs = path9.join(dir, e.name);
770
+ let real;
771
+ try {
772
+ if (!fs9.statSync(abs).isDirectory()) return [];
773
+ real = fs9.realpathSync(abs);
774
+ } catch {
775
+ return [];
776
+ }
777
+ if (fs9.existsSync(path9.join(abs, "SKILL.md"))) return [{ dir: abs, real, source, managed }];
778
+ return depth < 2 ? findSkills(abs, source, managed || e.name === "synced" || e.name.startsWith("."), depth + 1) : [];
779
+ });
780
+ }
781
+ function frontmatter(text3) {
782
+ const m = /^---\r?\n([\s\S]*?)\r?\n---/.exec(text3);
783
+ if (!m) return null;
784
+ try {
785
+ const data = YAML3.parse(m[1], { logLevel: "error" });
786
+ return data && typeof data === "object" ? data : null;
787
+ } catch {
788
+ return null;
789
+ }
790
+ }
791
+ function listFiles(dir) {
792
+ try {
793
+ return fs9.readdirSync(dir, { recursive: true, withFileTypes: true }).map((e) => ({ e, rel: path9.relative(dir, path9.join(e.parentPath ?? e.path ?? dir, e.name)).split(path9.sep).join("/") })).filter(({ e, rel }) => e.isFile() && rel !== "SKILL.md").map(({ rel }) => rel).sort().slice(0, 100);
794
+ } catch {
795
+ return [];
796
+ }
797
+ }
798
+ function listSkills(root2) {
799
+ const byReal = /* @__PURE__ */ new Map();
800
+ for (const source of Object.keys(SOURCE_DIRS)) {
801
+ for (const f of findSkills(sourceDir(source), source, false)) byReal.set(f.real, [...byReal.get(f.real) ?? [], f]);
802
+ }
803
+ const userTags = readTags();
804
+ const skills = [];
805
+ for (const [real, found] of byReal) {
806
+ const text3 = fs9.readFileSync(path9.join(real, "SKILL.md"), "utf8");
807
+ const fm = frontmatter(text3);
808
+ const folder = path9.basename(found[0].dir);
809
+ const name = typeof fm?.name === "string" ? fm.name : folder;
810
+ const description = typeof fm?.description === "string" ? fm.description.trim() : "";
811
+ const issues = [];
812
+ if (!fm) issues.push("frontmatter");
813
+ else {
814
+ if (typeof fm.name !== "string" || !/^[a-z0-9]+(-[a-z0-9]+)*$/.test(fm.name) || fm.name.length > 64) issues.push("name");
815
+ else if (fm.name !== folder) issues.push("name-dir");
816
+ if (!description) issues.push("description");
817
+ else if (description.length > 1024) issues.push("description-long");
818
+ }
819
+ const declared = fm?.metadata?.tags;
820
+ const metaTags = Array.isArray(declared) ? declared.map(String) : typeof declared === "string" ? declared.split(",") : [];
821
+ skills.push({
822
+ key: real,
823
+ name,
824
+ folder,
825
+ description,
826
+ locations: found.map((f) => ({ source: f.source, path: display(f.dir) })),
827
+ managed: found.every((f) => f.managed),
828
+ files: listFiles(real),
829
+ tags: [.../* @__PURE__ */ new Set([...userTags[name] ?? [], ...metaTags.map((t) => t.trim().toLowerCase()).filter(Boolean)])],
830
+ issues,
831
+ installed: Object.keys(PROJECT_TARGETS).filter((t) => fs9.existsSync(path9.join(root2, PROJECT_TARGETS[t], folder, "SKILL.md")))
832
+ });
833
+ }
834
+ return skills.sort((a, b) => a.name.localeCompare(b.name));
835
+ }
836
+ function getSkill(root2, key) {
837
+ const skill = typeof key === "string" ? listSkills(root2).find((s) => s.key === key) : void 0;
838
+ if (!skill) throw Object.assign(new Error("Skill not found"), { code: "ENOENT" });
839
+ return skill;
840
+ }
841
+ function projectTargets(root2) {
842
+ const has2 = (p3) => fs9.existsSync(path9.join(root2, p3));
843
+ const out = [];
844
+ if (has2(".claude") || has2("CLAUDE.md")) out.push("claude");
845
+ if (has2(".agents") || has2("AGENTS.md") || has2(".codex") || has2(".gemini") || has2(".github/skills")) out.push("agents");
846
+ return out.length ? out : ["claude"];
847
+ }
848
+ function installSkill2(root2, skill, targets, overwrite) {
849
+ const dests = targets.map((t) => path9.join(root2, PROJECT_TARGETS[t], skill.folder));
850
+ const existing = dests.filter((d) => fs9.existsSync(d));
851
+ if (existing.length && !overwrite) return { conflict: existing.map((d) => path9.relative(root2, d).split(path9.sep).join("/")) };
852
+ for (const dest of dests) {
853
+ fs9.rmSync(dest, { recursive: true, force: true });
854
+ fs9.mkdirSync(path9.dirname(dest), { recursive: true });
855
+ fs9.cpSync(skill.key, dest, { recursive: true, dereference: true });
856
+ }
857
+ return { ok: true };
858
+ }
859
+
435
860
  // src/server/index.ts
436
861
  var IMAGE_MIME = {
437
862
  ".png": "image/png",
@@ -479,7 +904,7 @@ function isLocalRequest(req) {
479
904
  async function startServer({ root: root2, port, open: openBrowser }) {
480
905
  const config = () => readConfig(root2) ?? { mainDoc: null, langs: [] };
481
906
  const sourceLang = () => projectSourceLang(root2, readConfig(root2)?.mainDoc);
482
- const viewLang = (value) => isLang(value) && value !== sourceLang() && config().langs.includes(value) ? value : null;
907
+ const viewLang = (value) => isLangCode(value) && value !== sourceLang() && config().langs.includes(value) ? value : null;
483
908
  const mdPath = (rel) => {
484
909
  if (typeof rel !== "string" || !rel.toLowerCase().endsWith(".md")) throw new HttpError(400, "Expected a .md path");
485
910
  safeResolve(root2, rel);
@@ -489,24 +914,38 @@ async function startServer({ root: root2, port, open: openBrowser }) {
489
914
  if (typeof rel !== "string" || !rel.trim()) throw new HttpError(400, "Expected a path");
490
915
  const abs = safeResolve(root2, rel);
491
916
  if (abs === root2) throw new HttpError(400, "Refusing to delete the project root");
492
- if (!fs6.existsSync(abs)) throw new HttpError(404, "Not found");
917
+ if (!fs10.existsSync(abs)) throw new HttpError(404, "Not found");
493
918
  return abs;
494
919
  };
495
920
  const routes = {
496
921
  "GET /api/info": () => {
497
922
  const c = config();
498
- return { name: path6.basename(root2), mainDoc: c.mainDoc ?? null, sourceLang: sourceLang(), langs: c.langs.filter((l) => l !== sourceLang()), git: isGitRepo(root2) };
923
+ return {
924
+ name: path10.basename(root2),
925
+ mainDoc: c.mainDoc ?? null,
926
+ sourceLang: sourceLang(),
927
+ langs: c.langs.filter((l) => l !== sourceLang()),
928
+ langNames: Object.fromEntries(c.langs.map((l) => [l, langName(l, c)])),
929
+ git: isGitRepo(root2),
930
+ proposals: proposalsState(root2),
931
+ author: authorName(root2)
932
+ };
499
933
  },
500
934
  "PUT /api/config": (_, body) => {
501
935
  const c = config();
502
- if (Array.isArray(body.langs)) c.langs = body.langs.filter(isLang);
936
+ if (Array.isArray(body.langs)) c.langs = body.langs.filter(isLangCode);
503
937
  if (isLang(body.sourceLang) && body.sourceLang !== sourceLang()) {
504
938
  if (!c.mainDoc) throw new HttpError(400, "No main document configured; run `mdrules init`");
505
- applyRulesToFile(path6.join(root2, c.mainDoc), void 0, body.sourceLang);
939
+ applyRulesToFile(path10.join(root2, c.mainDoc), void 0, body.sourceLang);
506
940
  }
507
941
  writeConfig(root2, c);
508
942
  return { ...c, langs: c.langs.filter((l) => l !== sourceLang()) };
509
943
  },
944
+ "PUT /api/proposals": (_, body) => {
945
+ if (body.active) activateProposals(root2, body.layout === "structured" ? "structured" : "general", config().mainDoc);
946
+ else deactivateProposals(root2, config().mainDoc);
947
+ return proposalsState(root2);
948
+ },
510
949
  "GET /api/settings": () => readSettings(),
511
950
  "PUT /api/settings": (_, body) => writeSettings(body),
512
951
  "GET /api/tree": (url2) => {
@@ -519,10 +958,10 @@ async function startServer({ root: root2, port, open: openBrowser }) {
519
958
  "GET /api/file": (url2) => {
520
959
  const rel = mdPath(url2.searchParams.get("path"));
521
960
  const l = viewLang(url2.searchParams.get("lang"));
522
- const source = fs6.readFileSync(safeResolve(root2, rel), "utf8");
961
+ const source = fs10.readFileSync(safeResolve(root2, rel), "utf8");
523
962
  if (l) {
524
963
  const tPath = translationPath(root2, l, rel);
525
- if (fs6.existsSync(tPath)) return { path: rel, content: fs6.readFileSync(tPath, "utf8"), target: "translation", status: fileStatus(root2, l, rel) };
964
+ if (fs10.existsSync(tPath)) return { path: rel, content: fs10.readFileSync(tPath, "utf8"), target: "translation", status: fileStatus(root2, l, rel) };
526
965
  return { path: rel, content: source, target: "source", status: "missing" };
527
966
  }
528
967
  return { path: rel, content: source, target: "source", status: null };
@@ -531,31 +970,40 @@ async function startServer({ root: root2, port, open: openBrowser }) {
531
970
  const rel = mdPath(body.path);
532
971
  const l = viewLang(body.lang);
533
972
  const file = body.target === "translation" && l ? translationPath(root2, l, rel) : safeResolve(root2, rel);
534
- fs6.mkdirSync(path6.dirname(file), { recursive: true });
535
- fs6.writeFileSync(file, String(body.content ?? ""));
973
+ fs10.mkdirSync(path10.dirname(file), { recursive: true });
974
+ fs10.writeFileSync(file, String(body.content ?? ""));
536
975
  return { ok: true, status: l ? fileStatus(root2, l, rel) : null };
537
976
  },
538
977
  "POST /api/file": (_, body) => {
539
978
  const rel = mdPath(body.path);
540
979
  const file = safeResolve(root2, rel);
541
- if (fs6.existsSync(file)) throw new HttpError(409, "File already exists");
542
- fs6.mkdirSync(path6.dirname(file), { recursive: true });
543
- const title = path6.basename(rel, ".md").replace(/^\d+[-_]/, "").replace(/[-_]/g, " ");
544
- fs6.writeFileSync(file, `# ${title.charAt(0).toUpperCase() + title.slice(1)}
980
+ if (fs10.existsSync(file)) throw new HttpError(409, "File already exists");
981
+ fs10.mkdirSync(path10.dirname(file), { recursive: true });
982
+ const title = path10.basename(rel, ".md").replace(/^\d+[-_]/, "").replace(/[-_]/g, " ");
983
+ fs10.writeFileSync(file, `# ${title.charAt(0).toUpperCase() + title.slice(1)}
545
984
 
546
985
  `);
547
986
  return { ok: true, path: rel };
548
987
  },
988
+ // Only a mkdir: nothing else is touched, so editors that have the project open are unaffected.
989
+ "POST /api/dir": (_, body) => {
990
+ if (typeof body.path !== "string" || !body.path.trim()) throw new HttpError(400, "Expected a path");
991
+ const dir = safeResolve(root2, body.path);
992
+ if (dir === root2) throw new HttpError(400, "Expected a path");
993
+ if (fs10.existsSync(dir)) throw new HttpError(409, "A file or folder with that name already exists");
994
+ fs10.mkdirSync(dir, { recursive: true });
995
+ return { ok: true, path: toPosix(path10.relative(root2, dir)) };
996
+ },
549
997
  "GET /api/delete-preview": (url2) => {
550
998
  const abs = deletable(url2.searchParams.get("path"));
551
- if (!fs6.statSync(abs).isDirectory()) return { kind: "file", files: 1, md: 1 };
552
- const files = fs6.readdirSync(abs, { recursive: true, withFileTypes: true }).filter((e) => e.isFile());
999
+ if (!fs10.statSync(abs).isDirectory()) return { kind: "file", files: 1, md: 1 };
1000
+ const files = fs10.readdirSync(abs, { recursive: true, withFileTypes: true }).filter((e) => e.isFile());
553
1001
  return { kind: "dir", files: files.length, md: files.filter((e) => e.name.toLowerCase().endsWith(".md")).length };
554
1002
  },
555
1003
  "DELETE /api/path": (url2) => {
556
- const rel = toPosix(path6.relative(root2, deletable(url2.searchParams.get("path"))));
557
- fs6.rmSync(safeResolve(root2, rel), { recursive: true });
558
- for (const l of config().langs) fs6.rmSync(translationPath(root2, l, rel), { recursive: true, force: true });
1004
+ const rel = toPosix(path10.relative(root2, deletable(url2.searchParams.get("path"))));
1005
+ fs10.rmSync(safeResolve(root2, rel), { recursive: true });
1006
+ for (const l of config().langs) fs10.rmSync(translationPath(root2, l, rel), { recursive: true, force: true });
559
1007
  return { ok: true };
560
1008
  },
561
1009
  "POST /api/ignore": (_, body) => {
@@ -564,21 +1012,48 @@ async function startServer({ root: root2, port, open: openBrowser }) {
564
1012
  return { ok: true };
565
1013
  },
566
1014
  "GET /api/diff": (url2) => ({ diff: gitDiff(root2, mdPath(url2.searchParams.get("path"))) }),
567
- "GET /api/dashboard": () => ({ docs: summarize(root2) }),
1015
+ "GET /api/dashboard": () => {
1016
+ const sdd = summarizeSdd(root2);
1017
+ const shown = sddPrefixes(sdd);
1018
+ return { docs: summarize(root2).filter((d) => !shown.some((p3) => d.path.startsWith(p3))), sdd };
1019
+ },
568
1020
  "GET /api/rules": () => {
569
1021
  const mainDoc = readConfig(root2)?.mainDoc;
570
- const text = mainDoc && fs6.existsSync(path6.join(root2, mainDoc)) ? fs6.readFileSync(path6.join(root2, mainDoc), "utf8") : "";
571
- return { rules: listRules(), applied: appliedRuleIds(text), mainDoc: mainDoc ?? null };
1022
+ const text3 = mainDoc && fs10.existsSync(path10.join(root2, mainDoc)) ? fs10.readFileSync(path10.join(root2, mainDoc), "utf8") : "";
1023
+ return { rules: listRules(), applied: appliedRuleIds(text3), mainDoc: mainDoc ?? null };
572
1024
  },
573
1025
  "PUT /api/rules": (_, body) => saveRule(body),
574
1026
  "DELETE /api/rules": (url2) => {
575
1027
  deleteRule(url2.searchParams.get("id") ?? "");
576
1028
  return { ok: true };
577
1029
  },
1030
+ // Global skills: reads and writes are limited to SKILL.md of discovered skill folders.
1031
+ "GET /api/skills": () => ({ skills: listSkills(root2), sources: skillSources(), targets: projectTargets(root2) }),
1032
+ "PUT /api/skills/tags": (_, body) => {
1033
+ if (typeof body.name !== "string" || !Array.isArray(body.tags)) throw new HttpError(400, "Expected name and tags");
1034
+ return { tags: setSkillTags(body.name, body.tags.map(String)) };
1035
+ },
1036
+ "GET /api/skills/file": (url2) => {
1037
+ const skill = getSkill(root2, url2.searchParams.get("key"));
1038
+ return { path: skill.key, content: fs10.readFileSync(path10.join(skill.key, "SKILL.md"), "utf8"), target: "source", status: null };
1039
+ },
1040
+ "PUT /api/skills/file": (_, body) => {
1041
+ const skill = getSkill(root2, body.key);
1042
+ if (skill.managed) throw new HttpError(403, "This skill is managed by the app that installed it");
1043
+ fs10.writeFileSync(path10.join(skill.key, "SKILL.md"), String(body.content ?? ""));
1044
+ return { ok: true, status: null };
1045
+ },
1046
+ "POST /api/skills/install": (_, body) => {
1047
+ const targets = (Array.isArray(body.targets) ? body.targets : []).filter((t) => typeof t === "string" && t in PROJECT_TARGETS);
1048
+ if (!targets.length) throw new HttpError(400, "Expected at least one target");
1049
+ const result = installSkill2(root2, getSkill(root2, body.key), targets, !!body.overwrite);
1050
+ if (result.conflict) throw new HttpError(409, result.conflict.join(", "));
1051
+ return result;
1052
+ },
578
1053
  "POST /api/rules/apply": (_, body) => {
579
1054
  const mainDoc = readConfig(root2)?.mainDoc;
580
1055
  if (!mainDoc) throw new HttpError(400, "No main document configured; run `mdrules init`");
581
- applyRulesToFile(path6.join(root2, mainDoc), body.ids ?? []);
1056
+ applyRulesToFile(path10.join(root2, mainDoc), body.ids ?? []);
582
1057
  return { ok: true };
583
1058
  }
584
1059
  };
@@ -610,13 +1085,13 @@ async function startServer({ root: root2, port, open: openBrowser }) {
610
1085
  res.writeHead(404).end();
611
1086
  return;
612
1087
  }
613
- const type = IMAGE_MIME[path6.extname(file).toLowerCase()];
614
- if (!type || !fs6.existsSync(file) || !fs6.statSync(file).isFile()) {
1088
+ const type = IMAGE_MIME[path10.extname(file).toLowerCase()];
1089
+ if (!type || !fs10.existsSync(file) || !fs10.statSync(file).isFile()) {
615
1090
  res.writeHead(404).end();
616
1091
  return;
617
1092
  }
618
1093
  res.writeHead(200, { "content-type": type, "cache-control": "no-cache" });
619
- fs6.createReadStream(file).pipe(res);
1094
+ fs10.createReadStream(file).pipe(res);
620
1095
  }
621
1096
  const actualPort = await listen(server, port);
622
1097
  const wss = new WebSocketServer({ server, path: "/ws", verifyClient: ({ req }) => isLocalRequest(req) });
@@ -629,17 +1104,17 @@ async function startServer({ root: root2, port, open: openBrowser }) {
629
1104
  ignoreInitial: true,
630
1105
  // Unreadable folders (e.g. caches owned by another user) are skipped instead of failing.
631
1106
  ignorePermissionErrors: true,
632
- ignored: (p2, stats) => {
633
- const rel = toPosix(path6.relative(root2, p2));
1107
+ ignored: (p3, stats) => {
1108
+ const rel = toPosix(path10.relative(root2, p3));
634
1109
  if (rel === ".mdrules") return false;
635
1110
  if (rel.startsWith(".mdrules/")) return !rel.startsWith(".mdrules/i18n") && rel !== ".mdrules/state.json" && rel !== ".mdrules/config.json";
636
1111
  return skips(rel, stats ? stats.isDirectory() : void 0);
637
1112
  }
638
1113
  }).on("error", (err) => console.warn(pc.yellow(` file watcher: ${err.message}`))).on("all", (event, file) => {
639
- const abs = path6.resolve(file);
1114
+ const abs = path10.resolve(file);
640
1115
  if (!abs.endsWith(".md") && !abs.endsWith(".json") && !abs.endsWith(".mdrulesignore") && event !== "addDir" && event !== "unlinkDir") return;
641
- let rel = toPosix(path6.relative(root2, abs));
642
- const i18n = toPosix(path6.relative(root2, path6.join(stateDir(root2), "i18n")));
1116
+ let rel = toPosix(path10.relative(root2, abs));
1117
+ const i18n = toPosix(path10.relative(root2, path10.join(stateDir(root2), "i18n")));
643
1118
  if (rel.startsWith(i18n + "/")) rel = rel.split("/").slice(3).join("/");
644
1119
  broadcast({ type: "fs", event, path: rel });
645
1120
  });
@@ -647,7 +1122,7 @@ async function startServer({ root: root2, port, open: openBrowser }) {
647
1122
  console.log(`
648
1123
  ${pc.bold("mdrules")} ${pc.dim("\u2192")} ${pc.cyan(url)} ${pc.dim(root2)}
649
1124
  `);
650
- if (!fs6.existsSync(WEB_DIR)) console.log(pc.yellow(" Web UI not built: run `pnpm build`, or `pnpm dev` and open http://localhost:4320\n"));
1125
+ if (!fs10.existsSync(WEB_DIR)) console.log(pc.yellow(" Web UI not built: run `pnpm build`, or `pnpm dev` and open http://localhost:4320\n"));
651
1126
  else if (openBrowser) open(url).catch(() => {
652
1127
  });
653
1128
  }
@@ -661,79 +1136,142 @@ function listen(server, port) {
661
1136
  });
662
1137
  }
663
1138
  function serveStatic(pathname, res) {
664
- let file = path6.join(WEB_DIR, path6.normalize(pathname).replace(/^(\.\.[/\\])+/, ""));
665
- if (!file.startsWith(WEB_DIR) || !fs6.existsSync(file) || fs6.statSync(file).isDirectory()) file = path6.join(WEB_DIR, "index.html");
666
- if (!fs6.existsSync(file)) {
1139
+ let file = path10.join(WEB_DIR, path10.normalize(pathname).replace(/^(\.\.[/\\])+/, ""));
1140
+ if (!file.startsWith(WEB_DIR) || !fs10.existsSync(file) || fs10.statSync(file).isDirectory()) file = path10.join(WEB_DIR, "index.html");
1141
+ if (!fs10.existsSync(file)) {
667
1142
  res.writeHead(404).end("Web UI not built. Run `pnpm build`.");
668
1143
  return;
669
1144
  }
670
- res.writeHead(200, { "content-type": MIME[path6.extname(file)] ?? "application/octet-stream" });
671
- fs6.createReadStream(file).pipe(res);
1145
+ res.writeHead(200, { "content-type": MIME[path10.extname(file)] ?? "application/octet-stream" });
1146
+ fs10.createReadStream(file).pipe(res);
672
1147
  }
673
1148
 
674
1149
  // src/cli/init.ts
675
- import fs8 from "fs";
676
- import path8 from "path";
1150
+ import fs12 from "fs";
1151
+ import path12 from "path";
677
1152
  import * as p from "@clack/prompts";
678
1153
  import pc2 from "picocolors";
679
1154
 
680
- // src/core/scaffold.ts
681
- import fs7 from "fs";
682
- import path7 from "path";
683
- var today = () => (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
684
- function fill(text, vars) {
685
- return text.replace(/\{\{(\w+)\}\}/g, (_, k) => vars[k] ?? "");
686
- }
687
- function copyTree(src, dest, vars, created, root2) {
688
- for (const e of fs7.readdirSync(src, { withFileTypes: true })) {
689
- const from = path7.join(src, e.name);
690
- const to = path7.join(dest, e.name);
691
- if (e.isDirectory()) {
692
- fs7.mkdirSync(to, { recursive: true });
693
- copyTree(from, to, vars, created, root2);
694
- } else if (!fs7.existsSync(to)) {
695
- fs7.writeFileSync(to, fill(fs7.readFileSync(from, "utf8"), vars));
696
- created.push(toPosix(path7.relative(root2, to)));
697
- }
1155
+ // src/core/method.ts
1156
+ import fs11 from "fs";
1157
+ import path11 from "path";
1158
+ import YAML4 from "yaml";
1159
+ var SKIP = /* @__PURE__ */ new Set(["docs", "node_modules", "dist", "build", "coverage", "tmp"]);
1160
+ function detectAreas(root2) {
1161
+ const globs = [];
1162
+ try {
1163
+ const ws = YAML4.parse(fs11.readFileSync(path11.join(root2, "pnpm-workspace.yaml"), "utf8"), { logLevel: "error" });
1164
+ if (Array.isArray(ws?.packages)) globs.push(...ws.packages);
1165
+ } catch {
698
1166
  }
1167
+ try {
1168
+ const ws = JSON.parse(fs11.readFileSync(path11.join(root2, "package.json"), "utf8")).workspaces;
1169
+ globs.push(...Array.isArray(ws) ? ws : Array.isArray(ws?.packages) ? ws.packages : []);
1170
+ } catch {
1171
+ }
1172
+ const tops = [...globs.map((g) => String(g).replace(/^\.\//, "").split("/")[0]), ...findSubprojects(scanTree(root2)).map((p3) => p3.split("/")[0])];
1173
+ return [...new Set(tops)].filter((d) => d && !d.includes("*") && !d.startsWith(".") && !d.startsWith("!") && !SKIP.has(d) && fs11.statSync(path11.join(root2, d), { throwIfNoEntry: false })?.isDirectory()).sort();
1174
+ }
1175
+ var KNOWN_PREFIXES = {
1176
+ apps: "APP",
1177
+ app: "APP",
1178
+ services: "SVC",
1179
+ service: "SVC",
1180
+ packages: "PKG",
1181
+ libs: "LIB",
1182
+ lib: "LIB",
1183
+ infra: "INF",
1184
+ infrastructure: "INF",
1185
+ proto: "PRO",
1186
+ backend: "BE",
1187
+ frontend: "FE",
1188
+ web: "WEB",
1189
+ api: "API",
1190
+ mobile: "MOB",
1191
+ tools: "TLS",
1192
+ scripts: "SCR"
1193
+ };
1194
+ function suggestPrefix(dir, used) {
1195
+ const base = KNOWN_PREFIXES[dir.toLowerCase()] ?? (dir.replace(/[^a-z0-9]/gi, "").slice(0, 3).toUpperCase() || "AREA");
1196
+ let prefix = base;
1197
+ for (let i = 2; used.has(prefix) || prefix === "CORE"; i++) prefix = `${base}${i}`;
1198
+ used.add(prefix);
1199
+ return prefix;
699
1200
  }
700
- function scaffoldDocs(root2) {
1201
+ var rootPrefix = (areas) => areas.length ? "CORE" : "T";
1202
+ function scaffoldMethod(root2, mainDoc, areas) {
701
1203
  const created = [];
702
- fs7.mkdirSync(path7.join(root2, "docs"), { recursive: true });
703
- copyTree(path7.join(TEMPLATES_DIR, "docs"), path7.join(root2, "docs"), { date: today() }, created, root2);
1204
+ const name = path11.basename(root2);
1205
+ copyTree(path11.join(TEMPLATES_DIR, "method", "root"), root2, { date: today(), name, prefix: rootPrefix(areas), area: areas.length ? "core" : name }, created, root2);
1206
+ for (const a of areas) {
1207
+ const vars = { date: today(), name, prefix: a.prefix, area: a.dir };
1208
+ const dir = path11.join(root2, a.dir);
1209
+ copyTree(path11.join(TEMPLATES_DIR, "method", "area"), dir, vars, created, root2);
1210
+ let doc = MAIN_DOCS.map((d) => path11.join(dir, d)).find((f) => fs11.existsSync(f));
1211
+ if (!doc) {
1212
+ doc = path11.join(dir, mainDoc);
1213
+ fs11.writeFileSync(doc, fill(fs11.readFileSync(path11.join(TEMPLATES_DIR, "method", "area-main.md"), "utf8"), vars));
1214
+ created.push(path11.posix.join(a.dir, mainDoc));
1215
+ }
1216
+ writeManagedBlock(doc, areaBlock(a), true);
1217
+ }
704
1218
  return created;
705
1219
  }
706
- function createMainDoc(root2, name) {
707
- const file = path7.join(root2, name);
708
- if (fs7.existsSync(file)) return;
709
- const text = fs7.readFileSync(path7.join(TEMPLATES_DIR, "main.md"), "utf8");
710
- fs7.writeFileSync(file, fill(text, { name: path7.basename(root2), date: today() }));
711
- }
712
- function installSkill(root2) {
713
- const dir = path7.join(root2, ".claude", "skills", "mdrules");
714
- fs7.mkdirSync(dir, { recursive: true });
715
- fs7.copyFileSync(path7.join(TEMPLATES_DIR, "skill", "SKILL.md"), path7.join(dir, "SKILL.md"));
1220
+ function areaBlock(a) {
1221
+ return [
1222
+ `- Tasks: prefix \`${a.prefix}\` \xB7 index in \`docs/TASKS.md\` \xB7 one file per task in \`docs/tasks/\``,
1223
+ "- Area architecture: `docs/ARCHITECTURE.md` \xB7 cross-cutting: `../docs/architecture/`"
1224
+ ].join("\n");
716
1225
  }
717
- function ensureGitignore(root2) {
718
- const file = path7.join(root2, ".gitignore");
719
- const text = fs7.existsSync(file) ? fs7.readFileSync(file, "utf8") : "";
720
- if (text.split("\n").some((l) => [".mdrules", ".mdrules/", "/.mdrules", "/.mdrules/"].includes(l.trim()))) return;
721
- fs7.writeFileSync(file, text + (text && !text.endsWith("\n") ? "\n" : "") + ".mdrules/\n");
1226
+ function methodSection(areas) {
1227
+ const taskDirs = ["docs/tasks", ...areas.map((a) => `${a.dir}/docs/tasks`)].join(" ");
1228
+ const map = areas.length ? [
1229
+ "",
1230
+ "### Map",
1231
+ "",
1232
+ "| Folder | Docs | Task prefix |",
1233
+ "|---|---|---|",
1234
+ ...areas.map((a) => `| \`${a.dir}/\` | \`${a.dir}/docs/\` (architecture, tasks) | ${a.prefix} |`),
1235
+ `| \`docs/\` | vision, cross-cutting architecture, phases and cross-area tasks | ${rootPrefix(areas)} |`
1236
+ ] : [];
1237
+ return [
1238
+ "## Workflow (mdrules method)",
1239
+ "",
1240
+ "How we work: `docs/README.md`. Vision: `docs/vision.md` \xB7 architecture: `docs/architecture/` \xB7 phases: `docs/phases/` \xB7 tasks: `docs/TASKS.md`.",
1241
+ ...map,
1242
+ "",
1243
+ "### Session start",
1244
+ "",
1245
+ `1. Find tasks in progress: \`grep -rl "^status: in-progress" --include=*.md ${taskDirs}\`.`,
1246
+ '2. Open the one with the most recent `updated`, read its "Next step" and tell the user in one line where we left off.',
1247
+ "3. If there are none, ask the user what to work on.",
1248
+ "",
1249
+ "### During the session",
1250
+ "",
1251
+ `- Read only what the task needs: its file, ${areas.length ? "the `CLAUDE.md`/`AGENTS.md` and `docs/` of its area, " : ""}and the one file in \`docs/architecture/\` that applies.`,
1252
+ '- Tick acceptance criteria as they are met and keep "Next step" current.',
1253
+ "- Before replacing a decision, propose it with the reasons; then update it in its one place.",
1254
+ "",
1255
+ "### Tasks",
1256
+ "",
1257
+ "- The user defines tasks: propose them, but create them only when asked (`/mdrules-new-task`).",
1258
+ `- One task = one file in ${areas.length ? "`<area>/docs/tasks/`; cross-area tasks in `docs/tasks/`" : "`docs/tasks/`"}. \`TASKS.md\` is only an index; each phase lists its tasks.`,
1259
+ "- At the end of a session: `/mdrules-close-session`.",
1260
+ "",
1261
+ "### Documentation without redundancy",
1262
+ "",
1263
+ "- Each fact lives in one place; everything else links to it.",
1264
+ areas.length ? "- A decision affecting one area goes in `<area>/docs/ARCHITECTURE.md`; several areas, in `docs/architecture/`." : "- Decisions go in `docs/architecture/`, one file per topic.",
1265
+ "- Main docs hold orientation, rules and commands only: never decisions or task lists."
1266
+ ].join("\n");
722
1267
  }
723
- function hideInVsCode(root2) {
724
- const file = path7.join(root2, ".vscode", "settings.json");
725
- let settings = {};
726
- if (fs7.existsSync(file)) {
727
- try {
728
- settings = JSON.parse(fs7.readFileSync(file, "utf8"));
729
- } catch {
730
- return false;
731
- }
1268
+ function installMethodSkills(root2) {
1269
+ const src = path11.join(TEMPLATES_DIR, "method", "skills");
1270
+ for (const name of fs11.readdirSync(src)) {
1271
+ const dest = path11.join(root2, ".claude", "skills", name);
1272
+ fs11.mkdirSync(dest, { recursive: true });
1273
+ fs11.copyFileSync(path11.join(src, name, "SKILL.md"), path11.join(dest, "SKILL.md"));
732
1274
  }
733
- settings["files.exclude"] = { ...settings["files.exclude"] ?? {}, "**/.mdrules": true };
734
- fs7.mkdirSync(path7.dirname(file), { recursive: true });
735
- fs7.writeFileSync(file, JSON.stringify(settings, null, 2) + "\n");
736
- return true;
737
1275
  }
738
1276
 
739
1277
  // src/cli/init.ts
@@ -745,7 +1283,7 @@ function check(value) {
745
1283
  return value;
746
1284
  }
747
1285
  async function chooseMainDoc(root2) {
748
- const existing = MAIN_DOCS.filter((d) => fs8.existsSync(path8.join(root2, d)));
1286
+ const existing = MAIN_DOCS.filter((d) => fs12.existsSync(path12.join(root2, d)));
749
1287
  if (existing.length === 2) {
750
1288
  return check(
751
1289
  await p.select({
@@ -777,31 +1315,119 @@ async function chooseMainDoc(root2) {
777
1315
  p.log.success(`Created ${choice}`);
778
1316
  return choice;
779
1317
  }
1318
+ var WORKFLOW_LABEL = { none: "None", mdrules: "mdrules", openspec: "OpenSpec", speckit: "GitHub Spec Kit" };
1319
+ var WORKFLOW_GUIDE = { none: "", mdrules: "docs/README.md", openspec: "openspec/README.md", speckit: "specs/README.md" };
1320
+ async function chooseAreas(root2) {
1321
+ const candidates = detectAreas(root2);
1322
+ if (!candidates.length) return [];
1323
+ const dirs2 = check(
1324
+ await p.multiselect({
1325
+ message: "Monorepo detected. Which folders are areas with their own docs and tasks? (none: one docs/ for everything)",
1326
+ options: candidates.map((d) => ({ value: d, label: `${d}/` })),
1327
+ initialValues: candidates,
1328
+ required: false
1329
+ })
1330
+ );
1331
+ const used = /* @__PURE__ */ new Set();
1332
+ const areas = dirs2.map((dir) => ({ dir, prefix: suggestPrefix(dir, used) }));
1333
+ if (!areas.length) return areas;
1334
+ const ok = check(await p.confirm({ message: `Task prefixes: ${areas.map((a) => `${a.dir} \u2192 ${a.prefix}`).join(", ")}, cross-area \u2192 CORE. Use them?`, initialValue: true }));
1335
+ if (ok) return areas;
1336
+ for (const a of areas) {
1337
+ a.prefix = check(
1338
+ await p.text({
1339
+ message: `Task prefix for ${a.dir}/`,
1340
+ initialValue: a.prefix,
1341
+ validate: (v) => /^[A-Z][A-Z0-9]{0,5}$/.test(v ?? "") && v !== "CORE" && !areas.some((o) => o !== a && o.prefix === v) ? void 0 : "Uppercase letters and digits, unique, not CORE"
1342
+ })
1343
+ );
1344
+ }
1345
+ return areas;
1346
+ }
1347
+ async function chooseWorkflow(root2, mainDoc, previous) {
1348
+ const detected = detectWorkflow(root2);
1349
+ if (detected) {
1350
+ p.log.info(`${WORKFLOW_LABEL[detected]} found: mdrules shows it in the dashboard.`);
1351
+ return { workflow: detected };
1352
+ }
1353
+ for (; ; ) {
1354
+ const choice = check(
1355
+ await p.select({
1356
+ message: "Create a base structure for specs and planning?",
1357
+ options: [
1358
+ { value: "none", label: "None" },
1359
+ { value: "mdrules", label: "mdrules method", hint: "vision, architecture, phases and one file per task; per area in monorepos" },
1360
+ { value: "openspec", label: "OpenSpec", hint: "specs + proposed changes, good for existing projects" },
1361
+ { value: "speckit", label: "GitHub Spec Kit", hint: "constitution \u2192 spec \u2192 plan \u2192 tasks, per feature" }
1362
+ ],
1363
+ initialValue: previous ?? "none"
1364
+ })
1365
+ );
1366
+ if (choice === "none") return { workflow: choice };
1367
+ if (choice === "mdrules") {
1368
+ const areas = await chooseAreas(root2);
1369
+ const created = scaffoldMethod(root2, mainDoc, areas);
1370
+ installMethodSkills(root2);
1371
+ const big = areas.length > 0 || flattenFiles(scanTree(root2)).length > 40;
1372
+ const layout = big ? check(
1373
+ await p.select({
1374
+ message: "The AI can write proposals you answer by ticking options and commenting. How should they be organized?",
1375
+ options: [
1376
+ { value: "structured", label: "By topic and area", hint: "docs/proposals/auth/\u2026, services/docs/proposals/\u2026" },
1377
+ { value: "general", label: "One folder", hint: "docs/proposals/" }
1378
+ ]
1379
+ })
1380
+ ) : "general";
1381
+ activateProposals(root2, layout, mainDoc);
1382
+ if (created.length) p.log.success(`Created ${created.length} files: ${created.slice(0, 8).join(", ")}${created.length > 8 ? "\u2026" : ""}`);
1383
+ p.log.info(`How it works: ${pc2.cyan("docs/README.md")}. Skills: /mdrules-new-task, /mdrules-close-session.`);
1384
+ return { workflow: choice, section: methodSection(areas) };
1385
+ }
1386
+ let official = true;
1387
+ if (choice === "speckit" && !hasUv()) {
1388
+ const next = check(
1389
+ await p.select({
1390
+ message: "Spec Kit needs uv (Python) to install its AI commands, and uv was not found.",
1391
+ options: [
1392
+ { value: "back", label: "Choose another option" },
1393
+ { value: "skeleton", label: "Create the structure only", hint: "the guide explains how to install Spec Kit later" }
1394
+ ]
1395
+ })
1396
+ );
1397
+ if (next === "back") continue;
1398
+ official = false;
1399
+ }
1400
+ const initial = mainDoc === "AGENTS.md" ? "codex" : "claude";
1401
+ const agentOptions = Object.keys(SDD_AGENTS).map((a) => ({ value: a, label: SDD_AGENTS[a].label }));
1402
+ const agents = !official ? [initial] : choice === "openspec" ? check(await p.multiselect({ message: "Install the OpenSpec commands for which agents?", options: agentOptions, initialValues: [initial], required: false })) : [check(await p.select({ message: "Install the Spec Kit commands for which agent?", options: agentOptions, initialValue: initial }))];
1403
+ const spin = p.spinner();
1404
+ spin.start(official ? `Installing ${WORKFLOW_LABEL[choice]}\u2026` : "Creating the structure\u2026");
1405
+ const res = installWorkflow(root2, choice, agents, official);
1406
+ spin.stop(res.official ? `${WORKFLOW_LABEL[choice]} installed` : "Structure created");
1407
+ if (official && !res.official) p.log.warn(`Couldn't run the ${WORKFLOW_LABEL[choice]} installer:
1408
+ ${pc2.dim(res.error ?? "")}
1409
+ Created the structure only.`);
1410
+ if (!res.official) p.log.info(`To add the AI commands later, see ${pc2.cyan(WORKFLOW_GUIDE[choice])}.`);
1411
+ if (res.ignored.length) p.log.info(`Agent commands hidden in mdrules (${res.ignored.length} folders); the eye button in the sidebar shows them.`);
1412
+ return { workflow: choice };
1413
+ }
1414
+ }
780
1415
  async function runInit(root2) {
781
1416
  p.intro(pc2.bgCyan(pc2.black(" mdrules ")));
782
1417
  const previous = readConfig(root2);
783
1418
  const mainDoc = await chooseMainDoc(root2);
784
- const mainFile = path8.join(root2, mainDoc);
785
- if (!fs8.existsSync(path8.join(root2, "docs"))) {
786
- const create = check(await p.confirm({ message: "Create the base /docs structure?", initialValue: true }));
787
- if (create) p.log.success(`Created ${scaffoldDocs(root2).join(", ")}`);
788
- }
789
- const mainText = fs8.readFileSync(mainFile, "utf8");
1419
+ const mainFile = path12.join(root2, mainDoc);
1420
+ const { workflow, section: section2 } = await chooseWorkflow(root2, mainDoc, previous?.workflow);
1421
+ applyWorkflowToFile(mainFile, workflow, section2);
1422
+ const mainText = fs12.readFileSync(mainFile, "utf8");
790
1423
  const sourceLang = check(
791
1424
  await p.select({
792
- message: "Source language of the .md files (what the AI reads and writes)",
1425
+ message: "Language you work in (every .md file is written in it)",
793
1426
  options: LANGS.map((l) => ({ value: l, label: LANG_NAMES[l], hint: l === "en" ? "default" : void 0 })),
794
1427
  initialValue: sourceLangOf(mainText)
795
1428
  })
796
1429
  );
797
- const langs = check(
798
- await p.multiselect({
799
- message: "Translation languages (view and edit docs in them; the source files never change language)",
800
- options: LANGS.filter((l) => l !== sourceLang).map((l) => ({ value: l, label: LANG_NAMES[l] })),
801
- initialValues: (previous?.langs ?? []).filter((l) => l !== sourceLang),
802
- required: false
803
- })
804
- );
1430
+ const langs = (previous?.langs ?? []).filter((l) => l !== sourceLang);
805
1431
  const rules = listRules();
806
1432
  const applied = appliedRuleIds(mainText);
807
1433
  const selected = check(
@@ -823,8 +1449,8 @@ async function runInit(root2) {
823
1449
  })
824
1450
  );
825
1451
  for (const dir of targets) {
826
- const doc = MAIN_DOCS.find((d) => fs8.existsSync(path8.join(root2, dir, d)));
827
- applyRulesToFile(path8.join(root2, dir, doc), selected, sourceLang);
1452
+ const doc = MAIN_DOCS.find((d) => fs12.existsSync(path12.join(root2, dir, d)));
1453
+ applyRulesToFile(path12.join(root2, dir, doc), selected, sourceLang);
828
1454
  }
829
1455
  }
830
1456
  ensureGitignore(root2);
@@ -833,19 +1459,84 @@ async function runInit(root2) {
833
1459
  if (hide && !hideInVsCode(root2)) p.log.warn('.vscode/settings.json has comments; add "**/.mdrules": true to files.exclude by hand.');
834
1460
  }
835
1461
  installSkill(root2);
836
- const config = { mainDoc, langs, initializedAt: previous?.initializedAt ?? (/* @__PURE__ */ new Date()).toISOString() };
1462
+ const config = { mainDoc, langs, langNames: previous?.langNames, workflow, proposals: readConfig(root2)?.proposals, initializedAt: previous?.initializedAt ?? (/* @__PURE__ */ new Date()).toISOString() };
837
1463
  writeConfig(root2, config);
838
- p.outro(`Ready. Skill installed in ${pc2.cyan(".claude/skills/mdrules")}`);
1464
+ p.outro(`Ready. Skill installed in ${pc2.cyan(".claude/skills/mdrules")}. To review the docs in another language: ${pc2.cyan("mdrules translate")}`);
839
1465
  return config;
840
1466
  }
841
1467
 
1468
+ // src/cli/translate.ts
1469
+ import * as p2 from "@clack/prompts";
1470
+ import pc3 from "picocolors";
1471
+ function check2(value) {
1472
+ if (p2.isCancel(value)) {
1473
+ p2.cancel("Cancelled.");
1474
+ process.exit(0);
1475
+ }
1476
+ return value;
1477
+ }
1478
+ var OTHER = "__other__";
1479
+ async function runTranslate(root2) {
1480
+ const config = readConfig(root2);
1481
+ if (!config) {
1482
+ console.log(`Run ${pc3.cyan("mdrules init")} first.`);
1483
+ process.exitCode = 1;
1484
+ return;
1485
+ }
1486
+ p2.intro(pc3.bgCyan(pc3.black(" mdrules translate ")));
1487
+ const source = projectSourceLang(root2, config.mainDoc);
1488
+ const custom = config.langs.filter((l) => !isLang(l));
1489
+ const picked = check2(
1490
+ await p2.multiselect({
1491
+ message: `Languages to review the docs in (the files stay in ${langName(source)})`,
1492
+ options: [
1493
+ ...LANGS.filter((l) => l !== source).map((l) => ({ value: l, label: LANG_NAMES[l] })),
1494
+ ...custom.map((l) => ({ value: l, label: `${langName(l, config)} (${l})`, hint: "interface in English" })),
1495
+ { value: OTHER, label: "Another language\u2026", hint: "type it; the interface stays in English" }
1496
+ ],
1497
+ initialValues: config.langs.filter((l) => l !== source),
1498
+ required: false
1499
+ })
1500
+ );
1501
+ const langs = picked.filter((l) => l !== OTHER);
1502
+ const names = { ...config.langNames ?? {} };
1503
+ if (picked.includes(OTHER)) {
1504
+ for (; ; ) {
1505
+ const name = check2(await p2.text({ message: "Language name (as the AI should understand it)", placeholder: "Japanese", validate: (v) => v?.trim() ? void 0 : "Type a language" })).trim();
1506
+ const code = check2(
1507
+ await p2.text({
1508
+ message: `Short code for ${name} (folder name of its translations)`,
1509
+ initialValue: name.slice(0, 2).toLowerCase(),
1510
+ validate: (v) => !isLangCode(v) ? "Two or three letters, e.g. ja or zh-tw" : v === source || langs.includes(v) ? "Already in the list" : void 0
1511
+ })
1512
+ ).toLowerCase();
1513
+ langs.push(code);
1514
+ names[code] = name;
1515
+ if (!check2(await p2.confirm({ message: "Add another language?", initialValue: false }))) break;
1516
+ }
1517
+ }
1518
+ writeConfig(root2, { ...config, langs, langNames: names });
1519
+ if (!langs.length) {
1520
+ p2.outro("No translation languages: mdrules shows the docs in their source language only.");
1521
+ return;
1522
+ }
1523
+ p2.outro(
1524
+ [
1525
+ `Translations: ${langs.map((l) => langName(l, { ...config, langNames: names })).join(", ")}.`,
1526
+ `Ask your agent for ${pc3.cyan("/mdrules sync")} to translate the docs; then pick the language at the top of mdrules.`,
1527
+ ...langs.some((l) => !isLang(l)) ? ["Menus stay in English for languages typed by hand."] : []
1528
+ ].join("\n")
1529
+ );
1530
+ }
1531
+
842
1532
  // src/cli/index.ts
843
1533
  var HELP = `mdrules \u2014 visual editor for your project's Markdown
844
1534
 
845
1535
  Usage
846
1536
  mdrules init if needed, then open the editor
847
- mdrules init (re)run the setup: main doc, docs/, rules, language
1537
+ mdrules init (re)run the setup: main doc, specs structure, rules, languages
848
1538
  mdrules serve open the editor without running init
1539
+ mdrules translate choose languages to review the docs in (or: mdrules --translate)
849
1540
  mdrules sync --status [--json] translations + files not in the source language
850
1541
  mdrules sync --mark [--lang xx] <path...>|--all record translations as in sync
851
1542
  mdrules sync --keep <path...> keep files in their current language
@@ -865,6 +1556,7 @@ var root = process.env.MDRULES_ROOT ?? process.cwd();
865
1556
  async function main() {
866
1557
  if (flag("--help") || flag("-h")) return console.log(HELP);
867
1558
  if (command === "sync") return sync();
1559
+ if (command === "translate" || flag("--translate")) return runTranslate(root);
868
1560
  if (command === "init") {
869
1561
  await runInit(root);
870
1562
  return;
@@ -883,26 +1575,26 @@ function sync() {
883
1575
  const report = syncReport(root);
884
1576
  const paths = (after) => args.slice(args.indexOf(after) + 1).filter((a, i, all) => !a.startsWith("--") && all[i - 1] !== "--lang");
885
1577
  if (flag("--keep")) {
886
- for (const p2 of paths("--keep")) {
1578
+ for (const p3 of paths("--keep")) {
887
1579
  try {
888
- console.log(`kept ${p2} (${keepLanguage(root, p2)})`);
1580
+ console.log(`kept ${p3} (${keepLanguage(root, p3)})`);
889
1581
  } catch (err) {
890
- console.error(`skipped ${p2}: ${err.message}`);
1582
+ console.error(`skipped ${p3}: ${err.message}`);
891
1583
  }
892
1584
  }
893
1585
  return;
894
1586
  }
895
1587
  if (flag("--mark")) {
896
1588
  const only = option("--lang");
897
- const langs = only && isLang(only) ? [only] : report.langs;
1589
+ const langs = only && report.langs.includes(only) ? [only] : report.langs;
898
1590
  const files = flag("--all") ? [...new Set(report.files.map((f) => f.path))] : paths("--mark");
899
- for (const p2 of files) {
1591
+ for (const p3 of files) {
900
1592
  for (const l of langs) {
901
1593
  try {
902
- markSynced(root, l, p2);
903
- console.log(`synced ${l} ${p2}`);
1594
+ markSynced(root, l, p3);
1595
+ console.log(`synced ${l} ${p3}`);
904
1596
  } catch (err) {
905
- if (!flag("--all")) console.error(`skipped ${l} ${p2}: ${err.message}`);
1597
+ if (!flag("--all")) console.error(`skipped ${l} ${p3}: ${err.message}`);
906
1598
  }
907
1599
  }
908
1600
  }