@rafiqui987/markode 0.2.7 → 0.3.1
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 +240 -109
- package/dist/cli.js +1289 -639
- package/dist/web/assets/{abnfDiagram-WEYOIA3C-DpbJC7Xp.js → abnfDiagram-WEYOIA3C-C-xz1zhH.js} +1 -1
- package/dist/web/assets/{arc-DC4i8MWg.js → arc-0NKn9s7n.js} +1 -1
- package/dist/web/assets/{architecture-WOLXFQ4H-eBViYpWx.js → architecture-WOLXFQ4H-yvNPw04L.js} +1 -1
- package/dist/web/assets/{architectureDiagram-ZHVYKT53-BUdoz7It.js → architectureDiagram-ZHVYKT53-DiNkPsUS.js} +1 -1
- package/dist/web/assets/{blockDiagram-RJAE7QA3-g9DAd_oM.js → blockDiagram-RJAE7QA3-Bm5an-ZR.js} +1 -1
- package/dist/web/assets/{c4Diagram-XDPPPE54-DRNtmahu.js → c4Diagram-XDPPPE54-DCIkW1i3.js} +1 -1
- package/dist/web/assets/channel-BkrEjK89.js +1 -0
- package/dist/web/assets/{chunk-2E7WHVMD-pLVU_fk3.js → chunk-2E7WHVMD-w-cvaztJ.js} +1 -1
- package/dist/web/assets/{chunk-4EMNKR2T-CILPvkQp.js → chunk-4EMNKR2T-Dz1c4ddf.js} +1 -1
- package/dist/web/assets/{chunk-55XCU76K-D3MGCCrv.js → chunk-55XCU76K-BkW2QTCZ.js} +1 -1
- package/dist/web/assets/{chunk-7S6XSTFO-CN1GsbIT.js → chunk-7S6XSTFO-SqLAsU6N.js} +1 -1
- package/dist/web/assets/{chunk-AQDTSKZB-CUWKsFsd.js → chunk-AQDTSKZB-DwIPdMmQ.js} +1 -1
- package/dist/web/assets/{chunk-DSNLSHJZ-CwfiRg01.js → chunk-DSNLSHJZ-BNEo5raG.js} +1 -1
- package/dist/web/assets/{chunk-HAGFOTED-Do-8YoBu.js → chunk-HAGFOTED-BokAjJjC.js} +1 -1
- package/dist/web/assets/{chunk-HNE4Q36P-1-JjBgqP.js → chunk-HNE4Q36P-BlJCzrNG.js} +1 -1
- package/dist/web/assets/{chunk-NQOKRVIF-BEHeLMtz.js → chunk-NQOKRVIF-CK1D9VKN.js} +1 -1
- package/dist/web/assets/{chunk-UHW7PFBP-CC1DKtp5.js → chunk-UHW7PFBP-ycMkWOji.js} +1 -1
- package/dist/web/assets/{chunk-VQ5W62QI-CWY0eXsE.js → chunk-VQ5W62QI-CNI8j3BI.js} +1 -1
- package/dist/web/assets/{chunk-WQQL4CDK-BJl_ufDd.js → chunk-WQQL4CDK-DvQQgYrd.js} +1 -1
- package/dist/web/assets/{chunk-XXDRQBXY-DKVbKHOf.js → chunk-XXDRQBXY-cFqAbOh1.js} +1 -1
- package/dist/web/assets/{chunk-YZO67HFG-dhkHuKWt.js → chunk-YZO67HFG-CkfGL1zX.js} +1 -1
- package/dist/web/assets/{chunk-ZAUX2LTX-CA6m357j.js → chunk-ZAUX2LTX-BwdZydyj.js} +1 -1
- package/dist/web/assets/{chunk-ZCHMQSOR-DUBbqrP7.js → chunk-ZCHMQSOR-CXDXBIC6.js} +1 -1
- package/dist/web/assets/{classDiagram-v2-4MYWR3UA-PXy4ulRK.js → classDiagram-v2-4MYWR3UA-B95ypqg8.js} +1 -1
- package/dist/web/assets/{cose-bilkent-JH36ORCC-BDYEjy3u.js → cose-bilkent-JH36ORCC-C8-ToTGn.js} +1 -1
- package/dist/web/assets/{cynefin-EF2NZ3EQ-BOMUQs4F.js → cynefin-EF2NZ3EQ-CHth0Nt9.js} +1 -1
- package/dist/web/assets/{cynefinDiagram-WF24UDRP-cX0NpQ7L.js → cynefinDiagram-WF24UDRP-CMcwwxC8.js} +1 -1
- package/dist/web/assets/{dagre-NK4GIZVX-BAP6Gj-L.js → dagre-NK4GIZVX-BZzAgRpg.js} +1 -1
- package/dist/web/assets/{diagram-CBK3MWTC-B5AONjPA.js → diagram-CBK3MWTC-DGwxJbVy.js} +1 -1
- package/dist/web/assets/{diagram-I5XIIV2D-BkwhoH5d.js → diagram-I5XIIV2D-B17CYdAP.js} +1 -1
- package/dist/web/assets/{diagram-ODIL2UCW-DUFps_5l.js → diagram-ODIL2UCW-CYPFYS2M.js} +1 -1
- package/dist/web/assets/{diagram-PFBEJUXE-Dj94WcA4.js → diagram-PFBEJUXE-BQMumROl.js} +1 -1
- package/dist/web/assets/{diagram-RJA3I4VV-BRhr049b.js → diagram-RJA3I4VV-vHTuDO1-.js} +1 -1
- package/dist/web/assets/{diagram-TYUNFSNR-C9EtKuKD.js → diagram-TYUNFSNR-C_WYz7qL.js} +1 -1
- package/dist/web/assets/{dist-DTXSqpYP.js → dist-8bzcdWlP.js} +1 -1
- package/dist/web/assets/{dist-B91b1nu8.js → dist-8k6FSdRu.js} +1 -1
- package/dist/web/assets/{dist-BOIkyUDE.js → dist-B1oQ1VTT.js} +1 -1
- package/dist/web/assets/{dist-CMFTG-Cz.js → dist-B5uXb_Wi.js} +1 -1
- package/dist/web/assets/{dist-6_cnylgs.js → dist-BOCA4kQq.js} +1 -1
- package/dist/web/assets/{dist-DMEslBFn.js → dist-BT3nK1GA.js} +1 -1
- package/dist/web/assets/{dist-uYDRxuA0.js → dist-BruzXrfs.js} +1 -1
- package/dist/web/assets/{dist-CXAToOJL.js → dist-BwF8pAVO.js} +1 -1
- package/dist/web/assets/{dist-B-4tAY5T.js → dist-CFUj2tCy.js} +2 -2
- package/dist/web/assets/{dist-C6oRdq_x.js → dist-CM1-Tp30.js} +1 -1
- package/dist/web/assets/{dist-D5_2d6Ki.js → dist-CubY9gnU.js} +1 -1
- package/dist/web/assets/{dist-CUajnf5G.js → dist-D5EOvBuF.js} +1 -1
- package/dist/web/assets/{dist-CRKS1wXG.js → dist-DFcDEdTm.js} +1 -1
- package/dist/web/assets/{dist-CxtEXJtW.js → dist-DV0HO3Fe.js} +1 -1
- package/dist/web/assets/{dist-DfKlgEia.js → dist-DZQ_bwip.js} +1 -1
- package/dist/web/assets/{dist-SlIdOLRu.js → dist-PwT2nxki.js} +1 -1
- package/dist/web/assets/{dist-C7gsSbg8.js → dist-QI7QavRs.js} +1 -1
- package/dist/web/assets/{dist-BhvCwF6v.js → dist-cSM0Lli0.js} +1 -1
- package/dist/web/assets/{dist-hiB1k3yb.js → dist-ga2OvaLo.js} +1 -1
- package/dist/web/assets/{ebnfDiagram-V4P3TTPT-hUPVHW_L.js → ebnfDiagram-V4P3TTPT-DLMwyX-U.js} +1 -1
- package/dist/web/assets/{elk-QONTBCTQ-CK3_-qCI.js → elk-QONTBCTQ-DVvxbtF4.js} +1 -1
- package/dist/web/assets/{erDiagram-KYQIGQAA-BnzshuRd.js → erDiagram-KYQIGQAA-DPXicJ1t.js} +1 -1
- package/dist/web/assets/{eventmodeling-K75KTNOO-YpRkYpNJ.js → eventmodeling-K75KTNOO-DpHVZP_6.js} +1 -1
- package/dist/web/assets/flowDiagram-IUGLLWBU-DrcHkm4d.js +1 -0
- package/dist/web/assets/{ganttDiagram-R7GPNTJY-Cof-TrXj.js → ganttDiagram-R7GPNTJY-Ch6NDX5Q.js} +1 -1
- package/dist/web/assets/{gitGraph-VSP46ZUC-ClyrQrUV.js → gitGraph-VSP46ZUC-CIhLB663.js} +1 -1
- package/dist/web/assets/{gitGraphDiagram-PDYL7XHY-DcHBmgeW.js → gitGraphDiagram-PDYL7XHY-Ci_MIbqs.js} +1 -1
- package/dist/web/assets/index-CQGURl2V.css +1 -0
- package/dist/web/assets/index-CxwfaWVo.js +132 -0
- package/dist/web/assets/{info-OHQRW6UA-DJuVnR49.js → info-OHQRW6UA-Dau8xD88.js} +1 -1
- package/dist/web/assets/{infoDiagram-MVDRKAX5-SPivATVi.js → infoDiagram-MVDRKAX5-4spgggd7.js} +1 -1
- package/dist/web/assets/{ishikawaDiagram-HPEHV2GE-luEQCi0p.js → ishikawaDiagram-HPEHV2GE-DgFczAdB.js} +1 -1
- package/dist/web/assets/{journeyDiagram-BOSHW7O7-CDWNQ0FV.js → journeyDiagram-BOSHW7O7-CJKwCuyG.js} +1 -1
- package/dist/web/assets/{kanban-definition-FNSA4HC5-kCg2Ntni.js → kanban-definition-FNSA4HC5-B_HPa_Q8.js} +1 -1
- package/dist/web/assets/{line-Cs-2UOsh.js → line-Bil8yI0B.js} +1 -1
- package/dist/web/assets/{linear-Dn78ogyi.js → linear-DJSxHb3I.js} +1 -1
- package/dist/web/assets/{mermaid-parser.core-XUq3kRg7.js → mermaid-parser.core-ByEdgqAb.js} +3 -3
- package/dist/web/assets/{mermaid.core-KP0iScKW.js → mermaid.core-CXUI3iYT.js} +4 -4
- package/dist/web/assets/{mindmap-definition-6OY57UG3-DYfhBOEV.js → mindmap-definition-6OY57UG3-w7xG6UiS.js} +1 -1
- package/dist/web/assets/{packet-JDAUHWVQ-m_mxaPh2.js → packet-JDAUHWVQ-YvTb_dXP.js} +1 -1
- package/dist/web/assets/{pegDiagram-ECIUJNBX-C57XlI0z.js → pegDiagram-ECIUJNBX-D5FI1bVJ.js} +1 -1
- package/dist/web/assets/{pie-XZMESJXO-COU-cPpy.js → pie-XZMESJXO-BH2bih-K.js} +1 -1
- package/dist/web/assets/{pieDiagram-GNMPY74T-Cgv_MwBq.js → pieDiagram-GNMPY74T-DUR82NlG.js} +1 -1
- package/dist/web/assets/{quadrantDiagram-D57K4MC3-DLP-IJBg.js → quadrantDiagram-D57K4MC3-DauAEIyL.js} +1 -1
- package/dist/web/assets/{radar-ABXABTNO-DVnhW8Tu.js → radar-ABXABTNO-CdUXfEdf.js} +1 -1
- package/dist/web/assets/{railroad-I3PHUGI6-CbNbR4f3.js → railroad-I3PHUGI6-Djt90nhE.js} +1 -1
- package/dist/web/assets/{railroad-abnf-LNEFI6M7-QapD3MRY.js → railroad-abnf-LNEFI6M7-XqY0pGCO.js} +1 -1
- package/dist/web/assets/{railroad-ebnf-DWYD2UWJ-CSlvjrRs.js → railroad-ebnf-DWYD2UWJ-BNSOgId4.js} +1 -1
- package/dist/web/assets/{railroad-peg-JR7BVNR7-CabNa_kN.js → railroad-peg-JR7BVNR7-DW2TRqOr.js} +1 -1
- package/dist/web/assets/{railroadDiagram-Y72NRBL7-DnlOXd0I.js → railroadDiagram-Y72NRBL7-CTDNRHBf.js} +1 -1
- package/dist/web/assets/{requirementDiagram-EQYSW5RA-Dq1MJEdy.js → requirementDiagram-EQYSW5RA-DJ5H8uqE.js} +1 -1
- package/dist/web/assets/{sankeyDiagram-SY24JHO5--LVb7hhV.js → sankeyDiagram-SY24JHO5-Xa_3Bd4U.js} +1 -1
- package/dist/web/assets/{sequenceDiagram-BLEHNHQU-Cg8W2Kc8.js → sequenceDiagram-BLEHNHQU-2jNpfI3_.js} +1 -1
- package/dist/web/assets/{src-w1h_Rr-8.js → src-DgNmQVK4.js} +1 -1
- package/dist/web/assets/{stateDiagram-v2-A6IESKWZ-ck5PGLTp.js → stateDiagram-v2-A6IESKWZ-DtxzihUG.js} +1 -1
- package/dist/web/assets/{swimlanes-B3ENRKFQ--RxIRIY6.js → swimlanes-B3ENRKFQ-DJBCOj2u.js} +1 -1
- package/dist/web/assets/swimlanesDiagram-AYQRV4AK-_cBuwUIk.js +8 -0
- package/dist/web/assets/{timeline-definition-JXYKGLGB-Cgtckb20.js → timeline-definition-JXYKGLGB-C-ixUm6b.js} +1 -1
- package/dist/web/assets/{treeView-D4JQ5SDB-Byi5cU2l.js → treeView-D4JQ5SDB-yWRFH4mR.js} +1 -1
- package/dist/web/assets/{treemap-SAJKECNS-DJ7_3_F8.js → treemap-SAJKECNS-q3FZ9UjJ.js} +1 -1
- package/dist/web/assets/{usecaseDiagram-WPOLBNNX-DkTE13SV.js → usecaseDiagram-WPOLBNNX-Ch9QG7L1.js} +1 -1
- package/dist/web/assets/{vennDiagram-Q3Y3IDCW-C5G521q2.js → vennDiagram-Q3Y3IDCW-85m0XMhF.js} +1 -1
- package/dist/web/assets/{wardley-7MLQ67FV-PChC3W6T.js → wardley-7MLQ67FV-S2NRiIdM.js} +1 -1
- package/dist/web/assets/{wardleyDiagram-IVAIDKWA-B9sJfj7x.js → wardleyDiagram-IVAIDKWA-q8ZLrvVG.js} +1 -1
- package/dist/web/assets/{xychartDiagram-4URNHDU4-CmuAuTkp.js → xychartDiagram-4URNHDU4-eSUy1iT7.js} +1 -1
- package/dist/web/index.html +4 -4
- package/package.json +4 -2
- package/templates/proposals/SKILL.md +14 -6
- package/templates/skill/SKILL.md +18 -0
- package/dist/web/assets/channel-B4Lj3Yy_.js +0 -1
- package/dist/web/assets/flowDiagram-IUGLLWBU-DA73lQLy.js +0 -1
- package/dist/web/assets/index-CFg9w5Ih.js +0 -63
- package/dist/web/assets/index-CT9Wzb6W.css +0 -1
- package/dist/web/assets/swimlanesDiagram-AYQRV4AK-CeoYxgix.js +0 -8
package/dist/cli.js
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/cli/index.ts
|
|
4
|
-
import
|
|
5
|
-
import
|
|
4
|
+
import fs20 from "fs";
|
|
5
|
+
import path20 from "path";
|
|
6
6
|
|
|
7
|
-
// src/core/
|
|
8
|
-
import
|
|
9
|
-
import
|
|
7
|
+
// src/core/changes.ts
|
|
8
|
+
import crypto from "crypto";
|
|
9
|
+
import fs2 from "fs";
|
|
10
|
+
import path2 from "path";
|
|
10
11
|
|
|
11
12
|
// src/core/config.ts
|
|
12
13
|
import fs from "fs";
|
|
@@ -53,7 +54,10 @@ function missingMainDoc(dir) {
|
|
|
53
54
|
}
|
|
54
55
|
function linkMainDoc(dir) {
|
|
55
56
|
const missing = missingMainDoc(dir);
|
|
56
|
-
if (missing)
|
|
57
|
+
if (missing) {
|
|
58
|
+
assertInside(path.join(dir, missing));
|
|
59
|
+
fs.writeFileSync(path.join(dir, missing), POINTERS[missing]);
|
|
60
|
+
}
|
|
57
61
|
return missing;
|
|
58
62
|
}
|
|
59
63
|
var linkAgentsDoc = (dir) => missingMainDoc(dir) === "CLAUDE.md" && !!linkMainDoc(dir);
|
|
@@ -71,18 +75,57 @@ function readConfig(root2) {
|
|
|
71
75
|
try {
|
|
72
76
|
const raw = JSON.parse(fs.readFileSync(configFile(root2), "utf8"));
|
|
73
77
|
const langs = Array.isArray(raw.langs) ? raw.langs : raw.lang ? [raw.lang] : [];
|
|
74
|
-
|
|
78
|
+
const mainDoc = MAIN_DOCS.includes(raw.mainDoc) ? raw.mainDoc : null;
|
|
79
|
+
const workflow = ["none", "markode", "openspec", "speckit"].includes(raw.workflow) ? raw.workflow : void 0;
|
|
80
|
+
const proposals = raw.proposals === "general" || raw.proposals === "structured" ? raw.proposals : void 0;
|
|
81
|
+
const langNames = raw.langNames && typeof raw.langNames === "object" ? Object.fromEntries(Object.entries(raw.langNames).filter(([k, v]) => isLangCode(k) && typeof v === "string").map(([k, v]) => [k, v.slice(0, 40)])) : void 0;
|
|
82
|
+
const flags = raw.features && typeof raw.features === "object" ? Object.fromEntries(["rules", "translations", "dashboard", "skills"].filter((k) => typeof raw.features[k] === "boolean").map((k) => [k, raw.features[k]])) : void 0;
|
|
83
|
+
const savedRules = Array.isArray(raw.savedRules) ? raw.savedRules.filter((id) => typeof id === "string" && /^[\w-]+$/.test(id)) : void 0;
|
|
84
|
+
return { mainDoc, langs: langs.filter(isLangCode), langNames, workflow, proposals, skipLink: raw.skipLink === true || void 0, features: flags, savedRules, savedSourceLang: isLang(raw.savedSourceLang) ? raw.savedSourceLang : void 0, initializedAt: typeof raw.initializedAt === "string" ? raw.initializedAt : void 0 };
|
|
75
85
|
} catch {
|
|
76
86
|
return null;
|
|
77
87
|
}
|
|
78
88
|
}
|
|
79
89
|
function writeConfig(root2, config) {
|
|
90
|
+
assertInside(configFile(root2));
|
|
80
91
|
fs.mkdirSync(stateDir(root2), { recursive: true });
|
|
81
92
|
fs.writeFileSync(configFile(root2), JSON.stringify(config, null, 2) + "\n");
|
|
82
93
|
}
|
|
94
|
+
var isInside = (abs, base) => abs === base || abs.startsWith(base + path.sep);
|
|
95
|
+
function realOf(abs) {
|
|
96
|
+
let probe = abs;
|
|
97
|
+
const rest = [];
|
|
98
|
+
for (; ; ) {
|
|
99
|
+
try {
|
|
100
|
+
fs.lstatSync(probe);
|
|
101
|
+
break;
|
|
102
|
+
} catch {
|
|
103
|
+
const up = path.dirname(probe);
|
|
104
|
+
if (up === probe) return null;
|
|
105
|
+
rest.unshift(path.basename(probe));
|
|
106
|
+
probe = up;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
try {
|
|
110
|
+
return path.join(fs.realpathSync(probe), ...rest);
|
|
111
|
+
} catch {
|
|
112
|
+
return null;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
var projectRoot = null;
|
|
116
|
+
function setProjectRoot(root2) {
|
|
117
|
+
projectRoot = fs.realpathSync(root2);
|
|
118
|
+
}
|
|
119
|
+
function assertInside(abs, root2 = projectRoot) {
|
|
120
|
+
if (!root2) return;
|
|
121
|
+
const base = fs.realpathSync(root2);
|
|
122
|
+
const real = realOf(abs);
|
|
123
|
+
if (!real || !isInside(real, base)) throw new Error(`Refusing to write outside the project (symlink?): ${abs}`);
|
|
124
|
+
}
|
|
83
125
|
function safeResolve(root2, rel) {
|
|
84
126
|
const abs = path.resolve(root2, rel);
|
|
85
|
-
if (abs
|
|
127
|
+
if (!isInside(abs, root2)) throw new Error(`Path outside project: ${rel}`);
|
|
128
|
+
assertInside(abs, root2);
|
|
86
129
|
return abs;
|
|
87
130
|
}
|
|
88
131
|
var toPosix = (p3) => p3.split(path.sep).join("/");
|
|
@@ -102,28 +145,183 @@ function writeSettings(patch) {
|
|
|
102
145
|
return next;
|
|
103
146
|
}
|
|
104
147
|
|
|
148
|
+
// src/core/changes.ts
|
|
149
|
+
var logDir = (root2) => path2.join(GLOBAL_DIR, "projects", crypto.createHash("sha256").update(fs2.realpathSync(root2)).digest("hex").slice(0, 16));
|
|
150
|
+
var logFile = (root2) => path2.join(logDir(root2), "activity.jsonl");
|
|
151
|
+
var cursorFile = (root2) => path2.join(logDir(root2), "activity-cursor.json");
|
|
152
|
+
var MAX_LINES = 1e3;
|
|
153
|
+
var ENTRY_HEAD = /^<!-- (comment|reply) by ([^,\n]+), (\d{4}-\d{2}-\d{2})(?:, on "((?:[^"\\\n]|\\.)*)")?:\s?/;
|
|
154
|
+
var HEADING = /^#{1,6}\s+(.*?)\s*#*\s*$/;
|
|
155
|
+
var MAX_ENTRY = 2e4;
|
|
156
|
+
function allEntries(text3) {
|
|
157
|
+
const out = [];
|
|
158
|
+
for (let i = 0; ; ) {
|
|
159
|
+
const s = text3.indexOf("<!--", i);
|
|
160
|
+
const e = s < 0 ? -1 : text3.indexOf("-->", s + 4);
|
|
161
|
+
if (e < 0) return out;
|
|
162
|
+
i = e + 3;
|
|
163
|
+
if (e - s > MAX_ENTRY) continue;
|
|
164
|
+
const raw = text3.slice(s, i);
|
|
165
|
+
const m = ENTRY_HEAD.exec(raw);
|
|
166
|
+
if (m) out.push({ raw, index: s, author: m[2], quote: m[4] ?? "", text: raw.slice(m[0].length, -3).trim() });
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
var entries = (text3) => allEntries(text3).filter((e) => e.author !== "AI" && e.text);
|
|
170
|
+
function stripEntries(text3) {
|
|
171
|
+
let out = "";
|
|
172
|
+
let from = 0;
|
|
173
|
+
for (const e of allEntries(text3)) {
|
|
174
|
+
const end = e.index + e.raw.length;
|
|
175
|
+
const lineStart = text3.lastIndexOf("\n", e.index - 1) + 1;
|
|
176
|
+
const nl = text3.indexOf("\n", end);
|
|
177
|
+
const lineEnd = nl < 0 ? text3.length : nl;
|
|
178
|
+
const alone = !text3.slice(Math.max(lineStart, from), e.index).trim() && !text3.slice(end, lineEnd).trim();
|
|
179
|
+
if (alone && lineStart >= from) {
|
|
180
|
+
out += text3.slice(from, lineStart);
|
|
181
|
+
from = nl < 0 ? text3.length : nl + 1;
|
|
182
|
+
} else {
|
|
183
|
+
out += text3.slice(from, e.index);
|
|
184
|
+
from = end;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
return out + text3.slice(from);
|
|
188
|
+
}
|
|
189
|
+
function headings(text3) {
|
|
190
|
+
const out = [];
|
|
191
|
+
let index = 0;
|
|
192
|
+
for (const line of text3.split("\n")) {
|
|
193
|
+
const h = HEADING.exec(line);
|
|
194
|
+
if (h) out.push({ index, title: h[1] });
|
|
195
|
+
index += line.length + 1;
|
|
196
|
+
}
|
|
197
|
+
return out;
|
|
198
|
+
}
|
|
199
|
+
function sectionAt(list, index) {
|
|
200
|
+
let lo = 0;
|
|
201
|
+
let hi = list.length - 1;
|
|
202
|
+
let found;
|
|
203
|
+
while (lo <= hi) {
|
|
204
|
+
const mid = lo + hi >> 1;
|
|
205
|
+
if (list[mid].index <= index) found = list[mid].title, lo = mid + 1;
|
|
206
|
+
else hi = mid - 1;
|
|
207
|
+
}
|
|
208
|
+
return found;
|
|
209
|
+
}
|
|
210
|
+
var isProposal = (text3) => /^type:\s*proposal\s*$/m.test(/^---\r?\n([\s\S]*?)\r?\n---/.exec(text3)?.[1] ?? "");
|
|
211
|
+
function logSave(root2, rel, before, after) {
|
|
212
|
+
if (before === after || isProposal(after)) return;
|
|
213
|
+
const at = (/* @__PURE__ */ new Date()).toISOString();
|
|
214
|
+
const old = new Set(entries(before).map((e) => e.raw));
|
|
215
|
+
const sections = headings(after);
|
|
216
|
+
const out = entries(after).filter((e) => !old.has(e.raw)).map((e) => ({ at, path: rel, kind: "comment", section: sectionAt(sections, e.index), author: e.author, quote: e.quote || void 0, text: e.text }));
|
|
217
|
+
const a = stripEntries(before).split("\n");
|
|
218
|
+
const b = stripEntries(after).split("\n");
|
|
219
|
+
if (a.join("\n") !== b.join("\n")) {
|
|
220
|
+
let i = 0;
|
|
221
|
+
while (i < a.length && i < b.length && a[i] === b[i]) i++;
|
|
222
|
+
out.push({ at, path: rel, kind: "edit", section: sectionAt(headings(b.join("\n")), b.slice(0, i + 1).join("\n").length) });
|
|
223
|
+
}
|
|
224
|
+
if (!out.length) return;
|
|
225
|
+
const file = logFile(root2);
|
|
226
|
+
fs2.mkdirSync(logDir(root2), { recursive: true });
|
|
227
|
+
fs2.appendFileSync(file, out.map((e) => JSON.stringify(e)).join("\n") + "\n");
|
|
228
|
+
const text3 = fs2.readFileSync(file, "utf8");
|
|
229
|
+
const lines = text3.split("\n").filter(Boolean);
|
|
230
|
+
if (lines.length > MAX_LINES) {
|
|
231
|
+
const kept = lines.slice(-MAX_LINES / 2).join("\n") + "\n";
|
|
232
|
+
fs2.writeFileSync(file, kept);
|
|
233
|
+
const removed = Buffer.byteLength(text3) - Buffer.byteLength(kept);
|
|
234
|
+
const cursor = readCursor(root2);
|
|
235
|
+
if (cursor) writeCursor(root2, { reported: Math.max(0, cursor.reported - removed) });
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
function readCursor(root2) {
|
|
239
|
+
try {
|
|
240
|
+
const c = JSON.parse(fs2.readFileSync(cursorFile(root2), "utf8"));
|
|
241
|
+
return Number.isSafeInteger(c?.reported) && c.reported >= 0 ? { reported: c.reported } : null;
|
|
242
|
+
} catch {
|
|
243
|
+
return null;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
function writeCursor(root2, c) {
|
|
247
|
+
fs2.mkdirSync(logDir(root2), { recursive: true });
|
|
248
|
+
fs2.writeFileSync(cursorFile(root2), JSON.stringify(c) + "\n");
|
|
249
|
+
}
|
|
250
|
+
var logSize = (root2) => fs2.existsSync(logFile(root2)) ? fs2.statSync(logFile(root2)).size : 0;
|
|
251
|
+
function startSession(root2) {
|
|
252
|
+
writeCursor(root2, { reported: logSize(root2) });
|
|
253
|
+
}
|
|
254
|
+
function takeChanges(root2) {
|
|
255
|
+
const size = logSize(root2);
|
|
256
|
+
const cursor = readCursor(root2);
|
|
257
|
+
const from = cursor && cursor.reported <= size ? cursor.reported : size;
|
|
258
|
+
if (from === size) {
|
|
259
|
+
if (!cursor) writeCursor(root2, { reported: size });
|
|
260
|
+
return [];
|
|
261
|
+
}
|
|
262
|
+
const fd = fs2.openSync(logFile(root2), "r");
|
|
263
|
+
const buf = Buffer.alloc(size - from);
|
|
264
|
+
fs2.readSync(fd, buf, 0, buf.length, from);
|
|
265
|
+
fs2.closeSync(fd);
|
|
266
|
+
writeCursor(root2, { reported: size });
|
|
267
|
+
return buf.toString("utf8").split("\n").filter(Boolean).flatMap((l) => {
|
|
268
|
+
try {
|
|
269
|
+
const e = JSON.parse(l);
|
|
270
|
+
const str = (v) => typeof v === "string" ? v.slice(0, 2e3) : void 0;
|
|
271
|
+
if (e.kind !== "edit" && e.kind !== "comment" || typeof e.path !== "string") return [];
|
|
272
|
+
return [{ at: str(e.at) ?? "", path: e.path.slice(0, 500), kind: e.kind, section: str(e.section), author: str(e.author), quote: str(e.quote), text: str(e.text) }];
|
|
273
|
+
} catch {
|
|
274
|
+
return [];
|
|
275
|
+
}
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
function formatChanges(list) {
|
|
279
|
+
if (!list.length) return "";
|
|
280
|
+
const edits = /* @__PURE__ */ new Map();
|
|
281
|
+
for (const e of list.filter((e2) => e2.kind === "edit")) {
|
|
282
|
+
const sections = edits.get(e.path) ?? /* @__PURE__ */ new Set();
|
|
283
|
+
if (e.section) sections.add(e.section);
|
|
284
|
+
edits.set(e.path, sections);
|
|
285
|
+
}
|
|
286
|
+
const comments = list.filter((e) => e.kind === "comment");
|
|
287
|
+
const lines = ['markode: the user changed docs while you worked (see "While you work" in the markode skill).'];
|
|
288
|
+
if (edits.size) {
|
|
289
|
+
lines.push("", "Edited (re-read before working on that part; nothing to do otherwise):");
|
|
290
|
+
for (const [p3, s] of edits) lines.push(`- ${p3}${s.size ? ` (${[...s].map((x) => `"${x}"`).join(", ")})` : ""}`);
|
|
291
|
+
}
|
|
292
|
+
if (comments.length) {
|
|
293
|
+
lines.push("", "New comments (about your current work: reply below it and apply it; otherwise ask the user in one line whether to take it now):");
|
|
294
|
+
for (const c of comments) lines.push(`- ${c.path}${c.section ? ` \u203A "${c.section}"` : ""}${c.quote ? `, on "${c.quote}"` : ""}: ${c.text} (${c.author})`);
|
|
295
|
+
}
|
|
296
|
+
return lines.join("\n");
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// src/core/check.ts
|
|
300
|
+
import fs10 from "fs";
|
|
301
|
+
import path10 from "path";
|
|
302
|
+
|
|
105
303
|
// src/core/dashboard.ts
|
|
106
|
-
import
|
|
107
|
-
import
|
|
304
|
+
import fs4 from "fs";
|
|
305
|
+
import path4 from "path";
|
|
108
306
|
import YAML from "yaml";
|
|
109
307
|
|
|
110
308
|
// src/core/scan.ts
|
|
111
|
-
import
|
|
112
|
-
import
|
|
309
|
+
import fs3 from "fs";
|
|
310
|
+
import path3 from "path";
|
|
113
311
|
import ignore from "ignore";
|
|
114
312
|
var ALWAYS_SKIP = /* @__PURE__ */ new Set(["node_modules", ".git", ".markode", "dist", "build", ".next", ".turbo", "coverage", ".venv"]);
|
|
115
313
|
var IGNORE_FILE = ".markodeignore";
|
|
116
314
|
function loadIgnore(root2, file) {
|
|
117
315
|
const ig = ignore();
|
|
118
316
|
try {
|
|
119
|
-
ig.add(
|
|
317
|
+
ig.add(fs3.readFileSync(path3.join(root2, file), "utf8"));
|
|
120
318
|
} catch {
|
|
121
319
|
}
|
|
122
320
|
return ig;
|
|
123
321
|
}
|
|
124
322
|
function readIgnoreList(root2) {
|
|
125
323
|
try {
|
|
126
|
-
return
|
|
324
|
+
return fs3.readFileSync(path3.join(root2, IGNORE_FILE), "utf8").split("\n").map((l) => l.trim()).filter((l) => l && !l.startsWith("#"));
|
|
127
325
|
} catch {
|
|
128
326
|
return [];
|
|
129
327
|
}
|
|
@@ -132,7 +330,8 @@ function setIgnored(root2, rel, kind, ignored) {
|
|
|
132
330
|
const entry = kind === "dir" ? `${rel}/` : rel;
|
|
133
331
|
const lines = readIgnoreList(root2).filter((l) => l !== entry && l !== rel);
|
|
134
332
|
if (ignored) lines.push(entry);
|
|
135
|
-
|
|
333
|
+
assertInside(path3.join(root2, IGNORE_FILE));
|
|
334
|
+
fs3.writeFileSync(path3.join(root2, IGNORE_FILE), lines.length ? lines.join("\n") + "\n" : "");
|
|
136
335
|
}
|
|
137
336
|
function watchSkips(root2) {
|
|
138
337
|
const gitIgnore = loadIgnore(root2, ".gitignore");
|
|
@@ -143,41 +342,46 @@ function watchSkips(root2) {
|
|
|
143
342
|
return isDir2 !== true && gitIgnore.ignores(rel);
|
|
144
343
|
};
|
|
145
344
|
}
|
|
345
|
+
var isArchive = (rel) => {
|
|
346
|
+
const parts = rel.split("/");
|
|
347
|
+
return parts[parts.length - 1] === "applied" && parts.slice(0, -1).includes("proposals");
|
|
348
|
+
};
|
|
146
349
|
function scanTree(root2) {
|
|
147
350
|
const gitIgnore = loadIgnore(root2, ".gitignore");
|
|
148
351
|
const mdIgnore = loadIgnore(root2, IGNORE_FILE);
|
|
149
|
-
function walk(dirAbs, parentIgnored) {
|
|
150
|
-
let
|
|
352
|
+
function walk(dirAbs, parentIgnored, parentArchived) {
|
|
353
|
+
let entries2;
|
|
151
354
|
try {
|
|
152
|
-
|
|
355
|
+
entries2 = fs3.readdirSync(dirAbs, { withFileTypes: true });
|
|
153
356
|
} catch {
|
|
154
357
|
return [];
|
|
155
358
|
}
|
|
156
359
|
const nodes = [];
|
|
157
|
-
for (const e of
|
|
360
|
+
for (const e of entries2) {
|
|
158
361
|
if (ALWAYS_SKIP.has(e.name)) continue;
|
|
159
|
-
const abs =
|
|
160
|
-
const rel = toPosix(
|
|
362
|
+
const abs = path3.join(dirAbs, e.name);
|
|
363
|
+
const rel = toPosix(path3.relative(root2, abs));
|
|
161
364
|
if (e.isDirectory()) {
|
|
162
365
|
if (gitIgnore.ignores(rel + "/")) continue;
|
|
163
|
-
const
|
|
164
|
-
const
|
|
366
|
+
const archived = parentArchived || isArchive(rel);
|
|
367
|
+
const ignored = parentIgnored || archived || mdIgnore.ignores(rel + "/");
|
|
368
|
+
const children = walk(abs, ignored, archived);
|
|
165
369
|
if (!children.length && !isEmptyDir(abs)) continue;
|
|
166
370
|
const project = MAIN_DOCS.some((d) => children.some((c) => c.kind === "file" && c.name === d));
|
|
167
|
-
nodes.push({ name: e.name, path: rel, kind: "dir", project: project || void 0, ignored: ignored || void 0, children });
|
|
371
|
+
nodes.push({ name: e.name, path: rel, kind: "dir", project: project || void 0, ignored: ignored || void 0, archived: archived || void 0, children });
|
|
168
372
|
} else if (e.isFile() && e.name.toLowerCase().endsWith(".md")) {
|
|
169
373
|
if (gitIgnore.ignores(rel)) continue;
|
|
170
374
|
const ignored = parentIgnored || mdIgnore.ignores(rel);
|
|
171
|
-
nodes.push({ name: e.name, path: rel, kind: "file", ignored: ignored || void 0 });
|
|
375
|
+
nodes.push({ name: e.name, path: rel, kind: "file", ignored: ignored || void 0, archived: parentArchived || void 0 });
|
|
172
376
|
}
|
|
173
377
|
}
|
|
174
378
|
return nodes.sort(compareNodes);
|
|
175
379
|
}
|
|
176
|
-
return walk(root2, false);
|
|
380
|
+
return walk(root2, false, false);
|
|
177
381
|
}
|
|
178
382
|
function isEmptyDir(abs) {
|
|
179
383
|
try {
|
|
180
|
-
return
|
|
384
|
+
return fs3.readdirSync(abs).length === 0;
|
|
181
385
|
} catch {
|
|
182
386
|
return false;
|
|
183
387
|
}
|
|
@@ -219,12 +423,12 @@ var asText = (v) => v instanceof Date ? v.toISOString().slice(0, 10) : v == null
|
|
|
219
423
|
function summarize(root2) {
|
|
220
424
|
return flattenFiles(scanTree(root2)).flatMap((f) => {
|
|
221
425
|
if (f.path.split("/").some((part) => part.startsWith("."))) return [];
|
|
222
|
-
const text3 =
|
|
426
|
+
const text3 = fs4.readFileSync(path4.join(root2, f.path), "utf8");
|
|
223
427
|
const meta = parseFrontmatter(text3);
|
|
224
428
|
if (meta.type === "task" && f.name.startsWith("_")) return [];
|
|
225
429
|
const id = asText(meta.id);
|
|
226
|
-
const total = text3.match(
|
|
227
|
-
const done = text3.match(
|
|
430
|
+
const total = text3.match(/^[ \t]*[-*+] \[[ xX]\]/gm)?.length ?? 0;
|
|
431
|
+
const done = text3.match(/^[ \t]*[-*+] \[[xX]\]/gm)?.length ?? 0;
|
|
228
432
|
const heading2 = text3.match(/^#\s+(.+)$/m)?.[1];
|
|
229
433
|
const title = asText(meta.title) ?? (heading2 && id ? heading2.replace(new RegExp(`^${id.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\s*[\xB7:-]\\s*`), "") : heading2);
|
|
230
434
|
return {
|
|
@@ -242,13 +446,13 @@ function summarize(root2) {
|
|
|
242
446
|
}
|
|
243
447
|
|
|
244
448
|
// src/core/method.ts
|
|
245
|
-
import
|
|
246
|
-
import
|
|
247
|
-
import
|
|
449
|
+
import fs9 from "fs";
|
|
450
|
+
import path9 from "path";
|
|
451
|
+
import YAML3 from "yaml";
|
|
248
452
|
|
|
249
453
|
// src/core/profile.ts
|
|
250
|
-
import
|
|
251
|
-
import
|
|
454
|
+
import fs5 from "fs";
|
|
455
|
+
import path5 from "path";
|
|
252
456
|
var KIND_LABEL = { app: "App", library: "Library", mobile: "Mobile", data: "Data / ML", infra: "Infrastructure" };
|
|
253
457
|
var KIND_HINT = {
|
|
254
458
|
app: "web, API, CLI, desktop",
|
|
@@ -266,16 +470,16 @@ var EXPERIMENTS_LABEL = {
|
|
|
266
470
|
var SKIP = /* @__PURE__ */ new Set(["node_modules", "dist", "build", "coverage", "vendor", "target", ".git", ".venv", "venv"]);
|
|
267
471
|
var names = (abs) => {
|
|
268
472
|
try {
|
|
269
|
-
return
|
|
473
|
+
return fs5.readdirSync(abs, { withFileTypes: true });
|
|
270
474
|
} catch {
|
|
271
475
|
return [];
|
|
272
476
|
}
|
|
273
477
|
};
|
|
274
478
|
function find(abs, re, depth) {
|
|
275
479
|
for (const e of names(abs)) {
|
|
276
|
-
if (re.test(e.name)) return
|
|
480
|
+
if (re.test(e.name)) return path5.join(abs, e.name);
|
|
277
481
|
if (e.isDirectory() && depth > 0 && !SKIP.has(e.name) && !e.name.startsWith(".")) {
|
|
278
|
-
const hit = find(
|
|
482
|
+
const hit = find(path5.join(abs, e.name), re, depth - 1);
|
|
279
483
|
if (hit) return hit;
|
|
280
484
|
}
|
|
281
485
|
}
|
|
@@ -283,14 +487,14 @@ function find(abs, re, depth) {
|
|
|
283
487
|
}
|
|
284
488
|
var read = (abs) => {
|
|
285
489
|
try {
|
|
286
|
-
return
|
|
490
|
+
return fs5.readFileSync(abs, "utf8");
|
|
287
491
|
} catch {
|
|
288
492
|
return "";
|
|
289
493
|
}
|
|
290
494
|
};
|
|
291
495
|
var readJson = (abs) => {
|
|
292
496
|
try {
|
|
293
|
-
return JSON.parse(
|
|
497
|
+
return JSON.parse(fs5.readFileSync(abs, "utf8"));
|
|
294
498
|
} catch {
|
|
295
499
|
return null;
|
|
296
500
|
}
|
|
@@ -299,8 +503,8 @@ var APP_DEPS = ["next", "nuxt", "vite", "react-scripts", "@angular/core", "@svel
|
|
|
299
503
|
function guessKind(dir) {
|
|
300
504
|
const found = {};
|
|
301
505
|
const add = (kind, why) => (found[kind] ??= []).push(why);
|
|
302
|
-
const has2 = (name) =>
|
|
303
|
-
const pkg = readJson(
|
|
506
|
+
const has2 = (name) => fs5.existsSync(path5.join(dir, name));
|
|
507
|
+
const pkg = readJson(path5.join(dir, "package.json"));
|
|
304
508
|
const deps = { ...pkg?.dependencies, ...pkg?.devDependencies };
|
|
305
509
|
if (has2("pubspec.yaml")) add("mobile", "Flutter (pubspec.yaml)");
|
|
306
510
|
if (deps["react-native"] || deps.expo) add("mobile", "React Native");
|
|
@@ -312,7 +516,7 @@ function guessKind(dir) {
|
|
|
312
516
|
if (has2("Pulumi.yaml")) add("infra", "Pulumi");
|
|
313
517
|
if (find(dir, /^(Chart|kustomization)\.ya?ml$/, 2)) add("infra", "Helm / Kustomize");
|
|
314
518
|
if (pkg && !pkg.private && !pkg.bin && (pkg.main || pkg.exports || pkg.module)) add("library", "package.json exports");
|
|
315
|
-
if (/^\[lib\]/m.test(read(
|
|
519
|
+
if (/^\[lib\]/m.test(read(path5.join(dir, "Cargo.toml")))) add("library", "Cargo [lib]");
|
|
316
520
|
if (pkg?.bin) add("app", "CLI (bin)");
|
|
317
521
|
const framework = APP_DEPS.find((d) => deps[d]);
|
|
318
522
|
if (framework) add("app", framework);
|
|
@@ -330,13 +534,13 @@ function guessKind(dir) {
|
|
|
330
534
|
function findContracts(root2) {
|
|
331
535
|
const hit = find(root2, /(\.proto|^openapi.*\.ya?ml|^openapi.*\.json|^asyncapi.*\.ya?ml)$/i, 3);
|
|
332
536
|
if (!hit) return null;
|
|
333
|
-
const dir =
|
|
334
|
-
return dir ? `${dir}/` :
|
|
537
|
+
const dir = path5.relative(root2, path5.dirname(hit)).split(path5.sep).join("/");
|
|
538
|
+
return dir ? `${dir}/` : path5.basename(hit);
|
|
335
539
|
}
|
|
336
540
|
var looksLikeMicroservices = (root2, projects) => projects >= 2 && (!!findContracts(root2) || !!find(root2, /^(docker-)?compose(\.[\w-]+)?\.ya?ml$/, 0));
|
|
337
541
|
function detectProfile(dir, kind) {
|
|
338
|
-
const has2 = (name) =>
|
|
339
|
-
const pkg = readJson(
|
|
542
|
+
const has2 = (name) => fs5.existsSync(path5.join(dir, name));
|
|
543
|
+
const pkg = readJson(path5.join(dir, "package.json"));
|
|
340
544
|
if (kind === "library") return { kind, published: !!pkg?.publishConfig || !!pkg?.name && !pkg.private };
|
|
341
545
|
if (kind === "mobile") {
|
|
342
546
|
const platforms = ["ios", "android", "web"].filter((p3) => has2(p3));
|
|
@@ -349,7 +553,7 @@ function detectProfile(dir, kind) {
|
|
|
349
553
|
}
|
|
350
554
|
if (kind === "infra") {
|
|
351
555
|
const envDir = ["envs", "environments", "env"].find((d) => has2(d));
|
|
352
|
-
const envs = envDir ? names(
|
|
556
|
+
const envs = envDir ? names(path5.join(dir, envDir)).filter((e) => e.isDirectory()).map((e) => e.name) : [];
|
|
353
557
|
return { kind, environments: envs.length ? envs.join(", ") : "dev, staging, prod" };
|
|
354
558
|
}
|
|
355
559
|
return { kind };
|
|
@@ -370,83 +574,88 @@ function kindGuidance(p3) {
|
|
|
370
574
|
}
|
|
371
575
|
var contractsGuidance = (contracts) => `Microservices: contracts (APIs, events) live in \`${contracts}\`. Changing one is a shared decision (\`docs/architecture/contracts.md\`) and every consumer gets a task.`;
|
|
372
576
|
function existingDocs(root2) {
|
|
373
|
-
const docs =
|
|
577
|
+
const docs = path5.join(root2, "docs");
|
|
374
578
|
const out = [];
|
|
375
579
|
for (const e of names(docs)) {
|
|
376
580
|
if (e.name.startsWith(".") || e.name === "tasks" || e.name === "proposals") continue;
|
|
377
581
|
if (e.isFile() && e.name.toLowerCase().endsWith(".md")) out.push(`docs/${e.name}`);
|
|
378
|
-
else if (e.isDirectory() && find(
|
|
582
|
+
else if (e.isDirectory() && find(path5.join(docs, e.name), /\.md$/i, 2)) out.push(`docs/${e.name}/`);
|
|
379
583
|
}
|
|
380
|
-
for (const adr of ["adr", "decisions"]) if (find(
|
|
584
|
+
for (const adr of ["adr", "decisions"]) if (find(path5.join(root2, adr), /\.md$/i, 1)) out.push(`${adr}/`);
|
|
381
585
|
return out.sort();
|
|
382
586
|
}
|
|
383
587
|
|
|
384
588
|
// src/core/scaffold.ts
|
|
385
|
-
import
|
|
386
|
-
import
|
|
589
|
+
import fs6 from "fs";
|
|
590
|
+
import path6 from "path";
|
|
387
591
|
var today = () => (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
388
592
|
function fill(text3, vars) {
|
|
389
593
|
return text3.replace(/\{\{(\w+)\}\}/g, (_, k) => vars[k] ?? "");
|
|
390
594
|
}
|
|
391
595
|
function copyTree(src, dest, vars, created, root2) {
|
|
392
|
-
for (const e of
|
|
393
|
-
const from =
|
|
394
|
-
const to =
|
|
596
|
+
for (const e of fs6.readdirSync(src, { withFileTypes: true })) {
|
|
597
|
+
const from = path6.join(src, e.name);
|
|
598
|
+
const to = path6.join(dest, fill(e.name, vars));
|
|
599
|
+
assertInside(to);
|
|
395
600
|
if (e.isDirectory()) {
|
|
396
|
-
|
|
601
|
+
fs6.mkdirSync(to, { recursive: true });
|
|
397
602
|
copyTree(from, to, vars, created, root2);
|
|
398
|
-
} else if (!
|
|
399
|
-
|
|
400
|
-
created.push(toPosix(
|
|
603
|
+
} else if (!fs6.existsSync(to)) {
|
|
604
|
+
fs6.writeFileSync(to, fill(fs6.readFileSync(from, "utf8"), vars));
|
|
605
|
+
created.push(toPosix(path6.relative(root2, to)));
|
|
401
606
|
}
|
|
402
607
|
}
|
|
403
608
|
}
|
|
404
609
|
function createMainDoc(root2, name) {
|
|
405
|
-
const file =
|
|
406
|
-
if (
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
}
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
}
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
610
|
+
const file = path6.join(root2, name);
|
|
611
|
+
if (fs6.existsSync(file)) return;
|
|
612
|
+
assertInside(file);
|
|
613
|
+
const text3 = fs6.readFileSync(path6.join(TEMPLATES_DIR, "main.md"), "utf8");
|
|
614
|
+
fs6.writeFileSync(file, fill(text3, { name: path6.basename(root2), date: today() }));
|
|
615
|
+
}
|
|
616
|
+
var vsCodeFile = (root2) => path6.join(root2, ".vscode", "settings.json");
|
|
617
|
+
var HIDDEN = "**/.markode";
|
|
618
|
+
function readVsCode(root2) {
|
|
619
|
+
if (!fs6.existsSync(vsCodeFile(root2))) return {};
|
|
620
|
+
try {
|
|
621
|
+
return JSON.parse(fs6.readFileSync(vsCodeFile(root2), "utf8"));
|
|
622
|
+
} catch {
|
|
623
|
+
return null;
|
|
624
|
+
}
|
|
420
625
|
}
|
|
626
|
+
var isHiddenInVsCode = (root2) => readVsCode(root2)?.["files.exclude"]?.[HIDDEN] === true;
|
|
421
627
|
function hideInVsCode(root2) {
|
|
422
|
-
const
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
return false;
|
|
429
|
-
}
|
|
430
|
-
}
|
|
431
|
-
settings["files.exclude"] = { ...settings["files.exclude"] ?? {}, "**/.markode": true };
|
|
432
|
-
fs5.mkdirSync(path5.dirname(file), { recursive: true });
|
|
433
|
-
fs5.writeFileSync(file, JSON.stringify(settings, null, 2) + "\n");
|
|
628
|
+
const settings = readVsCode(root2);
|
|
629
|
+
if (!settings) return false;
|
|
630
|
+
assertInside(vsCodeFile(root2));
|
|
631
|
+
settings["files.exclude"] = { ...settings["files.exclude"] ?? {}, [HIDDEN]: true };
|
|
632
|
+
fs6.mkdirSync(path6.dirname(vsCodeFile(root2)), { recursive: true });
|
|
633
|
+
fs6.writeFileSync(vsCodeFile(root2), JSON.stringify(settings, null, 2) + "\n");
|
|
434
634
|
return true;
|
|
435
635
|
}
|
|
636
|
+
function showInVsCode(root2) {
|
|
637
|
+
const settings = readVsCode(root2);
|
|
638
|
+
if (!settings?.["files.exclude"]?.[HIDDEN]) return;
|
|
639
|
+
delete settings["files.exclude"][HIDDEN];
|
|
640
|
+
if (!Object.keys(settings["files.exclude"]).length) delete settings["files.exclude"];
|
|
641
|
+
assertInside(vsCodeFile(root2));
|
|
642
|
+
fs6.writeFileSync(vsCodeFile(root2), JSON.stringify(settings, null, 2) + "\n");
|
|
643
|
+
}
|
|
436
644
|
function missingMainDocs(root2) {
|
|
437
645
|
return ["", ...findSubprojects(scanTree(root2))].flatMap((dir) => {
|
|
438
|
-
const doc = missingMainDoc(
|
|
439
|
-
return doc ? [{ path:
|
|
646
|
+
const doc = missingMainDoc(path6.join(root2, dir));
|
|
647
|
+
return doc ? [{ path: path6.posix.join(dir, doc), pointsTo: doc === "CLAUDE.md" ? "AGENTS.md" : "CLAUDE.md" }] : [];
|
|
440
648
|
});
|
|
441
649
|
}
|
|
442
650
|
function linkMainDocs(root2) {
|
|
443
|
-
return missingMainDocs(root2).flatMap((m) => linkMainDoc(
|
|
651
|
+
return missingMainDocs(root2).flatMap((m) => linkMainDoc(path6.join(root2, path6.posix.dirname(m.path))) ? [m.path] : []);
|
|
444
652
|
}
|
|
445
653
|
|
|
446
654
|
// src/core/sdd.ts
|
|
447
655
|
import { spawnSync } from "child_process";
|
|
448
|
-
import
|
|
449
|
-
import
|
|
656
|
+
import fs7 from "fs";
|
|
657
|
+
import os2 from "os";
|
|
658
|
+
import path7 from "path";
|
|
450
659
|
var SDD_AGENTS = {
|
|
451
660
|
claude: { label: "Claude Code", openspec: "claude", speckit: "claude" },
|
|
452
661
|
codex: { label: "Codex", openspec: "codex", speckit: "codex" },
|
|
@@ -457,11 +666,11 @@ var SDD_AGENTS = {
|
|
|
457
666
|
var OPENSPEC_PKG = "@fission-ai/openspec@latest";
|
|
458
667
|
var SPECKIT_SRC = "git+https://github.com/github/spec-kit.git";
|
|
459
668
|
function detectWorkflow(root2) {
|
|
460
|
-
if (
|
|
461
|
-
if (
|
|
669
|
+
if (fs7.existsSync(path7.join(root2, "openspec"))) return "openspec";
|
|
670
|
+
if (fs7.existsSync(path7.join(root2, ".specify"))) return "speckit";
|
|
462
671
|
return null;
|
|
463
672
|
}
|
|
464
|
-
var has = (cmd) => spawnSync(cmd, ["--version"], { stdio: "ignore", shell: process.platform === "win32" }).status === 0;
|
|
673
|
+
var has = (cmd) => spawnSync(cmd, ["--version"], { cwd: os2.homedir(), stdio: "ignore", shell: process.platform === "win32" }).status === 0;
|
|
465
674
|
var hasUv = () => has("uvx");
|
|
466
675
|
function installCommand(workflow, agents) {
|
|
467
676
|
if (workflow === "openspec") {
|
|
@@ -482,11 +691,11 @@ function installWorkflow(root2, workflow, agents, official) {
|
|
|
482
691
|
else result.error = (run.error?.message ?? `${run.stderr ?? ""}
|
|
483
692
|
${run.stdout ?? ""}`).trim().split("\n").slice(-6).join("\n");
|
|
484
693
|
}
|
|
485
|
-
copyTree(
|
|
694
|
+
copyTree(path7.join(TEMPLATES_DIR, "sdd", workflow), root2, { date: today2() }, result.created, root2);
|
|
486
695
|
const dirs2 = /* @__PURE__ */ new Set();
|
|
487
696
|
for (const f of flattenFiles(scanTree(root2), true)) {
|
|
488
697
|
if (before.has(f.path) || !f.path.startsWith(".") || f.path.startsWith(".specify/memory/")) continue;
|
|
489
|
-
dirs2.add(
|
|
698
|
+
dirs2.add(path7.posix.dirname(f.path));
|
|
490
699
|
}
|
|
491
700
|
for (const dir of dirs2) setIgnored(root2, dir, "dir", true);
|
|
492
701
|
result.ignored = [...dirs2].sort();
|
|
@@ -520,87 +729,89 @@ function applyWorkflowToFile(file, workflow, text3) {
|
|
|
520
729
|
function writeManagedBlock(file, body, top = false, name = "workflow") {
|
|
521
730
|
const START2 = `<!-- markode:${name}:start -->`;
|
|
522
731
|
const END2 = `<!-- markode:${name}:end -->`;
|
|
523
|
-
|
|
732
|
+
assertInside(file);
|
|
733
|
+
const text3 = fs7.readFileSync(file, "utf8");
|
|
524
734
|
const s = text3.indexOf(START2);
|
|
525
735
|
const e = text3.indexOf(END2);
|
|
526
736
|
const existing = s >= 0 && e > s ? text3.slice(s, e + END2.length) : null;
|
|
527
737
|
if (body === null) {
|
|
528
|
-
if (existing)
|
|
738
|
+
if (existing) fs7.writeFileSync(file, text3.replace(new RegExp(`\\n*${escapeRe(existing)}\\n?`), "\n"));
|
|
529
739
|
return;
|
|
530
740
|
}
|
|
531
741
|
const section2 = `${START2}
|
|
532
742
|
${body}
|
|
533
743
|
${END2}`;
|
|
534
|
-
if (existing
|
|
744
|
+
if (existing === section2) return;
|
|
745
|
+
if (existing) return fs7.writeFileSync(file, text3.replace(existing, () => section2));
|
|
535
746
|
const title = top ? /^# .*\n/m.exec(text3) : null;
|
|
536
|
-
if (title) return
|
|
537
|
-
|
|
747
|
+
if (title) return fs7.writeFileSync(file, text3.slice(0, title.index + title[0].length) + "\n" + section2 + "\n" + text3.slice(title.index + title[0].length));
|
|
748
|
+
fs7.writeFileSync(file, text3.replace(/\s*$/, "") + "\n\n" + section2 + "\n");
|
|
538
749
|
}
|
|
539
750
|
var escapeRe = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
540
751
|
var dirs = (abs) => {
|
|
541
752
|
try {
|
|
542
|
-
return
|
|
753
|
+
return fs7.readdirSync(abs, { withFileTypes: true }).filter((e) => e.isDirectory() && !e.name.startsWith(".")).map((e) => e.name).sort();
|
|
543
754
|
} catch {
|
|
544
755
|
return [];
|
|
545
756
|
}
|
|
546
757
|
};
|
|
547
758
|
var read2 = (abs) => {
|
|
548
759
|
try {
|
|
549
|
-
return
|
|
760
|
+
return fs7.readFileSync(abs, "utf8");
|
|
550
761
|
} catch {
|
|
551
762
|
return null;
|
|
552
763
|
}
|
|
553
764
|
};
|
|
554
|
-
var progress = (text3) => ({ done: text3?.match(
|
|
765
|
+
var progress = (text3) => ({ done: text3?.match(/^[ \t]*[-*+] \[[xX]\]/gm)?.length ?? 0, total: text3?.match(/^[ \t]*[-*+] \[[ xX]\]/gm)?.length ?? 0 });
|
|
555
766
|
var heading = (text3) => text3?.match(/^#\s+(.+)$/m)?.[1].trim() ?? null;
|
|
556
767
|
var countSpecs = (abs) => {
|
|
557
768
|
try {
|
|
558
|
-
return
|
|
769
|
+
return fs7.readdirSync(abs, { recursive: true }).filter((f) => String(f).endsWith("spec.md")).length;
|
|
559
770
|
} catch {
|
|
560
771
|
return 0;
|
|
561
772
|
}
|
|
562
773
|
};
|
|
563
774
|
function summarizeSdd(root2) {
|
|
564
775
|
const out = {};
|
|
565
|
-
const
|
|
566
|
-
if (
|
|
567
|
-
const rel = (...p3) => toPosix(
|
|
776
|
+
const os5 = path7.join(root2, "openspec");
|
|
777
|
+
if (fs7.existsSync(os5)) {
|
|
778
|
+
const rel = (...p3) => toPosix(path7.join("openspec", ...p3));
|
|
568
779
|
out.openspec = {
|
|
569
|
-
changes: dirs(
|
|
570
|
-
const dir =
|
|
571
|
-
const proposal = read2(
|
|
572
|
-
const tasks = read2(
|
|
780
|
+
changes: dirs(path7.join(os5, "changes")).filter((id) => id !== "archive").map((id) => {
|
|
781
|
+
const dir = path7.join(os5, "changes", id);
|
|
782
|
+
const proposal = read2(path7.join(dir, "proposal.md"));
|
|
783
|
+
const tasks = read2(path7.join(dir, "tasks.md"));
|
|
573
784
|
return {
|
|
574
785
|
id,
|
|
575
786
|
// "Proposal: Add login" → "Add login"
|
|
576
787
|
title: heading(proposal)?.replace(/^(change\s+)?proposal:\s*/i, "") ?? id,
|
|
577
788
|
path: rel("changes", id, proposal !== null ? "proposal.md" : "tasks.md"),
|
|
578
|
-
artifacts: { proposal: proposal !== null, design:
|
|
789
|
+
artifacts: { proposal: proposal !== null, design: fs7.existsSync(path7.join(dir, "design.md")), tasks: tasks !== null, specs: countSpecs(path7.join(dir, "specs")) },
|
|
579
790
|
tasks: progress(tasks)
|
|
580
791
|
};
|
|
581
792
|
}),
|
|
582
|
-
specs: dirs(
|
|
583
|
-
archived: dirs(
|
|
584
|
-
const file = ["proposal.md", "tasks.md"].find((f) =>
|
|
793
|
+
specs: dirs(path7.join(os5, "specs")).filter((id) => fs7.existsSync(path7.join(os5, "specs", id, "spec.md"))).map((id) => ({ id, path: rel("specs", id, "spec.md") })),
|
|
794
|
+
archived: dirs(path7.join(os5, "changes", "archive")).reverse().slice(0, 12).map((id) => {
|
|
795
|
+
const file = ["proposal.md", "tasks.md"].find((f) => fs7.existsSync(path7.join(os5, "changes", "archive", id, f)));
|
|
585
796
|
return { id, path: file ? rel("changes", "archive", id, file) : "" };
|
|
586
797
|
})
|
|
587
798
|
};
|
|
588
799
|
}
|
|
589
|
-
if (
|
|
590
|
-
const constitution = read2(
|
|
800
|
+
if (fs7.existsSync(path7.join(root2, ".specify"))) {
|
|
801
|
+
const constitution = read2(path7.join(root2, ".specify", "memory", "constitution.md"));
|
|
591
802
|
out.speckit = {
|
|
592
803
|
// The template is full of [PLACEHOLDERS] until /speckit-constitution fills it.
|
|
593
804
|
constitution: constitution === null ? null : { path: ".specify/memory/constitution.md", filled: !/\[[A-Z_0-9]+\]/.test(constitution) },
|
|
594
|
-
features: dirs(
|
|
595
|
-
const dir =
|
|
596
|
-
const spec = read2(
|
|
597
|
-
const tasks = read2(
|
|
805
|
+
features: dirs(path7.join(root2, "specs")).map((id) => {
|
|
806
|
+
const dir = path7.join(root2, "specs", id);
|
|
807
|
+
const spec = read2(path7.join(dir, "spec.md"));
|
|
808
|
+
const tasks = read2(path7.join(dir, "tasks.md"));
|
|
598
809
|
return {
|
|
599
810
|
id,
|
|
600
811
|
// "Feature Specification: Login" → "Login"
|
|
601
812
|
title: heading(spec)?.replace(/^feature specification:\s*/i, "") ?? id,
|
|
602
|
-
path: toPosix(
|
|
603
|
-
artifacts: { spec: spec !== null, plan:
|
|
813
|
+
path: toPosix(path7.join("specs", id, spec !== null ? "spec.md" : "plan.md")),
|
|
814
|
+
artifacts: { spec: spec !== null, plan: fs7.existsSync(path7.join(dir, "plan.md")), tasks: tasks !== null },
|
|
604
815
|
tasks: progress(tasks)
|
|
605
816
|
};
|
|
606
817
|
})
|
|
@@ -612,27 +823,181 @@ function sddPrefixes(summary) {
|
|
|
612
823
|
return [...summary.openspec ? ["openspec/"] : [], ...summary.speckit ? ["specs/", ".specify/"] : []];
|
|
613
824
|
}
|
|
614
825
|
|
|
826
|
+
// src/core/skills.ts
|
|
827
|
+
import fs8 from "fs";
|
|
828
|
+
import os3 from "os";
|
|
829
|
+
import path8 from "path";
|
|
830
|
+
import YAML2 from "yaml";
|
|
831
|
+
var SOURCE_DIRS = {
|
|
832
|
+
claude: [".claude", "skills"],
|
|
833
|
+
// Shared folder read by Codex, Copilot and Gemini CLI, and where `npx skills` installs.
|
|
834
|
+
agents: [".agents", "skills"],
|
|
835
|
+
codex: [".codex", "skills"],
|
|
836
|
+
gemini: [".gemini", "skills"],
|
|
837
|
+
copilot: [".copilot", "skills"],
|
|
838
|
+
cursor: [".cursor", "skills"]
|
|
839
|
+
};
|
|
840
|
+
var PROJECT_TARGETS = { claude: ".claude/skills", agents: ".agents/skills" };
|
|
841
|
+
var READS = { claude: ["claude"], agents: ["agents", "codex", "gemini", "copilot", "cursor"] };
|
|
842
|
+
var sourceDir = (source) => path8.join(os3.homedir(), ...SOURCE_DIRS[source]);
|
|
843
|
+
var display = (p3) => p3.startsWith(os3.homedir() + path8.sep) ? "~" + p3.slice(os3.homedir().length) : p3;
|
|
844
|
+
function skillSources() {
|
|
845
|
+
return Object.keys(SOURCE_DIRS).map((id) => ({ id, path: display(sourceDir(id)), exists: fs8.existsSync(sourceDir(id)) }));
|
|
846
|
+
}
|
|
847
|
+
var tagsFile = () => path8.join(GLOBAL_DIR, "skills.json");
|
|
848
|
+
function readTags() {
|
|
849
|
+
try {
|
|
850
|
+
return JSON.parse(fs8.readFileSync(tagsFile(), "utf8")).tags ?? {};
|
|
851
|
+
} catch {
|
|
852
|
+
return {};
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
function setSkillTags(name, tags) {
|
|
856
|
+
const all = readTags();
|
|
857
|
+
const clean = [...new Set(tags.map((t) => t.trim().toLowerCase()).filter(Boolean))];
|
|
858
|
+
if (clean.length) all[name] = clean;
|
|
859
|
+
else delete all[name];
|
|
860
|
+
fs8.mkdirSync(GLOBAL_DIR, { recursive: true });
|
|
861
|
+
fs8.writeFileSync(tagsFile(), JSON.stringify({ tags: all }, null, 2) + "\n");
|
|
862
|
+
return clean;
|
|
863
|
+
}
|
|
864
|
+
function findSkills(dir, source, managed, depth = 0) {
|
|
865
|
+
let entries2;
|
|
866
|
+
try {
|
|
867
|
+
entries2 = fs8.readdirSync(dir, { withFileTypes: true });
|
|
868
|
+
} catch {
|
|
869
|
+
return [];
|
|
870
|
+
}
|
|
871
|
+
return entries2.flatMap((e) => {
|
|
872
|
+
const abs = path8.join(dir, e.name);
|
|
873
|
+
let real;
|
|
874
|
+
try {
|
|
875
|
+
if (!fs8.statSync(abs).isDirectory()) return [];
|
|
876
|
+
real = fs8.realpathSync(abs);
|
|
877
|
+
} catch {
|
|
878
|
+
return [];
|
|
879
|
+
}
|
|
880
|
+
if (fs8.existsSync(path8.join(abs, "SKILL.md"))) return [{ dir: abs, real, source, managed }];
|
|
881
|
+
return depth < 2 ? findSkills(abs, source, managed || e.name === "synced" || e.name.startsWith("."), depth + 1) : [];
|
|
882
|
+
});
|
|
883
|
+
}
|
|
884
|
+
function frontmatter(text3) {
|
|
885
|
+
const m = /^---\r?\n([\s\S]*?)\r?\n---/.exec(text3);
|
|
886
|
+
if (!m) return null;
|
|
887
|
+
try {
|
|
888
|
+
const data = YAML2.parse(m[1], { logLevel: "error" });
|
|
889
|
+
return data && typeof data === "object" ? data : null;
|
|
890
|
+
} catch {
|
|
891
|
+
return null;
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
function listFiles(dir) {
|
|
895
|
+
try {
|
|
896
|
+
return fs8.readdirSync(dir, { recursive: true, withFileTypes: true }).map((e) => ({ e, rel: path8.relative(dir, path8.join(e.parentPath ?? e.path ?? dir, e.name)).split(path8.sep).join("/") })).filter(({ e, rel }) => e.isFile() && rel !== "SKILL.md").map(({ rel }) => rel).sort().slice(0, 100);
|
|
897
|
+
} catch {
|
|
898
|
+
return [];
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
function listSkills(root2) {
|
|
902
|
+
const byReal = /* @__PURE__ */ new Map();
|
|
903
|
+
for (const source of Object.keys(SOURCE_DIRS)) {
|
|
904
|
+
for (const f of findSkills(sourceDir(source), source, false)) byReal.set(f.real, [...byReal.get(f.real) ?? [], f]);
|
|
905
|
+
}
|
|
906
|
+
const userTags = readTags();
|
|
907
|
+
const used = projectTargets(root2);
|
|
908
|
+
const skills = [];
|
|
909
|
+
for (const [real, found] of byReal) {
|
|
910
|
+
const text3 = fs8.readFileSync(path8.join(real, "SKILL.md"), "utf8");
|
|
911
|
+
const fm = frontmatter(text3);
|
|
912
|
+
const folder = path8.basename(found[0].dir);
|
|
913
|
+
const name = typeof fm?.name === "string" ? fm.name : folder;
|
|
914
|
+
const description = typeof fm?.description === "string" ? fm.description.trim() : "";
|
|
915
|
+
const issues = [];
|
|
916
|
+
if (!fm) issues.push("frontmatter");
|
|
917
|
+
else {
|
|
918
|
+
if (typeof fm.name !== "string" || !/^[a-z0-9]+(-[a-z0-9]+)*$/.test(fm.name) || fm.name.length > 64) issues.push("name");
|
|
919
|
+
else if (fm.name !== folder) issues.push("name-dir");
|
|
920
|
+
if (!description) issues.push("description");
|
|
921
|
+
else if (description.length > 1024) issues.push("description-long");
|
|
922
|
+
}
|
|
923
|
+
const installed = Object.keys(PROJECT_TARGETS).filter((t) => fs8.existsSync(path8.join(root2, PROJECT_TARGETS[t], folder, "SKILL.md")));
|
|
924
|
+
const declared = fm?.metadata?.tags;
|
|
925
|
+
const metaTags = Array.isArray(declared) ? declared.map(String) : typeof declared === "string" ? declared.split(",") : [];
|
|
926
|
+
skills.push({
|
|
927
|
+
key: real,
|
|
928
|
+
name,
|
|
929
|
+
folder,
|
|
930
|
+
description,
|
|
931
|
+
locations: found.map((f) => ({ source: f.source, path: display(f.dir) })),
|
|
932
|
+
managed: found.every((f) => f.managed),
|
|
933
|
+
files: listFiles(real),
|
|
934
|
+
tags: [.../* @__PURE__ */ new Set([...userTags[name] ?? [], ...metaTags.map((t) => t.trim().toLowerCase()).filter(Boolean)])],
|
|
935
|
+
issues,
|
|
936
|
+
installed,
|
|
937
|
+
status: Object.fromEntries(
|
|
938
|
+
used.map((t) => {
|
|
939
|
+
const global = found.some((f) => READS[t].includes(f.source));
|
|
940
|
+
const local = installed.includes(t);
|
|
941
|
+
return [t, global && local ? "both" : global ? "global" : local ? "project" : "missing"];
|
|
942
|
+
})
|
|
943
|
+
),
|
|
944
|
+
differs: installed.filter((t) => {
|
|
945
|
+
try {
|
|
946
|
+
return fs8.readFileSync(path8.join(root2, PROJECT_TARGETS[t], folder, "SKILL.md"), "utf8") !== text3;
|
|
947
|
+
} catch {
|
|
948
|
+
return false;
|
|
949
|
+
}
|
|
950
|
+
})
|
|
951
|
+
});
|
|
952
|
+
}
|
|
953
|
+
return skills.sort((a, b) => a.name.localeCompare(b.name));
|
|
954
|
+
}
|
|
955
|
+
function getSkill(root2, key) {
|
|
956
|
+
const skill = typeof key === "string" ? listSkills(root2).find((s) => s.key === key) : void 0;
|
|
957
|
+
if (!skill) throw Object.assign(new Error("Skill not found"), { code: "ENOENT" });
|
|
958
|
+
return skill;
|
|
959
|
+
}
|
|
960
|
+
function projectTargets(root2) {
|
|
961
|
+
const has2 = (p3) => fs8.existsSync(path8.join(root2, p3));
|
|
962
|
+
const out = [];
|
|
963
|
+
if (has2(".claude") || has2("CLAUDE.md")) out.push("claude");
|
|
964
|
+
if (has2(".agents") || has2("AGENTS.md") || has2(".codex") || has2(".gemini") || has2(".github/skills")) out.push("agents");
|
|
965
|
+
return out.length ? out : ["claude"];
|
|
966
|
+
}
|
|
967
|
+
function installSkill(root2, skill, targets, overwrite) {
|
|
968
|
+
const dests = targets.map((t) => path8.join(root2, PROJECT_TARGETS[t], skill.folder));
|
|
969
|
+
const existing = dests.filter((d) => fs8.existsSync(d));
|
|
970
|
+
if (existing.length && !overwrite) return { conflict: existing.map((d) => path8.relative(root2, d).split(path8.sep).join("/")) };
|
|
971
|
+
for (const dest of dests) {
|
|
972
|
+
assertInside(dest, root2);
|
|
973
|
+
fs8.rmSync(dest, { recursive: true, force: true });
|
|
974
|
+
fs8.mkdirSync(path8.dirname(dest), { recursive: true });
|
|
975
|
+
fs8.cpSync(skill.key, dest, { recursive: true, dereference: true });
|
|
976
|
+
}
|
|
977
|
+
return { ok: true };
|
|
978
|
+
}
|
|
979
|
+
|
|
615
980
|
// src/core/method.ts
|
|
616
981
|
var SKIP2 = /* @__PURE__ */ new Set(["docs", "node_modules", "dist", "build", "coverage", "tmp", "vendor", "target"]);
|
|
617
982
|
var MANIFESTS = ["package.json", "pyproject.toml", "go.mod", "Cargo.toml", "pom.xml", "build.gradle", "build.gradle.kts", "composer.json", "Gemfile", "deno.json", ".git"];
|
|
618
|
-
var isDir = (abs) => !!
|
|
983
|
+
var isDir = (abs) => !!fs9.statSync(abs, { throwIfNoEntry: false })?.isDirectory();
|
|
619
984
|
var subdirs = (abs) => {
|
|
620
985
|
try {
|
|
621
|
-
return
|
|
986
|
+
return fs9.readdirSync(abs, { withFileTypes: true }).filter((e) => e.isDirectory() && !e.name.startsWith(".") && !SKIP2.has(e.name)).map((e) => e.name);
|
|
622
987
|
} catch {
|
|
623
988
|
return [];
|
|
624
989
|
}
|
|
625
990
|
};
|
|
626
|
-
var isProject = (abs) => MANIFESTS.some((m) =>
|
|
991
|
+
var isProject = (abs) => MANIFESTS.some((m) => fs9.existsSync(path9.join(abs, m)));
|
|
627
992
|
function detectAreas(root2) {
|
|
628
993
|
const globs = [];
|
|
629
994
|
try {
|
|
630
|
-
const ws =
|
|
995
|
+
const ws = YAML3.parse(fs9.readFileSync(path9.join(root2, "pnpm-workspace.yaml"), "utf8"), { logLevel: "error" });
|
|
631
996
|
if (Array.isArray(ws?.packages)) globs.push(...ws.packages);
|
|
632
997
|
} catch {
|
|
633
998
|
}
|
|
634
999
|
try {
|
|
635
|
-
const ws = JSON.parse(
|
|
1000
|
+
const ws = JSON.parse(fs9.readFileSync(path9.join(root2, "package.json"), "utf8")).workspaces;
|
|
636
1001
|
globs.push(...Array.isArray(ws) ? ws : Array.isArray(ws?.packages) ? ws.packages : []);
|
|
637
1002
|
} catch {
|
|
638
1003
|
}
|
|
@@ -643,25 +1008,25 @@ function detectAreas(root2) {
|
|
|
643
1008
|
const base = g.replace(/\/\*\*?$/, "");
|
|
644
1009
|
if (base.includes("*")) continue;
|
|
645
1010
|
if (base === g) found.add(g);
|
|
646
|
-
else for (const name of subdirs(
|
|
1011
|
+
else for (const name of subdirs(path9.join(root2, base))) found.add(`${base}/${name}`);
|
|
647
1012
|
}
|
|
648
1013
|
if (!isProject(root2)) {
|
|
649
1014
|
for (const top of subdirs(root2)) {
|
|
650
|
-
if (isProject(
|
|
651
|
-
else for (const name of subdirs(
|
|
1015
|
+
if (isProject(path9.join(root2, top))) found.add(top);
|
|
1016
|
+
else for (const name of subdirs(path9.join(root2, top))) if (isProject(path9.join(root2, top, name))) found.add(`${top}/${name}`);
|
|
652
1017
|
}
|
|
653
1018
|
}
|
|
654
1019
|
let unsure = false;
|
|
655
1020
|
if (!globs.length && isProject(root2)) {
|
|
656
|
-
const tops = subdirs(root2).filter((top) => isProject(
|
|
1021
|
+
const tops = subdirs(root2).filter((top) => isProject(path9.join(root2, top)));
|
|
657
1022
|
if (tops.length >= 2 && tops.some((t) => !found.has(t))) tops.forEach((t) => found.add(t)), unsure = true;
|
|
658
1023
|
}
|
|
659
|
-
const dirs2 = [...found].filter((d) => d && !d.split("/").some((part) => part.startsWith(".") || SKIP2.has(part)) && isDir(
|
|
1024
|
+
const dirs2 = [...found].filter((d) => d && !d.split("/").some((part) => part.startsWith(".") || SKIP2.has(part)) && isDir(path9.join(root2, d)));
|
|
660
1025
|
return { dirs: dirs2.filter((d) => !dirs2.some((o) => o.startsWith(d + "/"))).sort(), unsure };
|
|
661
1026
|
}
|
|
662
1027
|
function areaDescription(root2, dir) {
|
|
663
1028
|
try {
|
|
664
|
-
return String(JSON.parse(
|
|
1029
|
+
return String(JSON.parse(fs9.readFileSync(path9.join(root2, dir, "package.json"), "utf8")).description ?? "").trim();
|
|
665
1030
|
} catch {
|
|
666
1031
|
return "";
|
|
667
1032
|
}
|
|
@@ -686,7 +1051,7 @@ var KNOWN_PREFIXES = {
|
|
|
686
1051
|
scripts: "SCR"
|
|
687
1052
|
};
|
|
688
1053
|
function suggestPrefix(dir, used) {
|
|
689
|
-
dir =
|
|
1054
|
+
dir = path9.posix.basename(dir);
|
|
690
1055
|
const base = KNOWN_PREFIXES[dir.toLowerCase()] ?? (dir.replace(/[^a-z0-9]/gi, "").slice(0, 3).toUpperCase() || "AREA");
|
|
691
1056
|
let prefix = base;
|
|
692
1057
|
for (let i = 2; used.has(prefix) || prefix === "CORE"; i++) prefix = `${base}${i}`;
|
|
@@ -696,24 +1061,25 @@ function suggestPrefix(dir, used) {
|
|
|
696
1061
|
var rootPrefix = (areas) => areas.length ? "CORE" : "T";
|
|
697
1062
|
function scaffoldMethod(root2, mainDoc, areas) {
|
|
698
1063
|
const created = [];
|
|
699
|
-
const name =
|
|
700
|
-
copyTree(
|
|
1064
|
+
const name = path9.basename(root2);
|
|
1065
|
+
copyTree(path9.join(TEMPLATES_DIR, "method", "root"), root2, { date: today(), name, prefix: rootPrefix(areas), area: areas.length ? "core" : name }, created, root2);
|
|
701
1066
|
for (const a of areas) {
|
|
702
1067
|
const vars = { date: today(), name, prefix: a.prefix, area: a.dir, up: "../".repeat(a.dir.split("/").length) };
|
|
703
|
-
const dir =
|
|
704
|
-
copyTree(
|
|
1068
|
+
const dir = path9.join(root2, a.dir);
|
|
1069
|
+
copyTree(path9.join(TEMPLATES_DIR, "method", "area"), dir, vars, created, root2);
|
|
705
1070
|
let doc = contentDoc(dir);
|
|
706
1071
|
if (!doc) {
|
|
707
|
-
doc =
|
|
708
|
-
|
|
709
|
-
|
|
1072
|
+
doc = path9.join(dir, mainDoc);
|
|
1073
|
+
assertInside(doc);
|
|
1074
|
+
fs9.writeFileSync(doc, fill(fs9.readFileSync(path9.join(TEMPLATES_DIR, "method", "area-main.md"), "utf8"), vars));
|
|
1075
|
+
created.push(path9.posix.join(a.dir, mainDoc));
|
|
710
1076
|
}
|
|
711
|
-
if (claudeUsed(root2, mainDoc) && linkAgentsDoc(dir)) created.push(
|
|
1077
|
+
if (claudeUsed(root2, mainDoc) && linkAgentsDoc(dir)) created.push(path9.posix.join(a.dir, "CLAUDE.md"));
|
|
712
1078
|
writeManagedBlock(doc, areaBlock(a, vars.up, mainDoc), true);
|
|
713
1079
|
}
|
|
714
1080
|
return created;
|
|
715
1081
|
}
|
|
716
|
-
var claudeUsed = (root2, mainDoc) => mainDoc === "CLAUDE.md" ||
|
|
1082
|
+
var claudeUsed = (root2, mainDoc) => mainDoc === "CLAUDE.md" || fs9.existsSync(path9.join(root2, "CLAUDE.md"));
|
|
717
1083
|
function areaBlock(a, up, mainDoc) {
|
|
718
1084
|
const guidance = a.profile && kindGuidance(a.profile);
|
|
719
1085
|
return [
|
|
@@ -778,8 +1144,25 @@ function methodSection(areas, root2, ctx = {}) {
|
|
|
778
1144
|
].join("\n");
|
|
779
1145
|
}
|
|
780
1146
|
function installMethodSkills(root2) {
|
|
781
|
-
const src =
|
|
782
|
-
for (const
|
|
1147
|
+
const src = path9.join(TEMPLATES_DIR, "method", "skills");
|
|
1148
|
+
for (const target of projectTargets(root2))
|
|
1149
|
+
for (const name of fs9.readdirSync(src)) {
|
|
1150
|
+
const dest = path9.join(root2, PROJECT_TARGETS[target], name);
|
|
1151
|
+
assertInside(dest);
|
|
1152
|
+
fs9.cpSync(path9.join(src, name), dest, { recursive: true });
|
|
1153
|
+
}
|
|
1154
|
+
}
|
|
1155
|
+
function setupMethod(root2, mainDoc, monorepo) {
|
|
1156
|
+
const used = /* @__PURE__ */ new Set();
|
|
1157
|
+
const areas = monorepo === "single" ? [] : detectAreas(root2).dirs.map((dir) => ({ dir, prefix: suggestPrefix(dir, used) }));
|
|
1158
|
+
for (const a of areas) a.profile = detectProfile(path9.join(root2, a.dir), guessKind(path9.join(root2, a.dir)).kind);
|
|
1159
|
+
const contracts = looksLikeMicroservices(root2, areas.length) ? findContracts(root2) : null;
|
|
1160
|
+
const profile = areas.length ? void 0 : detectProfile(root2, guessKind(root2).kind);
|
|
1161
|
+
const existing = existingDocs(root2);
|
|
1162
|
+
const created = scaffoldMethod(root2, mainDoc, areas);
|
|
1163
|
+
installMethodSkills(root2);
|
|
1164
|
+
applyWorkflowToFile(path9.join(root2, mainDoc), "markode", methodSection(areas, root2, { profile, contracts, existing }));
|
|
1165
|
+
return { created, areas };
|
|
783
1166
|
}
|
|
784
1167
|
|
|
785
1168
|
// src/core/check.ts
|
|
@@ -788,13 +1171,13 @@ var LINK = /!?\[[^\]]*\]\(([^)\s]+)(?:\s+"[^"]*")?\)/g;
|
|
|
788
1171
|
function checkDocs(root2) {
|
|
789
1172
|
const docs = summarize(root2);
|
|
790
1173
|
const status = new Map(docs.map((d) => [d.path, d.status ?? ""]));
|
|
791
|
-
const read3 = (rel) =>
|
|
1174
|
+
const read3 = (rel) => fs10.readFileSync(path10.join(root2, rel), "utf8");
|
|
792
1175
|
const phases = [];
|
|
793
1176
|
for (const phase of docs.filter((d) => d.type === "phase")) {
|
|
794
1177
|
for (const line of read3(phase.path).split("\n")) {
|
|
795
1178
|
const m = TASK_LINE.exec(line);
|
|
796
1179
|
if (!m) continue;
|
|
797
|
-
const task =
|
|
1180
|
+
const task = path10.posix.normalize(path10.posix.join(path10.posix.dirname(phase.path), m[2].split("#")[0]));
|
|
798
1181
|
const s = status.get(task);
|
|
799
1182
|
if (s === void 0) continue;
|
|
800
1183
|
const ticked = m[1] !== " ";
|
|
@@ -811,7 +1194,7 @@ function checkDocs(root2) {
|
|
|
811
1194
|
const target = m[1];
|
|
812
1195
|
if (/^[a-z][\w+.-]*:|^#|^\/|[<{]/i.test(target)) continue;
|
|
813
1196
|
const file = decodeURIComponent(target.split(/[#?]/)[0]);
|
|
814
|
-
if (file && !
|
|
1197
|
+
if (file && !fs10.existsSync(path10.join(root2, path10.dirname(doc.path), file))) links.push({ file: doc.path, line: i + 1, target });
|
|
815
1198
|
}
|
|
816
1199
|
});
|
|
817
1200
|
}
|
|
@@ -820,49 +1203,83 @@ function checkDocs(root2) {
|
|
|
820
1203
|
function projectsTable(root2) {
|
|
821
1204
|
const doc = mainDocOf(root2);
|
|
822
1205
|
if (!doc) return null;
|
|
823
|
-
const text3 =
|
|
1206
|
+
const text3 = fs10.readFileSync(path10.join(root2, doc), "utf8");
|
|
824
1207
|
const section2 = /^### Projects\n([\s\S]*?)(?=^#{1,3} |<!-- markode:workflow:end -->)/m.exec(text3)?.[1];
|
|
825
1208
|
if (!section2) return null;
|
|
826
1209
|
const listed = [...section2.matchAll(/^\|\s*`([^`]+?)\/?`\s*\|/gm)].map((m) => m[1]).filter((d) => d !== "docs");
|
|
827
1210
|
const found = detectAreas(root2).dirs;
|
|
828
|
-
return { missing: found.filter((d) => !listed.includes(d)), gone: listed.filter((d) => !
|
|
1211
|
+
return { missing: found.filter((d) => !listed.includes(d)), gone: listed.filter((d) => !fs10.existsSync(path10.join(root2, d))) };
|
|
829
1212
|
}
|
|
830
1213
|
|
|
831
1214
|
// src/core/defaults.ts
|
|
832
|
-
import
|
|
833
|
-
import
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
1215
|
+
import fs15 from "fs";
|
|
1216
|
+
import path15 from "path";
|
|
1217
|
+
|
|
1218
|
+
// src/core/features.ts
|
|
1219
|
+
import fs14 from "fs";
|
|
1220
|
+
import path14 from "path";
|
|
1221
|
+
|
|
1222
|
+
// src/core/hooks.ts
|
|
1223
|
+
import fs11 from "fs";
|
|
1224
|
+
import path11 from "path";
|
|
1225
|
+
var EVENTS = {
|
|
1226
|
+
SessionStart: "changes --start",
|
|
1227
|
+
UserPromptSubmit: "changes --hook UserPromptSubmit",
|
|
1228
|
+
PostToolUse: "changes --hook PostToolUse"
|
|
844
1229
|
};
|
|
845
|
-
var
|
|
846
|
-
|
|
1230
|
+
var LOCAL = "settings.local.json";
|
|
1231
|
+
var SHARED = "settings.json";
|
|
1232
|
+
var settingsFile2 = (root2, name = LOCAL) => path11.join(root2, ".claude", name);
|
|
1233
|
+
function cli() {
|
|
1234
|
+
const dirs2 = (process.env.PATH ?? "").split(path11.delimiter).filter(Boolean);
|
|
1235
|
+
if (dirs2.some((d) => ["markode", "markode.cmd"].some((f) => fs11.existsSync(path11.join(d, f))))) return "markode";
|
|
1236
|
+
const name = JSON.parse(fs11.readFileSync(path11.join(PKG_ROOT, "package.json"), "utf8")).name;
|
|
1237
|
+
return `npx -y --prefer-offline ${name}`;
|
|
1238
|
+
}
|
|
1239
|
+
var isOurs = (entry) => Array.isArray(entry?.hooks) && entry.hooks.some((h) => typeof h?.command === "string" && / changes --(start|hook)\b/.test(h.command));
|
|
1240
|
+
function readSettings2(root2, name = LOCAL) {
|
|
1241
|
+
const file = settingsFile2(root2, name);
|
|
1242
|
+
if (!fs11.existsSync(file)) return {};
|
|
847
1243
|
try {
|
|
848
|
-
|
|
849
|
-
if (!isLang(saved.sourceLang)) saved.sourceLang = BASE_DEFAULTS.sourceLang;
|
|
850
|
-
if (!MAIN_DOCS.includes(saved.mainDoc)) saved.mainDoc = BASE_DEFAULTS.mainDoc;
|
|
851
|
-
if (saved.monorepo !== "single") saved.monorepo = "per-project";
|
|
852
|
-
return saved;
|
|
1244
|
+
return JSON.parse(fs11.readFileSync(file, "utf8"));
|
|
853
1245
|
} catch {
|
|
854
1246
|
return null;
|
|
855
1247
|
}
|
|
856
1248
|
}
|
|
857
|
-
function
|
|
858
|
-
|
|
859
|
-
|
|
1249
|
+
function hasClaudeHooks(root2) {
|
|
1250
|
+
return [LOCAL, SHARED].some((name) => {
|
|
1251
|
+
const hooks = readSettings2(root2, name)?.hooks ?? {};
|
|
1252
|
+
return Object.keys(EVENTS).every((e) => (hooks[e] ?? []).some(isOurs));
|
|
1253
|
+
});
|
|
1254
|
+
}
|
|
1255
|
+
function writeHooks(root2, name, add) {
|
|
1256
|
+
const settings = readSettings2(root2, name);
|
|
1257
|
+
if (!settings) return false;
|
|
1258
|
+
if (!add && !fs11.existsSync(settingsFile2(root2, name))) return true;
|
|
1259
|
+
const base = cli();
|
|
1260
|
+
settings.hooks ??= {};
|
|
1261
|
+
for (const [event, args2] of Object.entries(EVENTS)) {
|
|
1262
|
+
const list = (settings.hooks[event] ?? []).filter((e) => !isOurs(e));
|
|
1263
|
+
if (add) list.push({ hooks: [{ type: "command", command: `${base} ${args2}` }] });
|
|
1264
|
+
if (list.length) settings.hooks[event] = list;
|
|
1265
|
+
else delete settings.hooks[event];
|
|
1266
|
+
}
|
|
1267
|
+
if (!Object.keys(settings.hooks).length) delete settings.hooks;
|
|
1268
|
+
assertInside(settingsFile2(root2, name));
|
|
1269
|
+
if (!Object.keys(settings).length) return fs11.rmSync(settingsFile2(root2, name), { force: true }), true;
|
|
1270
|
+
fs11.mkdirSync(path11.dirname(settingsFile2(root2, name)), { recursive: true });
|
|
1271
|
+
fs11.writeFileSync(settingsFile2(root2, name), JSON.stringify(settings, null, 2) + "\n");
|
|
1272
|
+
return true;
|
|
1273
|
+
}
|
|
1274
|
+
function installClaudeHooks(root2) {
|
|
1275
|
+
if (!writeHooks(root2, LOCAL, true)) return false;
|
|
1276
|
+
writeHooks(root2, SHARED, false);
|
|
1277
|
+
return true;
|
|
1278
|
+
}
|
|
1279
|
+
function removeClaudeHooks(root2) {
|
|
1280
|
+
writeHooks(root2, LOCAL, false);
|
|
1281
|
+
writeHooks(root2, SHARED, false);
|
|
860
1282
|
}
|
|
861
|
-
|
|
862
|
-
// src/core/i18n.ts
|
|
863
|
-
import crypto from "crypto";
|
|
864
|
-
import fs11 from "fs";
|
|
865
|
-
import path11 from "path";
|
|
866
1283
|
|
|
867
1284
|
// src/core/langdetect.ts
|
|
868
1285
|
var STOPWORDS = {
|
|
@@ -874,8 +1291,20 @@ var STOPWORDS = {
|
|
|
874
1291
|
it: ["il", "lo", "la", "gli", "le", "di", "del", "della", "che", "e", "\xE8", "un", "una", "per", "con", "non", "sono", "si", "come", "al", "alla", "pi\xF9", "ma", "suo", "questo", "questa", "quando", "ogni", "deve", "anche"]
|
|
875
1292
|
};
|
|
876
1293
|
var SETS = Object.fromEntries(Object.entries(STOPWORDS).map(([l, w]) => [l, new Set(w)]));
|
|
1294
|
+
function cut(text3, open2, close) {
|
|
1295
|
+
let out = "";
|
|
1296
|
+
let i = 0;
|
|
1297
|
+
for (; ; ) {
|
|
1298
|
+
const s = text3.indexOf(open2, i);
|
|
1299
|
+
const e = s < 0 ? -1 : text3.indexOf(close, s + open2.length);
|
|
1300
|
+
if (e < 0) return out + text3.slice(i);
|
|
1301
|
+
out += text3.slice(i, s) + " ";
|
|
1302
|
+
i = e + close.length;
|
|
1303
|
+
}
|
|
1304
|
+
}
|
|
1305
|
+
var SAMPLE = 2e5;
|
|
877
1306
|
function prose(md) {
|
|
878
|
-
return md.replace(/^---\r?\n[\s\S]*?\r?\n---/, " ")
|
|
1307
|
+
return cut(cut(md.slice(0, SAMPLE).replace(/^---\r?\n[\s\S]*?\r?\n---/, " "), "```", "```"), "<!--", "-->").replace(/`[^`]*`/g, " ").replace(/\]\([^)]*\)/g, " ").replace(/https?:\/\/\S+/g, " ").replace(/[|#>*_\-[\]()]/g, " ");
|
|
879
1308
|
}
|
|
880
1309
|
function detectLang(md) {
|
|
881
1310
|
const words = prose(md).toLowerCase().match(/[\p{L}]+/gu) ?? [];
|
|
@@ -887,43 +1316,146 @@ function detectLang(md) {
|
|
|
887
1316
|
return best.lang;
|
|
888
1317
|
}
|
|
889
1318
|
|
|
1319
|
+
// src/core/proposals.ts
|
|
1320
|
+
import fs12 from "fs";
|
|
1321
|
+
import path12 from "path";
|
|
1322
|
+
|
|
1323
|
+
// src/core/git.ts
|
|
1324
|
+
import { execFileSync } from "child_process";
|
|
1325
|
+
import os4 from "os";
|
|
1326
|
+
function runGit(root2, args2) {
|
|
1327
|
+
return execFileSync("git", ["-C", root2, "-c", "core.fsmonitor=false", "-c", "core.quotepath=off", ...args2], {
|
|
1328
|
+
cwd: os4.homedir(),
|
|
1329
|
+
encoding: "utf8",
|
|
1330
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
1331
|
+
maxBuffer: 32 * 1024 * 1024
|
|
1332
|
+
});
|
|
1333
|
+
}
|
|
1334
|
+
function git(root2, args2) {
|
|
1335
|
+
try {
|
|
1336
|
+
return runGit(root2, args2);
|
|
1337
|
+
} catch (err) {
|
|
1338
|
+
return typeof err?.stdout === "string" && err.stdout ? err.stdout : null;
|
|
1339
|
+
}
|
|
1340
|
+
}
|
|
1341
|
+
var isGitRepo = (root2) => git(root2, ["rev-parse", "--is-inside-work-tree"])?.trim() === "true";
|
|
1342
|
+
function gitStatuses(root2) {
|
|
1343
|
+
const prefix = git(root2, ["rev-parse", "--show-prefix"])?.trim() ?? "";
|
|
1344
|
+
const out = git(root2, ["status", "--porcelain=v1", "-z", "--untracked-files=all", "--", "."]);
|
|
1345
|
+
const result = {};
|
|
1346
|
+
if (!out) return result;
|
|
1347
|
+
const parts = out.split("\0");
|
|
1348
|
+
for (let i = 0; i < parts.length; i++) {
|
|
1349
|
+
const entry = parts[i];
|
|
1350
|
+
if (entry.length < 4) continue;
|
|
1351
|
+
const code = entry.slice(0, 2);
|
|
1352
|
+
let file = entry.slice(3);
|
|
1353
|
+
if (code[0] === "R" || code[0] === "C") i++;
|
|
1354
|
+
if (!file.toLowerCase().endsWith(".md")) continue;
|
|
1355
|
+
if (prefix && file.startsWith(prefix)) file = file.slice(prefix.length);
|
|
1356
|
+
if (file.startsWith(".markode/")) continue;
|
|
1357
|
+
result[file] = code === "??" ? "U" : code.includes("D") ? "D" : code.includes("A") ? "A" : "M";
|
|
1358
|
+
}
|
|
1359
|
+
return result;
|
|
1360
|
+
}
|
|
1361
|
+
function gitDiff(root2, rel) {
|
|
1362
|
+
const status = gitStatuses(root2)[rel];
|
|
1363
|
+
const diff = ["diff", "--no-ext-diff", "--no-textconv"];
|
|
1364
|
+
if (status === "U") return git(root2, [...diff, "--no-index", "--", "/dev/null", rel]) ?? "";
|
|
1365
|
+
const hasHead = git(root2, ["rev-parse", "--verify", "HEAD"]) !== null;
|
|
1366
|
+
return git(root2, hasHead ? [...diff, "HEAD", "--", rel] : [...diff, "--cached", "--", rel]) ?? "";
|
|
1367
|
+
}
|
|
1368
|
+
|
|
1369
|
+
// src/core/proposals.ts
|
|
1370
|
+
var SKILL = "markode-proposals";
|
|
1371
|
+
var skillDirs = (root2) => Object.values(PROJECT_TARGETS).map((t) => path12.join(root2, t, SKILL));
|
|
1372
|
+
function proposalsState(root2) {
|
|
1373
|
+
return { active: skillDirs(root2).some((d) => fs12.existsSync(path12.join(d, "SKILL.md"))), layout: readConfig(root2)?.proposals ?? "general" };
|
|
1374
|
+
}
|
|
1375
|
+
var WHERE = {
|
|
1376
|
+
general: "`docs/proposals/NNN-short-name.md` (NNN: the next number in that folder, counting `applied/`)",
|
|
1377
|
+
structured: "`docs/proposals/<topic>/NNN-short-name.md`, one folder per topic (e.g. `sidebar`, `auth`; NNN: the next number in it, counting its `applied/`); in a monorepo, a proposal for a single project goes in `<project>/docs/proposals/<topic>/` (e.g. `apps/web/docs/proposals/auth/`). Reuse an existing topic folder; ask the user before creating a new one"
|
|
1378
|
+
};
|
|
1379
|
+
function section(layout) {
|
|
1380
|
+
return [
|
|
1381
|
+
"## Proposals",
|
|
1382
|
+
"",
|
|
1383
|
+
"When an answer would be long, has options or a design to validate, or the user asks for one, write a",
|
|
1384
|
+
"temporary proposal the user answers in markode instead of a chat answer (skill: `markode-proposals`).",
|
|
1385
|
+
"",
|
|
1386
|
+
`- Where: ${WHERE[layout]}.`,
|
|
1387
|
+
"- After writing one, run `markode wait <path>` in the background: it returns when the user answers in markode.",
|
|
1388
|
+
"- The user ticks options and comments (`<!-- comment by \u2026 -->`). When it is answered, reply to the comments,",
|
|
1389
|
+
" update every doc that depends on the decision (tasks, phases, architecture\u2026), then archive the proposal:",
|
|
1390
|
+
" `status: applied`, moved to the `applied/` folder next to it. Never read or search archived proposals",
|
|
1391
|
+
" unless the user asks about a past one."
|
|
1392
|
+
].join("\n");
|
|
1393
|
+
}
|
|
1394
|
+
function mainDocs(root2, mainDoc) {
|
|
1395
|
+
const doc = mainDoc ?? mainDocOf(root2);
|
|
1396
|
+
return doc ? [path12.join(root2, doc)] : [];
|
|
1397
|
+
}
|
|
1398
|
+
function activateProposals(root2, layout, mainDoc) {
|
|
1399
|
+
const text3 = fill(fs12.readFileSync(path12.join(TEMPLATES_DIR, "proposals", "SKILL.md"), "utf8"), { where: WHERE[layout] });
|
|
1400
|
+
for (const target of projectTargets(root2)) {
|
|
1401
|
+
const file = path12.join(root2, PROJECT_TARGETS[target], SKILL, "SKILL.md");
|
|
1402
|
+
assertInside(file);
|
|
1403
|
+
fs12.mkdirSync(path12.dirname(file), { recursive: true });
|
|
1404
|
+
if (!fs12.existsSync(file) || fs12.readFileSync(file, "utf8") !== text3) fs12.writeFileSync(file, text3);
|
|
1405
|
+
}
|
|
1406
|
+
fs12.mkdirSync(path12.join(root2, "docs", "proposals"), { recursive: true });
|
|
1407
|
+
for (const file of mainDocs(root2, mainDoc)) writeManagedBlock(file, section(layout), false, "proposals");
|
|
1408
|
+
writeConfig(root2, { ...readConfig(root2) ?? { mainDoc, langs: [] }, proposals: layout });
|
|
1409
|
+
}
|
|
1410
|
+
function deactivateProposals(root2, mainDoc) {
|
|
1411
|
+
for (const dir of skillDirs(root2)) if (fs12.existsSync(dir)) assertInside(dir), fs12.rmSync(dir, { recursive: true, force: true });
|
|
1412
|
+
for (const file of mainDocs(root2, mainDoc)) writeManagedBlock(file, null, false, "proposals");
|
|
1413
|
+
}
|
|
1414
|
+
function authorName(root2) {
|
|
1415
|
+
try {
|
|
1416
|
+
return runGit(root2, ["config", "user.name"]).trim() || null;
|
|
1417
|
+
} catch {
|
|
1418
|
+
return null;
|
|
1419
|
+
}
|
|
1420
|
+
}
|
|
1421
|
+
|
|
890
1422
|
// src/core/rules.ts
|
|
891
|
-
import
|
|
892
|
-
import
|
|
893
|
-
import
|
|
894
|
-
var rulesDir = () =>
|
|
1423
|
+
import fs13 from "fs";
|
|
1424
|
+
import path13 from "path";
|
|
1425
|
+
import YAML4 from "yaml";
|
|
1426
|
+
var rulesDir = () => path13.join(GLOBAL_DIR, "rules");
|
|
895
1427
|
var START = "<!-- markode:rules:start -->";
|
|
896
1428
|
var END = "<!-- markode:rules:end -->";
|
|
897
1429
|
var slugify = (s) => s.toLowerCase().normalize("NFD").replace(/[̀-ͯ]/g, "").replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
|
|
898
1430
|
function seedRules() {
|
|
899
|
-
if (
|
|
900
|
-
|
|
901
|
-
const src =
|
|
902
|
-
for (const f of
|
|
1431
|
+
if (fs13.existsSync(rulesDir())) return;
|
|
1432
|
+
fs13.mkdirSync(rulesDir(), { recursive: true });
|
|
1433
|
+
const src = path13.join(TEMPLATES_DIR, "rules");
|
|
1434
|
+
for (const f of fs13.readdirSync(src)) fs13.copyFileSync(path13.join(src, f), path13.join(rulesDir(), f));
|
|
903
1435
|
}
|
|
904
1436
|
function parseRule(id, text3) {
|
|
905
1437
|
const m = text3.match(/^---\n([\s\S]*?)\n---\n?([\s\S]*)$/);
|
|
906
|
-
const meta = m ?
|
|
1438
|
+
const meta = m ? YAML4.parse(m[1], { logLevel: "error" }) ?? {} : {};
|
|
907
1439
|
return { id, title: meta.title ?? id, body: (m ? m[2] : text3).trim() };
|
|
908
1440
|
}
|
|
909
1441
|
function listRules() {
|
|
910
1442
|
seedRules();
|
|
911
|
-
return
|
|
1443
|
+
return fs13.readdirSync(rulesDir()).filter((f) => f.endsWith(".md")).map((f) => parseRule(f.slice(0, -3), fs13.readFileSync(path13.join(rulesDir(), f), "utf8"))).sort((a, b) => a.title.localeCompare(b.title));
|
|
912
1444
|
}
|
|
913
1445
|
function saveRule(rule) {
|
|
914
1446
|
seedRules();
|
|
915
1447
|
const id = rule.id || slugify(rule.title) || `rule-${Date.now()}`;
|
|
916
1448
|
if (!/^[\w-]+$/.test(id)) throw new Error(`Invalid rule id: ${id}`);
|
|
917
1449
|
const text3 = `---
|
|
918
|
-
${
|
|
1450
|
+
${YAML4.stringify({ title: rule.title }).trim()}
|
|
919
1451
|
---
|
|
920
1452
|
${rule.body.trim()}
|
|
921
1453
|
`;
|
|
922
|
-
|
|
1454
|
+
fs13.writeFileSync(path13.join(rulesDir(), `${id}.md`), text3);
|
|
923
1455
|
return { id, title: rule.title, body: rule.body.trim() };
|
|
924
1456
|
}
|
|
925
1457
|
function deleteRule(id) {
|
|
926
|
-
|
|
1458
|
+
fs13.rmSync(path13.join(rulesDir(), `${path13.basename(id)}.md`), { force: true });
|
|
927
1459
|
}
|
|
928
1460
|
function appliedRuleIds(docText) {
|
|
929
1461
|
const section2 = extractSection(docText);
|
|
@@ -949,54 +1481,282 @@ function projectSourceLang(root2, mainDoc) {
|
|
|
949
1481
|
const doc = mainDoc ?? mainDocOf(root2);
|
|
950
1482
|
if (!doc) return DEFAULT_SOURCE;
|
|
951
1483
|
try {
|
|
952
|
-
return sourceLangOf(
|
|
1484
|
+
return sourceLangOf(fs13.readFileSync(path13.join(root2, doc), "utf8"));
|
|
1485
|
+
} catch {
|
|
1486
|
+
return DEFAULT_SOURCE;
|
|
1487
|
+
}
|
|
1488
|
+
}
|
|
1489
|
+
function renderSection(rules, sourceLang) {
|
|
1490
|
+
const body = rules.map((r) => `<!-- rule:${r.id} -->
|
|
1491
|
+
${r.body}`).join("\n\n");
|
|
1492
|
+
return [
|
|
1493
|
+
START,
|
|
1494
|
+
"## Working rules",
|
|
1495
|
+
"",
|
|
1496
|
+
"This project uses markode: follow its `markode` skill (`.claude/skills/markode/` or `.agents/skills/markode/`) when editing any `.md` file.",
|
|
1497
|
+
"",
|
|
1498
|
+
`<!-- markode:source-lang:${sourceLang} -->`,
|
|
1499
|
+
`- Source language of every \`.md\` file: **${LANG_NAMES[sourceLang]}** (\`${sourceLang}\`).`,
|
|
1500
|
+
...body ? ["", body] : [],
|
|
1501
|
+
END
|
|
1502
|
+
].join("\n");
|
|
1503
|
+
}
|
|
1504
|
+
function applyRules(docText, rules, sourceLang = sourceLangOf(docText)) {
|
|
1505
|
+
const section2 = renderSection(rules, sourceLang);
|
|
1506
|
+
const existing = extractSection(docText);
|
|
1507
|
+
if (existing) return docText.replace(existing, () => section2);
|
|
1508
|
+
return docText.replace(/\s*$/, "") + "\n\n" + section2 + "\n";
|
|
1509
|
+
}
|
|
1510
|
+
function applyRulesToFile(file, ids, sourceLang) {
|
|
1511
|
+
const text3 = fs13.existsSync(file) ? fs13.readFileSync(file, "utf8") : "";
|
|
1512
|
+
const all = listRules();
|
|
1513
|
+
const written = writtenRules(text3);
|
|
1514
|
+
const rules = (ids ?? appliedRuleIds(text3)).map((id) => all.find((r) => r.id === id) ?? written[id]).filter((r) => !!r);
|
|
1515
|
+
assertInside(file);
|
|
1516
|
+
fs13.writeFileSync(file, applyRules(text3, rules, sourceLang ?? sourceLangOf(text3)));
|
|
1517
|
+
}
|
|
1518
|
+
|
|
1519
|
+
// src/core/features.ts
|
|
1520
|
+
var FEATURES = ["agent", "rules", "proposals", "live", "translations", "dashboard", "skills", "vscode"];
|
|
1521
|
+
var REQUIRES = { rules: "agent", proposals: "agent", live: "agent", translations: "agent" };
|
|
1522
|
+
var FEATURE_INFO = {
|
|
1523
|
+
agent: { label: "AI agent", hint: "teaches your agent (Claude Code, Codex, Cursor\u2026) to work with markode docs; you can comment on any text for it" },
|
|
1524
|
+
rules: { label: "Working rules", hint: "rules the agent follows (ask before committing, protect secrets\u2026), chosen in the Rules tab and written in CLAUDE.md/AGENTS.md" },
|
|
1525
|
+
proposals: { label: "Proposals", hint: "the agent answers big questions with a doc you answer by ticking options and commenting" },
|
|
1526
|
+
live: { label: "Live changes", hint: "Claude Code hears about your edits and comments while it works (personal hooks in .claude/settings.local.json)" },
|
|
1527
|
+
translations: { label: "Translations", hint: "read and edit the docs in other languages; the agent translates them" },
|
|
1528
|
+
dashboard: { label: "Dashboard and tasks", hint: "a panel with the state of docs, phases and tasks; with the AI agent it also creates the task structure" },
|
|
1529
|
+
skills: { label: "Skills", hint: "a tab to browse, edit and add to the project the skills installed for your agents" },
|
|
1530
|
+
vscode: { label: "Hide in VS Code", hint: "hides markode's .markode/ folder (translations, state) from the VS Code explorer" }
|
|
1531
|
+
};
|
|
1532
|
+
var AGENT_SET = ["agent", "rules", "proposals", "live", "translations", "skills", "vscode"];
|
|
1533
|
+
var PROFILE_FEATURES = {
|
|
1534
|
+
viewer: [],
|
|
1535
|
+
agent: AGENT_SET,
|
|
1536
|
+
full: [...AGENT_SET, "dashboard"]
|
|
1537
|
+
};
|
|
1538
|
+
var PROFILE_INFO = {
|
|
1539
|
+
viewer: { label: "Viewer only", hint: "see and edit the .md files; nothing is written to the project" },
|
|
1540
|
+
agent: { label: "Viewer + AI agent", hint: "adds the agent skill, rules, proposals, comments, live changes and translations" },
|
|
1541
|
+
full: { label: "Complete", hint: "all of the above plus the dashboard and a docs structure for vision, phases and tasks" }
|
|
1542
|
+
};
|
|
1543
|
+
var isProfile = (v) => typeof v === "string" && v in PROFILE_FEATURES;
|
|
1544
|
+
var isFeature = (v) => FEATURES.includes(v);
|
|
1545
|
+
function withRequired(list) {
|
|
1546
|
+
const out = /* @__PURE__ */ new Set();
|
|
1547
|
+
for (const f of list) {
|
|
1548
|
+
const need = REQUIRES[f];
|
|
1549
|
+
if (need) out.add(need);
|
|
1550
|
+
out.add(f);
|
|
1551
|
+
}
|
|
1552
|
+
return FEATURES.filter((f) => out.has(f));
|
|
1553
|
+
}
|
|
1554
|
+
var UI_ONLY = ["rules", "translations", "dashboard", "skills"];
|
|
1555
|
+
var SKILL2 = "markode";
|
|
1556
|
+
var skillDirs2 = (root2) => Object.values(PROJECT_TARGETS).map((t) => path14.join(root2, t, SKILL2));
|
|
1557
|
+
var hasAgentSkill = (root2) => skillDirs2(root2).some((d) => fs14.existsSync(path14.join(d, "SKILL.md")));
|
|
1558
|
+
function featureState(root2) {
|
|
1559
|
+
const flags = readConfig(root2)?.features ?? {};
|
|
1560
|
+
const agent = hasAgentSkill(root2);
|
|
1561
|
+
return {
|
|
1562
|
+
agent,
|
|
1563
|
+
rules: agent && flags.rules !== false,
|
|
1564
|
+
proposals: agent && proposalsState(root2).active,
|
|
1565
|
+
live: agent && hasClaudeHooks(root2),
|
|
1566
|
+
translations: agent && flags.translations !== false,
|
|
1567
|
+
dashboard: flags.dashboard !== false,
|
|
1568
|
+
skills: flags.skills !== false,
|
|
1569
|
+
vscode: isHiddenInVsCode(root2)
|
|
1570
|
+
};
|
|
1571
|
+
}
|
|
1572
|
+
function setFlags(root2, patch) {
|
|
1573
|
+
const c = readConfig(root2) ?? { mainDoc: mainDocOf(root2), langs: [] };
|
|
1574
|
+
const { savedRules, ...flags } = patch;
|
|
1575
|
+
writeConfig(root2, { ...c, features: { ...c.features, ...flags }, ..."savedRules" in patch ? { savedRules } : {} });
|
|
1576
|
+
}
|
|
1577
|
+
function detectSourceLang(root2, fallback) {
|
|
1578
|
+
const votes = /* @__PURE__ */ new Map();
|
|
1579
|
+
for (const f of flattenFiles(scanTree(root2)).slice(0, 200)) {
|
|
1580
|
+
try {
|
|
1581
|
+
const lang = detectLang(fs14.readFileSync(path14.join(root2, f.path), "utf8"));
|
|
1582
|
+
if (lang) votes.set(lang, (votes.get(lang) ?? 0) + 1);
|
|
1583
|
+
} catch {
|
|
1584
|
+
}
|
|
1585
|
+
}
|
|
1586
|
+
return [...votes].sort((a, b) => b[1] - a[1])[0]?.[0] ?? fallback;
|
|
1587
|
+
}
|
|
1588
|
+
function ensureMainDoc(root2, preferred) {
|
|
1589
|
+
const existing = mainDocOf(root2);
|
|
1590
|
+
if (existing) return existing;
|
|
1591
|
+
const doc = preferred ?? (projectTargets(root2).includes("claude") ? "CLAUDE.md" : "AGENTS.md");
|
|
1592
|
+
createMainDoc(root2, doc);
|
|
1593
|
+
return doc;
|
|
1594
|
+
}
|
|
1595
|
+
function installAgentSkill(root2) {
|
|
1596
|
+
const src = path14.join(TEMPLATES_DIR, "skill", "SKILL.md");
|
|
1597
|
+
for (const target of projectTargets(root2)) {
|
|
1598
|
+
const dir = path14.join(root2, PROJECT_TARGETS[target], SKILL2);
|
|
1599
|
+
fs14.mkdirSync(dir, { recursive: true });
|
|
1600
|
+
const dest = path14.join(dir, "SKILL.md");
|
|
1601
|
+
assertInside(dest);
|
|
1602
|
+
const text3 = fs14.readFileSync(src, "utf8");
|
|
1603
|
+
if (!fs14.existsSync(dest) || fs14.readFileSync(dest, "utf8") !== text3) fs14.writeFileSync(dest, text3);
|
|
1604
|
+
}
|
|
1605
|
+
}
|
|
1606
|
+
function allMainDocs(root2) {
|
|
1607
|
+
return ["", ...findSubprojects(scanTree(root2))].map((d) => contentDoc(path14.join(root2, d))).filter((f) => !!f);
|
|
1608
|
+
}
|
|
1609
|
+
var METHOD_SKILLS = () => fs14.readdirSync(path14.join(TEMPLATES_DIR, "method", "skills"));
|
|
1610
|
+
function setFeature(root2, feature, on, opts = {}) {
|
|
1611
|
+
const state = featureState(root2);
|
|
1612
|
+
const changed = {};
|
|
1613
|
+
if (on) {
|
|
1614
|
+
const need = REQUIRES[feature];
|
|
1615
|
+
if (need && !state[need]) Object.assign(changed, setFeature(root2, need, true, opts));
|
|
1616
|
+
} else if (feature === "agent") {
|
|
1617
|
+
for (const f of FEATURES) if (REQUIRES[f] === "agent" && state[f]) Object.assign(changed, setFeature(root2, f, false, opts));
|
|
1618
|
+
}
|
|
1619
|
+
if (state[feature] === on && !UI_ONLY.includes(feature)) return changed;
|
|
1620
|
+
apply(root2, feature, on, opts);
|
|
1621
|
+
changed[feature] = on;
|
|
1622
|
+
return changed;
|
|
1623
|
+
}
|
|
1624
|
+
function apply(root2, feature, on, opts) {
|
|
1625
|
+
switch (feature) {
|
|
1626
|
+
case "agent": {
|
|
1627
|
+
if (on) {
|
|
1628
|
+
const lang = readConfig(root2)?.savedSourceLang ?? detectSourceLang(root2, opts.sourceLang ?? "en");
|
|
1629
|
+
const doc = path14.join(root2, ensureMainDoc(root2, opts.mainDoc));
|
|
1630
|
+
installAgentSkill(root2);
|
|
1631
|
+
const text3 = fs14.readFileSync(doc, "utf8");
|
|
1632
|
+
const declared = /<!-- markode:source-lang:\w+ -->/.test(text3);
|
|
1633
|
+
applyRulesToFile(doc, appliedRuleIds(text3), declared ? sourceLangOf(text3) : lang);
|
|
1634
|
+
} else {
|
|
1635
|
+
const doc = mainDocOf(root2);
|
|
1636
|
+
const c = readConfig(root2);
|
|
1637
|
+
if (doc && c) writeConfig(root2, { ...c, savedSourceLang: sourceLangOf(fs14.readFileSync(path14.join(root2, doc), "utf8")) });
|
|
1638
|
+
for (const d of skillDirs2(root2)) if (fs14.existsSync(d)) assertInside(d), fs14.rmSync(d, { recursive: true, force: true });
|
|
1639
|
+
for (const doc2 of allMainDocs(root2)) writeManagedBlock(doc2, null, false, "rules");
|
|
1640
|
+
}
|
|
1641
|
+
return;
|
|
1642
|
+
}
|
|
1643
|
+
case "rules": {
|
|
1644
|
+
const doc = mainDocOf(root2);
|
|
1645
|
+
const file = doc && path14.join(root2, doc);
|
|
1646
|
+
if (on) {
|
|
1647
|
+
const saved = readConfig(root2)?.savedRules ?? [];
|
|
1648
|
+
const current = file ? appliedRuleIds(fs14.readFileSync(file, "utf8")) : [];
|
|
1649
|
+
const ids = current.length ? current : saved.length ? saved : opts.rules ?? [];
|
|
1650
|
+
if (file && ids.length) applyRulesToFile(file, ids);
|
|
1651
|
+
setFlags(root2, { rules: true, savedRules: void 0 });
|
|
1652
|
+
} else {
|
|
1653
|
+
const ids = file ? appliedRuleIds(fs14.readFileSync(file, "utf8")) : [];
|
|
1654
|
+
if (file) applyRulesToFile(file, []);
|
|
1655
|
+
setFlags(root2, { rules: false, savedRules: ids.length ? ids : void 0 });
|
|
1656
|
+
}
|
|
1657
|
+
return;
|
|
1658
|
+
}
|
|
1659
|
+
case "proposals":
|
|
1660
|
+
if (on) activateProposals(root2, readConfig(root2)?.proposals ?? (findSubprojects(scanTree(root2)).length ? "structured" : "general"), mainDocOf(root2));
|
|
1661
|
+
else deactivateProposals(root2, mainDocOf(root2));
|
|
1662
|
+
return;
|
|
1663
|
+
case "live":
|
|
1664
|
+
if (on) installClaudeHooks(root2);
|
|
1665
|
+
else removeClaudeHooks(root2);
|
|
1666
|
+
return;
|
|
1667
|
+
case "translations":
|
|
1668
|
+
setFlags(root2, { translations: on });
|
|
1669
|
+
return;
|
|
1670
|
+
case "dashboard": {
|
|
1671
|
+
setFlags(root2, { dashboard: on });
|
|
1672
|
+
if (on && hasAgentSkill(root2) && !detectWorkflow(root2) && (readConfig(root2)?.workflow ?? "none") === "none") {
|
|
1673
|
+
const mainDoc = ensureMainDoc(root2, opts.mainDoc);
|
|
1674
|
+
setupMethod(root2, mainDoc, opts.monorepo ?? "per-project");
|
|
1675
|
+
const c = readConfig(root2);
|
|
1676
|
+
writeConfig(root2, { ...c, workflow: "markode" });
|
|
1677
|
+
}
|
|
1678
|
+
if (!on && readConfig(root2)?.workflow === "markode") {
|
|
1679
|
+
for (const name of METHOD_SKILLS())
|
|
1680
|
+
for (const t of Object.values(PROJECT_TARGETS)) {
|
|
1681
|
+
const dir = path14.join(root2, t, name);
|
|
1682
|
+
if (fs14.existsSync(dir)) assertInside(dir), fs14.rmSync(dir, { recursive: true, force: true });
|
|
1683
|
+
}
|
|
1684
|
+
for (const doc of allMainDocs(root2)) writeManagedBlock(doc, null, false, "workflow");
|
|
1685
|
+
const c = readConfig(root2);
|
|
1686
|
+
writeConfig(root2, { ...c, workflow: "none" });
|
|
1687
|
+
}
|
|
1688
|
+
return;
|
|
1689
|
+
}
|
|
1690
|
+
case "skills":
|
|
1691
|
+
setFlags(root2, { skills: on });
|
|
1692
|
+
return;
|
|
1693
|
+
case "vscode":
|
|
1694
|
+
if (on) hideInVsCode(root2);
|
|
1695
|
+
else showInVsCode(root2);
|
|
1696
|
+
return;
|
|
1697
|
+
}
|
|
1698
|
+
}
|
|
1699
|
+
function refreshInstalled(root2) {
|
|
1700
|
+
if (!hasAgentSkill(root2)) return;
|
|
1701
|
+
installAgentSkill(root2);
|
|
1702
|
+
const proposals = proposalsState(root2);
|
|
1703
|
+
if (proposals.active) activateProposals(root2, proposals.layout, mainDocOf(root2));
|
|
1704
|
+
if (readConfig(root2)?.workflow === "markode") installMethodSkills(root2);
|
|
1705
|
+
}
|
|
1706
|
+
function ignoreStateDir(root2) {
|
|
1707
|
+
const file = path14.join(stateDir(root2), ".gitignore");
|
|
1708
|
+
if (fs14.existsSync(file)) return;
|
|
1709
|
+
assertInside(file);
|
|
1710
|
+
fs14.mkdirSync(stateDir(root2), { recursive: true });
|
|
1711
|
+
fs14.writeFileSync(file, "# markode's personal state: never committed\n*\n");
|
|
1712
|
+
}
|
|
1713
|
+
|
|
1714
|
+
// src/core/defaults.ts
|
|
1715
|
+
var BASE_DEFAULTS = {
|
|
1716
|
+
mode: "ask",
|
|
1717
|
+
features: PROFILE_FEATURES.agent,
|
|
1718
|
+
mainDoc: "CLAUDE.md",
|
|
1719
|
+
monorepo: "per-project",
|
|
1720
|
+
sourceLang: "en",
|
|
1721
|
+
rules: [],
|
|
1722
|
+
rulesInSubprojects: false
|
|
1723
|
+
};
|
|
1724
|
+
var defaultsFile = () => path15.join(GLOBAL_DIR, "defaults.json");
|
|
1725
|
+
function readDefaults() {
|
|
1726
|
+
try {
|
|
1727
|
+
const raw = JSON.parse(fs15.readFileSync(defaultsFile(), "utf8"));
|
|
1728
|
+
const saved = { ...BASE_DEFAULTS, ...raw };
|
|
1729
|
+
if (!isLang(saved.sourceLang)) saved.sourceLang = BASE_DEFAULTS.sourceLang;
|
|
1730
|
+
if (!MAIN_DOCS.includes(saved.mainDoc)) saved.mainDoc = BASE_DEFAULTS.mainDoc;
|
|
1731
|
+
if (saved.monorepo !== "single") saved.monorepo = "per-project";
|
|
1732
|
+
saved.features = Array.isArray(raw.features) ? raw.features.filter(isFeature) : PROFILE_FEATURES.agent.filter((f) => (f !== "live" || raw.claudeHooks !== false) && (f !== "vscode" || raw.hideInVsCode !== false)).concat(raw.workflow === "markode" ? ["dashboard"] : []);
|
|
1733
|
+
return saved;
|
|
953
1734
|
} catch {
|
|
954
|
-
return
|
|
1735
|
+
return null;
|
|
955
1736
|
}
|
|
956
1737
|
}
|
|
957
|
-
function
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
return [
|
|
961
|
-
START,
|
|
962
|
-
"## Working rules",
|
|
963
|
-
"",
|
|
964
|
-
"This project uses markode: follow `.claude/skills/markode/SKILL.md` when editing any `.md` file.",
|
|
965
|
-
"",
|
|
966
|
-
`<!-- markode:source-lang:${sourceLang} -->`,
|
|
967
|
-
`- Source language of every \`.md\` file: **${LANG_NAMES[sourceLang]}** (\`${sourceLang}\`).`,
|
|
968
|
-
...body ? ["", body] : [],
|
|
969
|
-
END
|
|
970
|
-
].join("\n");
|
|
971
|
-
}
|
|
972
|
-
function applyRules(docText, rules, sourceLang = sourceLangOf(docText)) {
|
|
973
|
-
const section2 = renderSection(rules, sourceLang);
|
|
974
|
-
const existing = extractSection(docText);
|
|
975
|
-
if (existing) return docText.replace(existing, () => section2);
|
|
976
|
-
return docText.replace(/\s*$/, "") + "\n\n" + section2 + "\n";
|
|
977
|
-
}
|
|
978
|
-
function applyRulesToFile(file, ids, sourceLang) {
|
|
979
|
-
const text3 = fs10.existsSync(file) ? fs10.readFileSync(file, "utf8") : "";
|
|
980
|
-
const all = listRules();
|
|
981
|
-
const written = writtenRules(text3);
|
|
982
|
-
const rules = (ids ?? appliedRuleIds(text3)).map((id) => all.find((r) => r.id === id) ?? written[id]).filter((r) => !!r);
|
|
983
|
-
fs10.writeFileSync(file, applyRules(text3, rules, sourceLang ?? sourceLangOf(text3)));
|
|
1738
|
+
function writeDefaults(defaults) {
|
|
1739
|
+
fs15.mkdirSync(GLOBAL_DIR, { recursive: true });
|
|
1740
|
+
fs15.writeFileSync(defaultsFile(), JSON.stringify(defaults, null, 2) + "\n");
|
|
984
1741
|
}
|
|
985
1742
|
|
|
986
1743
|
// src/core/i18n.ts
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
1744
|
+
import crypto2 from "crypto";
|
|
1745
|
+
import fs16 from "fs";
|
|
1746
|
+
import path16 from "path";
|
|
1747
|
+
var stateFile = (root2) => path16.join(stateDir(root2), "state.json");
|
|
1748
|
+
var translationPath = (root2, lang, rel) => path16.join(stateDir(root2), "i18n", lang, rel);
|
|
1749
|
+
var hash = (text3) => crypto2.createHash("sha1").update(text3).digest("hex").slice(0, 12);
|
|
990
1750
|
var readText2 = (abs) => {
|
|
991
1751
|
try {
|
|
992
|
-
return
|
|
1752
|
+
return fs16.readFileSync(abs, "utf8");
|
|
993
1753
|
} catch {
|
|
994
1754
|
return null;
|
|
995
1755
|
}
|
|
996
1756
|
};
|
|
997
1757
|
function readState(root2) {
|
|
998
1758
|
try {
|
|
999
|
-
const raw = JSON.parse(
|
|
1759
|
+
const raw = JSON.parse(fs16.readFileSync(stateFile(root2), "utf8"));
|
|
1000
1760
|
if (!raw.translations) return { translations: raw, keepLanguage: {} };
|
|
1001
1761
|
return { translations: raw.translations ?? {}, keepLanguage: raw.keepLanguage ?? {} };
|
|
1002
1762
|
} catch {
|
|
@@ -1004,11 +1764,12 @@ function readState(root2) {
|
|
|
1004
1764
|
}
|
|
1005
1765
|
}
|
|
1006
1766
|
function writeState(root2, state) {
|
|
1007
|
-
|
|
1008
|
-
|
|
1767
|
+
fs16.mkdirSync(stateDir(root2), { recursive: true });
|
|
1768
|
+
assertInside(stateFile(root2));
|
|
1769
|
+
fs16.writeFileSync(stateFile(root2), JSON.stringify(state, null, 2) + "\n");
|
|
1009
1770
|
}
|
|
1010
1771
|
function fileStatus(root2, lang, rel) {
|
|
1011
|
-
const source = readText2(
|
|
1772
|
+
const source = readText2(path16.join(root2, rel));
|
|
1012
1773
|
const translation = readText2(translationPath(root2, lang, rel));
|
|
1013
1774
|
if (translation === null || source === null) return "missing";
|
|
1014
1775
|
const recorded = readState(root2).translations[lang]?.[rel];
|
|
@@ -1025,14 +1786,14 @@ function allStatuses(root2, lang) {
|
|
|
1025
1786
|
path: f.path,
|
|
1026
1787
|
lang,
|
|
1027
1788
|
status: fileStatus(root2, lang, f.path),
|
|
1028
|
-
translation:
|
|
1789
|
+
translation: path16.relative(root2, translationPath(root2, lang, f.path))
|
|
1029
1790
|
}));
|
|
1030
1791
|
}
|
|
1031
1792
|
function foreignFiles(root2, sourceLang) {
|
|
1032
1793
|
const { keepLanguage: keepLanguage2 } = readState(root2);
|
|
1033
1794
|
const out = [];
|
|
1034
1795
|
for (const f of flattenFiles(scanTree(root2))) {
|
|
1035
|
-
const text3 = readText2(
|
|
1796
|
+
const text3 = readText2(path16.join(root2, f.path));
|
|
1036
1797
|
const detected = text3 ? detectLang(text3) : null;
|
|
1037
1798
|
if (detected && detected !== sourceLang && keepLanguage2[f.path] !== detected) out.push({ path: f.path, detected });
|
|
1038
1799
|
}
|
|
@@ -1052,17 +1813,17 @@ function syncReport(root2) {
|
|
|
1052
1813
|
};
|
|
1053
1814
|
}
|
|
1054
1815
|
function markSynced(root2, lang, rel) {
|
|
1055
|
-
const source = readText2(
|
|
1816
|
+
const source = readText2(path16.join(root2, rel));
|
|
1056
1817
|
const translation = readText2(translationPath(root2, lang, rel));
|
|
1057
1818
|
if (source === null) throw new Error(`Source not found: ${rel}`);
|
|
1058
|
-
if (translation === null) throw new Error(`Translation not found: ${
|
|
1819
|
+
if (translation === null) throw new Error(`Translation not found: ${path16.relative(root2, translationPath(root2, lang, rel))}`);
|
|
1059
1820
|
const state = readState(root2);
|
|
1060
1821
|
state.translations[lang] ??= {};
|
|
1061
1822
|
state.translations[lang][rel] = { source: hash(source), translation: hash(translation) };
|
|
1062
1823
|
writeState(root2, state);
|
|
1063
1824
|
}
|
|
1064
1825
|
function keepLanguage(root2, rel) {
|
|
1065
|
-
const text3 = readText2(
|
|
1826
|
+
const text3 = readText2(path16.join(root2, rel));
|
|
1066
1827
|
const detected = text3 ? detectLang(text3) : null;
|
|
1067
1828
|
if (!detected) throw new Error(`Could not detect the language of ${rel}`);
|
|
1068
1829
|
const state = readState(root2);
|
|
@@ -1072,14 +1833,14 @@ function keepLanguage(root2, rel) {
|
|
|
1072
1833
|
}
|
|
1073
1834
|
|
|
1074
1835
|
// src/core/wait.ts
|
|
1075
|
-
import
|
|
1076
|
-
import
|
|
1077
|
-
import
|
|
1078
|
-
var waitsDir = (root2) =>
|
|
1079
|
-
var pidFile = (root2, rel) =>
|
|
1080
|
-
var
|
|
1836
|
+
import fs17 from "fs";
|
|
1837
|
+
import path17 from "path";
|
|
1838
|
+
import YAML5 from "yaml";
|
|
1839
|
+
var waitsDir = (root2) => path17.join(stateDir(root2), "waits");
|
|
1840
|
+
var pidFile = (root2, rel) => path17.join(waitsDir(root2), `${encodeURIComponent(rel)}.pid`);
|
|
1841
|
+
var frontmatter2 = (text3) => {
|
|
1081
1842
|
try {
|
|
1082
|
-
return
|
|
1843
|
+
return YAML5.parse(/^---\r?\n([\s\S]*?)\r?\n---/.exec(text3)?.[1] ?? "", { logLevel: "error" }) ?? {};
|
|
1083
1844
|
} catch {
|
|
1084
1845
|
return {};
|
|
1085
1846
|
}
|
|
@@ -1095,30 +1856,31 @@ var isAlive = (pid) => {
|
|
|
1095
1856
|
function waitingOn(root2) {
|
|
1096
1857
|
let names2 = [];
|
|
1097
1858
|
try {
|
|
1098
|
-
names2 =
|
|
1859
|
+
names2 = fs17.readdirSync(waitsDir(root2)).filter((f) => f.endsWith(".pid"));
|
|
1099
1860
|
} catch {
|
|
1100
1861
|
}
|
|
1101
1862
|
return names2.flatMap((f) => {
|
|
1102
|
-
const pid = Number(
|
|
1863
|
+
const pid = Number(fs17.readFileSync(path17.join(waitsDir(root2), f), "utf8"));
|
|
1103
1864
|
return isAlive(pid) ? [decodeURIComponent(f.slice(0, -4))] : [];
|
|
1104
1865
|
});
|
|
1105
1866
|
}
|
|
1106
1867
|
function waitForAnswer(root2, rel) {
|
|
1107
|
-
const abs =
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1868
|
+
const abs = path17.join(root2, rel);
|
|
1869
|
+
fs17.mkdirSync(waitsDir(root2), { recursive: true });
|
|
1870
|
+
assertInside(pidFile(root2, rel));
|
|
1871
|
+
fs17.writeFileSync(pidFile(root2, rel), String(process.pid));
|
|
1872
|
+
const cleanup = () => fs17.rmSync(pidFile(root2, rel), { force: true });
|
|
1111
1873
|
process.once("exit", cleanup);
|
|
1112
1874
|
for (const sig of ["SIGINT", "SIGTERM", "SIGHUP"]) process.once(sig, () => process.exit(130));
|
|
1113
1875
|
return new Promise((resolve) => {
|
|
1114
1876
|
const poll = () => {
|
|
1115
1877
|
let text3;
|
|
1116
1878
|
try {
|
|
1117
|
-
text3 =
|
|
1879
|
+
text3 = fs17.readFileSync(abs, "utf8");
|
|
1118
1880
|
} catch {
|
|
1119
1881
|
return resolve("gone");
|
|
1120
1882
|
}
|
|
1121
|
-
const fm =
|
|
1883
|
+
const fm = frontmatter2(text3);
|
|
1122
1884
|
if (fm.type !== "proposal") return resolve("gone");
|
|
1123
1885
|
if (fm.status === "answered") return resolve("answered");
|
|
1124
1886
|
setTimeout(poll, 1500);
|
|
@@ -1128,8 +1890,8 @@ function waitForAnswer(root2, rel) {
|
|
|
1128
1890
|
}
|
|
1129
1891
|
var COMMENT = /^\s*<!-- (comment|reply) by (.+?), (\d{4}-\d{2}-\d{2})(?:, on "((?:[^"\\]|\\.)*)")?:\s?([\s\S]*?)\s*-->\s*$/;
|
|
1130
1892
|
function answerSummary(root2, rel) {
|
|
1131
|
-
const text3 =
|
|
1132
|
-
const fm =
|
|
1893
|
+
const text3 = fs17.readFileSync(path17.join(root2, rel), "utf8");
|
|
1894
|
+
const fm = frontmatter2(text3);
|
|
1133
1895
|
const body = text3.replace(/^---\r?\n[\s\S]*?\r?\n---\r?\n?/, "");
|
|
1134
1896
|
const lines = body.split("\n");
|
|
1135
1897
|
const chosen = [];
|
|
@@ -1202,236 +1964,13 @@ function answerSummary(root2, rel) {
|
|
|
1202
1964
|
}
|
|
1203
1965
|
|
|
1204
1966
|
// src/server/index.ts
|
|
1205
|
-
import
|
|
1967
|
+
import fs18 from "fs";
|
|
1206
1968
|
import http from "http";
|
|
1207
|
-
import
|
|
1969
|
+
import path18 from "path";
|
|
1208
1970
|
import { watch } from "chokidar";
|
|
1209
1971
|
import open from "open";
|
|
1210
1972
|
import pc from "picocolors";
|
|
1211
1973
|
import { WebSocketServer } from "ws";
|
|
1212
|
-
|
|
1213
|
-
// src/core/git.ts
|
|
1214
|
-
import { execFileSync } from "child_process";
|
|
1215
|
-
function git(root2, args2) {
|
|
1216
|
-
try {
|
|
1217
|
-
return execFileSync("git", ["-c", "core.quotepath=off", ...args2], { cwd: root2, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"], maxBuffer: 32 * 1024 * 1024 });
|
|
1218
|
-
} catch (err) {
|
|
1219
|
-
return typeof err?.stdout === "string" && err.stdout ? err.stdout : null;
|
|
1220
|
-
}
|
|
1221
|
-
}
|
|
1222
|
-
var isGitRepo = (root2) => git(root2, ["rev-parse", "--is-inside-work-tree"])?.trim() === "true";
|
|
1223
|
-
function gitStatuses(root2) {
|
|
1224
|
-
const prefix = git(root2, ["rev-parse", "--show-prefix"])?.trim() ?? "";
|
|
1225
|
-
const out = git(root2, ["status", "--porcelain=v1", "-z", "--untracked-files=all", "--", "."]);
|
|
1226
|
-
const result = {};
|
|
1227
|
-
if (!out) return result;
|
|
1228
|
-
const parts = out.split("\0");
|
|
1229
|
-
for (let i = 0; i < parts.length; i++) {
|
|
1230
|
-
const entry = parts[i];
|
|
1231
|
-
if (entry.length < 4) continue;
|
|
1232
|
-
const code = entry.slice(0, 2);
|
|
1233
|
-
let file = entry.slice(3);
|
|
1234
|
-
if (code[0] === "R" || code[0] === "C") i++;
|
|
1235
|
-
if (!file.toLowerCase().endsWith(".md")) continue;
|
|
1236
|
-
if (prefix && file.startsWith(prefix)) file = file.slice(prefix.length);
|
|
1237
|
-
if (file.startsWith(".markode/")) continue;
|
|
1238
|
-
result[file] = code === "??" ? "U" : code.includes("D") ? "D" : code.includes("A") ? "A" : "M";
|
|
1239
|
-
}
|
|
1240
|
-
return result;
|
|
1241
|
-
}
|
|
1242
|
-
function gitDiff(root2, rel) {
|
|
1243
|
-
const status = gitStatuses(root2)[rel];
|
|
1244
|
-
if (status === "U") return git(root2, ["diff", "--no-index", "--", "/dev/null", rel]) ?? "";
|
|
1245
|
-
const hasHead = git(root2, ["rev-parse", "--verify", "HEAD"]) !== null;
|
|
1246
|
-
return git(root2, hasHead ? ["diff", "HEAD", "--", rel] : ["diff", "--cached", "--", rel]) ?? "";
|
|
1247
|
-
}
|
|
1248
|
-
|
|
1249
|
-
// src/core/proposals.ts
|
|
1250
|
-
import { execFileSync as execFileSync2 } from "child_process";
|
|
1251
|
-
import fs13 from "fs";
|
|
1252
|
-
import path13 from "path";
|
|
1253
|
-
var SKILL = path13.join(".claude", "skills", "markode-proposals");
|
|
1254
|
-
function proposalsState(root2) {
|
|
1255
|
-
return { active: fs13.existsSync(path13.join(root2, SKILL, "SKILL.md")), layout: readConfig(root2)?.proposals ?? "general" };
|
|
1256
|
-
}
|
|
1257
|
-
var WHERE = {
|
|
1258
|
-
general: "`docs/proposals/NNN-short-name.md` (NNN: the next number in that folder)",
|
|
1259
|
-
structured: "`docs/proposals/<topic>/NNN-short-name.md`, one folder per topic (e.g. `sidebar`, `auth`); in a monorepo, a proposal for a single project goes in `<project>/docs/proposals/<topic>/` (e.g. `apps/web/docs/proposals/auth/`). Reuse an existing topic folder; ask the user before creating a new one"
|
|
1260
|
-
};
|
|
1261
|
-
function section(layout) {
|
|
1262
|
-
return [
|
|
1263
|
-
"## Proposals",
|
|
1264
|
-
"",
|
|
1265
|
-
"When an answer would be long, has options or a design to validate, or the user asks for one, write a",
|
|
1266
|
-
"temporary proposal the user answers in markode instead of a chat answer (skill: `markode-proposals`).",
|
|
1267
|
-
"",
|
|
1268
|
-
`- Where: ${WHERE[layout]}.`,
|
|
1269
|
-
"- After writing one, run `markode wait <path>` in the background: it returns when the user answers in markode.",
|
|
1270
|
-
"- The user ticks options and comments (`<!-- comment by \u2026 -->`). When it is answered, reply to the comments,",
|
|
1271
|
-
" update every doc that depends on the decision (tasks, phases, architecture\u2026), then delete the proposal."
|
|
1272
|
-
].join("\n");
|
|
1273
|
-
}
|
|
1274
|
-
function mainDocs(root2, mainDoc) {
|
|
1275
|
-
const doc = mainDoc ?? mainDocOf(root2);
|
|
1276
|
-
return doc ? [path13.join(root2, doc)] : [];
|
|
1277
|
-
}
|
|
1278
|
-
function activateProposals(root2, layout, mainDoc) {
|
|
1279
|
-
const dir = path13.join(root2, SKILL);
|
|
1280
|
-
fs13.mkdirSync(dir, { recursive: true });
|
|
1281
|
-
fs13.writeFileSync(path13.join(dir, "SKILL.md"), fill(fs13.readFileSync(path13.join(TEMPLATES_DIR, "proposals", "SKILL.md"), "utf8"), { where: WHERE[layout] }));
|
|
1282
|
-
fs13.mkdirSync(path13.join(root2, "docs", "proposals"), { recursive: true });
|
|
1283
|
-
for (const file of mainDocs(root2, mainDoc)) writeManagedBlock(file, section(layout), false, "proposals");
|
|
1284
|
-
writeConfig(root2, { ...readConfig(root2) ?? { mainDoc, langs: [] }, proposals: layout });
|
|
1285
|
-
}
|
|
1286
|
-
function deactivateProposals(root2, mainDoc) {
|
|
1287
|
-
fs13.rmSync(path13.join(root2, SKILL), { recursive: true, force: true });
|
|
1288
|
-
for (const file of mainDocs(root2, mainDoc)) writeManagedBlock(file, null, false, "proposals");
|
|
1289
|
-
}
|
|
1290
|
-
function authorName(root2) {
|
|
1291
|
-
try {
|
|
1292
|
-
return execFileSync2("git", ["config", "user.name"], { cwd: root2, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim() || null;
|
|
1293
|
-
} catch {
|
|
1294
|
-
return null;
|
|
1295
|
-
}
|
|
1296
|
-
}
|
|
1297
|
-
|
|
1298
|
-
// src/core/skills.ts
|
|
1299
|
-
import fs14 from "fs";
|
|
1300
|
-
import os2 from "os";
|
|
1301
|
-
import path14 from "path";
|
|
1302
|
-
import YAML5 from "yaml";
|
|
1303
|
-
var SOURCE_DIRS = {
|
|
1304
|
-
claude: [".claude", "skills"],
|
|
1305
|
-
// Shared folder read by Codex, Copilot and Gemini CLI, and where `npx skills` installs.
|
|
1306
|
-
agents: [".agents", "skills"],
|
|
1307
|
-
codex: [".codex", "skills"],
|
|
1308
|
-
gemini: [".gemini", "skills"],
|
|
1309
|
-
copilot: [".copilot", "skills"],
|
|
1310
|
-
cursor: [".cursor", "skills"]
|
|
1311
|
-
};
|
|
1312
|
-
var PROJECT_TARGETS = { claude: ".claude/skills", agents: ".agents/skills" };
|
|
1313
|
-
var sourceDir = (source) => path14.join(os2.homedir(), ...SOURCE_DIRS[source]);
|
|
1314
|
-
var display = (p3) => p3.startsWith(os2.homedir() + path14.sep) ? "~" + p3.slice(os2.homedir().length) : p3;
|
|
1315
|
-
function skillSources() {
|
|
1316
|
-
return Object.keys(SOURCE_DIRS).map((id) => ({ id, path: display(sourceDir(id)), exists: fs14.existsSync(sourceDir(id)) }));
|
|
1317
|
-
}
|
|
1318
|
-
var tagsFile = () => path14.join(GLOBAL_DIR, "skills.json");
|
|
1319
|
-
function readTags() {
|
|
1320
|
-
try {
|
|
1321
|
-
return JSON.parse(fs14.readFileSync(tagsFile(), "utf8")).tags ?? {};
|
|
1322
|
-
} catch {
|
|
1323
|
-
return {};
|
|
1324
|
-
}
|
|
1325
|
-
}
|
|
1326
|
-
function setSkillTags(name, tags) {
|
|
1327
|
-
const all = readTags();
|
|
1328
|
-
const clean = [...new Set(tags.map((t) => t.trim().toLowerCase()).filter(Boolean))];
|
|
1329
|
-
if (clean.length) all[name] = clean;
|
|
1330
|
-
else delete all[name];
|
|
1331
|
-
fs14.mkdirSync(GLOBAL_DIR, { recursive: true });
|
|
1332
|
-
fs14.writeFileSync(tagsFile(), JSON.stringify({ tags: all }, null, 2) + "\n");
|
|
1333
|
-
return clean;
|
|
1334
|
-
}
|
|
1335
|
-
function findSkills(dir, source, managed, depth = 0) {
|
|
1336
|
-
let entries;
|
|
1337
|
-
try {
|
|
1338
|
-
entries = fs14.readdirSync(dir, { withFileTypes: true });
|
|
1339
|
-
} catch {
|
|
1340
|
-
return [];
|
|
1341
|
-
}
|
|
1342
|
-
return entries.flatMap((e) => {
|
|
1343
|
-
const abs = path14.join(dir, e.name);
|
|
1344
|
-
let real;
|
|
1345
|
-
try {
|
|
1346
|
-
if (!fs14.statSync(abs).isDirectory()) return [];
|
|
1347
|
-
real = fs14.realpathSync(abs);
|
|
1348
|
-
} catch {
|
|
1349
|
-
return [];
|
|
1350
|
-
}
|
|
1351
|
-
if (fs14.existsSync(path14.join(abs, "SKILL.md"))) return [{ dir: abs, real, source, managed }];
|
|
1352
|
-
return depth < 2 ? findSkills(abs, source, managed || e.name === "synced" || e.name.startsWith("."), depth + 1) : [];
|
|
1353
|
-
});
|
|
1354
|
-
}
|
|
1355
|
-
function frontmatter2(text3) {
|
|
1356
|
-
const m = /^---\r?\n([\s\S]*?)\r?\n---/.exec(text3);
|
|
1357
|
-
if (!m) return null;
|
|
1358
|
-
try {
|
|
1359
|
-
const data = YAML5.parse(m[1], { logLevel: "error" });
|
|
1360
|
-
return data && typeof data === "object" ? data : null;
|
|
1361
|
-
} catch {
|
|
1362
|
-
return null;
|
|
1363
|
-
}
|
|
1364
|
-
}
|
|
1365
|
-
function listFiles(dir) {
|
|
1366
|
-
try {
|
|
1367
|
-
return fs14.readdirSync(dir, { recursive: true, withFileTypes: true }).map((e) => ({ e, rel: path14.relative(dir, path14.join(e.parentPath ?? e.path ?? dir, e.name)).split(path14.sep).join("/") })).filter(({ e, rel }) => e.isFile() && rel !== "SKILL.md").map(({ rel }) => rel).sort().slice(0, 100);
|
|
1368
|
-
} catch {
|
|
1369
|
-
return [];
|
|
1370
|
-
}
|
|
1371
|
-
}
|
|
1372
|
-
function listSkills(root2) {
|
|
1373
|
-
const byReal = /* @__PURE__ */ new Map();
|
|
1374
|
-
for (const source of Object.keys(SOURCE_DIRS)) {
|
|
1375
|
-
for (const f of findSkills(sourceDir(source), source, false)) byReal.set(f.real, [...byReal.get(f.real) ?? [], f]);
|
|
1376
|
-
}
|
|
1377
|
-
const userTags = readTags();
|
|
1378
|
-
const skills = [];
|
|
1379
|
-
for (const [real, found] of byReal) {
|
|
1380
|
-
const text3 = fs14.readFileSync(path14.join(real, "SKILL.md"), "utf8");
|
|
1381
|
-
const fm = frontmatter2(text3);
|
|
1382
|
-
const folder = path14.basename(found[0].dir);
|
|
1383
|
-
const name = typeof fm?.name === "string" ? fm.name : folder;
|
|
1384
|
-
const description = typeof fm?.description === "string" ? fm.description.trim() : "";
|
|
1385
|
-
const issues = [];
|
|
1386
|
-
if (!fm) issues.push("frontmatter");
|
|
1387
|
-
else {
|
|
1388
|
-
if (typeof fm.name !== "string" || !/^[a-z0-9]+(-[a-z0-9]+)*$/.test(fm.name) || fm.name.length > 64) issues.push("name");
|
|
1389
|
-
else if (fm.name !== folder) issues.push("name-dir");
|
|
1390
|
-
if (!description) issues.push("description");
|
|
1391
|
-
else if (description.length > 1024) issues.push("description-long");
|
|
1392
|
-
}
|
|
1393
|
-
const declared = fm?.metadata?.tags;
|
|
1394
|
-
const metaTags = Array.isArray(declared) ? declared.map(String) : typeof declared === "string" ? declared.split(",") : [];
|
|
1395
|
-
skills.push({
|
|
1396
|
-
key: real,
|
|
1397
|
-
name,
|
|
1398
|
-
folder,
|
|
1399
|
-
description,
|
|
1400
|
-
locations: found.map((f) => ({ source: f.source, path: display(f.dir) })),
|
|
1401
|
-
managed: found.every((f) => f.managed),
|
|
1402
|
-
files: listFiles(real),
|
|
1403
|
-
tags: [.../* @__PURE__ */ new Set([...userTags[name] ?? [], ...metaTags.map((t) => t.trim().toLowerCase()).filter(Boolean)])],
|
|
1404
|
-
issues,
|
|
1405
|
-
installed: Object.keys(PROJECT_TARGETS).filter((t) => fs14.existsSync(path14.join(root2, PROJECT_TARGETS[t], folder, "SKILL.md")))
|
|
1406
|
-
});
|
|
1407
|
-
}
|
|
1408
|
-
return skills.sort((a, b) => a.name.localeCompare(b.name));
|
|
1409
|
-
}
|
|
1410
|
-
function getSkill(root2, key) {
|
|
1411
|
-
const skill = typeof key === "string" ? listSkills(root2).find((s) => s.key === key) : void 0;
|
|
1412
|
-
if (!skill) throw Object.assign(new Error("Skill not found"), { code: "ENOENT" });
|
|
1413
|
-
return skill;
|
|
1414
|
-
}
|
|
1415
|
-
function projectTargets(root2) {
|
|
1416
|
-
const has2 = (p3) => fs14.existsSync(path14.join(root2, p3));
|
|
1417
|
-
const out = [];
|
|
1418
|
-
if (has2(".claude") || has2("CLAUDE.md")) out.push("claude");
|
|
1419
|
-
if (has2(".agents") || has2("AGENTS.md") || has2(".codex") || has2(".gemini") || has2(".github/skills")) out.push("agents");
|
|
1420
|
-
return out.length ? out : ["claude"];
|
|
1421
|
-
}
|
|
1422
|
-
function installSkill2(root2, skill, targets, overwrite) {
|
|
1423
|
-
const dests = targets.map((t) => path14.join(root2, PROJECT_TARGETS[t], skill.folder));
|
|
1424
|
-
const existing = dests.filter((d) => fs14.existsSync(d));
|
|
1425
|
-
if (existing.length && !overwrite) return { conflict: existing.map((d) => path14.relative(root2, d).split(path14.sep).join("/")) };
|
|
1426
|
-
for (const dest of dests) {
|
|
1427
|
-
fs14.rmSync(dest, { recursive: true, force: true });
|
|
1428
|
-
fs14.mkdirSync(path14.dirname(dest), { recursive: true });
|
|
1429
|
-
fs14.cpSync(skill.key, dest, { recursive: true, dereference: true });
|
|
1430
|
-
}
|
|
1431
|
-
return { ok: true };
|
|
1432
|
-
}
|
|
1433
|
-
|
|
1434
|
-
// src/server/index.ts
|
|
1435
1974
|
var IMAGE_MIME = {
|
|
1436
1975
|
".png": "image/png",
|
|
1437
1976
|
".jpg": "image/jpeg",
|
|
@@ -1458,9 +1997,13 @@ var HttpError = class extends Error {
|
|
|
1458
1997
|
}
|
|
1459
1998
|
status;
|
|
1460
1999
|
};
|
|
2000
|
+
var MAX_BODY = 20 * 1024 * 1024;
|
|
1461
2001
|
async function readBody(req) {
|
|
1462
2002
|
let data = "";
|
|
1463
|
-
for await (const chunk of req)
|
|
2003
|
+
for await (const chunk of req) {
|
|
2004
|
+
data += chunk;
|
|
2005
|
+
if (data.length > MAX_BODY) throw new HttpError(413, "Request too large");
|
|
2006
|
+
}
|
|
1464
2007
|
if (!data) return {};
|
|
1465
2008
|
if (!req.headers["content-type"]?.startsWith("application/json")) throw new HttpError(415, "Expected application/json");
|
|
1466
2009
|
return JSON.parse(data);
|
|
@@ -1470,6 +2013,8 @@ function isLocalRequest(req) {
|
|
|
1470
2013
|
const host = req.headers.host ?? "";
|
|
1471
2014
|
try {
|
|
1472
2015
|
if (!LOCAL_HOSTS.has(new URL(`http://${host}`).hostname)) return false;
|
|
2016
|
+
const site = req.headers["sec-fetch-site"];
|
|
2017
|
+
if (site && site !== "same-origin" && site !== "none") return false;
|
|
1473
2018
|
return !req.headers.origin || new URL(req.headers.origin).host === host;
|
|
1474
2019
|
} catch {
|
|
1475
2020
|
return false;
|
|
@@ -1477,8 +2022,9 @@ function isLocalRequest(req) {
|
|
|
1477
2022
|
}
|
|
1478
2023
|
async function startServer({ root: root2, port, open: openBrowser }) {
|
|
1479
2024
|
const config = () => readConfig(root2) ?? { mainDoc: null, langs: [] };
|
|
2025
|
+
const langs = () => featureState(root2).translations ? config().langs : [];
|
|
1480
2026
|
const sourceLang = () => projectSourceLang(root2, readConfig(root2)?.mainDoc);
|
|
1481
|
-
const viewLang = (value) => isLangCode(value) && value !== sourceLang() &&
|
|
2027
|
+
const viewLang = (value) => isLangCode(value) && value !== sourceLang() && langs().includes(value) ? value : null;
|
|
1482
2028
|
const mdPath = (rel) => {
|
|
1483
2029
|
if (typeof rel !== "string" || !rel.toLowerCase().endsWith(".md")) throw new HttpError(400, "Expected a .md path");
|
|
1484
2030
|
safeResolve(root2, rel);
|
|
@@ -1488,24 +2034,36 @@ async function startServer({ root: root2, port, open: openBrowser }) {
|
|
|
1488
2034
|
if (typeof rel !== "string" || !rel.trim()) throw new HttpError(400, "Expected a path");
|
|
1489
2035
|
const abs = safeResolve(root2, rel);
|
|
1490
2036
|
if (abs === root2) throw new HttpError(400, "Refusing to delete the project root");
|
|
1491
|
-
if (!
|
|
2037
|
+
if (!fs18.existsSync(abs)) throw new HttpError(404, "Not found");
|
|
1492
2038
|
return abs;
|
|
1493
2039
|
};
|
|
2040
|
+
const translationFile = (lang, rel) => {
|
|
2041
|
+
const file = translationPath(root2, lang, rel);
|
|
2042
|
+
assertInside(file, root2);
|
|
2043
|
+
return file;
|
|
2044
|
+
};
|
|
1494
2045
|
const routes = {
|
|
1495
2046
|
"GET /api/info": () => {
|
|
1496
2047
|
const c = config();
|
|
1497
2048
|
return {
|
|
1498
|
-
name:
|
|
2049
|
+
name: path18.basename(root2),
|
|
1499
2050
|
mainDoc: c.mainDoc ?? null,
|
|
1500
2051
|
sourceLang: sourceLang(),
|
|
1501
|
-
langs:
|
|
2052
|
+
langs: langs().filter((l) => l !== sourceLang()),
|
|
1502
2053
|
langNames: Object.fromEntries(c.langs.map((l) => [l, langName(l, c)])),
|
|
1503
2054
|
git: isGitRepo(root2),
|
|
1504
2055
|
proposals: proposalsState(root2),
|
|
1505
2056
|
author: authorName(root2),
|
|
1506
|
-
unlinked: c.skipLink ? [] : missingMainDocs(root2)
|
|
2057
|
+
unlinked: c.skipLink ? [] : missingMainDocs(root2),
|
|
2058
|
+
features: featureState(root2)
|
|
1507
2059
|
};
|
|
1508
2060
|
},
|
|
2061
|
+
/** Turn a module on or off (with the ones it needs or that need it); returns every module's state. */
|
|
2062
|
+
"PUT /api/features": (_, body) => {
|
|
2063
|
+
if (!isFeature(body.feature)) throw new HttpError(400, `Expected one of ${FEATURES.join(", ")}`);
|
|
2064
|
+
setFeature(root2, body.feature, !!body.on, readDefaults() ?? BASE_DEFAULTS);
|
|
2065
|
+
return featureState(root2);
|
|
2066
|
+
},
|
|
1509
2067
|
"PUT /api/config": (_, body) => {
|
|
1510
2068
|
const c = config();
|
|
1511
2069
|
if (Array.isArray(body.langs)) c.langs = body.langs.filter(isLangCode);
|
|
@@ -1515,7 +2073,7 @@ async function startServer({ root: root2, port, open: openBrowser }) {
|
|
|
1515
2073
|
}
|
|
1516
2074
|
if (isLang(body.sourceLang) && body.sourceLang !== sourceLang()) {
|
|
1517
2075
|
if (!c.mainDoc) throw new HttpError(400, "No main document configured; run `markode init`");
|
|
1518
|
-
applyRulesToFile(
|
|
2076
|
+
applyRulesToFile(path18.join(root2, c.mainDoc), void 0, body.sourceLang);
|
|
1519
2077
|
}
|
|
1520
2078
|
writeConfig(root2, c);
|
|
1521
2079
|
return { ...c, langs: c.langs.filter((l) => l !== sourceLang()) };
|
|
@@ -1539,17 +2097,17 @@ async function startServer({ root: root2, port, open: openBrowser }) {
|
|
|
1539
2097
|
"GET /api/tree": (url2) => {
|
|
1540
2098
|
const l = viewLang(url2.searchParams.get("lang"));
|
|
1541
2099
|
const sync2 = l ? Object.fromEntries(allStatuses(root2, l).map((e) => [e.path, e.status])) : {};
|
|
1542
|
-
const foreign = Object.fromEntries(foreignFiles(root2, sourceLang()).map((f) => [f.path, f.detected]));
|
|
2100
|
+
const foreign = featureState(root2).translations ? Object.fromEntries(foreignFiles(root2, sourceLang()).map((f) => [f.path, f.detected])) : {};
|
|
1543
2101
|
return { tree: scanTree(root2), git: gitStatuses(root2), sync: sync2, foreign };
|
|
1544
2102
|
},
|
|
1545
2103
|
"POST /api/keep-language": (_, body) => ({ lang: keepLanguage(root2, mdPath(body.path)) }),
|
|
1546
2104
|
"GET /api/file": (url2) => {
|
|
1547
2105
|
const rel = mdPath(url2.searchParams.get("path"));
|
|
1548
2106
|
const l = viewLang(url2.searchParams.get("lang"));
|
|
1549
|
-
const source =
|
|
2107
|
+
const source = fs18.readFileSync(safeResolve(root2, rel), "utf8");
|
|
1550
2108
|
if (l) {
|
|
1551
|
-
const tPath =
|
|
1552
|
-
if (
|
|
2109
|
+
const tPath = translationFile(l, rel);
|
|
2110
|
+
if (fs18.existsSync(tPath)) return { path: rel, content: fs18.readFileSync(tPath, "utf8"), target: "translation", status: fileStatus(root2, l, rel) };
|
|
1553
2111
|
return { path: rel, content: source, target: "source", status: "missing" };
|
|
1554
2112
|
}
|
|
1555
2113
|
return { path: rel, content: source, target: "source", status: null };
|
|
@@ -1557,18 +2115,20 @@ async function startServer({ root: root2, port, open: openBrowser }) {
|
|
|
1557
2115
|
"PUT /api/file": (_, body) => {
|
|
1558
2116
|
const rel = mdPath(body.path);
|
|
1559
2117
|
const l = viewLang(body.lang);
|
|
1560
|
-
const file = body.target === "translation" && l ?
|
|
1561
|
-
|
|
1562
|
-
|
|
2118
|
+
const file = body.target === "translation" && l ? translationFile(l, rel) : safeResolve(root2, rel);
|
|
2119
|
+
fs18.mkdirSync(path18.dirname(file), { recursive: true });
|
|
2120
|
+
const content = String(body.content ?? "");
|
|
2121
|
+
if (!l && featureState(root2).agent) logSave(root2, rel, fs18.existsSync(file) ? fs18.readFileSync(file, "utf8") : "", content);
|
|
2122
|
+
fs18.writeFileSync(file, content);
|
|
1563
2123
|
return { ok: true, status: l ? fileStatus(root2, l, rel) : null };
|
|
1564
2124
|
},
|
|
1565
2125
|
"POST /api/file": (_, body) => {
|
|
1566
2126
|
const rel = mdPath(body.path);
|
|
1567
2127
|
const file = safeResolve(root2, rel);
|
|
1568
|
-
if (
|
|
1569
|
-
|
|
1570
|
-
const title =
|
|
1571
|
-
|
|
2128
|
+
if (fs18.existsSync(file)) throw new HttpError(409, "File already exists");
|
|
2129
|
+
fs18.mkdirSync(path18.dirname(file), { recursive: true });
|
|
2130
|
+
const title = path18.basename(rel, ".md").replace(/^\d+[-_]/, "").replace(/[-_]/g, " ");
|
|
2131
|
+
fs18.writeFileSync(file, `# ${title.charAt(0).toUpperCase() + title.slice(1)}
|
|
1572
2132
|
|
|
1573
2133
|
`);
|
|
1574
2134
|
return { ok: true, path: rel };
|
|
@@ -1578,23 +2138,24 @@ async function startServer({ root: root2, port, open: openBrowser }) {
|
|
|
1578
2138
|
if (typeof body.path !== "string" || !body.path.trim()) throw new HttpError(400, "Expected a path");
|
|
1579
2139
|
const dir = safeResolve(root2, body.path);
|
|
1580
2140
|
if (dir === root2) throw new HttpError(400, "Expected a path");
|
|
1581
|
-
if (
|
|
1582
|
-
|
|
1583
|
-
return { ok: true, path: toPosix(
|
|
2141
|
+
if (fs18.existsSync(dir)) throw new HttpError(409, "A file or folder with that name already exists");
|
|
2142
|
+
fs18.mkdirSync(dir, { recursive: true });
|
|
2143
|
+
return { ok: true, path: toPosix(path18.relative(root2, dir)) };
|
|
1584
2144
|
},
|
|
1585
2145
|
"GET /api/delete-preview": (url2) => {
|
|
1586
2146
|
const abs = deletable(url2.searchParams.get("path"));
|
|
1587
|
-
if (!
|
|
1588
|
-
const files =
|
|
2147
|
+
if (!fs18.statSync(abs).isDirectory()) return { kind: "file", files: 1, md: 1 };
|
|
2148
|
+
const files = fs18.readdirSync(abs, { recursive: true, withFileTypes: true }).filter((e) => e.isFile());
|
|
1589
2149
|
return { kind: "dir", files: files.length, md: files.filter((e) => e.name.toLowerCase().endsWith(".md")).length };
|
|
1590
2150
|
},
|
|
1591
2151
|
"DELETE /api/path": (url2) => {
|
|
1592
|
-
const rel = toPosix(
|
|
1593
|
-
|
|
1594
|
-
for (const l of config().langs)
|
|
2152
|
+
const rel = toPosix(path18.relative(root2, deletable(url2.searchParams.get("path"))));
|
|
2153
|
+
fs18.rmSync(safeResolve(root2, rel), { recursive: true });
|
|
2154
|
+
for (const l of config().langs) fs18.rmSync(translationFile(l, rel), { recursive: true, force: true });
|
|
1595
2155
|
return { ok: true };
|
|
1596
2156
|
},
|
|
1597
2157
|
"POST /api/ignore": (_, body) => {
|
|
2158
|
+
if (typeof body.path !== "string" || /[\r\n]/.test(body.path)) throw new HttpError(400, "Expected a path");
|
|
1598
2159
|
safeResolve(root2, body.path);
|
|
1599
2160
|
setIgnored(root2, body.path, body.kind === "dir" ? "dir" : "file", !!body.ignored);
|
|
1600
2161
|
return { ok: true };
|
|
@@ -1607,7 +2168,7 @@ async function startServer({ root: root2, port, open: openBrowser }) {
|
|
|
1607
2168
|
},
|
|
1608
2169
|
"GET /api/rules": () => {
|
|
1609
2170
|
const mainDoc = readConfig(root2)?.mainDoc;
|
|
1610
|
-
const text3 = mainDoc &&
|
|
2171
|
+
const text3 = mainDoc && fs18.existsSync(path18.join(root2, mainDoc)) ? fs18.readFileSync(path18.join(root2, mainDoc), "utf8") : "";
|
|
1611
2172
|
return { rules: listRules(), applied: appliedRuleIds(text3), mainDoc: mainDoc ?? null };
|
|
1612
2173
|
},
|
|
1613
2174
|
"PUT /api/rules": (_, body) => saveRule(body),
|
|
@@ -1623,29 +2184,31 @@ async function startServer({ root: root2, port, open: openBrowser }) {
|
|
|
1623
2184
|
},
|
|
1624
2185
|
"GET /api/skills/file": (url2) => {
|
|
1625
2186
|
const skill = getSkill(root2, url2.searchParams.get("key"));
|
|
1626
|
-
return { path: skill.key, content:
|
|
2187
|
+
return { path: skill.key, content: fs18.readFileSync(path18.join(skill.key, "SKILL.md"), "utf8"), target: "source", status: null };
|
|
1627
2188
|
},
|
|
1628
2189
|
"PUT /api/skills/file": (_, body) => {
|
|
1629
2190
|
const skill = getSkill(root2, body.key);
|
|
1630
2191
|
if (skill.managed) throw new HttpError(403, "This skill is managed by the app that installed it");
|
|
1631
|
-
|
|
2192
|
+
fs18.writeFileSync(path18.join(skill.key, "SKILL.md"), String(body.content ?? ""));
|
|
1632
2193
|
return { ok: true, status: null };
|
|
1633
2194
|
},
|
|
1634
2195
|
"POST /api/skills/install": (_, body) => {
|
|
1635
2196
|
const targets = (Array.isArray(body.targets) ? body.targets : []).filter((t) => typeof t === "string" && t in PROJECT_TARGETS);
|
|
1636
2197
|
if (!targets.length) throw new HttpError(400, "Expected at least one target");
|
|
1637
|
-
const result =
|
|
2198
|
+
const result = installSkill(root2, getSkill(root2, body.key), targets, !!body.overwrite);
|
|
1638
2199
|
if (result.conflict) throw new HttpError(409, result.conflict.join(", "));
|
|
1639
2200
|
return result;
|
|
1640
2201
|
},
|
|
1641
2202
|
"POST /api/rules/apply": (_, body) => {
|
|
1642
2203
|
const mainDoc = readConfig(root2)?.mainDoc;
|
|
1643
2204
|
if (!mainDoc) throw new HttpError(400, "No main document configured; run `markode init`");
|
|
1644
|
-
applyRulesToFile(
|
|
2205
|
+
applyRulesToFile(path18.join(root2, mainDoc), body.ids ?? []);
|
|
1645
2206
|
return { ok: true };
|
|
1646
2207
|
}
|
|
1647
2208
|
};
|
|
1648
2209
|
const server = http.createServer(async (req, res) => {
|
|
2210
|
+
res.setHeader("X-Frame-Options", "DENY");
|
|
2211
|
+
res.setHeader("X-Content-Type-Options", "nosniff");
|
|
1649
2212
|
if (!isLocalRequest(req)) {
|
|
1650
2213
|
res.writeHead(403).end("Forbidden");
|
|
1651
2214
|
return;
|
|
@@ -1673,13 +2236,13 @@ async function startServer({ root: root2, port, open: openBrowser }) {
|
|
|
1673
2236
|
res.writeHead(404).end();
|
|
1674
2237
|
return;
|
|
1675
2238
|
}
|
|
1676
|
-
const type = IMAGE_MIME[
|
|
1677
|
-
if (!type || !
|
|
2239
|
+
const type = IMAGE_MIME[path18.extname(file).toLowerCase()];
|
|
2240
|
+
if (!type || !fs18.existsSync(file) || !fs18.statSync(file).isFile()) {
|
|
1678
2241
|
res.writeHead(404).end();
|
|
1679
2242
|
return;
|
|
1680
2243
|
}
|
|
1681
|
-
res.writeHead(200, { "content-type": type, "cache-control": "no-cache", "content-security-policy": "sandbox
|
|
1682
|
-
|
|
2244
|
+
res.writeHead(200, { "content-type": type, "cache-control": "no-cache", "content-security-policy": "sandbox; frame-ancestors 'none'" });
|
|
2245
|
+
fs18.createReadStream(file).pipe(res);
|
|
1683
2246
|
}
|
|
1684
2247
|
const actualPort = await listen(server, port);
|
|
1685
2248
|
const wss = new WebSocketServer({ server, path: "/ws", verifyClient: ({ req }) => isLocalRequest(req) });
|
|
@@ -1693,16 +2256,16 @@ async function startServer({ root: root2, port, open: openBrowser }) {
|
|
|
1693
2256
|
// Unreadable folders (e.g. caches owned by another user) are skipped instead of failing.
|
|
1694
2257
|
ignorePermissionErrors: true,
|
|
1695
2258
|
ignored: (p3, stats) => {
|
|
1696
|
-
const rel = toPosix(
|
|
2259
|
+
const rel = toPosix(path18.relative(root2, p3));
|
|
1697
2260
|
if (rel === ".markode") return false;
|
|
1698
2261
|
if (rel.startsWith(".markode/")) return !rel.startsWith(".markode/i18n") && rel !== ".markode/state.json" && rel !== ".markode/config.json";
|
|
1699
2262
|
return skips(rel, stats ? stats.isDirectory() : void 0);
|
|
1700
2263
|
}
|
|
1701
2264
|
}).on("error", (err) => console.warn(pc.yellow(` file watcher: ${err.message}`))).on("all", (event, file) => {
|
|
1702
|
-
const abs =
|
|
2265
|
+
const abs = path18.resolve(file);
|
|
1703
2266
|
if (!abs.endsWith(".md") && !abs.endsWith(".json") && !abs.endsWith(".markodeignore") && event !== "addDir" && event !== "unlinkDir") return;
|
|
1704
|
-
let rel = toPosix(
|
|
1705
|
-
const i18n = toPosix(
|
|
2267
|
+
let rel = toPosix(path18.relative(root2, abs));
|
|
2268
|
+
const i18n = toPosix(path18.relative(root2, path18.join(stateDir(root2), "i18n")));
|
|
1706
2269
|
if (rel.startsWith(i18n + "/")) rel = rel.split("/").slice(3).join("/");
|
|
1707
2270
|
broadcast({ type: "fs", event, path: rel });
|
|
1708
2271
|
});
|
|
@@ -1710,7 +2273,7 @@ async function startServer({ root: root2, port, open: openBrowser }) {
|
|
|
1710
2273
|
console.log(`
|
|
1711
2274
|
${pc.bold("markode")} ${pc.dim("\u2192")} ${pc.cyan(url)} ${pc.dim(root2)}
|
|
1712
2275
|
`);
|
|
1713
|
-
if (!
|
|
2276
|
+
if (!fs18.existsSync(WEB_DIR)) console.log(pc.yellow(" Web UI not built: run `pnpm build`, or `pnpm dev` and open http://localhost:4320\n"));
|
|
1714
2277
|
else if (openBrowser) open(url).catch(() => {
|
|
1715
2278
|
});
|
|
1716
2279
|
}
|
|
@@ -1724,19 +2287,19 @@ function listen(server, port) {
|
|
|
1724
2287
|
});
|
|
1725
2288
|
}
|
|
1726
2289
|
function serveStatic(pathname, res) {
|
|
1727
|
-
let file =
|
|
1728
|
-
if (!file.startsWith(WEB_DIR) || !
|
|
1729
|
-
if (!
|
|
2290
|
+
let file = path18.join(WEB_DIR, path18.normalize(pathname).replace(/^(\.\.[/\\])+/, ""));
|
|
2291
|
+
if (!file.startsWith(WEB_DIR) || !fs18.existsSync(file) || fs18.statSync(file).isDirectory()) file = path18.join(WEB_DIR, "index.html");
|
|
2292
|
+
if (!fs18.existsSync(file)) {
|
|
1730
2293
|
res.writeHead(404).end("Web UI not built. Run `pnpm build`.");
|
|
1731
2294
|
return;
|
|
1732
2295
|
}
|
|
1733
|
-
res.writeHead(200, { "content-type": MIME[
|
|
1734
|
-
|
|
2296
|
+
res.writeHead(200, { "content-type": MIME[path18.extname(file)] ?? "application/octet-stream", "content-security-policy": "frame-ancestors 'none'" });
|
|
2297
|
+
fs18.createReadStream(file).pipe(res);
|
|
1735
2298
|
}
|
|
1736
2299
|
|
|
1737
2300
|
// src/cli/init.ts
|
|
1738
|
-
import
|
|
1739
|
-
import
|
|
2301
|
+
import fs19 from "fs";
|
|
2302
|
+
import path19 from "path";
|
|
1740
2303
|
import * as p from "@clack/prompts";
|
|
1741
2304
|
import pc2 from "picocolors";
|
|
1742
2305
|
function check(value) {
|
|
@@ -1747,12 +2310,12 @@ function check(value) {
|
|
|
1747
2310
|
return value;
|
|
1748
2311
|
}
|
|
1749
2312
|
async function offerClaudeLink(root2, d) {
|
|
1750
|
-
if (
|
|
2313
|
+
if (fs19.existsSync(path19.join(root2, "CLAUDE.md"))) return;
|
|
1751
2314
|
const link2 = d ? d.mainDoc === "CLAUDE.md" : check(await p.confirm({ message: "Claude Code reads CLAUDE.md, not AGENTS.md. Add a CLAUDE.md that imports AGENTS.md?", initialValue: true }));
|
|
1752
2315
|
if (link2 && linkAgentsDoc(root2)) p.log.success("Created CLAUDE.md (imports AGENTS.md)");
|
|
1753
2316
|
}
|
|
1754
2317
|
async function chooseMainDoc(root2, d) {
|
|
1755
|
-
const existing = MAIN_DOCS.filter((d2) =>
|
|
2318
|
+
const existing = MAIN_DOCS.filter((d2) => fs19.existsSync(path19.join(root2, d2)) && !isPointer(fs19.readFileSync(path19.join(root2, d2), "utf8"), d2));
|
|
1756
2319
|
if (d) {
|
|
1757
2320
|
if (existing.length === 1 && existing[0] === "AGENTS.md") await offerClaudeLink(root2, d);
|
|
1758
2321
|
if (existing.length) return existing.includes(d.mainDoc) ? d.mainDoc : existing[0];
|
|
@@ -1798,7 +2361,7 @@ var WORKFLOW_OPTIONS = [
|
|
|
1798
2361
|
{ value: "openspec", label: "OpenSpec", hint: "specs + proposed changes, good for existing projects" },
|
|
1799
2362
|
{ value: "speckit", label: "GitHub Spec Kit", hint: "constitution \u2192 spec \u2192 plan \u2192 tasks, per feature" }
|
|
1800
2363
|
];
|
|
1801
|
-
var PROPOSALS_QUESTION = "The
|
|
2364
|
+
var PROPOSALS_QUESTION = "The agent can write proposals you answer by ticking options and commenting. How should they be organized?";
|
|
1802
2365
|
var PROPOSAL_OPTIONS = [
|
|
1803
2366
|
{ value: "structured", label: "By topic and area", hint: "docs/proposals/auth/\u2026, services/docs/proposals/\u2026" },
|
|
1804
2367
|
{ value: "general", label: "One folder", hint: "docs/proposals/" }
|
|
@@ -1811,7 +2374,6 @@ var MONOREPO_OPTIONS = [
|
|
|
1811
2374
|
var SOURCE_LANG_QUESTION = "Language you work in (every .md file is written in it)";
|
|
1812
2375
|
var SOURCE_LANG_MARK = /<!-- markode:source-lang:\w+ -->/;
|
|
1813
2376
|
var LANG_OPTIONS = LANGS.map((l) => ({ value: l, label: LANG_NAMES[l], hint: l === "en" ? "default" : void 0 }));
|
|
1814
|
-
var HIDE_QUESTION = "Hide the .markode/ folder in VS Code?";
|
|
1815
2377
|
var ruleOptions = (rules) => rules.map((r) => ({ value: r.id, label: r.title, hint: r.body.split("\n")[0].replace(/^- /, "") }));
|
|
1816
2378
|
async function chooseRules(message, rules, initial) {
|
|
1817
2379
|
const pick = check(
|
|
@@ -1830,11 +2392,10 @@ async function chooseRules(message, rules, initial) {
|
|
|
1830
2392
|
var agentOptions = () => Object.keys(SDD_AGENTS).map((a) => ({ value: a, label: SDD_AGENTS[a].label }));
|
|
1831
2393
|
var WORKFLOW_LABEL = { none: "None", markode: "markode", openspec: "OpenSpec", speckit: "GitHub Spec Kit" };
|
|
1832
2394
|
var WORKFLOW_GUIDE = { none: "", markode: ".claude/skills/markode-method/SKILL.md", openspec: "openspec/README.md", speckit: "specs/README.md" };
|
|
1833
|
-
|
|
1834
|
-
async function chooseAreas(root2, d) {
|
|
2395
|
+
async function chooseAreas(root2) {
|
|
1835
2396
|
const { dirs: candidates, unsure } = detectAreas(root2);
|
|
1836
2397
|
if (!candidates.length) return [];
|
|
1837
|
-
if (unsure
|
|
2398
|
+
if (unsure) {
|
|
1838
2399
|
const separate = check(
|
|
1839
2400
|
await p.confirm({
|
|
1840
2401
|
message: `${candidates.map((c) => `${c}/`).join(", ")} each have their own manifest. Are they separate projects (e.g. a front end and a back end)?`,
|
|
@@ -1843,17 +2404,13 @@ async function chooseAreas(root2, d) {
|
|
|
1843
2404
|
);
|
|
1844
2405
|
if (!separate) return [];
|
|
1845
2406
|
}
|
|
1846
|
-
if (d) {
|
|
1847
|
-
const used2 = /* @__PURE__ */ new Set();
|
|
1848
|
-
return d.monorepo === "single" ? [] : candidates.map((dir) => ({ dir, prefix: suggestPrefix(dir, used2) }));
|
|
1849
|
-
}
|
|
1850
2407
|
p.log.info(`Monorepo detected: ${candidates.map((c) => `${c}/`).join(", ")}`);
|
|
1851
2408
|
const layout = check(await p.select({ message: MONOREPO_QUESTION, options: MONOREPO_OPTIONS }));
|
|
1852
2409
|
if (layout === "single") return [];
|
|
1853
2410
|
const dirs2 = check(
|
|
1854
2411
|
await p.multiselect({
|
|
1855
2412
|
message: "Which projects get their own docs?",
|
|
1856
|
-
options: candidates.map((
|
|
2413
|
+
options: candidates.map((d) => ({ value: d, label: `${d}/` })),
|
|
1857
2414
|
initialValues: candidates,
|
|
1858
2415
|
required: false
|
|
1859
2416
|
})
|
|
@@ -1875,8 +2432,8 @@ async function chooseAreas(root2, d) {
|
|
|
1875
2432
|
return areas;
|
|
1876
2433
|
}
|
|
1877
2434
|
async function chooseKind(root2, dir) {
|
|
1878
|
-
const guess = guessKind(
|
|
1879
|
-
if (!guess.unclear
|
|
2435
|
+
const guess = guessKind(path19.join(root2, dir));
|
|
2436
|
+
if (!guess.unclear) return guess.kind;
|
|
1880
2437
|
const where = dir ? `${dir}/` : "this project";
|
|
1881
2438
|
const seen = guess.signals.length ? `found: ${guess.signals.join(", ")}` : "no code yet";
|
|
1882
2439
|
return check(
|
|
@@ -1887,9 +2444,9 @@ async function chooseKind(root2, dir) {
|
|
|
1887
2444
|
})
|
|
1888
2445
|
);
|
|
1889
2446
|
}
|
|
1890
|
-
async function askProfile(root2, dir, kind
|
|
1891
|
-
const base = detectProfile(
|
|
1892
|
-
if (
|
|
2447
|
+
async function askProfile(root2, dir, kind) {
|
|
2448
|
+
const base = detectProfile(path19.join(root2, dir), kind);
|
|
2449
|
+
if (kind === "app") return base;
|
|
1893
2450
|
const where = dir ? ` (${dir}/)` : "";
|
|
1894
2451
|
if (kind === "library") return { kind, published: check(await p.confirm({ message: `Is the library${where} published (npm, PyPI\u2026)?`, initialValue: base.published })) };
|
|
1895
2452
|
if (kind === "mobile") {
|
|
@@ -1917,10 +2474,9 @@ async function askProfile(root2, dir, kind, d) {
|
|
|
1917
2474
|
const environments = check(await p.text({ message: `Environments${where}`, initialValue: base.environments, defaultValue: base.environments }));
|
|
1918
2475
|
return { kind, environments: environments.trim() };
|
|
1919
2476
|
}
|
|
1920
|
-
async function chooseContracts(root2, areas
|
|
2477
|
+
async function chooseContracts(root2, areas) {
|
|
1921
2478
|
if (!looksLikeMicroservices(root2, areas.length)) return null;
|
|
1922
2479
|
const found = findContracts(root2);
|
|
1923
|
-
if (d && found || !canAsk()) return found;
|
|
1924
2480
|
const answer = check(
|
|
1925
2481
|
await p.text({
|
|
1926
2482
|
message: "Several services: where do their contracts (APIs, events) live? (e.g. proto/, an OpenAPI file; empty if none)",
|
|
@@ -1930,9 +2486,9 @@ async function chooseContracts(root2, areas, d) {
|
|
|
1930
2486
|
);
|
|
1931
2487
|
return answer.trim() || null;
|
|
1932
2488
|
}
|
|
1933
|
-
async function chooseExisting(root2
|
|
2489
|
+
async function chooseExisting(root2) {
|
|
1934
2490
|
const found = existingDocs(root2);
|
|
1935
|
-
if (!found.length
|
|
2491
|
+
if (!found.length) return found;
|
|
1936
2492
|
return check(
|
|
1937
2493
|
await p.multiselect({
|
|
1938
2494
|
message: "These docs already exist. Which are the official ones? (the agent updates them instead of creating new ones)",
|
|
@@ -1942,38 +2498,43 @@ async function chooseExisting(root2, d) {
|
|
|
1942
2498
|
})
|
|
1943
2499
|
);
|
|
1944
2500
|
}
|
|
1945
|
-
async function chooseWorkflow(root2, mainDoc, previous,
|
|
2501
|
+
async function chooseWorkflow(root2, mainDoc, previous, quiet) {
|
|
1946
2502
|
const detected = detectWorkflow(root2);
|
|
1947
2503
|
if (detected) {
|
|
1948
2504
|
p.log.info(`${WORKFLOW_LABEL[detected]} found: markode shows it in the dashboard.`);
|
|
1949
|
-
return
|
|
2505
|
+
return detected;
|
|
2506
|
+
}
|
|
2507
|
+
if (previous === "markode") return previous;
|
|
2508
|
+
if (quiet) {
|
|
2509
|
+
const { created, areas } = setupMethod(root2, mainDoc, quiet.monorepo);
|
|
2510
|
+
activateProposals(root2, areas.length ? "structured" : "general", mainDoc);
|
|
2511
|
+
if (created.length) p.log.success(`Tasks structure: ${created.length} files (${created.slice(0, 5).join(", ")}${created.length > 5 ? "\u2026" : ""})`);
|
|
2512
|
+
return "markode";
|
|
1950
2513
|
}
|
|
1951
2514
|
for (; ; ) {
|
|
1952
|
-
const choice =
|
|
1953
|
-
if (choice === "none") return
|
|
2515
|
+
const choice = check(await p.select({ message: "Create a base structure for specs and planning?", options: WORKFLOW_OPTIONS, initialValue: previous ?? "markode" }));
|
|
2516
|
+
if (choice === "none") return choice;
|
|
1954
2517
|
if (choice === "markode") {
|
|
1955
|
-
const existing = await chooseExisting(root2
|
|
1956
|
-
const areas = await chooseAreas(root2
|
|
1957
|
-
for (const a of areas) a.profile = await askProfile(root2, a.dir, await chooseKind(root2, a.dir)
|
|
1958
|
-
const contracts = await chooseContracts(root2, areas
|
|
1959
|
-
const profile = areas.length ? void 0 : await askProfile(root2, "", await chooseKind(root2, "")
|
|
2518
|
+
const existing = await chooseExisting(root2);
|
|
2519
|
+
const areas = await chooseAreas(root2);
|
|
2520
|
+
for (const a of areas) a.profile = await askProfile(root2, a.dir, await chooseKind(root2, a.dir));
|
|
2521
|
+
const contracts = await chooseContracts(root2, areas);
|
|
2522
|
+
const profile = areas.length ? void 0 : await askProfile(root2, "", await chooseKind(root2, ""));
|
|
1960
2523
|
const created = scaffoldMethod(root2, mainDoc, areas);
|
|
1961
2524
|
installMethodSkills(root2);
|
|
2525
|
+
applyWorkflowToFile(path19.join(root2, mainDoc), "markode", methodSection(areas, root2, { profile, contracts, existing }));
|
|
1962
2526
|
const big = !areas.length && flattenFiles(scanTree(root2)).length > 40;
|
|
1963
|
-
const layout = areas.length ? "structured" : !big
|
|
2527
|
+
const layout = areas.length ? "structured" : !big ? "general" : check(await p.select({ message: PROPOSALS_QUESTION, options: PROPOSAL_OPTIONS }));
|
|
1964
2528
|
activateProposals(root2, layout, mainDoc);
|
|
1965
2529
|
if (created.length) p.log.success(`Created ${created.length} files: ${created.slice(0, 8).join(", ")}${created.length > 8 ? "\u2026" : ""}`);
|
|
1966
2530
|
p.log.info(`Docs grow when there is something to write. Skills: /markode-status, /markode-new-task, /markode-close-session.`);
|
|
1967
|
-
return
|
|
2531
|
+
return choice;
|
|
1968
2532
|
}
|
|
1969
2533
|
let official = true;
|
|
1970
|
-
if (choice === "speckit" && !hasUv()
|
|
1971
|
-
p.log.warn("Spec Kit needs uv (Python) to install its AI commands, and uv was not found: creating the structure only.");
|
|
1972
|
-
official = false;
|
|
1973
|
-
} else if (choice === "speckit" && !hasUv()) {
|
|
2534
|
+
if (choice === "speckit" && !hasUv()) {
|
|
1974
2535
|
const next = check(
|
|
1975
2536
|
await p.select({
|
|
1976
|
-
message: "Spec Kit needs uv (Python) to install its
|
|
2537
|
+
message: "Spec Kit needs uv (Python) to install its agent commands, and uv was not found.",
|
|
1977
2538
|
options: [
|
|
1978
2539
|
{ value: "back", label: "Choose another option" },
|
|
1979
2540
|
{ value: "skeleton", label: "Create the structure only", hint: "the guide explains how to install Spec Kit later" }
|
|
@@ -1984,7 +2545,7 @@ async function chooseWorkflow(root2, mainDoc, previous, d) {
|
|
|
1984
2545
|
official = false;
|
|
1985
2546
|
}
|
|
1986
2547
|
const initial = mainDoc === "AGENTS.md" ? "codex" : "claude";
|
|
1987
|
-
const agents = !official ? [initial] :
|
|
2548
|
+
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 }))];
|
|
1988
2549
|
const spin = p.spinner();
|
|
1989
2550
|
spin.start(official ? `Installing ${WORKFLOW_LABEL[choice]}\u2026` : "Creating the structure\u2026");
|
|
1990
2551
|
const res = installWorkflow(root2, choice, agents, official);
|
|
@@ -1992,51 +2553,86 @@ async function chooseWorkflow(root2, mainDoc, previous, d) {
|
|
|
1992
2553
|
if (official && !res.official) p.log.warn(`Couldn't run the ${WORKFLOW_LABEL[choice]} installer:
|
|
1993
2554
|
${pc2.dim(res.error ?? "")}
|
|
1994
2555
|
Created the structure only.`);
|
|
1995
|
-
if (!res.official) p.log.info(`To add the
|
|
2556
|
+
if (!res.official) p.log.info(`To add the agent commands later, see ${pc2.cyan(WORKFLOW_GUIDE[choice])}.`);
|
|
1996
2557
|
if (res.ignored.length) p.log.info(`Agent commands hidden in markode (${res.ignored.length} folders); the eye button in the sidebar shows them.`);
|
|
1997
|
-
return
|
|
2558
|
+
return choice;
|
|
1998
2559
|
}
|
|
1999
2560
|
}
|
|
2000
|
-
|
|
2561
|
+
var featureOptions = () => FEATURES.map((f) => ({ value: f, label: FEATURE_INFO[f].label, hint: FEATURE_INFO[f].hint + (REQUIRES[f] ? ` (needs ${FEATURE_INFO[REQUIRES[f]].label})` : "") }));
|
|
2562
|
+
async function askFeatures(message = "What should markode install in this project? (the viewer is always included)", initial = PROFILE_FEATURES.agent) {
|
|
2563
|
+
const same = (a, b) => a.length === b.length && a.every((f) => b.includes(f));
|
|
2564
|
+
const current = Object.keys(PROFILE_FEATURES).find((k) => same(PROFILE_FEATURES[k], initial)) ?? "custom";
|
|
2565
|
+
const choice = check(
|
|
2566
|
+
await p.select({
|
|
2567
|
+
message,
|
|
2568
|
+
options: [
|
|
2569
|
+
...Object.keys(PROFILE_INFO).map((k) => ({ value: k, label: PROFILE_INFO[k].label + (k === "agent" ? " (recommended)" : ""), hint: PROFILE_INFO[k].hint })),
|
|
2570
|
+
{ value: "custom", label: "Choose the modules", hint: "pick each one; any of them can be turned on or off later in Settings" }
|
|
2571
|
+
],
|
|
2572
|
+
initialValue: current
|
|
2573
|
+
})
|
|
2574
|
+
);
|
|
2575
|
+
if (choice !== "custom") return PROFILE_FEATURES[choice];
|
|
2576
|
+
return withRequired(check(await p.multiselect({ message: "Modules (space to toggle)", options: featureOptions(), initialValues: initial, required: false })));
|
|
2577
|
+
}
|
|
2578
|
+
async function runInit(root2, plan2) {
|
|
2001
2579
|
p.intro(pc2.bgCyan(pc2.black(" markode ")));
|
|
2002
|
-
|
|
2580
|
+
const d = plan2.defaults;
|
|
2003
2581
|
const previous = readConfig(root2);
|
|
2004
|
-
const
|
|
2005
|
-
const
|
|
2006
|
-
const
|
|
2007
|
-
|
|
2008
|
-
|
|
2009
|
-
|
|
2010
|
-
|
|
2011
|
-
|
|
2012
|
-
|
|
2013
|
-
|
|
2014
|
-
|
|
2015
|
-
|
|
2016
|
-
|
|
2017
|
-
const
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
|
|
2023
|
-
|
|
2024
|
-
|
|
2025
|
-
|
|
2582
|
+
const features2 = withRequired(plan2.features);
|
|
2583
|
+
const has2 = (f) => features2.includes(f);
|
|
2584
|
+
const detectedLang = detectSourceLang(root2, d?.sourceLang ?? "en");
|
|
2585
|
+
ignoreStateDir(root2);
|
|
2586
|
+
let mainDoc = previous?.mainDoc ?? mainDocOf(root2);
|
|
2587
|
+
let workflow = previous?.workflow ?? "none";
|
|
2588
|
+
if (has2("agent")) {
|
|
2589
|
+
mainDoc = await chooseMainDoc(root2, d);
|
|
2590
|
+
const mainFile = path19.join(root2, mainDoc);
|
|
2591
|
+
if (has2("dashboard")) workflow = await chooseWorkflow(root2, mainDoc, previous?.workflow, d);
|
|
2592
|
+
if (workflow === "openspec" || workflow === "speckit") applyWorkflowToFile(mainFile, workflow);
|
|
2593
|
+
const mainText = fs19.readFileSync(mainFile, "utf8");
|
|
2594
|
+
const sourceLang = d ? SOURCE_LANG_MARK.test(mainText) ? sourceLangOf(mainText) : detectedLang : check(await p.select({ message: SOURCE_LANG_QUESTION, options: LANG_OPTIONS, initialValue: SOURCE_LANG_MARK.test(mainText) ? sourceLangOf(mainText) : detectedLang }));
|
|
2595
|
+
const rules = listRules();
|
|
2596
|
+
const applied = appliedRuleIds(mainText);
|
|
2597
|
+
const selected = !has2("rules") ? applied : d ? [.../* @__PURE__ */ new Set([...applied, ...d.rules.filter((id) => rules.some((r) => r.id === id))])] : await chooseRules("Working rules for this project", rules, applied);
|
|
2598
|
+
applyRulesToFile(mainFile, selected, sourceLang);
|
|
2599
|
+
const subprojects = findSubprojects(scanTree(root2));
|
|
2600
|
+
if (subprojects.length && selected.length) {
|
|
2601
|
+
const targets = d ? d.rulesInSubprojects ? subprojects : [] : check(
|
|
2602
|
+
await p.multiselect({
|
|
2603
|
+
message: "Monorepo detected. Apply the same rules to these projects too?",
|
|
2604
|
+
options: subprojects.map((s) => ({ value: s, label: s })),
|
|
2605
|
+
required: false
|
|
2606
|
+
})
|
|
2607
|
+
);
|
|
2608
|
+
for (const dir of targets) applyRulesToFile(contentDoc(path19.join(root2, dir)), selected, sourceLang);
|
|
2026
2609
|
}
|
|
2027
|
-
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
|
|
2032
|
-
}
|
|
2033
|
-
|
|
2034
|
-
const
|
|
2610
|
+
installAgentSkill(root2);
|
|
2611
|
+
const proposals = proposalsState(root2);
|
|
2612
|
+
if (has2("proposals") || proposals.active) activateProposals(root2, proposals.active ? proposals.layout : subprojects.length ? "structured" : "general", mainDoc);
|
|
2613
|
+
if (has2("live") && projectTargets(root2).includes("claude") && !hasClaudeHooks(root2) && !installClaudeHooks(root2))
|
|
2614
|
+
p.log.warn('.claude/settings.local.json could not be read; add the markode hooks by hand (README: "Claude Code hooks").');
|
|
2615
|
+
}
|
|
2616
|
+
if (has2("vscode") && !isHiddenInVsCode(root2) && !hideInVsCode(root2)) p.log.warn('.vscode/settings.json has comments; add "**/.markode": true to files.exclude by hand.');
|
|
2617
|
+
const flags = { ...previous?.features };
|
|
2618
|
+
for (const f of ["rules", "translations", "dashboard", "skills"]) flags[f] = has2(f) || (previous?.initializedAt ? flags[f] !== false : false);
|
|
2619
|
+
const config = {
|
|
2620
|
+
...previous,
|
|
2621
|
+
mainDoc,
|
|
2622
|
+
langs: previous?.langs ?? [],
|
|
2623
|
+
workflow,
|
|
2624
|
+
proposals: readConfig(root2)?.proposals,
|
|
2625
|
+
features: flags,
|
|
2626
|
+
initializedAt: previous?.initializedAt ?? (/* @__PURE__ */ new Date()).toISOString()
|
|
2627
|
+
};
|
|
2035
2628
|
writeConfig(root2, config);
|
|
2036
|
-
|
|
2629
|
+
const on = featureState(root2);
|
|
2630
|
+
const lines = FEATURES.filter((f) => on[f]).map((f) => `${pc2.green("\u25CF")} ${FEATURE_INFO[f].label} ${pc2.dim("\u2014 " + FEATURE_INFO[f].hint)}`);
|
|
2631
|
+
p.note([`${pc2.green("\u25CF")} Viewer ${pc2.dim("\u2014 see and edit every .md file of the project")}`, ...lines].join("\n"), "Installed");
|
|
2632
|
+
p.outro(`Ready. Turn modules on or off in ${pc2.cyan("Settings \u2192 Features")}, or with ${pc2.cyan("markode features")}.`);
|
|
2037
2633
|
return config;
|
|
2038
2634
|
}
|
|
2039
|
-
async function runSetup(quick = false) {
|
|
2635
|
+
async function runSetup(quick = false, custom = false) {
|
|
2040
2636
|
const previous = readDefaults() ?? BASE_DEFAULTS;
|
|
2041
2637
|
if (quick) {
|
|
2042
2638
|
const d2 = { ...previous, mode: "ask" };
|
|
@@ -2044,42 +2640,39 @@ async function runSetup(quick = false) {
|
|
|
2044
2640
|
return d2;
|
|
2045
2641
|
}
|
|
2046
2642
|
p.intro(pc2.bgCyan(pc2.black(" markode setup ")));
|
|
2047
|
-
const
|
|
2643
|
+
const ask = check(
|
|
2048
2644
|
await p.select({
|
|
2049
2645
|
message: "How should markode set up your projects?",
|
|
2050
2646
|
options: [
|
|
2051
|
-
{ value: "defaults", label: "
|
|
2052
|
-
{ value: "ask", label: "Ask in each project" }
|
|
2647
|
+
{ value: "defaults", label: "Always the same way", hint: "choose the modules now; every new project gets them without questions" },
|
|
2648
|
+
{ value: "ask", label: "Ask in each project", hint: "one question per project: which modules" }
|
|
2053
2649
|
],
|
|
2054
2650
|
initialValue: previous.mode === "ask" && !readDefaults() ? "defaults" : previous.mode
|
|
2055
2651
|
})
|
|
2056
2652
|
);
|
|
2057
|
-
if (
|
|
2058
|
-
const d2 = { ...previous, mode };
|
|
2653
|
+
if (ask === "ask") {
|
|
2654
|
+
const d2 = { ...previous, mode: "ask" };
|
|
2059
2655
|
writeDefaults(d2);
|
|
2060
|
-
p.outro(`Saved. Each project will ask
|
|
2656
|
+
p.outro(`Saved. Each project will ask which modules to install. Change it with ${pc2.cyan("markode setup")}`);
|
|
2061
2657
|
return d2;
|
|
2062
2658
|
}
|
|
2063
|
-
const
|
|
2064
|
-
|
|
2065
|
-
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
|
|
2074
|
-
|
|
2075
|
-
|
|
2076
|
-
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
const rulesInSubprojects = selected.length ? check(await p.confirm({ message: "In monorepos, apply the rules to the subprojects too?", initialValue: previous.rulesInSubprojects })) : false;
|
|
2081
|
-
const hideInVsCode2 = check(await p.confirm({ message: HIDE_QUESTION, initialValue: previous.hideInVsCode }));
|
|
2082
|
-
const d = { mode, mainDoc, workflow, agents, monorepo, sourceLang, rules: selected, rulesInSubprojects, hideInVsCode: hideInVsCode2 };
|
|
2659
|
+
const features2 = await askFeatures("What should every new project get? (the viewer is always included)", previous.features);
|
|
2660
|
+
let d = { ...previous, mode: "defaults", features: features2 };
|
|
2661
|
+
if (custom) {
|
|
2662
|
+
const mainDoc = check(
|
|
2663
|
+
await p.select({
|
|
2664
|
+
message: "Main document to create when a project has none",
|
|
2665
|
+
options: MAIN_DOCS.map((m) => ({ value: m, label: m, hint: m === "CLAUDE.md" ? "Claude Code" : "other agents" })),
|
|
2666
|
+
initialValue: previous.mainDoc
|
|
2667
|
+
})
|
|
2668
|
+
);
|
|
2669
|
+
const sourceLang = check(await p.select({ message: `${SOURCE_LANG_QUESTION}, when a project's docs don't show it`, options: LANG_OPTIONS, initialValue: previous.sourceLang }));
|
|
2670
|
+
const rules = listRules();
|
|
2671
|
+
const selected = features2.includes("rules") ? await chooseRules("Working rules for your projects", rules, previous.rules.filter((id) => rules.some((r) => r.id === id))) : previous.rules;
|
|
2672
|
+
const rulesInSubprojects = selected.length ? check(await p.confirm({ message: "In monorepos, apply the rules to the subprojects too?", initialValue: previous.rulesInSubprojects })) : false;
|
|
2673
|
+
const monorepo = features2.includes("dashboard") ? check(await p.select({ message: MONOREPO_QUESTION, options: MONOREPO_OPTIONS, initialValue: previous.monorepo })) : previous.monorepo;
|
|
2674
|
+
d = { ...d, mainDoc, sourceLang, rules: selected, rulesInSubprojects, monorepo };
|
|
2675
|
+
}
|
|
2083
2676
|
writeDefaults(d);
|
|
2084
2677
|
p.outro(`Saved in ${pc2.cyan(defaultsFile())}. Change them with ${pc2.cyan("markode setup")}; for a single project: ${pc2.cyan("markode init --custom")}`);
|
|
2085
2678
|
return d;
|
|
@@ -2153,15 +2746,20 @@ async function runTranslate(root2) {
|
|
|
2153
2746
|
var HELP = `markode \u2014 visual editor for your project's Markdown
|
|
2154
2747
|
|
|
2155
2748
|
Usage
|
|
2156
|
-
markode
|
|
2157
|
-
markode init (re)run the project setup:
|
|
2158
|
-
markode setup choose how projects are set up:
|
|
2749
|
+
markode set the project up if needed, then open the editor
|
|
2750
|
+
markode init (re)run the project setup: choose the modules to install (or use your defaults)
|
|
2751
|
+
markode setup choose how projects are set up: always the same modules, or asking each time
|
|
2752
|
+
markode features list the modules and whether they are on in this project
|
|
2753
|
+
markode features <module> on|off turn a module on or off (same as Settings \u2192 Features)
|
|
2159
2754
|
markode serve open the editor without running init
|
|
2160
2755
|
markode translate choose languages to review the docs in (or: markode --translate)
|
|
2161
2756
|
markode check [--json] what /markode sync fixes: answered proposals, phases vs tasks, projects table, broken links
|
|
2162
2757
|
markode wait <proposal> wait until the user answers a proposal in markode, then print the answer
|
|
2163
2758
|
markode link [--dry-run] where there is only CLAUDE.md or only AGENTS.md, add the other one pointing to it
|
|
2164
2759
|
markode link --skip don't offer it again in this project
|
|
2760
|
+
markode changes [--json] what the user edited or commented in markode since the agent was last told
|
|
2761
|
+
markode changes --start a new agent session: what happened before is old news
|
|
2762
|
+
markode changes --hook <event> the same, as Claude Code hook output (silent when nothing changed)
|
|
2165
2763
|
|
|
2166
2764
|
For the agent's /markode translate:
|
|
2167
2765
|
markode translate --status [--json] translations + files not in the source language
|
|
@@ -2170,8 +2768,11 @@ For the agent's /markode translate:
|
|
|
2170
2768
|
|
|
2171
2769
|
Options
|
|
2172
2770
|
-v, --version print the version
|
|
2173
|
-
-D first run:
|
|
2174
|
-
--
|
|
2771
|
+
-D first run: don't save your choice as the default for new projects
|
|
2772
|
+
--profile <viewer|agent|full> init: install this set of modules, without questions
|
|
2773
|
+
--yes init: no questions; your defaults, or the recommended modules (viewer + AI agent)
|
|
2774
|
+
--custom init: choose each module and answer every question for this project
|
|
2775
|
+
setup: also choose the main doc, language, rules and monorepo layout
|
|
2175
2776
|
--port <n> server port (default 4319)
|
|
2176
2777
|
--no-open do not open the browser
|
|
2177
2778
|
`;
|
|
@@ -2184,26 +2785,32 @@ var option = (name) => {
|
|
|
2184
2785
|
var command = args[0] && !args[0].startsWith("-") ? args[0] : "start";
|
|
2185
2786
|
var root = process.env.MARKODE_ROOT ?? process.cwd();
|
|
2186
2787
|
async function main() {
|
|
2788
|
+
if (fs20.existsSync(root)) setProjectRoot(root);
|
|
2187
2789
|
if (flag("--help") || flag("-h")) return console.log(HELP);
|
|
2188
|
-
if (flag("--version") || flag("-v")) return console.log(JSON.parse(
|
|
2790
|
+
if (flag("--version") || flag("-v")) return console.log(JSON.parse(fs20.readFileSync(path20.join(PKG_ROOT, "package.json"), "utf8")).version);
|
|
2189
2791
|
const translation = flag("--status") || flag("--mark") || flag("--keep");
|
|
2190
2792
|
if ((command === "sync" || command === "translate") && translation) return sync();
|
|
2191
2793
|
if (command === "translate" || flag("--translate")) return runTranslate(root);
|
|
2192
2794
|
if (command === "check") return check3();
|
|
2193
2795
|
if (command === "wait") return wait();
|
|
2194
2796
|
if (command === "link") return link();
|
|
2797
|
+
if (command === "changes") return changes();
|
|
2798
|
+
if (command === "features") return features();
|
|
2195
2799
|
if (command === "setup") {
|
|
2196
|
-
await runSetup(flag("
|
|
2800
|
+
await runSetup(false, flag("--custom"));
|
|
2197
2801
|
return;
|
|
2198
2802
|
}
|
|
2199
2803
|
if (command === "init") {
|
|
2200
|
-
|
|
2804
|
+
const p3 = await plan();
|
|
2805
|
+
if (p3) await runInit(root, p3);
|
|
2806
|
+
else console.log("No terminal to ask in: run `markode init --yes` or `--profile <viewer|agent|full>`.");
|
|
2201
2807
|
return;
|
|
2202
2808
|
}
|
|
2203
2809
|
if (command === "start" && !readConfig(root)?.initializedAt) {
|
|
2204
|
-
|
|
2205
|
-
|
|
2810
|
+
const p3 = await plan();
|
|
2811
|
+
if (p3) await runInit(root, p3);
|
|
2206
2812
|
}
|
|
2813
|
+
if (command === "start" || command === "serve") refreshInstalled(root);
|
|
2207
2814
|
const unlinked = command === "start" && process.stdout.isTTY && !readConfig(root)?.skipLink ? missingMainDocs(root) : [];
|
|
2208
2815
|
if (unlinked.length)
|
|
2209
2816
|
console.log(`Tip: Claude Code reads CLAUDE.md and other agents read AGENTS.md; ${unlinked.map((m) => m.path).join(", ")} missing. Run \`markode link\` (or \`markode link --skip\`).`);
|
|
@@ -2214,9 +2821,40 @@ async function main() {
|
|
|
2214
2821
|
console.log(HELP);
|
|
2215
2822
|
process.exitCode = 1;
|
|
2216
2823
|
}
|
|
2217
|
-
async function
|
|
2218
|
-
const
|
|
2219
|
-
|
|
2824
|
+
async function plan() {
|
|
2825
|
+
const saved = readDefaults();
|
|
2826
|
+
const d = saved ?? BASE_DEFAULTS;
|
|
2827
|
+
const profile = option("--profile");
|
|
2828
|
+
if (flag("--custom")) return { features: await askFeatures(), defaults: null };
|
|
2829
|
+
if (profile) {
|
|
2830
|
+
if (!isProfile(profile)) throw new Error(`Unknown profile "${profile}": use viewer, agent or full`);
|
|
2831
|
+
return { features: PROFILE_FEATURES[profile], defaults: d };
|
|
2832
|
+
}
|
|
2833
|
+
if (saved?.mode === "defaults" || flag("--yes")) return { features: d.features, defaults: d };
|
|
2834
|
+
if (!process.stdin.isTTY) return null;
|
|
2835
|
+
const features2 = await askFeatures();
|
|
2836
|
+
if (!saved && !flag("-D")) {
|
|
2837
|
+
writeDefaults({ ...d, mode: "defaults", features: features2 });
|
|
2838
|
+
console.log(` Saved as your default for new projects (they won't ask again). Change it with \`markode setup\`.`);
|
|
2839
|
+
}
|
|
2840
|
+
return { features: features2, defaults: d };
|
|
2841
|
+
}
|
|
2842
|
+
function features() {
|
|
2843
|
+
const [id, value] = [args[1], args[2]];
|
|
2844
|
+
if (id) {
|
|
2845
|
+
if (!isFeature(id) || value !== "on" && value !== "off") {
|
|
2846
|
+
console.error(`Usage: markode features <${FEATURES.join("|")}> on|off`);
|
|
2847
|
+
process.exitCode = 1;
|
|
2848
|
+
return;
|
|
2849
|
+
}
|
|
2850
|
+
const changed = setFeature(root, id, value === "on", readDefaults() ?? BASE_DEFAULTS);
|
|
2851
|
+
for (const [f, on] of Object.entries(changed)) console.log(`${on ? "on " : "off"} ${FEATURE_INFO[f].label}`);
|
|
2852
|
+
if (!Object.keys(changed).length) console.log(`${FEATURE_INFO[id].label} was already ${value}.`);
|
|
2853
|
+
return;
|
|
2854
|
+
}
|
|
2855
|
+
const state = featureState(root);
|
|
2856
|
+
console.log("on Viewer \u2014 see and edit every .md file of the project (always on)");
|
|
2857
|
+
for (const f of FEATURES) console.log(`${state[f] ? "on " : "off"} ${FEATURE_INFO[f].label} \u2014 ${FEATURE_INFO[f].hint}${REQUIRES[f] ? ` (needs ${FEATURE_INFO[REQUIRES[f]].label})` : ""}`);
|
|
2220
2858
|
}
|
|
2221
2859
|
function check3() {
|
|
2222
2860
|
const report = checkDocs(root);
|
|
@@ -2242,9 +2880,21 @@ function link() {
|
|
|
2242
2880
|
if (flag("--dry-run")) return console.log(missing.map((m) => `would create ${m.path} \u2192 ${m.pointsTo}`).join("\n"));
|
|
2243
2881
|
for (const p3 of linkMainDocs(root)) console.log(`created ${p3}`);
|
|
2244
2882
|
}
|
|
2883
|
+
function changes() {
|
|
2884
|
+
if (flag("--start")) return startSession(root);
|
|
2885
|
+
const list = takeChanges(root);
|
|
2886
|
+
const event = option("--hook");
|
|
2887
|
+
if (event) {
|
|
2888
|
+
const text3 = formatChanges(list);
|
|
2889
|
+
if (text3) console.log(JSON.stringify({ hookSpecificOutput: { hookEventName: event, additionalContext: text3 } }));
|
|
2890
|
+
return;
|
|
2891
|
+
}
|
|
2892
|
+
if (flag("--json")) return console.log(JSON.stringify(list, null, 2));
|
|
2893
|
+
console.log(formatChanges(list) || "No edits or comments from the user since you were last told.");
|
|
2894
|
+
}
|
|
2245
2895
|
async function wait() {
|
|
2246
2896
|
const rel = args[1];
|
|
2247
|
-
if (!rel || !
|
|
2897
|
+
if (!rel || !fs20.existsSync(path20.join(root, rel))) {
|
|
2248
2898
|
console.error(`Usage: markode wait <proposal.md> (relative to ${root})`);
|
|
2249
2899
|
process.exitCode = 1;
|
|
2250
2900
|
return;
|