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.
- package/README.md +21 -6
- package/dist/cli.js +343 -254
- package/dist/web/assets/{abnfDiagram-WEYOIA3C-COoXc57p.js → abnfDiagram-WEYOIA3C-DMRcmCIC.js} +1 -1
- package/dist/web/assets/{arc-CQ-mpttu.js → arc-DZzWcSCk.js} +1 -1
- package/dist/web/assets/{architecture-WOLXFQ4H-Cl3-F9oV.js → architecture-WOLXFQ4H-DMFg8Ew3.js} +1 -1
- package/dist/web/assets/{architectureDiagram-ZHVYKT53-C1LOUHfq.js → architectureDiagram-ZHVYKT53-DWkP0LWv.js} +1 -1
- package/dist/web/assets/{blockDiagram-RJAE7QA3-YeQnD2ds.js → blockDiagram-RJAE7QA3-DrKNefYo.js} +1 -1
- package/dist/web/assets/{c4Diagram-XDPPPE54-XLmSlndV.js → c4Diagram-XDPPPE54-wzxeN4C2.js} +1 -1
- package/dist/web/assets/channel-De1Bo4WJ.js +1 -0
- package/dist/web/assets/{chunk-2E7WHVMD-HGXqy_LQ.js → chunk-2E7WHVMD-D_HCdfuS.js} +1 -1
- package/dist/web/assets/{chunk-4EMNKR2T-DsQggzQE.js → chunk-4EMNKR2T-dRBMJ4_-.js} +1 -1
- package/dist/web/assets/{chunk-55XCU76K-DY1xg8cl.js → chunk-55XCU76K-BfDf2f5L.js} +1 -1
- package/dist/web/assets/{chunk-7S6XSTFO-BuyzABLx.js → chunk-7S6XSTFO-DQBRPMDG.js} +1 -1
- package/dist/web/assets/{chunk-AQDTSKZB-CsDWfENX.js → chunk-AQDTSKZB-ClWiKzeu.js} +1 -1
- package/dist/web/assets/{chunk-DSNLSHJZ-CuiVxF9g.js → chunk-DSNLSHJZ-Ct9Lfuzm.js} +1 -1
- package/dist/web/assets/{chunk-HAGFOTED-CjjQypU8.js → chunk-HAGFOTED-Cno1MSCP.js} +1 -1
- package/dist/web/assets/{chunk-HNE4Q36P-B0-YOuZI.js → chunk-HNE4Q36P-CDmp78x5.js} +1 -1
- package/dist/web/assets/{chunk-NQOKRVIF-DMWnWh6r.js → chunk-NQOKRVIF-CqWTXOUR.js} +1 -1
- package/dist/web/assets/{chunk-UHW7PFBP-D3E4ik9Y.js → chunk-UHW7PFBP-psZpZtfm.js} +1 -1
- package/dist/web/assets/{chunk-VQ5W62QI-CiPAEjx6.js → chunk-VQ5W62QI-CCDvsLND.js} +1 -1
- package/dist/web/assets/{chunk-WQQL4CDK-StBWijFa.js → chunk-WQQL4CDK-Cu2Aopzy.js} +1 -1
- package/dist/web/assets/{chunk-XXDRQBXY-CINBf_Ay.js → chunk-XXDRQBXY-BWwvAa43.js} +1 -1
- package/dist/web/assets/{chunk-YZO67HFG-BSkchBSd.js → chunk-YZO67HFG-QHKY9fqM.js} +1 -1
- package/dist/web/assets/{chunk-ZAUX2LTX-CYfcgJpt.js → chunk-ZAUX2LTX-DOdmfo9d.js} +1 -1
- package/dist/web/assets/{chunk-ZCHMQSOR-CwWl64AI.js → chunk-ZCHMQSOR-BNE_AYuB.js} +1 -1
- package/dist/web/assets/{classDiagram-v2-4MYWR3UA-Ce6JKsB3.js → classDiagram-v2-4MYWR3UA-QVfxHH62.js} +1 -1
- package/dist/web/assets/{cose-bilkent-JH36ORCC-COClxHi_.js → cose-bilkent-JH36ORCC-1dupt6EA.js} +1 -1
- package/dist/web/assets/{cynefin-EF2NZ3EQ-BGZI2c8V.js → cynefin-EF2NZ3EQ-CAB7KRcw.js} +1 -1
- package/dist/web/assets/{cynefinDiagram-WF24UDRP-B24KUNeS.js → cynefinDiagram-WF24UDRP-D2M2_xSW.js} +1 -1
- package/dist/web/assets/{dagre-NK4GIZVX-DjCzI5LG.js → dagre-NK4GIZVX-CY4Eoama.js} +1 -1
- package/dist/web/assets/{diagram-CBK3MWTC-pa5xxV7I.js → diagram-CBK3MWTC-Ct0zNHAV.js} +1 -1
- package/dist/web/assets/{diagram-I5XIIV2D-BQtih66c.js → diagram-I5XIIV2D-gmcSomD3.js} +1 -1
- package/dist/web/assets/{diagram-ODIL2UCW-CtodvdqO.js → diagram-ODIL2UCW-Z01ngjlG.js} +1 -1
- package/dist/web/assets/{diagram-PFBEJUXE-B-CnrB9d.js → diagram-PFBEJUXE-By7lIQit.js} +1 -1
- package/dist/web/assets/{diagram-RJA3I4VV-Di8-srfd.js → diagram-RJA3I4VV-BbTc0lUo.js} +1 -1
- package/dist/web/assets/{diagram-TYUNFSNR-DT-cfj0M.js → diagram-TYUNFSNR-BZNmF7dF.js} +1 -1
- package/dist/web/assets/{dist-CLMm60_p.js → dist-B4Zd6qux.js} +1 -1
- package/dist/web/assets/{dist-CPFvkcDl.js → dist-BLO1tZtl.js} +1 -1
- package/dist/web/assets/{dist-BxXtIjV1.js → dist-Bag2LZ55.js} +1 -1
- package/dist/web/assets/{dist-B4Z2sV8l.js → dist-CW4Hp5az.js} +1 -1
- package/dist/web/assets/{dist-DevJcR0Y.js → dist-DFZHgUoB.js} +1 -1
- package/dist/web/assets/{dist-CA-ReuvA.js → dist-DHO-aIOo.js} +1 -1
- package/dist/web/assets/{dist-JJNpWgR6.js → dist-DY18ueDO.js} +1 -1
- package/dist/web/assets/{dist-5RSr3i1a.js → dist-FrDFGy8k.js} +1 -1
- package/dist/web/assets/{dist-B7yJAZLB.js → dist-luL4D3CY.js} +1 -1
- package/dist/web/assets/{dist-CZxj92ue.js → dist-oMwphMn-.js} +1 -1
- package/dist/web/assets/{dist-DVG9d1NU.js → dist-roXUU37l.js} +1 -1
- package/dist/web/assets/{ebnfDiagram-V4P3TTPT-5btoYlHi.js → ebnfDiagram-V4P3TTPT-JoyE0F88.js} +1 -1
- package/dist/web/assets/{elk-QONTBCTQ-BiXiP5mn.js → elk-QONTBCTQ-BNYZwxj2.js} +1 -1
- package/dist/web/assets/{erDiagram-KYQIGQAA-Csvahmwh.js → erDiagram-KYQIGQAA-BQCuW6RJ.js} +1 -1
- package/dist/web/assets/{eventmodeling-K75KTNOO-CTDk7azy.js → eventmodeling-K75KTNOO-DcSjO4xn.js} +1 -1
- package/dist/web/assets/flowDiagram-IUGLLWBU-C2mAXQNT.js +1 -0
- package/dist/web/assets/{ganttDiagram-R7GPNTJY-Kr_UYAyo.js → ganttDiagram-R7GPNTJY-BofMym7I.js} +1 -1
- package/dist/web/assets/{gitGraph-VSP46ZUC-DWAozJzq.js → gitGraph-VSP46ZUC-D5CJVdju.js} +1 -1
- package/dist/web/assets/{gitGraphDiagram-PDYL7XHY-DyTW4WQG.js → gitGraphDiagram-PDYL7XHY-BSH5YBss.js} +1 -1
- package/dist/web/assets/index-BUr29cVy.js +63 -0
- package/dist/web/assets/index-B_El5oZL.css +1 -0
- package/dist/web/assets/{info-OHQRW6UA-DiJj6mF5.js → info-OHQRW6UA-LJbVE13c.js} +1 -1
- package/dist/web/assets/{infoDiagram-MVDRKAX5-D02QrLp5.js → infoDiagram-MVDRKAX5-CcDzYgLq.js} +1 -1
- package/dist/web/assets/{ishikawaDiagram-HPEHV2GE-CXW4mRts.js → ishikawaDiagram-HPEHV2GE-BTAZWgme.js} +1 -1
- package/dist/web/assets/{journeyDiagram-BOSHW7O7-DwyTpaHZ.js → journeyDiagram-BOSHW7O7-D_lt8C1z.js} +1 -1
- package/dist/web/assets/{kanban-definition-FNSA4HC5-B8br7aDC.js → kanban-definition-FNSA4HC5-BnOn_93I.js} +1 -1
- package/dist/web/assets/{line-C7pCcPIA.js → line-D86czQCw.js} +1 -1
- package/dist/web/assets/{linear-Tw3CU3Fk.js → linear-DX5Fyu8p.js} +1 -1
- package/dist/web/assets/{mermaid-parser.core-DuxqJtRQ.js → mermaid-parser.core-BActkHhD.js} +3 -3
- package/dist/web/assets/{mermaid.core-DsOnw7nE.js → mermaid.core-Ce5oFJHI.js} +4 -4
- package/dist/web/assets/{mindmap-definition-6OY57UG3-Cx4RGd4t.js → mindmap-definition-6OY57UG3-D7CwuLeJ.js} +1 -1
- package/dist/web/assets/{packet-JDAUHWVQ-FgGACr61.js → packet-JDAUHWVQ-DBB-zZek.js} +1 -1
- package/dist/web/assets/{pegDiagram-ECIUJNBX-DbM0mpqP.js → pegDiagram-ECIUJNBX-CK1750zt.js} +1 -1
- package/dist/web/assets/{pie-XZMESJXO-D3_sBOVu.js → pie-XZMESJXO-DOtQfX6V.js} +1 -1
- package/dist/web/assets/{pieDiagram-GNMPY74T-COgyAYuf.js → pieDiagram-GNMPY74T-Cu5hilM3.js} +1 -1
- package/dist/web/assets/{quadrantDiagram-D57K4MC3-CHA07iGU.js → quadrantDiagram-D57K4MC3-C6llE2gk.js} +1 -1
- package/dist/web/assets/{radar-ABXABTNO-DecHbZ8y.js → radar-ABXABTNO-BqYteODl.js} +1 -1
- package/dist/web/assets/{railroad-I3PHUGI6-CX9gFbv6.js → railroad-I3PHUGI6-QkKJVgJ_.js} +1 -1
- package/dist/web/assets/{railroad-abnf-LNEFI6M7-7ekNszOQ.js → railroad-abnf-LNEFI6M7-BOVXdtiq.js} +1 -1
- package/dist/web/assets/{railroad-ebnf-DWYD2UWJ-DdczO4iK.js → railroad-ebnf-DWYD2UWJ-Dciqx8FR.js} +1 -1
- package/dist/web/assets/{railroad-peg-JR7BVNR7-tTF8C5h6.js → railroad-peg-JR7BVNR7-BJRps0kv.js} +1 -1
- package/dist/web/assets/{railroadDiagram-Y72NRBL7-7GM0UzXp.js → railroadDiagram-Y72NRBL7-BEp_79kB.js} +1 -1
- package/dist/web/assets/{requirementDiagram-EQYSW5RA-BSC7dW7G.js → requirementDiagram-EQYSW5RA-Bi1usS1S.js} +1 -1
- package/dist/web/assets/{sankeyDiagram-SY24JHO5-zbVVy5A9.js → sankeyDiagram-SY24JHO5-cBLrx4TR.js} +1 -1
- package/dist/web/assets/{sequenceDiagram-BLEHNHQU-DEe5_sJr.js → sequenceDiagram-BLEHNHQU-CwY8WSOO.js} +1 -1
- package/dist/web/assets/{src-BRIqFVDQ.js → src-BxxO9_wA.js} +1 -1
- package/dist/web/assets/{stateDiagram-v2-A6IESKWZ-D9iIEu7F.js → stateDiagram-v2-A6IESKWZ-CZAkkIQQ.js} +1 -1
- package/dist/web/assets/{swimlanes-B3ENRKFQ-kSlZwPrl.js → swimlanes-B3ENRKFQ-B06LEs1H.js} +1 -1
- package/dist/web/assets/swimlanesDiagram-AYQRV4AK-Cx4IBQfH.js +8 -0
- package/dist/web/assets/{timeline-definition-JXYKGLGB-D3FVcL1M.js → timeline-definition-JXYKGLGB-pBL_HcoZ.js} +1 -1
- package/dist/web/assets/{treeView-D4JQ5SDB-Dg-vnn_2.js → treeView-D4JQ5SDB-B3NYhcq3.js} +1 -1
- package/dist/web/assets/{treemap-SAJKECNS-SUcVM8SE.js → treemap-SAJKECNS-jUBClquT.js} +1 -1
- package/dist/web/assets/{usecaseDiagram-WPOLBNNX-pg19th6w.js → usecaseDiagram-WPOLBNNX-BEy-FZyr.js} +1 -1
- package/dist/web/assets/{vennDiagram-Q3Y3IDCW-C0bGBnZZ.js → vennDiagram-Q3Y3IDCW-CU_ZQagp.js} +1 -1
- package/dist/web/assets/{wardley-7MLQ67FV-DRUta4Q-.js → wardley-7MLQ67FV-DLfevnf3.js} +1 -1
- package/dist/web/assets/{wardleyDiagram-IVAIDKWA-CafLMkAZ.js → wardleyDiagram-IVAIDKWA-BnuVbP4c.js} +1 -1
- package/dist/web/assets/{xychartDiagram-4URNHDU4-CbBmnzba.js → xychartDiagram-4URNHDU4-pikAwF42.js} +1 -1
- package/dist/web/index.html +2 -2
- package/package.json +1 -1
- package/dist/web/assets/channel-Bu489uW7.js +0 -1
- package/dist/web/assets/flowDiagram-IUGLLWBU-CTlRlF-F.js +0 -1
- package/dist/web/assets/index-lmzHXuPr.css +0 -1
- package/dist/web/assets/index-qPUGRLvu.js +0 -63
- 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
|
|
63
|
-
import
|
|
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
|
|
90
|
-
import
|
|
119
|
+
import fs3 from "fs";
|
|
120
|
+
import path3 from "path";
|
|
91
121
|
import YAML from "yaml";
|
|
92
|
-
var rulesDir = () =>
|
|
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 (
|
|
98
|
-
|
|
99
|
-
const src =
|
|
100
|
-
for (const f of
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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) =>
|
|
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(
|
|
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 =
|
|
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
|
-
|
|
211
|
+
fs3.writeFileSync(file, applyRules(text3, rules, sourceLang ?? sourceLangOf(text3)));
|
|
181
212
|
}
|
|
182
213
|
|
|
183
214
|
// src/core/scan.ts
|
|
184
|
-
import
|
|
185
|
-
import
|
|
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(
|
|
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
|
|
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
|
-
|
|
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 =
|
|
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 =
|
|
233
|
-
const rel = toPosix(
|
|
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
|
|
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) =>
|
|
283
|
-
var translationPath = (root2, 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
|
|
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(
|
|
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
|
-
|
|
303
|
-
|
|
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(
|
|
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:
|
|
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(
|
|
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(
|
|
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: ${
|
|
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(
|
|
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
|
|
401
|
+
import fs11 from "fs";
|
|
371
402
|
import http from "http";
|
|
372
|
-
import
|
|
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
|
|
380
|
-
import
|
|
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 =
|
|
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
|
|
455
|
-
import
|
|
485
|
+
import fs9 from "fs";
|
|
486
|
+
import path9 from "path";
|
|
456
487
|
|
|
457
488
|
// src/core/scaffold.ts
|
|
458
|
-
import
|
|
459
|
-
import
|
|
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
|
|
466
|
-
const from =
|
|
467
|
-
const to =
|
|
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
|
-
|
|
500
|
+
fs7.mkdirSync(to, { recursive: true });
|
|
470
501
|
copyTree(from, to, vars, created, root2);
|
|
471
|
-
} else if (!
|
|
472
|
-
|
|
473
|
-
created.push(toPosix(
|
|
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 =
|
|
479
|
-
if (
|
|
480
|
-
const text3 =
|
|
481
|
-
|
|
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 =
|
|
485
|
-
|
|
486
|
-
|
|
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 =
|
|
490
|
-
const text3 =
|
|
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
|
-
|
|
523
|
+
fs7.writeFileSync(file, text3 + (text3 && !text3.endsWith("\n") ? "\n" : "") + ".mdrules/\n");
|
|
493
524
|
}
|
|
494
525
|
function hideInVsCode(root2) {
|
|
495
|
-
const file =
|
|
526
|
+
const file = path7.join(root2, ".vscode", "settings.json");
|
|
496
527
|
let settings = {};
|
|
497
|
-
if (
|
|
528
|
+
if (fs7.existsSync(file)) {
|
|
498
529
|
try {
|
|
499
|
-
settings = JSON.parse(
|
|
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
|
-
|
|
506
|
-
|
|
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
|
|
513
|
-
import
|
|
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 (
|
|
525
|
-
if (
|
|
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(
|
|
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(
|
|
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 =
|
|
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)
|
|
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
|
|
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
|
|
601
|
-
|
|
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
|
|
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
|
|
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
|
|
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 =
|
|
630
|
-
if (
|
|
631
|
-
const rel = (...p3) => toPosix(
|
|
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(
|
|
634
|
-
const dir =
|
|
635
|
-
const proposal = read(
|
|
636
|
-
const tasks = read(
|
|
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:
|
|
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(
|
|
647
|
-
archived: dirs(
|
|
648
|
-
const file = ["proposal.md", "tasks.md"].find((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 (
|
|
654
|
-
const constitution = read(
|
|
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(
|
|
659
|
-
const dir =
|
|
660
|
-
const spec = read(
|
|
661
|
-
const tasks = read(
|
|
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(
|
|
667
|
-
artifacts: { spec: spec !== null, plan:
|
|
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 =
|
|
711
|
+
var SKILL = path9.join(".claude", "skills", "mdrules-proposals");
|
|
681
712
|
function proposalsState(root2) {
|
|
682
|
-
return { active:
|
|
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) =>
|
|
702
|
-
return 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 =
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
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
|
-
|
|
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
|
|
756
|
+
import fs10 from "fs";
|
|
726
757
|
import os2 from "os";
|
|
727
|
-
import
|
|
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) =>
|
|
740
|
-
var display = (p3) => p3.startsWith(os2.homedir() +
|
|
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:
|
|
773
|
+
return Object.keys(SOURCE_DIRS).map((id) => ({ id, path: display(sourceDir(id)), exists: fs10.existsSync(sourceDir(id)) }));
|
|
743
774
|
}
|
|
744
|
-
var tagsFile = () =>
|
|
775
|
+
var tagsFile = () => path10.join(GLOBAL_DIR, "skills.json");
|
|
745
776
|
function readTags() {
|
|
746
777
|
try {
|
|
747
|
-
return JSON.parse(
|
|
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
|
-
|
|
758
|
-
|
|
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 =
|
|
795
|
+
entries = fs10.readdirSync(dir, { withFileTypes: true });
|
|
765
796
|
} catch {
|
|
766
797
|
return [];
|
|
767
798
|
}
|
|
768
799
|
return entries.flatMap((e) => {
|
|
769
|
-
const abs =
|
|
800
|
+
const abs = path10.join(dir, e.name);
|
|
770
801
|
let real;
|
|
771
802
|
try {
|
|
772
|
-
if (!
|
|
773
|
-
real =
|
|
803
|
+
if (!fs10.statSync(abs).isDirectory()) return [];
|
|
804
|
+
real = fs10.realpathSync(abs);
|
|
774
805
|
} catch {
|
|
775
806
|
return [];
|
|
776
807
|
}
|
|
777
|
-
if (
|
|
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
|
|
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 =
|
|
837
|
+
const text3 = fs10.readFileSync(path10.join(real, "SKILL.md"), "utf8");
|
|
807
838
|
const fm = frontmatter(text3);
|
|
808
|
-
const folder =
|
|
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) =>
|
|
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) =>
|
|
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) =>
|
|
850
|
-
const existing = dests.filter((d) =>
|
|
851
|
-
if (existing.length && !overwrite) return { conflict: existing.map((d) =>
|
|
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
|
-
|
|
854
|
-
|
|
855
|
-
|
|
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 (!
|
|
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:
|
|
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(
|
|
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 =
|
|
992
|
+
const source = fs11.readFileSync(safeResolve(root2, rel), "utf8");
|
|
962
993
|
if (l) {
|
|
963
994
|
const tPath = translationPath(root2, l, rel);
|
|
964
|
-
if (
|
|
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
|
-
|
|
974
|
-
|
|
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 (
|
|
981
|
-
|
|
982
|
-
const title =
|
|
983
|
-
|
|
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 (
|
|
994
|
-
|
|
995
|
-
return { ok: true, path: toPosix(
|
|
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 (!
|
|
1000
|
-
const files =
|
|
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(
|
|
1005
|
-
|
|
1006
|
-
for (const l of config().langs)
|
|
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 &&
|
|
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:
|
|
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
|
-
|
|
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(
|
|
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[
|
|
1089
|
-
if (!type || !
|
|
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
|
-
|
|
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(
|
|
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 =
|
|
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(
|
|
1117
|
-
const i18n = toPosix(
|
|
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 (!
|
|
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 =
|
|
1140
|
-
if (!file.startsWith(WEB_DIR) || !
|
|
1141
|
-
if (!
|
|
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[
|
|
1146
|
-
|
|
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
|
|
1151
|
-
import
|
|
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
|
|
1157
|
-
import
|
|
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(
|
|
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(
|
|
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) &&
|
|
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 =
|
|
1205
|
-
copyTree(
|
|
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 =
|
|
1209
|
-
copyTree(
|
|
1210
|
-
let doc = MAIN_DOCS.map((d) =>
|
|
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 =
|
|
1213
|
-
|
|
1214
|
-
created.push(
|
|
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 =
|
|
1270
|
-
for (const name of
|
|
1271
|
-
const dest =
|
|
1272
|
-
|
|
1273
|
-
|
|
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((
|
|
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((
|
|
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((
|
|
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((
|
|
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((
|
|
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
|
|
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 =
|
|
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 =
|
|
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((
|
|
1453
|
-
applyRulesToFile(
|
|
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:
|
|
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");
|