mdrules 0.2.0 → 0.2.3

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 (100) hide show
  1. package/README.md +21 -6
  2. package/dist/cli.js +343 -254
  3. package/dist/web/assets/{abnfDiagram-WEYOIA3C-COoXc57p.js → abnfDiagram-WEYOIA3C-DMRcmCIC.js} +1 -1
  4. package/dist/web/assets/{arc-CQ-mpttu.js → arc-DZzWcSCk.js} +1 -1
  5. package/dist/web/assets/{architecture-WOLXFQ4H-Cl3-F9oV.js → architecture-WOLXFQ4H-DMFg8Ew3.js} +1 -1
  6. package/dist/web/assets/{architectureDiagram-ZHVYKT53-C1LOUHfq.js → architectureDiagram-ZHVYKT53-DWkP0LWv.js} +1 -1
  7. package/dist/web/assets/{blockDiagram-RJAE7QA3-YeQnD2ds.js → blockDiagram-RJAE7QA3-DrKNefYo.js} +1 -1
  8. package/dist/web/assets/{c4Diagram-XDPPPE54-XLmSlndV.js → c4Diagram-XDPPPE54-wzxeN4C2.js} +1 -1
  9. package/dist/web/assets/channel-De1Bo4WJ.js +1 -0
  10. package/dist/web/assets/{chunk-2E7WHVMD-HGXqy_LQ.js → chunk-2E7WHVMD-D_HCdfuS.js} +1 -1
  11. package/dist/web/assets/{chunk-4EMNKR2T-DsQggzQE.js → chunk-4EMNKR2T-dRBMJ4_-.js} +1 -1
  12. package/dist/web/assets/{chunk-55XCU76K-DY1xg8cl.js → chunk-55XCU76K-BfDf2f5L.js} +1 -1
  13. package/dist/web/assets/{chunk-7S6XSTFO-BuyzABLx.js → chunk-7S6XSTFO-DQBRPMDG.js} +1 -1
  14. package/dist/web/assets/{chunk-AQDTSKZB-CsDWfENX.js → chunk-AQDTSKZB-ClWiKzeu.js} +1 -1
  15. package/dist/web/assets/{chunk-DSNLSHJZ-CuiVxF9g.js → chunk-DSNLSHJZ-Ct9Lfuzm.js} +1 -1
  16. package/dist/web/assets/{chunk-HAGFOTED-CjjQypU8.js → chunk-HAGFOTED-Cno1MSCP.js} +1 -1
  17. package/dist/web/assets/{chunk-HNE4Q36P-B0-YOuZI.js → chunk-HNE4Q36P-CDmp78x5.js} +1 -1
  18. package/dist/web/assets/{chunk-NQOKRVIF-DMWnWh6r.js → chunk-NQOKRVIF-CqWTXOUR.js} +1 -1
  19. package/dist/web/assets/{chunk-UHW7PFBP-D3E4ik9Y.js → chunk-UHW7PFBP-psZpZtfm.js} +1 -1
  20. package/dist/web/assets/{chunk-VQ5W62QI-CiPAEjx6.js → chunk-VQ5W62QI-CCDvsLND.js} +1 -1
  21. package/dist/web/assets/{chunk-WQQL4CDK-StBWijFa.js → chunk-WQQL4CDK-Cu2Aopzy.js} +1 -1
  22. package/dist/web/assets/{chunk-XXDRQBXY-CINBf_Ay.js → chunk-XXDRQBXY-BWwvAa43.js} +1 -1
  23. package/dist/web/assets/{chunk-YZO67HFG-BSkchBSd.js → chunk-YZO67HFG-QHKY9fqM.js} +1 -1
  24. package/dist/web/assets/{chunk-ZAUX2LTX-CYfcgJpt.js → chunk-ZAUX2LTX-DOdmfo9d.js} +1 -1
  25. package/dist/web/assets/{chunk-ZCHMQSOR-CwWl64AI.js → chunk-ZCHMQSOR-BNE_AYuB.js} +1 -1
  26. package/dist/web/assets/{classDiagram-v2-4MYWR3UA-Ce6JKsB3.js → classDiagram-v2-4MYWR3UA-QVfxHH62.js} +1 -1
  27. package/dist/web/assets/{cose-bilkent-JH36ORCC-COClxHi_.js → cose-bilkent-JH36ORCC-1dupt6EA.js} +1 -1
  28. package/dist/web/assets/{cynefin-EF2NZ3EQ-BGZI2c8V.js → cynefin-EF2NZ3EQ-CAB7KRcw.js} +1 -1
  29. package/dist/web/assets/{cynefinDiagram-WF24UDRP-B24KUNeS.js → cynefinDiagram-WF24UDRP-D2M2_xSW.js} +1 -1
  30. package/dist/web/assets/{dagre-NK4GIZVX-DjCzI5LG.js → dagre-NK4GIZVX-CY4Eoama.js} +1 -1
  31. package/dist/web/assets/{diagram-CBK3MWTC-pa5xxV7I.js → diagram-CBK3MWTC-Ct0zNHAV.js} +1 -1
  32. package/dist/web/assets/{diagram-I5XIIV2D-BQtih66c.js → diagram-I5XIIV2D-gmcSomD3.js} +1 -1
  33. package/dist/web/assets/{diagram-ODIL2UCW-CtodvdqO.js → diagram-ODIL2UCW-Z01ngjlG.js} +1 -1
  34. package/dist/web/assets/{diagram-PFBEJUXE-B-CnrB9d.js → diagram-PFBEJUXE-By7lIQit.js} +1 -1
  35. package/dist/web/assets/{diagram-RJA3I4VV-Di8-srfd.js → diagram-RJA3I4VV-BbTc0lUo.js} +1 -1
  36. package/dist/web/assets/{diagram-TYUNFSNR-DT-cfj0M.js → diagram-TYUNFSNR-BZNmF7dF.js} +1 -1
  37. package/dist/web/assets/{dist-CLMm60_p.js → dist-B4Zd6qux.js} +1 -1
  38. package/dist/web/assets/{dist-CPFvkcDl.js → dist-BLO1tZtl.js} +1 -1
  39. package/dist/web/assets/{dist-BxXtIjV1.js → dist-Bag2LZ55.js} +1 -1
  40. package/dist/web/assets/{dist-B4Z2sV8l.js → dist-CW4Hp5az.js} +1 -1
  41. package/dist/web/assets/{dist-DevJcR0Y.js → dist-DFZHgUoB.js} +1 -1
  42. package/dist/web/assets/{dist-CA-ReuvA.js → dist-DHO-aIOo.js} +1 -1
  43. package/dist/web/assets/{dist-JJNpWgR6.js → dist-DY18ueDO.js} +1 -1
  44. package/dist/web/assets/{dist-5RSr3i1a.js → dist-FrDFGy8k.js} +1 -1
  45. package/dist/web/assets/{dist-B7yJAZLB.js → dist-luL4D3CY.js} +1 -1
  46. package/dist/web/assets/{dist-CZxj92ue.js → dist-oMwphMn-.js} +1 -1
  47. package/dist/web/assets/{dist-DVG9d1NU.js → dist-roXUU37l.js} +1 -1
  48. package/dist/web/assets/{ebnfDiagram-V4P3TTPT-5btoYlHi.js → ebnfDiagram-V4P3TTPT-JoyE0F88.js} +1 -1
  49. package/dist/web/assets/{elk-QONTBCTQ-BiXiP5mn.js → elk-QONTBCTQ-BNYZwxj2.js} +1 -1
  50. package/dist/web/assets/{erDiagram-KYQIGQAA-Csvahmwh.js → erDiagram-KYQIGQAA-BQCuW6RJ.js} +1 -1
  51. package/dist/web/assets/{eventmodeling-K75KTNOO-CTDk7azy.js → eventmodeling-K75KTNOO-DcSjO4xn.js} +1 -1
  52. package/dist/web/assets/flowDiagram-IUGLLWBU-C2mAXQNT.js +1 -0
  53. package/dist/web/assets/{ganttDiagram-R7GPNTJY-Kr_UYAyo.js → ganttDiagram-R7GPNTJY-BofMym7I.js} +1 -1
  54. package/dist/web/assets/{gitGraph-VSP46ZUC-DWAozJzq.js → gitGraph-VSP46ZUC-D5CJVdju.js} +1 -1
  55. package/dist/web/assets/{gitGraphDiagram-PDYL7XHY-DyTW4WQG.js → gitGraphDiagram-PDYL7XHY-BSH5YBss.js} +1 -1
  56. package/dist/web/assets/index-BUr29cVy.js +63 -0
  57. package/dist/web/assets/index-B_El5oZL.css +1 -0
  58. package/dist/web/assets/{info-OHQRW6UA-DiJj6mF5.js → info-OHQRW6UA-LJbVE13c.js} +1 -1
  59. package/dist/web/assets/{infoDiagram-MVDRKAX5-D02QrLp5.js → infoDiagram-MVDRKAX5-CcDzYgLq.js} +1 -1
  60. package/dist/web/assets/{ishikawaDiagram-HPEHV2GE-CXW4mRts.js → ishikawaDiagram-HPEHV2GE-BTAZWgme.js} +1 -1
  61. package/dist/web/assets/{journeyDiagram-BOSHW7O7-DwyTpaHZ.js → journeyDiagram-BOSHW7O7-D_lt8C1z.js} +1 -1
  62. package/dist/web/assets/{kanban-definition-FNSA4HC5-B8br7aDC.js → kanban-definition-FNSA4HC5-BnOn_93I.js} +1 -1
  63. package/dist/web/assets/{line-C7pCcPIA.js → line-D86czQCw.js} +1 -1
  64. package/dist/web/assets/{linear-Tw3CU3Fk.js → linear-DX5Fyu8p.js} +1 -1
  65. package/dist/web/assets/{mermaid-parser.core-DuxqJtRQ.js → mermaid-parser.core-BActkHhD.js} +3 -3
  66. package/dist/web/assets/{mermaid.core-DsOnw7nE.js → mermaid.core-Ce5oFJHI.js} +4 -4
  67. package/dist/web/assets/{mindmap-definition-6OY57UG3-Cx4RGd4t.js → mindmap-definition-6OY57UG3-D7CwuLeJ.js} +1 -1
  68. package/dist/web/assets/{packet-JDAUHWVQ-FgGACr61.js → packet-JDAUHWVQ-DBB-zZek.js} +1 -1
  69. package/dist/web/assets/{pegDiagram-ECIUJNBX-DbM0mpqP.js → pegDiagram-ECIUJNBX-CK1750zt.js} +1 -1
  70. package/dist/web/assets/{pie-XZMESJXO-D3_sBOVu.js → pie-XZMESJXO-DOtQfX6V.js} +1 -1
  71. package/dist/web/assets/{pieDiagram-GNMPY74T-COgyAYuf.js → pieDiagram-GNMPY74T-Cu5hilM3.js} +1 -1
  72. package/dist/web/assets/{quadrantDiagram-D57K4MC3-CHA07iGU.js → quadrantDiagram-D57K4MC3-C6llE2gk.js} +1 -1
  73. package/dist/web/assets/{radar-ABXABTNO-DecHbZ8y.js → radar-ABXABTNO-BqYteODl.js} +1 -1
  74. package/dist/web/assets/{railroad-I3PHUGI6-CX9gFbv6.js → railroad-I3PHUGI6-QkKJVgJ_.js} +1 -1
  75. package/dist/web/assets/{railroad-abnf-LNEFI6M7-7ekNszOQ.js → railroad-abnf-LNEFI6M7-BOVXdtiq.js} +1 -1
  76. package/dist/web/assets/{railroad-ebnf-DWYD2UWJ-DdczO4iK.js → railroad-ebnf-DWYD2UWJ-Dciqx8FR.js} +1 -1
  77. package/dist/web/assets/{railroad-peg-JR7BVNR7-tTF8C5h6.js → railroad-peg-JR7BVNR7-BJRps0kv.js} +1 -1
  78. package/dist/web/assets/{railroadDiagram-Y72NRBL7-7GM0UzXp.js → railroadDiagram-Y72NRBL7-BEp_79kB.js} +1 -1
  79. package/dist/web/assets/{requirementDiagram-EQYSW5RA-BSC7dW7G.js → requirementDiagram-EQYSW5RA-Bi1usS1S.js} +1 -1
  80. package/dist/web/assets/{sankeyDiagram-SY24JHO5-zbVVy5A9.js → sankeyDiagram-SY24JHO5-cBLrx4TR.js} +1 -1
  81. package/dist/web/assets/{sequenceDiagram-BLEHNHQU-DEe5_sJr.js → sequenceDiagram-BLEHNHQU-CwY8WSOO.js} +1 -1
  82. package/dist/web/assets/{src-BRIqFVDQ.js → src-BxxO9_wA.js} +1 -1
  83. package/dist/web/assets/{stateDiagram-v2-A6IESKWZ-D9iIEu7F.js → stateDiagram-v2-A6IESKWZ-CZAkkIQQ.js} +1 -1
  84. package/dist/web/assets/{swimlanes-B3ENRKFQ-kSlZwPrl.js → swimlanes-B3ENRKFQ-B06LEs1H.js} +1 -1
  85. package/dist/web/assets/swimlanesDiagram-AYQRV4AK-Cx4IBQfH.js +8 -0
  86. package/dist/web/assets/{timeline-definition-JXYKGLGB-D3FVcL1M.js → timeline-definition-JXYKGLGB-pBL_HcoZ.js} +1 -1
  87. package/dist/web/assets/{treeView-D4JQ5SDB-Dg-vnn_2.js → treeView-D4JQ5SDB-B3NYhcq3.js} +1 -1
  88. package/dist/web/assets/{treemap-SAJKECNS-SUcVM8SE.js → treemap-SAJKECNS-jUBClquT.js} +1 -1
  89. package/dist/web/assets/{usecaseDiagram-WPOLBNNX-pg19th6w.js → usecaseDiagram-WPOLBNNX-BEy-FZyr.js} +1 -1
  90. package/dist/web/assets/{vennDiagram-Q3Y3IDCW-C0bGBnZZ.js → vennDiagram-Q3Y3IDCW-CU_ZQagp.js} +1 -1
  91. package/dist/web/assets/{wardley-7MLQ67FV-DRUta4Q-.js → wardley-7MLQ67FV-DLfevnf3.js} +1 -1
  92. package/dist/web/assets/{wardleyDiagram-IVAIDKWA-CafLMkAZ.js → wardleyDiagram-IVAIDKWA-BnuVbP4c.js} +1 -1
  93. package/dist/web/assets/{xychartDiagram-4URNHDU4-CbBmnzba.js → xychartDiagram-4URNHDU4-pikAwF42.js} +1 -1
  94. package/dist/web/index.html +2 -2
  95. package/package.json +1 -1
  96. package/dist/web/assets/channel-Bu489uW7.js +0 -1
  97. package/dist/web/assets/flowDiagram-IUGLLWBU-CTlRlF-F.js +0 -1
  98. package/dist/web/assets/index-lmzHXuPr.css +0 -1
  99. package/dist/web/assets/index-qPUGRLvu.js +0 -63
  100. package/dist/web/assets/swimlanesDiagram-AYQRV4AK-DUYiDwoF.js +0 -8
package/dist/cli.js CHANGED
@@ -41,7 +41,7 @@ function safeResolve(root2, rel) {
41
41
  return abs;
42
42
  }
43
43
  var toPosix = (p3) => p3.split(path.sep).join("/");
44
- var DEFAULT_SETTINGS = { font: "inter", size: 15, lineHeight: 1.65, padding: 28, width: "full", wrap: true };
44
+ var DEFAULT_SETTINGS = { font: "inter", size: 15, lineHeight: 1.65, padding: 28, width: "full", wrap: true, uiLang: "en" };
45
45
  var settingsFile = () => path.join(GLOBAL_DIR, "settings.json");
46
46
  function readSettings() {
47
47
  try {
@@ -57,10 +57,40 @@ function writeSettings(patch) {
57
57
  return next;
58
58
  }
59
59
 
60
+ // src/core/defaults.ts
61
+ import fs2 from "fs";
62
+ import path2 from "path";
63
+ var BASE_DEFAULTS = {
64
+ mode: "ask",
65
+ mainDoc: "CLAUDE.md",
66
+ workflow: "none",
67
+ agents: ["claude"],
68
+ proposals: "structured",
69
+ sourceLang: "en",
70
+ rules: [],
71
+ rulesInSubprojects: false,
72
+ hideInVsCode: true
73
+ };
74
+ var defaultsFile = () => path2.join(GLOBAL_DIR, "defaults.json");
75
+ function readDefaults() {
76
+ try {
77
+ const saved = { ...BASE_DEFAULTS, ...JSON.parse(fs2.readFileSync(defaultsFile(), "utf8")) };
78
+ if (!isLang(saved.sourceLang)) saved.sourceLang = BASE_DEFAULTS.sourceLang;
79
+ if (!MAIN_DOCS.includes(saved.mainDoc)) saved.mainDoc = BASE_DEFAULTS.mainDoc;
80
+ return saved;
81
+ } catch {
82
+ return null;
83
+ }
84
+ }
85
+ function writeDefaults(defaults2) {
86
+ fs2.mkdirSync(GLOBAL_DIR, { recursive: true });
87
+ fs2.writeFileSync(defaultsFile(), JSON.stringify(defaults2, null, 2) + "\n");
88
+ }
89
+
60
90
  // src/core/i18n.ts
61
91
  import crypto from "crypto";
62
- import fs4 from "fs";
63
- import path4 from "path";
92
+ import fs5 from "fs";
93
+ import path5 from "path";
64
94
 
65
95
  // src/core/langdetect.ts
66
96
  var STOPWORDS = {
@@ -86,18 +116,18 @@ function detectLang(md) {
86
116
  }
87
117
 
88
118
  // src/core/rules.ts
89
- import fs2 from "fs";
90
- import path2 from "path";
119
+ import fs3 from "fs";
120
+ import path3 from "path";
91
121
  import YAML from "yaml";
92
- var rulesDir = () => path2.join(GLOBAL_DIR, "rules");
122
+ var rulesDir = () => path3.join(GLOBAL_DIR, "rules");
93
123
  var START = "<!-- mdrules:rules:start -->";
94
124
  var END = "<!-- mdrules:rules:end -->";
95
125
  var slugify = (s) => s.toLowerCase().normalize("NFD").replace(/[̀-ͯ]/g, "").replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
96
126
  function seedRules() {
97
- if (fs2.existsSync(rulesDir())) return;
98
- fs2.mkdirSync(rulesDir(), { recursive: true });
99
- const src = path2.join(TEMPLATES_DIR, "rules");
100
- for (const f of fs2.readdirSync(src)) fs2.copyFileSync(path2.join(src, f), path2.join(rulesDir(), f));
127
+ if (fs3.existsSync(rulesDir())) return;
128
+ fs3.mkdirSync(rulesDir(), { recursive: true });
129
+ const src = path3.join(TEMPLATES_DIR, "rules");
130
+ for (const f of fs3.readdirSync(src)) fs3.copyFileSync(path3.join(src, f), path3.join(rulesDir(), f));
101
131
  }
102
132
  function parseRule(id, text3) {
103
133
  const m = text3.match(/^---\n([\s\S]*?)\n---\n?([\s\S]*)$/);
@@ -106,21 +136,22 @@ function parseRule(id, text3) {
106
136
  }
107
137
  function listRules() {
108
138
  seedRules();
109
- return fs2.readdirSync(rulesDir()).filter((f) => f.endsWith(".md")).map((f) => parseRule(f.slice(0, -3), fs2.readFileSync(path2.join(rulesDir(), f), "utf8"))).sort((a, b) => a.title.localeCompare(b.title));
139
+ return fs3.readdirSync(rulesDir()).filter((f) => f.endsWith(".md")).map((f) => parseRule(f.slice(0, -3), fs3.readFileSync(path3.join(rulesDir(), f), "utf8"))).sort((a, b) => a.title.localeCompare(b.title));
110
140
  }
111
141
  function saveRule(rule) {
112
142
  seedRules();
113
143
  const id = rule.id || slugify(rule.title) || `rule-${Date.now()}`;
144
+ if (!/^[\w-]+$/.test(id)) throw new Error(`Invalid rule id: ${id}`);
114
145
  const text3 = `---
115
146
  ${YAML.stringify({ title: rule.title }).trim()}
116
147
  ---
117
148
  ${rule.body.trim()}
118
149
  `;
119
- fs2.writeFileSync(path2.join(rulesDir(), `${id}.md`), text3);
150
+ fs3.writeFileSync(path3.join(rulesDir(), `${id}.md`), text3);
120
151
  return { id, title: rule.title, body: rule.body.trim() };
121
152
  }
122
153
  function deleteRule(id) {
123
- fs2.rmSync(path2.join(rulesDir(), `${path2.basename(id)}.md`), { force: true });
154
+ fs3.rmSync(path3.join(rulesDir(), `${path3.basename(id)}.md`), { force: true });
124
155
  }
125
156
  function appliedRuleIds(docText) {
126
157
  const section2 = extractSection(docText);
@@ -143,10 +174,10 @@ function sourceLangOf(docText) {
143
174
  return m && isLang(m[1]) ? m[1] : DEFAULT_SOURCE;
144
175
  }
145
176
  function projectSourceLang(root2, mainDoc) {
146
- const doc = mainDoc ?? MAIN_DOCS.find((d) => fs2.existsSync(path2.join(root2, d)));
177
+ const doc = mainDoc ?? MAIN_DOCS.find((d) => fs3.existsSync(path3.join(root2, d)));
147
178
  if (!doc) return DEFAULT_SOURCE;
148
179
  try {
149
- return sourceLangOf(fs2.readFileSync(path2.join(root2, doc), "utf8"));
180
+ return sourceLangOf(fs3.readFileSync(path3.join(root2, doc), "utf8"));
150
181
  } catch {
151
182
  return DEFAULT_SOURCE;
152
183
  }
@@ -169,34 +200,34 @@ ${r.body}`).join("\n\n");
169
200
  function applyRules(docText, rules, sourceLang = sourceLangOf(docText)) {
170
201
  const section2 = renderSection(rules, sourceLang);
171
202
  const existing = extractSection(docText);
172
- if (existing) return docText.replace(existing, section2);
203
+ if (existing) return docText.replace(existing, () => section2);
173
204
  return docText.replace(/\s*$/, "") + "\n\n" + section2 + "\n";
174
205
  }
175
206
  function applyRulesToFile(file, ids, sourceLang) {
176
- const text3 = fs2.existsSync(file) ? fs2.readFileSync(file, "utf8") : "";
207
+ const text3 = fs3.existsSync(file) ? fs3.readFileSync(file, "utf8") : "";
177
208
  const all = listRules();
178
209
  const written = writtenRules(text3);
179
210
  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)));
211
+ fs3.writeFileSync(file, applyRules(text3, rules, sourceLang ?? sourceLangOf(text3)));
181
212
  }
182
213
 
183
214
  // src/core/scan.ts
184
- import fs3 from "fs";
185
- import path3 from "path";
215
+ import fs4 from "fs";
216
+ import path4 from "path";
186
217
  import ignore from "ignore";
187
218
  var ALWAYS_SKIP = /* @__PURE__ */ new Set(["node_modules", ".git", ".mdrules", "dist", "build", ".next", ".turbo", "coverage", ".venv"]);
188
219
  var IGNORE_FILE = ".mdrulesignore";
189
220
  function loadIgnore(root2, file) {
190
221
  const ig = ignore();
191
222
  try {
192
- ig.add(fs3.readFileSync(path3.join(root2, file), "utf8"));
223
+ ig.add(fs4.readFileSync(path4.join(root2, file), "utf8"));
193
224
  } catch {
194
225
  }
195
226
  return ig;
196
227
  }
197
228
  function readIgnoreList(root2) {
198
229
  try {
199
- return fs3.readFileSync(path3.join(root2, IGNORE_FILE), "utf8").split("\n").map((l) => l.trim()).filter((l) => l && !l.startsWith("#"));
230
+ return fs4.readFileSync(path4.join(root2, IGNORE_FILE), "utf8").split("\n").map((l) => l.trim()).filter((l) => l && !l.startsWith("#"));
200
231
  } catch {
201
232
  return [];
202
233
  }
@@ -205,7 +236,7 @@ function setIgnored(root2, rel, kind, ignored) {
205
236
  const entry = kind === "dir" ? `${rel}/` : rel;
206
237
  const lines = readIgnoreList(root2).filter((l) => l !== entry && l !== rel);
207
238
  if (ignored) lines.push(entry);
208
- fs3.writeFileSync(path3.join(root2, IGNORE_FILE), lines.length ? lines.join("\n") + "\n" : "");
239
+ fs4.writeFileSync(path4.join(root2, IGNORE_FILE), lines.length ? lines.join("\n") + "\n" : "");
209
240
  }
210
241
  function watchSkips(root2) {
211
242
  const gitIgnore = loadIgnore(root2, ".gitignore");
@@ -222,15 +253,15 @@ function scanTree(root2) {
222
253
  function walk(dirAbs, parentIgnored) {
223
254
  let entries;
224
255
  try {
225
- entries = fs3.readdirSync(dirAbs, { withFileTypes: true });
256
+ entries = fs4.readdirSync(dirAbs, { withFileTypes: true });
226
257
  } catch {
227
258
  return [];
228
259
  }
229
260
  const nodes = [];
230
261
  for (const e of entries) {
231
262
  if (ALWAYS_SKIP.has(e.name)) continue;
232
- const abs = path3.join(dirAbs, e.name);
233
- const rel = toPosix(path3.relative(root2, abs));
263
+ const abs = path4.join(dirAbs, e.name);
264
+ const rel = toPosix(path4.relative(root2, abs));
234
265
  if (e.isDirectory()) {
235
266
  if (gitIgnore.ignores(rel + "/")) continue;
236
267
  const ignored = parentIgnored || mdIgnore.ignores(rel + "/");
@@ -250,7 +281,7 @@ function scanTree(root2) {
250
281
  }
251
282
  function isEmptyDir(abs) {
252
283
  try {
253
- return fs3.readdirSync(abs).length === 0;
284
+ return fs4.readdirSync(abs).length === 0;
254
285
  } catch {
255
286
  return false;
256
287
  }
@@ -279,19 +310,19 @@ function findSubprojects(nodes) {
279
310
  }
280
311
 
281
312
  // src/core/i18n.ts
282
- var stateFile = (root2) => path4.join(stateDir(root2), "state.json");
283
- var translationPath = (root2, lang, rel) => path4.join(stateDir(root2), "i18n", lang, rel);
313
+ var stateFile = (root2) => path5.join(stateDir(root2), "state.json");
314
+ var translationPath = (root2, lang, rel) => path5.join(stateDir(root2), "i18n", lang, rel);
284
315
  var hash = (text3) => crypto.createHash("sha1").update(text3).digest("hex").slice(0, 12);
285
316
  var readText = (abs) => {
286
317
  try {
287
- return fs4.readFileSync(abs, "utf8");
318
+ return fs5.readFileSync(abs, "utf8");
288
319
  } catch {
289
320
  return null;
290
321
  }
291
322
  };
292
323
  function readState(root2) {
293
324
  try {
294
- const raw = JSON.parse(fs4.readFileSync(stateFile(root2), "utf8"));
325
+ const raw = JSON.parse(fs5.readFileSync(stateFile(root2), "utf8"));
295
326
  if (!raw.translations) return { translations: raw, keepLanguage: {} };
296
327
  return { translations: raw.translations ?? {}, keepLanguage: raw.keepLanguage ?? {} };
297
328
  } catch {
@@ -299,11 +330,11 @@ function readState(root2) {
299
330
  }
300
331
  }
301
332
  function writeState(root2, state) {
302
- fs4.mkdirSync(stateDir(root2), { recursive: true });
303
- fs4.writeFileSync(stateFile(root2), JSON.stringify(state, null, 2) + "\n");
333
+ fs5.mkdirSync(stateDir(root2), { recursive: true });
334
+ fs5.writeFileSync(stateFile(root2), JSON.stringify(state, null, 2) + "\n");
304
335
  }
305
336
  function fileStatus(root2, lang, rel) {
306
- const source = readText(path4.join(root2, rel));
337
+ const source = readText(path5.join(root2, rel));
307
338
  const translation = readText(translationPath(root2, lang, rel));
308
339
  if (translation === null || source === null) return "missing";
309
340
  const recorded = readState(root2).translations[lang]?.[rel];
@@ -320,14 +351,14 @@ function allStatuses(root2, lang) {
320
351
  path: f.path,
321
352
  lang,
322
353
  status: fileStatus(root2, lang, f.path),
323
- translation: path4.relative(root2, translationPath(root2, lang, f.path))
354
+ translation: path5.relative(root2, translationPath(root2, lang, f.path))
324
355
  }));
325
356
  }
326
357
  function foreignFiles(root2, sourceLang) {
327
358
  const { keepLanguage: keepLanguage2 } = readState(root2);
328
359
  const out = [];
329
360
  for (const f of flattenFiles(scanTree(root2))) {
330
- const text3 = readText(path4.join(root2, f.path));
361
+ const text3 = readText(path5.join(root2, f.path));
331
362
  const detected = text3 ? detectLang(text3) : null;
332
363
  if (detected && detected !== sourceLang && keepLanguage2[f.path] !== detected) out.push({ path: f.path, detected });
333
364
  }
@@ -347,17 +378,17 @@ function syncReport(root2) {
347
378
  };
348
379
  }
349
380
  function markSynced(root2, lang, rel) {
350
- const source = readText(path4.join(root2, rel));
381
+ const source = readText(path5.join(root2, rel));
351
382
  const translation = readText(translationPath(root2, lang, rel));
352
383
  if (source === null) throw new Error(`Source not found: ${rel}`);
353
- if (translation === null) throw new Error(`Translation not found: ${path4.relative(root2, translationPath(root2, lang, rel))}`);
384
+ if (translation === null) throw new Error(`Translation not found: ${path5.relative(root2, translationPath(root2, lang, rel))}`);
354
385
  const state = readState(root2);
355
386
  state.translations[lang] ??= {};
356
387
  state.translations[lang][rel] = { source: hash(source), translation: hash(translation) };
357
388
  writeState(root2, state);
358
389
  }
359
390
  function keepLanguage(root2, rel) {
360
- const text3 = readText(path4.join(root2, rel));
391
+ const text3 = readText(path5.join(root2, rel));
361
392
  const detected = text3 ? detectLang(text3) : null;
362
393
  if (!detected) throw new Error(`Could not detect the language of ${rel}`);
363
394
  const state = readState(root2);
@@ -367,17 +398,17 @@ function keepLanguage(root2, rel) {
367
398
  }
368
399
 
369
400
  // src/server/index.ts
370
- import fs10 from "fs";
401
+ import fs11 from "fs";
371
402
  import http from "http";
372
- import path10 from "path";
403
+ import path11 from "path";
373
404
  import { watch } from "chokidar";
374
405
  import open from "open";
375
406
  import pc from "picocolors";
376
407
  import { WebSocketServer } from "ws";
377
408
 
378
409
  // src/core/dashboard.ts
379
- import fs5 from "fs";
380
- import path5 from "path";
410
+ import fs6 from "fs";
411
+ import path6 from "path";
381
412
  import YAML2 from "yaml";
382
413
  function parseFrontmatter(text3) {
383
414
  const m = text3.match(/^---\r?\n([\s\S]*?)\r?\n---/);
@@ -391,7 +422,7 @@ function parseFrontmatter(text3) {
391
422
  var asText = (v) => v instanceof Date ? v.toISOString().slice(0, 10) : v == null || typeof v === "object" ? null : String(v);
392
423
  function summarize(root2) {
393
424
  return flattenFiles(scanTree(root2)).flatMap((f) => {
394
- const text3 = fs5.readFileSync(path5.join(root2, f.path), "utf8");
425
+ const text3 = fs6.readFileSync(path6.join(root2, f.path), "utf8");
395
426
  const meta = parseFrontmatter(text3);
396
427
  if (meta.type === "task" && f.name.startsWith("_")) return [];
397
428
  const id = asText(meta.id);
@@ -451,66 +482,66 @@ function gitDiff(root2, rel) {
451
482
 
452
483
  // src/core/proposals.ts
453
484
  import { execFileSync as execFileSync2 } from "child_process";
454
- import fs8 from "fs";
455
- import path8 from "path";
485
+ import fs9 from "fs";
486
+ import path9 from "path";
456
487
 
457
488
  // src/core/scaffold.ts
458
- import fs6 from "fs";
459
- import path6 from "path";
489
+ import fs7 from "fs";
490
+ import path7 from "path";
460
491
  var today = () => (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
461
492
  function fill(text3, vars) {
462
493
  return text3.replace(/\{\{(\w+)\}\}/g, (_, k) => vars[k] ?? "");
463
494
  }
464
495
  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));
496
+ for (const e of fs7.readdirSync(src, { withFileTypes: true })) {
497
+ const from = path7.join(src, e.name);
498
+ const to = path7.join(dest, fill(e.name, vars));
468
499
  if (e.isDirectory()) {
469
- fs6.mkdirSync(to, { recursive: true });
500
+ fs7.mkdirSync(to, { recursive: true });
470
501
  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)));
502
+ } else if (!fs7.existsSync(to)) {
503
+ fs7.writeFileSync(to, fill(fs7.readFileSync(from, "utf8"), vars));
504
+ created.push(toPosix(path7.relative(root2, to)));
474
505
  }
475
506
  }
476
507
  }
477
508
  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() }));
509
+ const file = path7.join(root2, name);
510
+ if (fs7.existsSync(file)) return;
511
+ const text3 = fs7.readFileSync(path7.join(TEMPLATES_DIR, "main.md"), "utf8");
512
+ fs7.writeFileSync(file, fill(text3, { name: path7.basename(root2), date: today() }));
482
513
  }
483
514
  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"));
515
+ const dir = path7.join(root2, ".claude", "skills", "mdrules");
516
+ fs7.mkdirSync(dir, { recursive: true });
517
+ fs7.copyFileSync(path7.join(TEMPLATES_DIR, "skill", "SKILL.md"), path7.join(dir, "SKILL.md"));
487
518
  }
488
519
  function ensureGitignore(root2) {
489
- const file = path6.join(root2, ".gitignore");
490
- const text3 = fs6.existsSync(file) ? fs6.readFileSync(file, "utf8") : "";
520
+ const file = path7.join(root2, ".gitignore");
521
+ const text3 = fs7.existsSync(file) ? fs7.readFileSync(file, "utf8") : "";
491
522
  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");
523
+ fs7.writeFileSync(file, text3 + (text3 && !text3.endsWith("\n") ? "\n" : "") + ".mdrules/\n");
493
524
  }
494
525
  function hideInVsCode(root2) {
495
- const file = path6.join(root2, ".vscode", "settings.json");
526
+ const file = path7.join(root2, ".vscode", "settings.json");
496
527
  let settings = {};
497
- if (fs6.existsSync(file)) {
528
+ if (fs7.existsSync(file)) {
498
529
  try {
499
- settings = JSON.parse(fs6.readFileSync(file, "utf8"));
530
+ settings = JSON.parse(fs7.readFileSync(file, "utf8"));
500
531
  } catch {
501
532
  return false;
502
533
  }
503
534
  }
504
535
  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");
536
+ fs7.mkdirSync(path7.dirname(file), { recursive: true });
537
+ fs7.writeFileSync(file, JSON.stringify(settings, null, 2) + "\n");
507
538
  return true;
508
539
  }
509
540
 
510
541
  // src/core/sdd.ts
511
542
  import { spawnSync } from "child_process";
512
- import fs7 from "fs";
513
- import path7 from "path";
543
+ import fs8 from "fs";
544
+ import path8 from "path";
514
545
  var SDD_AGENTS = {
515
546
  claude: { label: "Claude Code", openspec: "claude", speckit: "claude" },
516
547
  codex: { label: "Codex", openspec: "codex", speckit: "codex" },
@@ -521,8 +552,8 @@ var SDD_AGENTS = {
521
552
  var OPENSPEC_PKG = "@fission-ai/openspec@latest";
522
553
  var SPECKIT_SRC = "git+https://github.com/github/spec-kit.git";
523
554
  function detectWorkflow(root2) {
524
- if (fs7.existsSync(path7.join(root2, "openspec"))) return "openspec";
525
- if (fs7.existsSync(path7.join(root2, ".specify"))) return "speckit";
555
+ if (fs8.existsSync(path8.join(root2, "openspec"))) return "openspec";
556
+ if (fs8.existsSync(path8.join(root2, ".specify"))) return "speckit";
526
557
  return null;
527
558
  }
528
559
  var has = (cmd) => spawnSync(cmd, ["--version"], { stdio: "ignore", shell: process.platform === "win32" }).status === 0;
@@ -546,11 +577,11 @@ function installWorkflow(root2, workflow, agents, official) {
546
577
  else result.error = (run.error?.message ?? `${run.stderr ?? ""}
547
578
  ${run.stdout ?? ""}`).trim().split("\n").slice(-6).join("\n");
548
579
  }
549
- copyTree(path7.join(TEMPLATES_DIR, "sdd", workflow), root2, { date: today2() }, result.created, root2);
580
+ copyTree(path8.join(TEMPLATES_DIR, "sdd", workflow), root2, { date: today2() }, result.created, root2);
550
581
  const dirs2 = /* @__PURE__ */ new Set();
551
582
  for (const f of flattenFiles(scanTree(root2), true)) {
552
583
  if (before.has(f.path) || !f.path.startsWith(".") || f.path.startsWith(".specify/memory/")) continue;
553
- dirs2.add(path7.posix.dirname(f.path));
584
+ dirs2.add(path8.posix.dirname(f.path));
554
585
  }
555
586
  for (const dir of dirs2) setIgnored(root2, dir, "dir", true);
556
587
  result.ignored = [...dirs2].sort();
@@ -584,33 +615,33 @@ function applyWorkflowToFile(file, workflow, text3) {
584
615
  function writeManagedBlock(file, body, top = false, name = "workflow") {
585
616
  const START2 = `<!-- mdrules:${name}:start -->`;
586
617
  const END2 = `<!-- mdrules:${name}:end -->`;
587
- const text3 = fs7.readFileSync(file, "utf8");
618
+ const text3 = fs8.readFileSync(file, "utf8");
588
619
  const s = text3.indexOf(START2);
589
620
  const e = text3.indexOf(END2);
590
621
  const existing = s >= 0 && e > s ? text3.slice(s, e + END2.length) : null;
591
622
  if (body === null) {
592
- if (existing) fs7.writeFileSync(file, text3.replace(new RegExp(`\\n*${escapeRe(existing)}\\n?`), "\n"));
623
+ if (existing) fs8.writeFileSync(file, text3.replace(new RegExp(`\\n*${escapeRe(existing)}\\n?`), "\n"));
593
624
  return;
594
625
  }
595
626
  const section2 = `${START2}
596
627
  ${body}
597
628
  ${END2}`;
598
- if (existing) return fs7.writeFileSync(file, text3.replace(existing, section2));
629
+ if (existing) return fs8.writeFileSync(file, text3.replace(existing, () => section2));
599
630
  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");
631
+ if (title) return fs8.writeFileSync(file, text3.slice(0, title.index + title[0].length) + "\n" + section2 + "\n" + text3.slice(title.index + title[0].length));
632
+ fs8.writeFileSync(file, text3.replace(/\s*$/, "") + "\n\n" + section2 + "\n");
602
633
  }
603
634
  var escapeRe = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
604
635
  var dirs = (abs) => {
605
636
  try {
606
- return fs7.readdirSync(abs, { withFileTypes: true }).filter((e) => e.isDirectory() && !e.name.startsWith(".")).map((e) => e.name).sort();
637
+ return fs8.readdirSync(abs, { withFileTypes: true }).filter((e) => e.isDirectory() && !e.name.startsWith(".")).map((e) => e.name).sort();
607
638
  } catch {
608
639
  return [];
609
640
  }
610
641
  };
611
642
  var read = (abs) => {
612
643
  try {
613
- return fs7.readFileSync(abs, "utf8");
644
+ return fs8.readFileSync(abs, "utf8");
614
645
  } catch {
615
646
  return null;
616
647
  }
@@ -619,52 +650,52 @@ var progress = (text3) => ({ done: text3?.match(/^\s*[-*+] \[[xX]\]/gm)?.length
619
650
  var heading = (text3) => text3?.match(/^#\s+(.+)$/m)?.[1].trim() ?? null;
620
651
  var countSpecs = (abs) => {
621
652
  try {
622
- return fs7.readdirSync(abs, { recursive: true }).filter((f) => String(f).endsWith("spec.md")).length;
653
+ return fs8.readdirSync(abs, { recursive: true }).filter((f) => String(f).endsWith("spec.md")).length;
623
654
  } catch {
624
655
  return 0;
625
656
  }
626
657
  };
627
658
  function summarizeSdd(root2) {
628
659
  const out = {};
629
- const os3 = path7.join(root2, "openspec");
630
- if (fs7.existsSync(os3)) {
631
- const rel = (...p3) => toPosix(path7.join("openspec", ...p3));
660
+ const os3 = path8.join(root2, "openspec");
661
+ if (fs8.existsSync(os3)) {
662
+ const rel = (...p3) => toPosix(path8.join("openspec", ...p3));
632
663
  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"));
664
+ changes: dirs(path8.join(os3, "changes")).filter((id) => id !== "archive").map((id) => {
665
+ const dir = path8.join(os3, "changes", id);
666
+ const proposal = read(path8.join(dir, "proposal.md"));
667
+ const tasks = read(path8.join(dir, "tasks.md"));
637
668
  return {
638
669
  id,
639
670
  // "Proposal: Add login" → "Add login"
640
671
  title: heading(proposal)?.replace(/^(change\s+)?proposal:\s*/i, "") ?? id,
641
672
  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")) },
673
+ artifacts: { proposal: proposal !== null, design: fs8.existsSync(path8.join(dir, "design.md")), tasks: tasks !== null, specs: countSpecs(path8.join(dir, "specs")) },
643
674
  tasks: progress(tasks)
644
675
  };
645
676
  }),
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)));
677
+ specs: dirs(path8.join(os3, "specs")).filter((id) => fs8.existsSync(path8.join(os3, "specs", id, "spec.md"))).map((id) => ({ id, path: rel("specs", id, "spec.md") })),
678
+ archived: dirs(path8.join(os3, "changes", "archive")).reverse().slice(0, 12).map((id) => {
679
+ const file = ["proposal.md", "tasks.md"].find((f) => fs8.existsSync(path8.join(os3, "changes", "archive", id, f)));
649
680
  return { id, path: file ? rel("changes", "archive", id, file) : "" };
650
681
  })
651
682
  };
652
683
  }
653
- if (fs7.existsSync(path7.join(root2, ".specify"))) {
654
- const constitution = read(path7.join(root2, ".specify", "memory", "constitution.md"));
684
+ if (fs8.existsSync(path8.join(root2, ".specify"))) {
685
+ const constitution = read(path8.join(root2, ".specify", "memory", "constitution.md"));
655
686
  out.speckit = {
656
687
  // The template is full of [PLACEHOLDERS] until /speckit-constitution fills it.
657
688
  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"));
689
+ features: dirs(path8.join(root2, "specs")).map((id) => {
690
+ const dir = path8.join(root2, "specs", id);
691
+ const spec = read(path8.join(dir, "spec.md"));
692
+ const tasks = read(path8.join(dir, "tasks.md"));
662
693
  return {
663
694
  id,
664
695
  // "Feature Specification: Login" → "Login"
665
696
  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 },
697
+ path: toPosix(path8.join("specs", id, spec !== null ? "spec.md" : "plan.md")),
698
+ artifacts: { spec: spec !== null, plan: fs8.existsSync(path8.join(dir, "plan.md")), tasks: tasks !== null },
668
699
  tasks: progress(tasks)
669
700
  };
670
701
  })
@@ -677,9 +708,9 @@ function sddPrefixes(summary) {
677
708
  }
678
709
 
679
710
  // src/core/proposals.ts
680
- var SKILL = path8.join(".claude", "skills", "mdrules-proposals");
711
+ var SKILL = path9.join(".claude", "skills", "mdrules-proposals");
681
712
  function proposalsState(root2) {
682
- return { active: fs8.existsSync(path8.join(root2, SKILL, "SKILL.md")), layout: readConfig(root2)?.proposals ?? "general" };
713
+ return { active: fs9.existsSync(path9.join(root2, SKILL, "SKILL.md")), layout: readConfig(root2)?.proposals ?? "general" };
683
714
  }
684
715
  var WHERE = {
685
716
  general: "`docs/proposals/NNN-short-name.md` (NNN: the next number in that folder)",
@@ -698,19 +729,19 @@ function section(layout) {
698
729
  ].join("\n");
699
730
  }
700
731
  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)] : [];
732
+ const doc = mainDoc ?? MAIN_DOCS.find((d) => fs9.existsSync(path9.join(root2, d)));
733
+ return doc ? [path9.join(root2, doc)] : [];
703
734
  }
704
735
  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 });
736
+ const dir = path9.join(root2, SKILL);
737
+ fs9.mkdirSync(dir, { recursive: true });
738
+ fs9.writeFileSync(path9.join(dir, "SKILL.md"), fill(fs9.readFileSync(path9.join(TEMPLATES_DIR, "proposals", "SKILL.md"), "utf8"), { where: WHERE[layout] }));
739
+ fs9.mkdirSync(path9.join(root2, "docs", "proposals"), { recursive: true });
709
740
  for (const file of mainDocs(root2, mainDoc)) writeManagedBlock(file, section(layout), false, "proposals");
710
741
  writeConfig(root2, { ...readConfig(root2) ?? { mainDoc, langs: [] }, proposals: layout });
711
742
  }
712
743
  function deactivateProposals(root2, mainDoc) {
713
- fs8.rmSync(path8.join(root2, SKILL), { recursive: true, force: true });
744
+ fs9.rmSync(path9.join(root2, SKILL), { recursive: true, force: true });
714
745
  for (const file of mainDocs(root2, mainDoc)) writeManagedBlock(file, null, false, "proposals");
715
746
  }
716
747
  function authorName(root2) {
@@ -722,9 +753,9 @@ function authorName(root2) {
722
753
  }
723
754
 
724
755
  // src/core/skills.ts
725
- import fs9 from "fs";
756
+ import fs10 from "fs";
726
757
  import os2 from "os";
727
- import path9 from "path";
758
+ import path10 from "path";
728
759
  import YAML3 from "yaml";
729
760
  var SOURCE_DIRS = {
730
761
  claude: [".claude", "skills"],
@@ -736,15 +767,15 @@ var SOURCE_DIRS = {
736
767
  cursor: [".cursor", "skills"]
737
768
  };
738
769
  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;
770
+ var sourceDir = (source) => path10.join(os2.homedir(), ...SOURCE_DIRS[source]);
771
+ var display = (p3) => p3.startsWith(os2.homedir() + path10.sep) ? "~" + p3.slice(os2.homedir().length) : p3;
741
772
  function skillSources() {
742
- return Object.keys(SOURCE_DIRS).map((id) => ({ id, path: display(sourceDir(id)), exists: fs9.existsSync(sourceDir(id)) }));
773
+ return Object.keys(SOURCE_DIRS).map((id) => ({ id, path: display(sourceDir(id)), exists: fs10.existsSync(sourceDir(id)) }));
743
774
  }
744
- var tagsFile = () => path9.join(GLOBAL_DIR, "skills.json");
775
+ var tagsFile = () => path10.join(GLOBAL_DIR, "skills.json");
745
776
  function readTags() {
746
777
  try {
747
- return JSON.parse(fs9.readFileSync(tagsFile(), "utf8")).tags ?? {};
778
+ return JSON.parse(fs10.readFileSync(tagsFile(), "utf8")).tags ?? {};
748
779
  } catch {
749
780
  return {};
750
781
  }
@@ -754,27 +785,27 @@ function setSkillTags(name, tags) {
754
785
  const clean = [...new Set(tags.map((t) => t.trim().toLowerCase()).filter(Boolean))];
755
786
  if (clean.length) all[name] = clean;
756
787
  else delete all[name];
757
- fs9.mkdirSync(GLOBAL_DIR, { recursive: true });
758
- fs9.writeFileSync(tagsFile(), JSON.stringify({ tags: all }, null, 2) + "\n");
788
+ fs10.mkdirSync(GLOBAL_DIR, { recursive: true });
789
+ fs10.writeFileSync(tagsFile(), JSON.stringify({ tags: all }, null, 2) + "\n");
759
790
  return clean;
760
791
  }
761
792
  function findSkills(dir, source, managed, depth = 0) {
762
793
  let entries;
763
794
  try {
764
- entries = fs9.readdirSync(dir, { withFileTypes: true });
795
+ entries = fs10.readdirSync(dir, { withFileTypes: true });
765
796
  } catch {
766
797
  return [];
767
798
  }
768
799
  return entries.flatMap((e) => {
769
- const abs = path9.join(dir, e.name);
800
+ const abs = path10.join(dir, e.name);
770
801
  let real;
771
802
  try {
772
- if (!fs9.statSync(abs).isDirectory()) return [];
773
- real = fs9.realpathSync(abs);
803
+ if (!fs10.statSync(abs).isDirectory()) return [];
804
+ real = fs10.realpathSync(abs);
774
805
  } catch {
775
806
  return [];
776
807
  }
777
- if (fs9.existsSync(path9.join(abs, "SKILL.md"))) return [{ dir: abs, real, source, managed }];
808
+ if (fs10.existsSync(path10.join(abs, "SKILL.md"))) return [{ dir: abs, real, source, managed }];
778
809
  return depth < 2 ? findSkills(abs, source, managed || e.name === "synced" || e.name.startsWith("."), depth + 1) : [];
779
810
  });
780
811
  }
@@ -790,7 +821,7 @@ function frontmatter(text3) {
790
821
  }
791
822
  function listFiles(dir) {
792
823
  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);
824
+ return fs10.readdirSync(dir, { recursive: true, withFileTypes: true }).map((e) => ({ e, rel: path10.relative(dir, path10.join(e.parentPath ?? e.path ?? dir, e.name)).split(path10.sep).join("/") })).filter(({ e, rel }) => e.isFile() && rel !== "SKILL.md").map(({ rel }) => rel).sort().slice(0, 100);
794
825
  } catch {
795
826
  return [];
796
827
  }
@@ -803,9 +834,9 @@ function listSkills(root2) {
803
834
  const userTags = readTags();
804
835
  const skills = [];
805
836
  for (const [real, found] of byReal) {
806
- const text3 = fs9.readFileSync(path9.join(real, "SKILL.md"), "utf8");
837
+ const text3 = fs10.readFileSync(path10.join(real, "SKILL.md"), "utf8");
807
838
  const fm = frontmatter(text3);
808
- const folder = path9.basename(found[0].dir);
839
+ const folder = path10.basename(found[0].dir);
809
840
  const name = typeof fm?.name === "string" ? fm.name : folder;
810
841
  const description = typeof fm?.description === "string" ? fm.description.trim() : "";
811
842
  const issues = [];
@@ -828,7 +859,7 @@ function listSkills(root2) {
828
859
  files: listFiles(real),
829
860
  tags: [.../* @__PURE__ */ new Set([...userTags[name] ?? [], ...metaTags.map((t) => t.trim().toLowerCase()).filter(Boolean)])],
830
861
  issues,
831
- installed: Object.keys(PROJECT_TARGETS).filter((t) => fs9.existsSync(path9.join(root2, PROJECT_TARGETS[t], folder, "SKILL.md")))
862
+ installed: Object.keys(PROJECT_TARGETS).filter((t) => fs10.existsSync(path10.join(root2, PROJECT_TARGETS[t], folder, "SKILL.md")))
832
863
  });
833
864
  }
834
865
  return skills.sort((a, b) => a.name.localeCompare(b.name));
@@ -839,20 +870,20 @@ function getSkill(root2, key) {
839
870
  return skill;
840
871
  }
841
872
  function projectTargets(root2) {
842
- const has2 = (p3) => fs9.existsSync(path9.join(root2, p3));
873
+ const has2 = (p3) => fs10.existsSync(path10.join(root2, p3));
843
874
  const out = [];
844
875
  if (has2(".claude") || has2("CLAUDE.md")) out.push("claude");
845
876
  if (has2(".agents") || has2("AGENTS.md") || has2(".codex") || has2(".gemini") || has2(".github/skills")) out.push("agents");
846
877
  return out.length ? out : ["claude"];
847
878
  }
848
879
  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("/")) };
880
+ const dests = targets.map((t) => path10.join(root2, PROJECT_TARGETS[t], skill.folder));
881
+ const existing = dests.filter((d) => fs10.existsSync(d));
882
+ if (existing.length && !overwrite) return { conflict: existing.map((d) => path10.relative(root2, d).split(path10.sep).join("/")) };
852
883
  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 });
884
+ fs10.rmSync(dest, { recursive: true, force: true });
885
+ fs10.mkdirSync(path10.dirname(dest), { recursive: true });
886
+ fs10.cpSync(skill.key, dest, { recursive: true, dereference: true });
856
887
  }
857
888
  return { ok: true };
858
889
  }
@@ -914,14 +945,14 @@ async function startServer({ root: root2, port, open: openBrowser }) {
914
945
  if (typeof rel !== "string" || !rel.trim()) throw new HttpError(400, "Expected a path");
915
946
  const abs = safeResolve(root2, rel);
916
947
  if (abs === root2) throw new HttpError(400, "Refusing to delete the project root");
917
- if (!fs10.existsSync(abs)) throw new HttpError(404, "Not found");
948
+ if (!fs11.existsSync(abs)) throw new HttpError(404, "Not found");
918
949
  return abs;
919
950
  };
920
951
  const routes = {
921
952
  "GET /api/info": () => {
922
953
  const c = config();
923
954
  return {
924
- name: path10.basename(root2),
955
+ name: path11.basename(root2),
925
956
  mainDoc: c.mainDoc ?? null,
926
957
  sourceLang: sourceLang(),
927
958
  langs: c.langs.filter((l) => l !== sourceLang()),
@@ -936,7 +967,7 @@ async function startServer({ root: root2, port, open: openBrowser }) {
936
967
  if (Array.isArray(body.langs)) c.langs = body.langs.filter(isLangCode);
937
968
  if (isLang(body.sourceLang) && body.sourceLang !== sourceLang()) {
938
969
  if (!c.mainDoc) throw new HttpError(400, "No main document configured; run `mdrules init`");
939
- applyRulesToFile(path10.join(root2, c.mainDoc), void 0, body.sourceLang);
970
+ applyRulesToFile(path11.join(root2, c.mainDoc), void 0, body.sourceLang);
940
971
  }
941
972
  writeConfig(root2, c);
942
973
  return { ...c, langs: c.langs.filter((l) => l !== sourceLang()) };
@@ -958,10 +989,10 @@ async function startServer({ root: root2, port, open: openBrowser }) {
958
989
  "GET /api/file": (url2) => {
959
990
  const rel = mdPath(url2.searchParams.get("path"));
960
991
  const l = viewLang(url2.searchParams.get("lang"));
961
- const source = fs10.readFileSync(safeResolve(root2, rel), "utf8");
992
+ const source = fs11.readFileSync(safeResolve(root2, rel), "utf8");
962
993
  if (l) {
963
994
  const tPath = translationPath(root2, l, rel);
964
- if (fs10.existsSync(tPath)) return { path: rel, content: fs10.readFileSync(tPath, "utf8"), target: "translation", status: fileStatus(root2, l, rel) };
995
+ if (fs11.existsSync(tPath)) return { path: rel, content: fs11.readFileSync(tPath, "utf8"), target: "translation", status: fileStatus(root2, l, rel) };
965
996
  return { path: rel, content: source, target: "source", status: "missing" };
966
997
  }
967
998
  return { path: rel, content: source, target: "source", status: null };
@@ -970,17 +1001,17 @@ async function startServer({ root: root2, port, open: openBrowser }) {
970
1001
  const rel = mdPath(body.path);
971
1002
  const l = viewLang(body.lang);
972
1003
  const file = body.target === "translation" && l ? translationPath(root2, l, rel) : safeResolve(root2, rel);
973
- fs10.mkdirSync(path10.dirname(file), { recursive: true });
974
- fs10.writeFileSync(file, String(body.content ?? ""));
1004
+ fs11.mkdirSync(path11.dirname(file), { recursive: true });
1005
+ fs11.writeFileSync(file, String(body.content ?? ""));
975
1006
  return { ok: true, status: l ? fileStatus(root2, l, rel) : null };
976
1007
  },
977
1008
  "POST /api/file": (_, body) => {
978
1009
  const rel = mdPath(body.path);
979
1010
  const file = safeResolve(root2, rel);
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)}
1011
+ if (fs11.existsSync(file)) throw new HttpError(409, "File already exists");
1012
+ fs11.mkdirSync(path11.dirname(file), { recursive: true });
1013
+ const title = path11.basename(rel, ".md").replace(/^\d+[-_]/, "").replace(/[-_]/g, " ");
1014
+ fs11.writeFileSync(file, `# ${title.charAt(0).toUpperCase() + title.slice(1)}
984
1015
 
985
1016
  `);
986
1017
  return { ok: true, path: rel };
@@ -990,20 +1021,20 @@ async function startServer({ root: root2, port, open: openBrowser }) {
990
1021
  if (typeof body.path !== "string" || !body.path.trim()) throw new HttpError(400, "Expected a path");
991
1022
  const dir = safeResolve(root2, body.path);
992
1023
  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)) };
1024
+ if (fs11.existsSync(dir)) throw new HttpError(409, "A file or folder with that name already exists");
1025
+ fs11.mkdirSync(dir, { recursive: true });
1026
+ return { ok: true, path: toPosix(path11.relative(root2, dir)) };
996
1027
  },
997
1028
  "GET /api/delete-preview": (url2) => {
998
1029
  const abs = deletable(url2.searchParams.get("path"));
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());
1030
+ if (!fs11.statSync(abs).isDirectory()) return { kind: "file", files: 1, md: 1 };
1031
+ const files = fs11.readdirSync(abs, { recursive: true, withFileTypes: true }).filter((e) => e.isFile());
1001
1032
  return { kind: "dir", files: files.length, md: files.filter((e) => e.name.toLowerCase().endsWith(".md")).length };
1002
1033
  },
1003
1034
  "DELETE /api/path": (url2) => {
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 });
1035
+ const rel = toPosix(path11.relative(root2, deletable(url2.searchParams.get("path"))));
1036
+ fs11.rmSync(safeResolve(root2, rel), { recursive: true });
1037
+ for (const l of config().langs) fs11.rmSync(translationPath(root2, l, rel), { recursive: true, force: true });
1007
1038
  return { ok: true };
1008
1039
  },
1009
1040
  "POST /api/ignore": (_, body) => {
@@ -1019,7 +1050,7 @@ async function startServer({ root: root2, port, open: openBrowser }) {
1019
1050
  },
1020
1051
  "GET /api/rules": () => {
1021
1052
  const mainDoc = readConfig(root2)?.mainDoc;
1022
- const text3 = mainDoc && fs10.existsSync(path10.join(root2, mainDoc)) ? fs10.readFileSync(path10.join(root2, mainDoc), "utf8") : "";
1053
+ const text3 = mainDoc && fs11.existsSync(path11.join(root2, mainDoc)) ? fs11.readFileSync(path11.join(root2, mainDoc), "utf8") : "";
1023
1054
  return { rules: listRules(), applied: appliedRuleIds(text3), mainDoc: mainDoc ?? null };
1024
1055
  },
1025
1056
  "PUT /api/rules": (_, body) => saveRule(body),
@@ -1035,12 +1066,12 @@ async function startServer({ root: root2, port, open: openBrowser }) {
1035
1066
  },
1036
1067
  "GET /api/skills/file": (url2) => {
1037
1068
  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 };
1069
+ return { path: skill.key, content: fs11.readFileSync(path11.join(skill.key, "SKILL.md"), "utf8"), target: "source", status: null };
1039
1070
  },
1040
1071
  "PUT /api/skills/file": (_, body) => {
1041
1072
  const skill = getSkill(root2, body.key);
1042
1073
  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 ?? ""));
1074
+ fs11.writeFileSync(path11.join(skill.key, "SKILL.md"), String(body.content ?? ""));
1044
1075
  return { ok: true, status: null };
1045
1076
  },
1046
1077
  "POST /api/skills/install": (_, body) => {
@@ -1053,7 +1084,7 @@ async function startServer({ root: root2, port, open: openBrowser }) {
1053
1084
  "POST /api/rules/apply": (_, body) => {
1054
1085
  const mainDoc = readConfig(root2)?.mainDoc;
1055
1086
  if (!mainDoc) throw new HttpError(400, "No main document configured; run `mdrules init`");
1056
- applyRulesToFile(path10.join(root2, mainDoc), body.ids ?? []);
1087
+ applyRulesToFile(path11.join(root2, mainDoc), body.ids ?? []);
1057
1088
  return { ok: true };
1058
1089
  }
1059
1090
  };
@@ -1085,13 +1116,13 @@ async function startServer({ root: root2, port, open: openBrowser }) {
1085
1116
  res.writeHead(404).end();
1086
1117
  return;
1087
1118
  }
1088
- const type = IMAGE_MIME[path10.extname(file).toLowerCase()];
1089
- if (!type || !fs10.existsSync(file) || !fs10.statSync(file).isFile()) {
1119
+ const type = IMAGE_MIME[path11.extname(file).toLowerCase()];
1120
+ if (!type || !fs11.existsSync(file) || !fs11.statSync(file).isFile()) {
1090
1121
  res.writeHead(404).end();
1091
1122
  return;
1092
1123
  }
1093
- res.writeHead(200, { "content-type": type, "cache-control": "no-cache" });
1094
- fs10.createReadStream(file).pipe(res);
1124
+ res.writeHead(200, { "content-type": type, "cache-control": "no-cache", "content-security-policy": "sandbox", "x-content-type-options": "nosniff" });
1125
+ fs11.createReadStream(file).pipe(res);
1095
1126
  }
1096
1127
  const actualPort = await listen(server, port);
1097
1128
  const wss = new WebSocketServer({ server, path: "/ws", verifyClient: ({ req }) => isLocalRequest(req) });
@@ -1105,16 +1136,16 @@ async function startServer({ root: root2, port, open: openBrowser }) {
1105
1136
  // Unreadable folders (e.g. caches owned by another user) are skipped instead of failing.
1106
1137
  ignorePermissionErrors: true,
1107
1138
  ignored: (p3, stats) => {
1108
- const rel = toPosix(path10.relative(root2, p3));
1139
+ const rel = toPosix(path11.relative(root2, p3));
1109
1140
  if (rel === ".mdrules") return false;
1110
1141
  if (rel.startsWith(".mdrules/")) return !rel.startsWith(".mdrules/i18n") && rel !== ".mdrules/state.json" && rel !== ".mdrules/config.json";
1111
1142
  return skips(rel, stats ? stats.isDirectory() : void 0);
1112
1143
  }
1113
1144
  }).on("error", (err) => console.warn(pc.yellow(` file watcher: ${err.message}`))).on("all", (event, file) => {
1114
- const abs = path10.resolve(file);
1145
+ const abs = path11.resolve(file);
1115
1146
  if (!abs.endsWith(".md") && !abs.endsWith(".json") && !abs.endsWith(".mdrulesignore") && event !== "addDir" && event !== "unlinkDir") return;
1116
- let rel = toPosix(path10.relative(root2, abs));
1117
- const i18n = toPosix(path10.relative(root2, path10.join(stateDir(root2), "i18n")));
1147
+ let rel = toPosix(path11.relative(root2, abs));
1148
+ const i18n = toPosix(path11.relative(root2, path11.join(stateDir(root2), "i18n")));
1118
1149
  if (rel.startsWith(i18n + "/")) rel = rel.split("/").slice(3).join("/");
1119
1150
  broadcast({ type: "fs", event, path: rel });
1120
1151
  });
@@ -1122,7 +1153,7 @@ async function startServer({ root: root2, port, open: openBrowser }) {
1122
1153
  console.log(`
1123
1154
  ${pc.bold("mdrules")} ${pc.dim("\u2192")} ${pc.cyan(url)} ${pc.dim(root2)}
1124
1155
  `);
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"));
1156
+ if (!fs11.existsSync(WEB_DIR)) console.log(pc.yellow(" Web UI not built: run `pnpm build`, or `pnpm dev` and open http://localhost:4320\n"));
1126
1157
  else if (openBrowser) open(url).catch(() => {
1127
1158
  });
1128
1159
  }
@@ -1136,41 +1167,41 @@ function listen(server, port) {
1136
1167
  });
1137
1168
  }
1138
1169
  function serveStatic(pathname, res) {
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)) {
1170
+ let file = path11.join(WEB_DIR, path11.normalize(pathname).replace(/^(\.\.[/\\])+/, ""));
1171
+ if (!file.startsWith(WEB_DIR) || !fs11.existsSync(file) || fs11.statSync(file).isDirectory()) file = path11.join(WEB_DIR, "index.html");
1172
+ if (!fs11.existsSync(file)) {
1142
1173
  res.writeHead(404).end("Web UI not built. Run `pnpm build`.");
1143
1174
  return;
1144
1175
  }
1145
- res.writeHead(200, { "content-type": MIME[path10.extname(file)] ?? "application/octet-stream" });
1146
- fs10.createReadStream(file).pipe(res);
1176
+ res.writeHead(200, { "content-type": MIME[path11.extname(file)] ?? "application/octet-stream" });
1177
+ fs11.createReadStream(file).pipe(res);
1147
1178
  }
1148
1179
 
1149
1180
  // src/cli/init.ts
1150
- import fs12 from "fs";
1151
- import path12 from "path";
1181
+ import fs13 from "fs";
1182
+ import path13 from "path";
1152
1183
  import * as p from "@clack/prompts";
1153
1184
  import pc2 from "picocolors";
1154
1185
 
1155
1186
  // src/core/method.ts
1156
- import fs11 from "fs";
1157
- import path11 from "path";
1187
+ import fs12 from "fs";
1188
+ import path12 from "path";
1158
1189
  import YAML4 from "yaml";
1159
1190
  var SKIP = /* @__PURE__ */ new Set(["docs", "node_modules", "dist", "build", "coverage", "tmp"]);
1160
1191
  function detectAreas(root2) {
1161
1192
  const globs = [];
1162
1193
  try {
1163
- const ws = YAML4.parse(fs11.readFileSync(path11.join(root2, "pnpm-workspace.yaml"), "utf8"), { logLevel: "error" });
1194
+ const ws = YAML4.parse(fs12.readFileSync(path12.join(root2, "pnpm-workspace.yaml"), "utf8"), { logLevel: "error" });
1164
1195
  if (Array.isArray(ws?.packages)) globs.push(...ws.packages);
1165
1196
  } catch {
1166
1197
  }
1167
1198
  try {
1168
- const ws = JSON.parse(fs11.readFileSync(path11.join(root2, "package.json"), "utf8")).workspaces;
1199
+ const ws = JSON.parse(fs12.readFileSync(path12.join(root2, "package.json"), "utf8")).workspaces;
1169
1200
  globs.push(...Array.isArray(ws) ? ws : Array.isArray(ws?.packages) ? ws.packages : []);
1170
1201
  } catch {
1171
1202
  }
1172
1203
  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();
1204
+ return [...new Set(tops)].filter((d) => d && !d.includes("*") && !d.startsWith(".") && !d.startsWith("!") && !SKIP.has(d) && fs12.statSync(path12.join(root2, d), { throwIfNoEntry: false })?.isDirectory()).sort();
1174
1205
  }
1175
1206
  var KNOWN_PREFIXES = {
1176
1207
  apps: "APP",
@@ -1201,17 +1232,17 @@ function suggestPrefix(dir, used) {
1201
1232
  var rootPrefix = (areas) => areas.length ? "CORE" : "T";
1202
1233
  function scaffoldMethod(root2, mainDoc, areas) {
1203
1234
  const created = [];
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);
1235
+ const name = path12.basename(root2);
1236
+ copyTree(path12.join(TEMPLATES_DIR, "method", "root"), root2, { date: today(), name, prefix: rootPrefix(areas), area: areas.length ? "core" : name }, created, root2);
1206
1237
  for (const a of areas) {
1207
1238
  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));
1239
+ const dir = path12.join(root2, a.dir);
1240
+ copyTree(path12.join(TEMPLATES_DIR, "method", "area"), dir, vars, created, root2);
1241
+ let doc = MAIN_DOCS.map((d) => path12.join(dir, d)).find((f) => fs12.existsSync(f));
1211
1242
  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));
1243
+ doc = path12.join(dir, mainDoc);
1244
+ fs12.writeFileSync(doc, fill(fs12.readFileSync(path12.join(TEMPLATES_DIR, "method", "area-main.md"), "utf8"), vars));
1245
+ created.push(path12.posix.join(a.dir, mainDoc));
1215
1246
  }
1216
1247
  writeManagedBlock(doc, areaBlock(a), true);
1217
1248
  }
@@ -1266,11 +1297,11 @@ function methodSection(areas) {
1266
1297
  ].join("\n");
1267
1298
  }
1268
1299
  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"));
1300
+ const src = path12.join(TEMPLATES_DIR, "method", "skills");
1301
+ for (const name of fs12.readdirSync(src)) {
1302
+ const dest = path12.join(root2, ".claude", "skills", name);
1303
+ fs12.mkdirSync(dest, { recursive: true });
1304
+ fs12.copyFileSync(path12.join(src, name, "SKILL.md"), path12.join(dest, "SKILL.md"));
1274
1305
  }
1275
1306
  }
1276
1307
 
@@ -1282,13 +1313,19 @@ function check(value) {
1282
1313
  }
1283
1314
  return value;
1284
1315
  }
1285
- async function chooseMainDoc(root2) {
1286
- const existing = MAIN_DOCS.filter((d) => fs12.existsSync(path12.join(root2, d)));
1316
+ async function chooseMainDoc(root2, d) {
1317
+ const existing = MAIN_DOCS.filter((d2) => fs13.existsSync(path13.join(root2, d2)));
1318
+ if (d) {
1319
+ if (existing.length) return existing.includes(d.mainDoc) ? d.mainDoc : existing[0];
1320
+ createMainDoc(root2, d.mainDoc);
1321
+ p.log.success(`Created ${d.mainDoc}`);
1322
+ return d.mainDoc;
1323
+ }
1287
1324
  if (existing.length === 2) {
1288
1325
  return check(
1289
1326
  await p.select({
1290
1327
  message: "Both CLAUDE.md and AGENTS.md exist. Which one is the main document?",
1291
- options: existing.map((d) => ({ value: d, label: d }))
1328
+ options: existing.map((d2) => ({ value: d2, label: d2 }))
1292
1329
  })
1293
1330
  );
1294
1331
  }
@@ -1298,7 +1335,7 @@ async function chooseMainDoc(root2) {
1298
1335
  message: `Found ${pc2.cyan(existing[0])}. Use it as the main document?`,
1299
1336
  options: [
1300
1337
  { value: existing[0], label: `Yes, use ${existing[0]}` },
1301
- ...MAIN_DOCS.filter((d) => d !== existing[0]).map((d) => ({ value: d, label: `No, create ${d}` }))
1338
+ ...MAIN_DOCS.filter((d2) => d2 !== existing[0]).map((d2) => ({ value: d2, label: `No, create ${d2}` }))
1302
1339
  ]
1303
1340
  })
1304
1341
  );
@@ -1308,22 +1345,43 @@ async function chooseMainDoc(root2) {
1308
1345
  const choice = check(
1309
1346
  await p.select({
1310
1347
  message: "No CLAUDE.md or AGENTS.md found. Which one should I create?",
1311
- options: MAIN_DOCS.map((d) => ({ value: d, label: d, hint: d === "CLAUDE.md" ? "Claude Code" : "other agents" }))
1348
+ options: MAIN_DOCS.map((d2) => ({ value: d2, label: d2, hint: d2 === "CLAUDE.md" ? "Claude Code" : "other agents" }))
1312
1349
  })
1313
1350
  );
1314
1351
  createMainDoc(root2, choice);
1315
1352
  p.log.success(`Created ${choice}`);
1316
1353
  return choice;
1317
1354
  }
1355
+ var WORKFLOW_OPTIONS = [
1356
+ { value: "none", label: "None" },
1357
+ { value: "mdrules", label: "mdrules method", hint: "vision, architecture, phases and one file per task; per area in monorepos" },
1358
+ { value: "openspec", label: "OpenSpec", hint: "specs + proposed changes, good for existing projects" },
1359
+ { value: "speckit", label: "GitHub Spec Kit", hint: "constitution \u2192 spec \u2192 plan \u2192 tasks, per feature" }
1360
+ ];
1361
+ var PROPOSALS_QUESTION = "The AI can write proposals you answer by ticking options and commenting. How should they be organized?";
1362
+ var PROPOSAL_OPTIONS = [
1363
+ { value: "structured", label: "By topic and area", hint: "docs/proposals/auth/\u2026, services/docs/proposals/\u2026" },
1364
+ { value: "general", label: "One folder", hint: "docs/proposals/" }
1365
+ ];
1366
+ var SOURCE_LANG_QUESTION = "Language you work in (every .md file is written in it)";
1367
+ var SOURCE_LANG_MARK = /<!-- mdrules:source-lang:\w+ -->/;
1368
+ var LANG_OPTIONS = LANGS.map((l) => ({ value: l, label: LANG_NAMES[l], hint: l === "en" ? "default" : void 0 }));
1369
+ var HIDE_QUESTION = "Hide the .mdrules/ folder in VS Code?";
1370
+ var ruleOptions = (rules) => rules.map((r) => ({ value: r.id, label: r.title, hint: r.body.split("\n")[0].replace(/^- /, "") }));
1371
+ var agentOptions = () => Object.keys(SDD_AGENTS).map((a) => ({ value: a, label: SDD_AGENTS[a].label }));
1318
1372
  var WORKFLOW_LABEL = { none: "None", mdrules: "mdrules", openspec: "OpenSpec", speckit: "GitHub Spec Kit" };
1319
1373
  var WORKFLOW_GUIDE = { none: "", mdrules: "docs/README.md", openspec: "openspec/README.md", speckit: "specs/README.md" };
1320
- async function chooseAreas(root2) {
1374
+ async function chooseAreas(root2, d) {
1321
1375
  const candidates = detectAreas(root2);
1322
1376
  if (!candidates.length) return [];
1377
+ if (d) {
1378
+ const used2 = /* @__PURE__ */ new Set();
1379
+ return candidates.map((dir) => ({ dir, prefix: suggestPrefix(dir, used2) }));
1380
+ }
1323
1381
  const dirs2 = check(
1324
1382
  await p.multiselect({
1325
1383
  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}/` })),
1384
+ options: candidates.map((d2) => ({ value: d2, label: `${d2}/` })),
1327
1385
  initialValues: candidates,
1328
1386
  required: false
1329
1387
  })
@@ -1344,47 +1402,31 @@ async function chooseAreas(root2) {
1344
1402
  }
1345
1403
  return areas;
1346
1404
  }
1347
- async function chooseWorkflow(root2, mainDoc, previous) {
1405
+ async function chooseWorkflow(root2, mainDoc, previous, d) {
1348
1406
  const detected = detectWorkflow(root2);
1349
1407
  if (detected) {
1350
1408
  p.log.info(`${WORKFLOW_LABEL[detected]} found: mdrules shows it in the dashboard.`);
1351
1409
  return { workflow: detected };
1352
1410
  }
1353
1411
  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
- );
1412
+ const choice = d ? d.workflow : check(await p.select({ message: "Create a base structure for specs and planning?", options: WORKFLOW_OPTIONS, initialValue: previous ?? "none" }));
1366
1413
  if (choice === "none") return { workflow: choice };
1367
1414
  if (choice === "mdrules") {
1368
- const areas = await chooseAreas(root2);
1415
+ const areas = await chooseAreas(root2, d);
1369
1416
  const created = scaffoldMethod(root2, mainDoc, areas);
1370
1417
  installMethodSkills(root2);
1371
1418
  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";
1419
+ const layout = !big ? "general" : d ? d.proposals : check(await p.select({ message: PROPOSALS_QUESTION, options: PROPOSAL_OPTIONS }));
1381
1420
  activateProposals(root2, layout, mainDoc);
1382
1421
  if (created.length) p.log.success(`Created ${created.length} files: ${created.slice(0, 8).join(", ")}${created.length > 8 ? "\u2026" : ""}`);
1383
1422
  p.log.info(`How it works: ${pc2.cyan("docs/README.md")}. Skills: /mdrules-new-task, /mdrules-close-session.`);
1384
1423
  return { workflow: choice, section: methodSection(areas) };
1385
1424
  }
1386
1425
  let official = true;
1387
- if (choice === "speckit" && !hasUv()) {
1426
+ if (choice === "speckit" && !hasUv() && d) {
1427
+ p.log.warn("Spec Kit needs uv (Python) to install its AI commands, and uv was not found: creating the structure only.");
1428
+ official = false;
1429
+ } else if (choice === "speckit" && !hasUv()) {
1388
1430
  const next = check(
1389
1431
  await p.select({
1390
1432
  message: "Spec Kit needs uv (Python) to install its AI commands, and uv was not found.",
@@ -1398,8 +1440,7 @@ async function chooseWorkflow(root2, mainDoc, previous) {
1398
1440
  official = false;
1399
1441
  }
1400
1442
  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 }))];
1443
+ const agents = !official ? [initial] : d ? choice === "speckit" ? [d.agents[0] ?? initial] : d.agents : 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
1444
  const spin = p.spinner();
1404
1445
  spin.start(official ? `Installing ${WORKFLOW_LABEL[choice]}\u2026` : "Creating the structure\u2026");
1405
1446
  const res = installWorkflow(root2, choice, agents, official);
@@ -1412,36 +1453,24 @@ Created the structure only.`);
1412
1453
  return { workflow: choice };
1413
1454
  }
1414
1455
  }
1415
- async function runInit(root2) {
1456
+ async function runInit(root2, d = null) {
1416
1457
  p.intro(pc2.bgCyan(pc2.black(" mdrules ")));
1458
+ if (d) p.log.info(`Using your defaults (${pc2.cyan(defaultsFile())}). To choose differently for this project: ${pc2.cyan("mdrules init --custom")}`);
1417
1459
  const previous = readConfig(root2);
1418
- const mainDoc = await chooseMainDoc(root2);
1419
- const mainFile = path12.join(root2, mainDoc);
1420
- const { workflow, section: section2 } = await chooseWorkflow(root2, mainDoc, previous?.workflow);
1460
+ const mainDoc = await chooseMainDoc(root2, d);
1461
+ const mainFile = path13.join(root2, mainDoc);
1462
+ const { workflow, section: section2 } = await chooseWorkflow(root2, mainDoc, previous?.workflow, d);
1421
1463
  applyWorkflowToFile(mainFile, workflow, section2);
1422
- const mainText = fs12.readFileSync(mainFile, "utf8");
1423
- const sourceLang = check(
1424
- await p.select({
1425
- message: "Language you work in (every .md file is written in it)",
1426
- options: LANGS.map((l) => ({ value: l, label: LANG_NAMES[l], hint: l === "en" ? "default" : void 0 })),
1427
- initialValue: sourceLangOf(mainText)
1428
- })
1429
- );
1464
+ const mainText = fs13.readFileSync(mainFile, "utf8");
1465
+ const sourceLang = d ? SOURCE_LANG_MARK.test(mainText) ? sourceLangOf(mainText) : d.sourceLang : check(await p.select({ message: SOURCE_LANG_QUESTION, options: LANG_OPTIONS, initialValue: sourceLangOf(mainText) }));
1430
1466
  const langs = (previous?.langs ?? []).filter((l) => l !== sourceLang);
1431
1467
  const rules = listRules();
1432
1468
  const applied = appliedRuleIds(mainText);
1433
- const selected = check(
1434
- await p.multiselect({
1435
- message: "Which working rules apply to this project?",
1436
- options: rules.map((r) => ({ value: r.id, label: r.title, hint: r.body.split("\n")[0].replace(/^- /, "") })),
1437
- initialValues: applied,
1438
- required: false
1439
- })
1440
- );
1469
+ const selected = d ? [.../* @__PURE__ */ new Set([...applied, ...d.rules.filter((id) => rules.some((r) => r.id === id))])] : check(await p.multiselect({ message: "Which working rules apply to this project?", options: ruleOptions(rules), initialValues: applied, required: false }));
1441
1470
  applyRulesToFile(mainFile, selected, sourceLang);
1442
1471
  const subprojects = findSubprojects(scanTree(root2));
1443
1472
  if (subprojects.length && selected.length) {
1444
- const targets = check(
1473
+ const targets = d ? d.rulesInSubprojects ? subprojects : [] : check(
1445
1474
  await p.multiselect({
1446
1475
  message: "Monorepo detected. Apply the same rules to these projects too?",
1447
1476
  options: subprojects.map((s) => ({ value: s, label: s })),
@@ -1449,13 +1478,13 @@ async function runInit(root2) {
1449
1478
  })
1450
1479
  );
1451
1480
  for (const dir of targets) {
1452
- const doc = MAIN_DOCS.find((d) => fs12.existsSync(path12.join(root2, dir, d)));
1453
- applyRulesToFile(path12.join(root2, dir, doc), selected, sourceLang);
1481
+ const doc = MAIN_DOCS.find((d2) => fs13.existsSync(path13.join(root2, dir, d2)));
1482
+ applyRulesToFile(path13.join(root2, dir, doc), selected, sourceLang);
1454
1483
  }
1455
1484
  }
1456
1485
  ensureGitignore(root2);
1457
1486
  if (!previous) {
1458
- const hide = check(await p.confirm({ message: "Hide the .mdrules/ folder in VS Code?", initialValue: true }));
1487
+ const hide = d ? d.hideInVsCode : check(await p.confirm({ message: HIDE_QUESTION, initialValue: true }));
1459
1488
  if (hide && !hideInVsCode(root2)) p.log.warn('.vscode/settings.json has comments; add "**/.mdrules": true to files.exclude by hand.');
1460
1489
  }
1461
1490
  installSkill(root2);
@@ -1464,6 +1493,54 @@ async function runInit(root2) {
1464
1493
  p.outro(`Ready. Skill installed in ${pc2.cyan(".claude/skills/mdrules")}. To review the docs in another language: ${pc2.cyan("mdrules translate")}`);
1465
1494
  return config;
1466
1495
  }
1496
+ async function runSetup(quick = false) {
1497
+ const previous = readDefaults() ?? BASE_DEFAULTS;
1498
+ if (quick) {
1499
+ const d2 = { ...previous, mode: "ask" };
1500
+ writeDefaults(d2);
1501
+ return d2;
1502
+ }
1503
+ p.intro(pc2.bgCyan(pc2.black(" mdrules setup ")));
1504
+ const mode = check(
1505
+ await p.select({
1506
+ message: "How should mdrules set up your projects?",
1507
+ options: [
1508
+ { value: "defaults", label: "With my answers", hint: "asked now, once; every project uses them" },
1509
+ { value: "ask", label: "Ask in each project" }
1510
+ ],
1511
+ initialValue: previous.mode === "ask" && !readDefaults() ? "defaults" : previous.mode
1512
+ })
1513
+ );
1514
+ if (mode === "ask") {
1515
+ const d2 = { ...previous, mode };
1516
+ writeDefaults(d2);
1517
+ p.outro(`Saved. Each project will ask its own questions. Change it with ${pc2.cyan("mdrules setup")}`);
1518
+ return d2;
1519
+ }
1520
+ const mainDoc = check(
1521
+ await p.select({
1522
+ message: "Main document to create when a project has none",
1523
+ options: MAIN_DOCS.map((d2) => ({ value: d2, label: d2, hint: d2 === "CLAUDE.md" ? "Claude Code" : "other agents" })),
1524
+ initialValue: previous.mainDoc
1525
+ })
1526
+ );
1527
+ const workflow = check(await p.select({ message: "Base structure for specs and planning", options: WORKFLOW_OPTIONS, initialValue: previous.workflow }));
1528
+ let agents = previous.agents;
1529
+ let proposals = previous.proposals;
1530
+ if (workflow === "openspec")
1531
+ agents = check(await p.multiselect({ message: "Install the OpenSpec commands for which agents?", options: agentOptions(), initialValues: previous.agents, required: false }));
1532
+ if (workflow === "speckit") agents = [check(await p.select({ message: "Install the Spec Kit commands for which agent?", options: agentOptions(), initialValue: previous.agents[0] }))];
1533
+ if (workflow === "mdrules") proposals = check(await p.select({ message: `${PROPOSALS_QUESTION} (big projects and monorepos)`, options: PROPOSAL_OPTIONS, initialValue: previous.proposals }));
1534
+ const sourceLang = check(await p.select({ message: `${SOURCE_LANG_QUESTION}; projects that already declare one keep it`, options: LANG_OPTIONS, initialValue: previous.sourceLang }));
1535
+ const rules = listRules();
1536
+ const selected = check(await p.multiselect({ message: "Working rules to apply", options: ruleOptions(rules), initialValues: previous.rules.filter((id) => rules.some((r) => r.id === id)), required: false }));
1537
+ const rulesInSubprojects = selected.length ? check(await p.confirm({ message: "In monorepos, apply the rules to the subprojects too?", initialValue: previous.rulesInSubprojects })) : false;
1538
+ const hideInVsCode2 = check(await p.confirm({ message: HIDE_QUESTION, initialValue: previous.hideInVsCode }));
1539
+ const d = { mode, mainDoc, workflow, agents, proposals, sourceLang, rules: selected, rulesInSubprojects, hideInVsCode: hideInVsCode2 };
1540
+ writeDefaults(d);
1541
+ p.outro(`Saved in ${pc2.cyan(defaultsFile())}. Change them with ${pc2.cyan("mdrules setup")}; for a single project: ${pc2.cyan("mdrules init --custom")}`);
1542
+ return d;
1543
+ }
1467
1544
 
1468
1545
  // src/cli/translate.ts
1469
1546
  import * as p2 from "@clack/prompts";
@@ -1534,7 +1611,8 @@ var HELP = `mdrules \u2014 visual editor for your project's Markdown
1534
1611
 
1535
1612
  Usage
1536
1613
  mdrules init if needed, then open the editor
1537
- mdrules init (re)run the setup: main doc, specs structure, rules, languages
1614
+ mdrules init (re)run the project setup: main doc, specs structure, rules, languages
1615
+ mdrules setup choose how projects are set up: with your saved answers, or asking each time
1538
1616
  mdrules serve open the editor without running init
1539
1617
  mdrules translate choose languages to review the docs in (or: mdrules --translate)
1540
1618
  mdrules sync --status [--json] translations + files not in the source language
@@ -1542,6 +1620,8 @@ Usage
1542
1620
  mdrules sync --keep <path...> keep files in their current language
1543
1621
 
1544
1622
  Options
1623
+ -D first run: skip saving defaults; every project asks its own questions
1624
+ --custom init: ask every question for this project, ignoring your defaults
1545
1625
  --port <n> server port (default 4319)
1546
1626
  --no-open do not open the browser
1547
1627
  `;
@@ -1557,12 +1637,17 @@ async function main() {
1557
1637
  if (flag("--help") || flag("-h")) return console.log(HELP);
1558
1638
  if (command === "sync") return sync();
1559
1639
  if (command === "translate" || flag("--translate")) return runTranslate(root);
1640
+ if (command === "setup") {
1641
+ await runSetup(flag("-D"));
1642
+ return;
1643
+ }
1560
1644
  if (command === "init") {
1561
- await runInit(root);
1645
+ await runInit(root, flag("--custom") ? null : await defaults());
1562
1646
  return;
1563
1647
  }
1564
1648
  if (command === "start" && !readConfig(root)) {
1565
- if (process.stdin.isTTY) await runInit(root);
1649
+ if (process.stdin.isTTY) await runInit(root, flag("--custom") ? null : await defaults());
1650
+ else if (readDefaults()?.mode === "defaults") await runInit(root, readDefaults());
1566
1651
  }
1567
1652
  if (command === "start" || command === "serve") {
1568
1653
  await startServer({ root, port: Number(option("--port") ?? 4319), open: !flag("--no-open") });
@@ -1571,6 +1656,10 @@ async function main() {
1571
1656
  console.log(HELP);
1572
1657
  process.exitCode = 1;
1573
1658
  }
1659
+ async function defaults() {
1660
+ const d = readDefaults() ?? (process.stdin.isTTY ? await runSetup(flag("-D")) : null);
1661
+ return d?.mode === "defaults" ? d : null;
1662
+ }
1574
1663
  function sync() {
1575
1664
  const report = syncReport(root);
1576
1665
  const paths = (after) => args.slice(args.indexOf(after) + 1).filter((a, i, all) => !a.startsWith("--") && all[i - 1] !== "--lang");