@zettelgeist/cli 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +202 -0
- package/README.md +64 -0
- package/dist/bin.js +2211 -0
- package/dist/bin.js.map +7 -0
- package/dist/templates/export.html +27 -0
- package/dist/templates/skill/SKILL.md +220 -0
- package/dist/viewer-bundle/architecture-YZFGNWBL-W2K3EQOM.js +14 -0
- package/dist/viewer-bundle/architecture-YZFGNWBL-W2K3EQOM.js.map +7 -0
- package/dist/viewer-bundle/architectureDiagram-Q4EWVU46-LCGRUEWT.js +8884 -0
- package/dist/viewer-bundle/architectureDiagram-Q4EWVU46-LCGRUEWT.js.map +7 -0
- package/dist/viewer-bundle/base.css +248 -0
- package/dist/viewer-bundle/blockDiagram-DXYQGD6D-KAM7BOAP.js +3777 -0
- package/dist/viewer-bundle/blockDiagram-DXYQGD6D-KAM7BOAP.js.map +7 -0
- package/dist/viewer-bundle/board.css +370 -0
- package/dist/viewer-bundle/c4Diagram-AHTNJAMY-C3FIQYXA.js +2596 -0
- package/dist/viewer-bundle/c4Diagram-AHTNJAMY-C3FIQYXA.js.map +7 -0
- package/dist/viewer-bundle/chunk-2QXGXAO5.js +68 -0
- package/dist/viewer-bundle/chunk-2QXGXAO5.js.map +7 -0
- package/dist/viewer-bundle/chunk-5PZT7VUU.js +67 -0
- package/dist/viewer-bundle/chunk-5PZT7VUU.js.map +7 -0
- package/dist/viewer-bundle/chunk-5YJCJF2C.js +112 -0
- package/dist/viewer-bundle/chunk-5YJCJF2C.js.map +7 -0
- package/dist/viewer-bundle/chunk-6LYV7PBV.js +1011 -0
- package/dist/viewer-bundle/chunk-6LYV7PBV.js.map +7 -0
- package/dist/viewer-bundle/chunk-6VW7D5WX.js +48 -0
- package/dist/viewer-bundle/chunk-6VW7D5WX.js.map +7 -0
- package/dist/viewer-bundle/chunk-76C5OSD4.js +2048 -0
- package/dist/viewer-bundle/chunk-76C5OSD4.js.map +7 -0
- package/dist/viewer-bundle/chunk-7NZMPQDX.js +6957 -0
- package/dist/viewer-bundle/chunk-7NZMPQDX.js.map +7 -0
- package/dist/viewer-bundle/chunk-A634GTZN.js +122 -0
- package/dist/viewer-bundle/chunk-A634GTZN.js.map +7 -0
- package/dist/viewer-bundle/chunk-AJQJUKMU.js +133 -0
- package/dist/viewer-bundle/chunk-AJQJUKMU.js.map +7 -0
- package/dist/viewer-bundle/chunk-BM2KPNFW.js +5556 -0
- package/dist/viewer-bundle/chunk-BM2KPNFW.js.map +7 -0
- package/dist/viewer-bundle/chunk-CIDUOCCG.js +25 -0
- package/dist/viewer-bundle/chunk-CIDUOCCG.js.map +7 -0
- package/dist/viewer-bundle/chunk-CZHJHAOR.js +6397 -0
- package/dist/viewer-bundle/chunk-CZHJHAOR.js.map +7 -0
- package/dist/viewer-bundle/chunk-D5RLIWY4.js +125 -0
- package/dist/viewer-bundle/chunk-D5RLIWY4.js.map +7 -0
- package/dist/viewer-bundle/chunk-DI52DQAC.js +44 -0
- package/dist/viewer-bundle/chunk-DI52DQAC.js.map +7 -0
- package/dist/viewer-bundle/chunk-EXJQLTIV.js +51 -0
- package/dist/viewer-bundle/chunk-EXJQLTIV.js.map +7 -0
- package/dist/viewer-bundle/chunk-G3PPZWPW.js +96 -0
- package/dist/viewer-bundle/chunk-G3PPZWPW.js.map +7 -0
- package/dist/viewer-bundle/chunk-GTW4IDD4.js +30297 -0
- package/dist/viewer-bundle/chunk-GTW4IDD4.js.map +7 -0
- package/dist/viewer-bundle/chunk-GVE7OA3Z.js +59 -0
- package/dist/viewer-bundle/chunk-GVE7OA3Z.js.map +7 -0
- package/dist/viewer-bundle/chunk-JBUVKVPY.js +2042 -0
- package/dist/viewer-bundle/chunk-JBUVKVPY.js.map +7 -0
- package/dist/viewer-bundle/chunk-JQLVOAQB.js +20 -0
- package/dist/viewer-bundle/chunk-JQLVOAQB.js.map +7 -0
- package/dist/viewer-bundle/chunk-LQMQSYLO.js +101 -0
- package/dist/viewer-bundle/chunk-LQMQSYLO.js.map +7 -0
- package/dist/viewer-bundle/chunk-MBFAQ3IK.js +34 -0
- package/dist/viewer-bundle/chunk-MBFAQ3IK.js.map +7 -0
- package/dist/viewer-bundle/chunk-N7G7IIKG.js +25 -0
- package/dist/viewer-bundle/chunk-N7G7IIKG.js.map +7 -0
- package/dist/viewer-bundle/chunk-NW4YG3NS.js +171 -0
- package/dist/viewer-bundle/chunk-NW4YG3NS.js.map +7 -0
- package/dist/viewer-bundle/chunk-ODEP5TKB.js +61 -0
- package/dist/viewer-bundle/chunk-ODEP5TKB.js.map +7 -0
- package/dist/viewer-bundle/chunk-OGKINV23.js +1050 -0
- package/dist/viewer-bundle/chunk-OGKINV23.js.map +7 -0
- package/dist/viewer-bundle/chunk-OGMSNDVH.js +1994 -0
- package/dist/viewer-bundle/chunk-OGMSNDVH.js.map +7 -0
- package/dist/viewer-bundle/chunk-QJVSDNAW.js +25 -0
- package/dist/viewer-bundle/chunk-QJVSDNAW.js.map +7 -0
- package/dist/viewer-bundle/chunk-RBTT26R4.js +2721 -0
- package/dist/viewer-bundle/chunk-RBTT26R4.js.map +7 -0
- package/dist/viewer-bundle/chunk-RQIPIIE2.js +48 -0
- package/dist/viewer-bundle/chunk-RQIPIIE2.js.map +7 -0
- package/dist/viewer-bundle/chunk-SRTYTXTX.js +22 -0
- package/dist/viewer-bundle/chunk-SRTYTXTX.js.map +7 -0
- package/dist/viewer-bundle/chunk-TRL7YIZG.js +1663 -0
- package/dist/viewer-bundle/chunk-TRL7YIZG.js.map +7 -0
- package/dist/viewer-bundle/chunk-U5T7X4BV.js +172 -0
- package/dist/viewer-bundle/chunk-U5T7X4BV.js.map +7 -0
- package/dist/viewer-bundle/chunk-UCAW6C6C.js +48 -0
- package/dist/viewer-bundle/chunk-UCAW6C6C.js.map +7 -0
- package/dist/viewer-bundle/chunk-UEAG4BJQ.js +93 -0
- package/dist/viewer-bundle/chunk-UEAG4BJQ.js.map +7 -0
- package/dist/viewer-bundle/chunk-UVRE3R6A.js +1039 -0
- package/dist/viewer-bundle/chunk-UVRE3R6A.js.map +7 -0
- package/dist/viewer-bundle/chunk-VODO7SV4.js +25029 -0
- package/dist/viewer-bundle/chunk-VODO7SV4.js.map +7 -0
- package/dist/viewer-bundle/chunk-YEU62MVS.js +682 -0
- package/dist/viewer-bundle/chunk-YEU62MVS.js.map +7 -0
- package/dist/viewer-bundle/chunk-YFQT7PPW.js +987 -0
- package/dist/viewer-bundle/chunk-YFQT7PPW.js.map +7 -0
- package/dist/viewer-bundle/chunk-Z4G7FG27.js +48 -0
- package/dist/viewer-bundle/chunk-Z4G7FG27.js.map +7 -0
- package/dist/viewer-bundle/chunk-ZW4Y7DIF.js +2044 -0
- package/dist/viewer-bundle/chunk-ZW4Y7DIF.js.map +7 -0
- package/dist/viewer-bundle/classDiagram-6PBFFD2Q-7VKYXLUX.js +46 -0
- package/dist/viewer-bundle/classDiagram-6PBFFD2Q-7VKYXLUX.js.map +7 -0
- package/dist/viewer-bundle/classDiagram-v2-HSJHXN6E-ACCNN7EN.js +46 -0
- package/dist/viewer-bundle/classDiagram-v2-HSJHXN6E-ACCNN7EN.js.map +7 -0
- package/dist/viewer-bundle/cose-bilkent-S5V4N54A-MUJHAA34.js +5009 -0
- package/dist/viewer-bundle/cose-bilkent-S5V4N54A-MUJHAA34.js.map +7 -0
- package/dist/viewer-bundle/dagre-KV5264BT-YC5VV3WF.js +739 -0
- package/dist/viewer-bundle/dagre-KV5264BT-YC5VV3WF.js.map +7 -0
- package/dist/viewer-bundle/dark.css +13 -0
- package/dist/viewer-bundle/detail.css +539 -0
- package/dist/viewer-bundle/diagram-5BDNPKRD-RXFPVFYK.js +214 -0
- package/dist/viewer-bundle/diagram-5BDNPKRD-RXFPVFYK.js.map +7 -0
- package/dist/viewer-bundle/diagram-G4DWMVQ6-KN7CBNBQ.js +578 -0
- package/dist/viewer-bundle/diagram-G4DWMVQ6-KN7CBNBQ.js.map +7 -0
- package/dist/viewer-bundle/diagram-MMDJMWI5-ZN6TQ7ZC.js +345 -0
- package/dist/viewer-bundle/diagram-MMDJMWI5-ZN6TQ7ZC.js.map +7 -0
- package/dist/viewer-bundle/diagram-TYMM5635-MMTUJ4KA.js +255 -0
- package/dist/viewer-bundle/diagram-TYMM5635-MMTUJ4KA.js.map +7 -0
- package/dist/viewer-bundle/docs.css +88 -0
- package/dist/viewer-bundle/edit-modal-BEGC2AO6.js +176 -0
- package/dist/viewer-bundle/edit-modal-BEGC2AO6.js.map +7 -0
- package/dist/viewer-bundle/erDiagram-SMLLAGMA-TBHMLD2E.js +1349 -0
- package/dist/viewer-bundle/erDiagram-SMLLAGMA-TBHMLD2E.js.map +7 -0
- package/dist/viewer-bundle/flowDiagram-DWJPFMVM-BZHLK6QB.js +2501 -0
- package/dist/viewer-bundle/flowDiagram-DWJPFMVM-BZHLK6QB.js.map +7 -0
- package/dist/viewer-bundle/ganttDiagram-T4ZO3ILL-YBARPTQR.js +2654 -0
- package/dist/viewer-bundle/ganttDiagram-T4ZO3ILL-YBARPTQR.js.map +7 -0
- package/dist/viewer-bundle/gitGraph-7Q5UKJZL-HENKIQDX.js +14 -0
- package/dist/viewer-bundle/gitGraph-7Q5UKJZL-HENKIQDX.js.map +7 -0
- package/dist/viewer-bundle/gitGraphDiagram-UUTBAWPF-M4VV3YVA.js +1946 -0
- package/dist/viewer-bundle/gitGraphDiagram-UUTBAWPF-M4VV3YVA.js.map +7 -0
- package/dist/viewer-bundle/index.html +28 -0
- package/dist/viewer-bundle/info-OMHHGYJF-E773USRS.js +14 -0
- package/dist/viewer-bundle/info-OMHHGYJF-E773USRS.js.map +7 -0
- package/dist/viewer-bundle/infoDiagram-42DDH7IO-C7JGUXKK.js +59 -0
- package/dist/viewer-bundle/infoDiagram-42DDH7IO-C7JGUXKK.js.map +7 -0
- package/dist/viewer-bundle/ishikawaDiagram-UXIWVN3A-YBC4X4VB.js +1012 -0
- package/dist/viewer-bundle/ishikawaDiagram-UXIWVN3A-YBC4X4VB.js.map +7 -0
- package/dist/viewer-bundle/journeyDiagram-VCZTEJTY-6WKVEOOO.js +1303 -0
- package/dist/viewer-bundle/journeyDiagram-VCZTEJTY-6WKVEOOO.js.map +7 -0
- package/dist/viewer-bundle/kanban-definition-6JOO6SKY-URTTHHO4.js +1131 -0
- package/dist/viewer-bundle/kanban-definition-6JOO6SKY-URTTHHO4.js.map +7 -0
- package/dist/viewer-bundle/katex-QN5266ZE.js +14318 -0
- package/dist/viewer-bundle/katex-QN5266ZE.js.map +7 -0
- package/dist/viewer-bundle/light.css +15 -0
- package/dist/viewer-bundle/main.js +4816 -0
- package/dist/viewer-bundle/main.js.map +7 -0
- package/dist/viewer-bundle/mermaid.core-AEBXU2JK.js +1708 -0
- package/dist/viewer-bundle/mermaid.core-AEBXU2JK.js.map +7 -0
- package/dist/viewer-bundle/mindmap-definition-QFDTVHPH-KUMAMRSF.js +1277 -0
- package/dist/viewer-bundle/mindmap-definition-QFDTVHPH-KUMAMRSF.js.map +7 -0
- package/dist/viewer-bundle/packet-4T2RLAQJ-IRYWWA66.js +14 -0
- package/dist/viewer-bundle/packet-4T2RLAQJ-IRYWWA66.js.map +7 -0
- package/dist/viewer-bundle/pico.classless.min.css +4 -0
- package/dist/viewer-bundle/pie-ZZUOXDRM-JYO4VL5N.js +14 -0
- package/dist/viewer-bundle/pie-ZZUOXDRM-JYO4VL5N.js.map +7 -0
- package/dist/viewer-bundle/pieDiagram-DEJITSTG-QOEHQN3N.js +238 -0
- package/dist/viewer-bundle/pieDiagram-DEJITSTG-QOEHQN3N.js.map +7 -0
- package/dist/viewer-bundle/prompt-modal-C4LHI7BS.js +12 -0
- package/dist/viewer-bundle/prompt-modal-C4LHI7BS.js.map +7 -0
- package/dist/viewer-bundle/quadrantDiagram-34T5L4WZ-SJNPUU5N.js +1409 -0
- package/dist/viewer-bundle/quadrantDiagram-34T5L4WZ-SJNPUU5N.js.map +7 -0
- package/dist/viewer-bundle/radar-PYXPWWZC-45BRYQSB.js +14 -0
- package/dist/viewer-bundle/radar-PYXPWWZC-45BRYQSB.js.map +7 -0
- package/dist/viewer-bundle/reason-modal-MK34MQ73.js +68 -0
- package/dist/viewer-bundle/reason-modal-MK34MQ73.js.map +7 -0
- package/dist/viewer-bundle/requirementDiagram-MS252O5E-UOMT3FCC.js +1311 -0
- package/dist/viewer-bundle/requirementDiagram-MS252O5E-UOMT3FCC.js.map +7 -0
- package/dist/viewer-bundle/sankeyDiagram-XADWPNL6-LAVJ5C6A.js +1263 -0
- package/dist/viewer-bundle/sankeyDiagram-XADWPNL6-LAVJ5C6A.js.map +7 -0
- package/dist/viewer-bundle/sequenceDiagram-FGHM5R23-3IWTOUNQ.js +4655 -0
- package/dist/viewer-bundle/sequenceDiagram-FGHM5R23-3IWTOUNQ.js.map +7 -0
- package/dist/viewer-bundle/stateDiagram-FHFEXIEX-S2OVQQON.js +495 -0
- package/dist/viewer-bundle/stateDiagram-FHFEXIEX-S2OVQQON.js.map +7 -0
- package/dist/viewer-bundle/stateDiagram-v2-QKLJ7IA2-XNZ3XXSV.js +44 -0
- package/dist/viewer-bundle/stateDiagram-v2-QKLJ7IA2-XNZ3XXSV.js.map +7 -0
- package/dist/viewer-bundle/timeline-definition-GMOUNBTQ-FHVZ7MHE.js +1646 -0
- package/dist/viewer-bundle/timeline-definition-GMOUNBTQ-FHVZ7MHE.js.map +7 -0
- package/dist/viewer-bundle/treeView-SZITEDCU-RXZXNYAM.js +14 -0
- package/dist/viewer-bundle/treeView-SZITEDCU-RXZXNYAM.js.map +7 -0
- package/dist/viewer-bundle/treemap-W4RFUUIX-2IGOFSJM.js +14 -0
- package/dist/viewer-bundle/treemap-W4RFUUIX-2IGOFSJM.js.map +7 -0
- package/dist/viewer-bundle/vennDiagram-DHZGUBPP-HEAOEXEZ.js +2544 -0
- package/dist/viewer-bundle/vennDiagram-DHZGUBPP-HEAOEXEZ.js.map +7 -0
- package/dist/viewer-bundle/wardley-RL74JXVD-VSPCLOX2.js +14 -0
- package/dist/viewer-bundle/wardley-RL74JXVD-VSPCLOX2.js.map +7 -0
- package/dist/viewer-bundle/wardleyDiagram-NUSXRM2D-EBY4FG3X.js +938 -0
- package/dist/viewer-bundle/wardleyDiagram-NUSXRM2D-EBY4FG3X.js.map +7 -0
- package/dist/viewer-bundle/xychartDiagram-5P7HB3ND-SSMUQEXK.js +1952 -0
- package/dist/viewer-bundle/xychartDiagram-5P7HB3ND-SSMUQEXK.js.map +7 -0
- package/package.json +51 -0
package/dist/bin.js
ADDED
|
@@ -0,0 +1,2211 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __esm = (fn, res) => function __init() {
|
|
6
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
7
|
+
};
|
|
8
|
+
var __export = (target, all) => {
|
|
9
|
+
for (var name in all)
|
|
10
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
// src/output.ts
|
|
14
|
+
function okEnvelope(data) {
|
|
15
|
+
return { ok: true, data };
|
|
16
|
+
}
|
|
17
|
+
function errorEnvelope(message, detail) {
|
|
18
|
+
if (detail === void 0) return { ok: false, error: { message } };
|
|
19
|
+
return { ok: false, error: { message, detail } };
|
|
20
|
+
}
|
|
21
|
+
function emit(ctx, env, humanRender) {
|
|
22
|
+
if (ctx.json) {
|
|
23
|
+
ctx.writeStdout(JSON.stringify(env) + "\n");
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
if (env.ok) {
|
|
27
|
+
ctx.writeStdout(humanRender() + "\n");
|
|
28
|
+
} else {
|
|
29
|
+
ctx.writeStderr(`error: ${env.error.message}
|
|
30
|
+
`);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
var realEmitContext;
|
|
34
|
+
var init_output = __esm({
|
|
35
|
+
"src/output.ts"() {
|
|
36
|
+
"use strict";
|
|
37
|
+
realEmitContext = (json) => ({
|
|
38
|
+
json,
|
|
39
|
+
writeStdout: (s) => process.stdout.write(s),
|
|
40
|
+
writeStderr: (s) => process.stderr.write(s)
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
// ../core/src/types.js
|
|
46
|
+
var init_types = __esm({
|
|
47
|
+
"../core/src/types.js"() {
|
|
48
|
+
"use strict";
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
// ../core/src/frontmatter.js
|
|
53
|
+
import matter from "gray-matter";
|
|
54
|
+
function parseFrontmatter(text) {
|
|
55
|
+
try {
|
|
56
|
+
const parsed = matter(text, {});
|
|
57
|
+
return {
|
|
58
|
+
data: parsed.data ?? {},
|
|
59
|
+
body: parsed.content,
|
|
60
|
+
error: null
|
|
61
|
+
};
|
|
62
|
+
} catch (err) {
|
|
63
|
+
const detail = err instanceof Error ? err.message : String(err);
|
|
64
|
+
return {
|
|
65
|
+
data: {},
|
|
66
|
+
body: text,
|
|
67
|
+
error: { code: "E_INVALID_FRONTMATTER", detail }
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
var init_frontmatter = __esm({
|
|
72
|
+
"../core/src/frontmatter.js"() {
|
|
73
|
+
"use strict";
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
// ../core/src/tasks.js
|
|
78
|
+
function parseTasks(body) {
|
|
79
|
+
const tasks = [];
|
|
80
|
+
const lines = body.split("\n");
|
|
81
|
+
let index = 0;
|
|
82
|
+
for (const line of lines) {
|
|
83
|
+
const m = line.match(TASK_LINE);
|
|
84
|
+
if (!m)
|
|
85
|
+
continue;
|
|
86
|
+
index += 1;
|
|
87
|
+
const checked = m[1] !== " ";
|
|
88
|
+
let text = (m[2] ?? "").trim();
|
|
89
|
+
text = text.replace(NUMERIC_PREFIX, "");
|
|
90
|
+
const tags = [];
|
|
91
|
+
const seen = /* @__PURE__ */ new Set();
|
|
92
|
+
for (const word of text.split(/\s+/)) {
|
|
93
|
+
if (KNOWN_TAGS.has(word) && !seen.has(word)) {
|
|
94
|
+
tags.push(word);
|
|
95
|
+
seen.add(word);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
const cleanedWords = text.split(/\s+/).filter((w) => !KNOWN_TAGS.has(w));
|
|
99
|
+
text = cleanedWords.join(" ").trim();
|
|
100
|
+
tasks.push({ index, checked, text, tags });
|
|
101
|
+
}
|
|
102
|
+
return tasks;
|
|
103
|
+
}
|
|
104
|
+
var TASK_LINE, NUMERIC_PREFIX, KNOWN_TAGS;
|
|
105
|
+
var init_tasks = __esm({
|
|
106
|
+
"../core/src/tasks.js"() {
|
|
107
|
+
"use strict";
|
|
108
|
+
TASK_LINE = /^[\s>]*[-*+]\s+\[([ xX])\]\s+(.*)$/;
|
|
109
|
+
NUMERIC_PREFIX = /^\d+\.\s+/;
|
|
110
|
+
KNOWN_TAGS = /* @__PURE__ */ new Set(["#human-only", "#agent-only", "#skip"]);
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
// ../core/src/loader.js
|
|
115
|
+
async function folderContainsMarkdown(fs11, dir) {
|
|
116
|
+
const entries = await fs11.readDir(dir);
|
|
117
|
+
for (const e of entries) {
|
|
118
|
+
if (e.isDir) {
|
|
119
|
+
if (await folderContainsMarkdown(fs11, `${dir}/${e.name}`))
|
|
120
|
+
return true;
|
|
121
|
+
} else if (e.name.endsWith(".md")) {
|
|
122
|
+
return true;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
return false;
|
|
126
|
+
}
|
|
127
|
+
async function loadAllSpecs(fs11, specsDir = "specs") {
|
|
128
|
+
if (!await fs11.exists(specsDir))
|
|
129
|
+
return [];
|
|
130
|
+
const entries = await fs11.readDir(specsDir);
|
|
131
|
+
const specs = [];
|
|
132
|
+
for (const entry of entries) {
|
|
133
|
+
if (!entry.isDir)
|
|
134
|
+
continue;
|
|
135
|
+
if (!SPEC_NAME.test(entry.name))
|
|
136
|
+
continue;
|
|
137
|
+
const dir = `${specsDir}/${entry.name}`;
|
|
138
|
+
if (!await folderContainsMarkdown(fs11, dir))
|
|
139
|
+
continue;
|
|
140
|
+
const spec = await loadSpec(fs11, entry.name, specsDir);
|
|
141
|
+
specs.push(spec);
|
|
142
|
+
}
|
|
143
|
+
specs.sort((a, b) => a.name.localeCompare(b.name));
|
|
144
|
+
return specs;
|
|
145
|
+
}
|
|
146
|
+
async function loadSpec(fs11, name, specsDir = "specs") {
|
|
147
|
+
const root = `${specsDir}/${name}`;
|
|
148
|
+
const requirementsPath = `${root}/requirements.md`;
|
|
149
|
+
const tasksPath = `${root}/tasks.md`;
|
|
150
|
+
const handoffPath = `${root}/handoff.md`;
|
|
151
|
+
const lensesDir = `${root}/lenses`;
|
|
152
|
+
let frontmatter = {};
|
|
153
|
+
let requirements = null;
|
|
154
|
+
if (await fs11.exists(requirementsPath)) {
|
|
155
|
+
const raw = await fs11.readFile(requirementsPath);
|
|
156
|
+
const parsed = parseFrontmatter(raw);
|
|
157
|
+
frontmatter = parsed.data;
|
|
158
|
+
requirements = parsed.body;
|
|
159
|
+
}
|
|
160
|
+
let tasks = [];
|
|
161
|
+
if (await fs11.exists(tasksPath)) {
|
|
162
|
+
const raw = await fs11.readFile(tasksPath);
|
|
163
|
+
const parsed = parseFrontmatter(raw);
|
|
164
|
+
tasks = parseTasks(parsed.body);
|
|
165
|
+
}
|
|
166
|
+
let handoff = null;
|
|
167
|
+
if (await fs11.exists(handoffPath)) {
|
|
168
|
+
handoff = await fs11.readFile(handoffPath);
|
|
169
|
+
}
|
|
170
|
+
const lenses = /* @__PURE__ */ new Map();
|
|
171
|
+
if (await fs11.exists(lensesDir)) {
|
|
172
|
+
const lensEntries = await fs11.readDir(lensesDir);
|
|
173
|
+
for (const e of lensEntries) {
|
|
174
|
+
if (e.isDir)
|
|
175
|
+
continue;
|
|
176
|
+
if (!e.name.endsWith(".md"))
|
|
177
|
+
continue;
|
|
178
|
+
const key = e.name.replace(/\.md$/, "");
|
|
179
|
+
lenses.set(key, await fs11.readFile(`${lensesDir}/${e.name}`));
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
return { name, frontmatter, requirements, tasks, handoff, lenses };
|
|
183
|
+
}
|
|
184
|
+
var SPEC_NAME;
|
|
185
|
+
var init_loader = __esm({
|
|
186
|
+
"../core/src/loader.js"() {
|
|
187
|
+
"use strict";
|
|
188
|
+
init_frontmatter();
|
|
189
|
+
init_tasks();
|
|
190
|
+
SPEC_NAME = /^[a-z0-9-]+$/;
|
|
191
|
+
}
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
// ../core/src/status.js
|
|
195
|
+
function isCounted(task) {
|
|
196
|
+
return !task.tags.includes("#skip");
|
|
197
|
+
}
|
|
198
|
+
function deriveStatus(spec, repo) {
|
|
199
|
+
const fm = spec.frontmatter.status;
|
|
200
|
+
if (typeof fm === "string" && VALID_STATUSES.has(fm)) {
|
|
201
|
+
return fm;
|
|
202
|
+
}
|
|
203
|
+
const counted = spec.tasks.filter(isCounted);
|
|
204
|
+
const claimed = repo.claimedSpecs.has(spec.name);
|
|
205
|
+
const merged = repo.mergedSpecs.has(spec.name);
|
|
206
|
+
if (counted.length === 0) {
|
|
207
|
+
return claimed ? "in-progress" : "draft";
|
|
208
|
+
}
|
|
209
|
+
const allChecked = counted.every((t) => t.checked);
|
|
210
|
+
const anyChecked = counted.some((t) => t.checked);
|
|
211
|
+
if (allChecked)
|
|
212
|
+
return merged ? "done" : "in-review";
|
|
213
|
+
if (anyChecked || claimed)
|
|
214
|
+
return "in-progress";
|
|
215
|
+
return "planned";
|
|
216
|
+
}
|
|
217
|
+
var VALID_STATUSES;
|
|
218
|
+
var init_status = __esm({
|
|
219
|
+
"../core/src/status.js"() {
|
|
220
|
+
"use strict";
|
|
221
|
+
VALID_STATUSES = /* @__PURE__ */ new Set([
|
|
222
|
+
"draft",
|
|
223
|
+
"planned",
|
|
224
|
+
"in-progress",
|
|
225
|
+
"in-review",
|
|
226
|
+
"done",
|
|
227
|
+
"blocked",
|
|
228
|
+
"cancelled"
|
|
229
|
+
]);
|
|
230
|
+
}
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
// ../core/src/graph.js
|
|
234
|
+
function buildGraph(specs) {
|
|
235
|
+
const nameSet = new Set(specs.map((s) => s.name));
|
|
236
|
+
const nodes = specs.map((s) => ({
|
|
237
|
+
name: s.name,
|
|
238
|
+
partOf: typeof s.frontmatter.part_of === "string" ? s.frontmatter.part_of : null
|
|
239
|
+
})).sort((a, b) => a.name.localeCompare(b.name));
|
|
240
|
+
const edges = [];
|
|
241
|
+
for (const s of specs) {
|
|
242
|
+
const deps = s.frontmatter.depends_on;
|
|
243
|
+
if (!Array.isArray(deps))
|
|
244
|
+
continue;
|
|
245
|
+
const seen = /* @__PURE__ */ new Set();
|
|
246
|
+
for (const d of deps) {
|
|
247
|
+
if (typeof d !== "string")
|
|
248
|
+
continue;
|
|
249
|
+
if (!nameSet.has(d))
|
|
250
|
+
continue;
|
|
251
|
+
if (seen.has(d))
|
|
252
|
+
continue;
|
|
253
|
+
seen.add(d);
|
|
254
|
+
edges.push({ from: s.name, to: d });
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
edges.sort((a, b) => a.from === b.from ? a.to.localeCompare(b.to) : a.from.localeCompare(b.from));
|
|
258
|
+
const blocks = edges.map((e) => ({ from: e.to, to: e.from })).sort((a, b) => a.from === b.from ? a.to.localeCompare(b.to) : a.from.localeCompare(b.from));
|
|
259
|
+
const cycles = detectCycles(specs.map((s) => s.name).sort(), edges);
|
|
260
|
+
return { nodes, edges, blocks, cycles };
|
|
261
|
+
}
|
|
262
|
+
function detectCycles(orderedNames, edges) {
|
|
263
|
+
const adj = /* @__PURE__ */ new Map();
|
|
264
|
+
for (const n of orderedNames)
|
|
265
|
+
adj.set(n, []);
|
|
266
|
+
for (const e of edges)
|
|
267
|
+
adj.get(e.from)?.push(e.to);
|
|
268
|
+
for (const list of adj.values())
|
|
269
|
+
list.sort();
|
|
270
|
+
const cycles = [];
|
|
271
|
+
const seen = /* @__PURE__ */ new Set();
|
|
272
|
+
const stack = [];
|
|
273
|
+
const onStack = /* @__PURE__ */ new Set();
|
|
274
|
+
const visited = /* @__PURE__ */ new Set();
|
|
275
|
+
function dfs(node) {
|
|
276
|
+
visited.add(node);
|
|
277
|
+
stack.push(node);
|
|
278
|
+
onStack.add(node);
|
|
279
|
+
for (const next of adj.get(node) ?? []) {
|
|
280
|
+
if (onStack.has(next)) {
|
|
281
|
+
const startIdx = stack.indexOf(next);
|
|
282
|
+
const raw = stack.slice(startIdx);
|
|
283
|
+
const canonical = canonicalize(raw);
|
|
284
|
+
const key = canonical.join("|");
|
|
285
|
+
if (!seen.has(key)) {
|
|
286
|
+
seen.add(key);
|
|
287
|
+
cycles.push(canonical);
|
|
288
|
+
}
|
|
289
|
+
} else if (!visited.has(next)) {
|
|
290
|
+
dfs(next);
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
stack.pop();
|
|
294
|
+
onStack.delete(node);
|
|
295
|
+
}
|
|
296
|
+
for (const n of orderedNames) {
|
|
297
|
+
if (!visited.has(n))
|
|
298
|
+
dfs(n);
|
|
299
|
+
}
|
|
300
|
+
cycles.sort((a, b) => a.join("|").localeCompare(b.join("|")));
|
|
301
|
+
return cycles;
|
|
302
|
+
}
|
|
303
|
+
function canonicalize(cycle) {
|
|
304
|
+
let minIdx = 0;
|
|
305
|
+
for (let i = 1; i < cycle.length; i += 1) {
|
|
306
|
+
if (cycle[i] < cycle[minIdx])
|
|
307
|
+
minIdx = i;
|
|
308
|
+
}
|
|
309
|
+
return cycle.slice(minIdx).concat(cycle.slice(0, minIdx));
|
|
310
|
+
}
|
|
311
|
+
var init_graph = __esm({
|
|
312
|
+
"../core/src/graph.js"() {
|
|
313
|
+
"use strict";
|
|
314
|
+
}
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
// ../core/src/validate.js
|
|
318
|
+
async function validateRepo(fs11, specsDir = "specs") {
|
|
319
|
+
const errors = [];
|
|
320
|
+
if (!await fs11.exists(specsDir)) {
|
|
321
|
+
return { errors };
|
|
322
|
+
}
|
|
323
|
+
const entries = await fs11.readDir(specsDir);
|
|
324
|
+
for (const e of entries) {
|
|
325
|
+
if (!e.isDir)
|
|
326
|
+
continue;
|
|
327
|
+
const hasMarkdown = await folderContainsMarkdown(fs11, `${specsDir}/${e.name}`);
|
|
328
|
+
if (!hasMarkdown) {
|
|
329
|
+
errors.push({ code: "E_EMPTY_SPEC", path: `${specsDir}/${e.name}` });
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
for (const e of entries) {
|
|
333
|
+
if (!e.isDir)
|
|
334
|
+
continue;
|
|
335
|
+
const reqPath = `${specsDir}/${e.name}/requirements.md`;
|
|
336
|
+
if (!await fs11.exists(reqPath))
|
|
337
|
+
continue;
|
|
338
|
+
const raw = await fs11.readFile(reqPath);
|
|
339
|
+
const parsed = parseFrontmatter(raw);
|
|
340
|
+
if (parsed.error) {
|
|
341
|
+
errors.push({ code: "E_INVALID_FRONTMATTER", path: reqPath, detail: parsed.error.detail });
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
const specs = await loadAllSpecs(fs11, specsDir);
|
|
345
|
+
const graph = buildGraph(specs);
|
|
346
|
+
for (const cycle of graph.cycles) {
|
|
347
|
+
errors.push({ code: "E_CYCLE", path: cycle });
|
|
348
|
+
}
|
|
349
|
+
errors.sort(compareErrors);
|
|
350
|
+
return { errors };
|
|
351
|
+
}
|
|
352
|
+
function compareErrors(a, b) {
|
|
353
|
+
if (a.code !== b.code)
|
|
354
|
+
return a.code.localeCompare(b.code);
|
|
355
|
+
const aPath = Array.isArray(a.path) ? a.path.join("|") : a.path;
|
|
356
|
+
const bPath = Array.isArray(b.path) ? b.path.join("|") : b.path;
|
|
357
|
+
return aPath.localeCompare(bPath);
|
|
358
|
+
}
|
|
359
|
+
var init_validate = __esm({
|
|
360
|
+
"../core/src/validate.js"() {
|
|
361
|
+
"use strict";
|
|
362
|
+
init_graph();
|
|
363
|
+
init_frontmatter();
|
|
364
|
+
init_loader();
|
|
365
|
+
}
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
// ../core/src/regen.js
|
|
369
|
+
function regenerateIndex(specs, repoState, existingIndex) {
|
|
370
|
+
const human = extractHumanRegion(existingIndex);
|
|
371
|
+
const auto = renderAutoRegion(specs, repoState);
|
|
372
|
+
if (human === "")
|
|
373
|
+
return `${MARKER}
|
|
374
|
+
|
|
375
|
+
${auto}`;
|
|
376
|
+
return `${human}
|
|
377
|
+
|
|
378
|
+
${MARKER}
|
|
379
|
+
|
|
380
|
+
${auto}`;
|
|
381
|
+
}
|
|
382
|
+
function extractHumanRegion(existing) {
|
|
383
|
+
if (existing === null || existing === "")
|
|
384
|
+
return "";
|
|
385
|
+
const idx = existing.indexOf(MARKER);
|
|
386
|
+
if (idx === -1)
|
|
387
|
+
return existing.replace(/\n+$/, "");
|
|
388
|
+
return existing.slice(0, idx).replace(/\n+$/, "");
|
|
389
|
+
}
|
|
390
|
+
function renderAutoRegion(specs, repoState) {
|
|
391
|
+
if (specs.length === 0) {
|
|
392
|
+
return "## State\n\n_No specs._\n\n## Graph\n\n_No specs._\n";
|
|
393
|
+
}
|
|
394
|
+
return `${renderStateTable(specs, repoState)}
|
|
395
|
+
${renderGraphBlock(specs)}`;
|
|
396
|
+
}
|
|
397
|
+
function renderStateTable(specs, repoState) {
|
|
398
|
+
const sorted = [...specs].sort((a, b) => a.name.localeCompare(b.name));
|
|
399
|
+
const rows = sorted.map((s) => {
|
|
400
|
+
const status = deriveStatus(s, repoState);
|
|
401
|
+
const progress = renderProgress(s);
|
|
402
|
+
const blockedBy = renderBlockedBy(s);
|
|
403
|
+
return `| ${s.name} | ${status} | ${progress} | ${blockedBy} |`;
|
|
404
|
+
});
|
|
405
|
+
return [
|
|
406
|
+
"## State",
|
|
407
|
+
"",
|
|
408
|
+
"| Spec | Status | Progress | Blocked by |",
|
|
409
|
+
"|------|--------|----------|------------|",
|
|
410
|
+
...rows,
|
|
411
|
+
""
|
|
412
|
+
].join("\n");
|
|
413
|
+
}
|
|
414
|
+
function renderProgress(spec) {
|
|
415
|
+
const counted = spec.tasks.filter((t) => !t.tags.includes("#skip"));
|
|
416
|
+
const checked = counted.filter((t) => t.checked).length;
|
|
417
|
+
return `${checked}/${counted.length}`;
|
|
418
|
+
}
|
|
419
|
+
function renderBlockedBy(spec) {
|
|
420
|
+
const v = spec.frontmatter.blocked_by;
|
|
421
|
+
if (typeof v === "string" && v.trim() !== "")
|
|
422
|
+
return v.trim();
|
|
423
|
+
return "\u2014";
|
|
424
|
+
}
|
|
425
|
+
function renderGraphBlock(specs) {
|
|
426
|
+
const graph = buildGraph(specs);
|
|
427
|
+
const lines = ["## Graph", "", "```mermaid", "graph TD"];
|
|
428
|
+
for (const node of graph.nodes)
|
|
429
|
+
lines.push(` ${node.name}`);
|
|
430
|
+
for (const edge of graph.edges)
|
|
431
|
+
lines.push(` ${edge.from} --> ${edge.to}`);
|
|
432
|
+
lines.push("```");
|
|
433
|
+
lines.push("");
|
|
434
|
+
return lines.join("\n");
|
|
435
|
+
}
|
|
436
|
+
var MARKER;
|
|
437
|
+
var init_regen = __esm({
|
|
438
|
+
"../core/src/regen.js"() {
|
|
439
|
+
"use strict";
|
|
440
|
+
init_graph();
|
|
441
|
+
init_status();
|
|
442
|
+
MARKER = "<!-- ZETTELGEIST:AUTO-GENERATED BELOW \u2014 do not edit -->";
|
|
443
|
+
}
|
|
444
|
+
});
|
|
445
|
+
|
|
446
|
+
// ../core/src/config.js
|
|
447
|
+
import yaml from "js-yaml";
|
|
448
|
+
async function loadConfig(fs11) {
|
|
449
|
+
const errors = [];
|
|
450
|
+
const defaults = { formatVersion: null, specsDir: "specs" };
|
|
451
|
+
const raw = await fs11.readFile(CONFIG_PATH);
|
|
452
|
+
let parsed;
|
|
453
|
+
try {
|
|
454
|
+
parsed = yaml.load(raw, { schema: yaml.JSON_SCHEMA });
|
|
455
|
+
} catch (err) {
|
|
456
|
+
const detail = err instanceof Error ? err.message : String(err);
|
|
457
|
+
errors.push({ code: "E_INVALID_FRONTMATTER", path: CONFIG_PATH, detail });
|
|
458
|
+
return { config: defaults, errors };
|
|
459
|
+
}
|
|
460
|
+
const obj = parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
|
|
461
|
+
const fv = obj["format_version"];
|
|
462
|
+
let formatVersion = null;
|
|
463
|
+
if (typeof fv === "string") {
|
|
464
|
+
formatVersion = fv;
|
|
465
|
+
} else {
|
|
466
|
+
errors.push({
|
|
467
|
+
code: "E_INVALID_FRONTMATTER",
|
|
468
|
+
path: CONFIG_PATH,
|
|
469
|
+
detail: "format_version must be a string"
|
|
470
|
+
});
|
|
471
|
+
}
|
|
472
|
+
let specsDir = "specs";
|
|
473
|
+
if ("specs_dir" in obj) {
|
|
474
|
+
const sd = obj["specs_dir"];
|
|
475
|
+
if (typeof sd === "string") {
|
|
476
|
+
specsDir = sd;
|
|
477
|
+
} else {
|
|
478
|
+
errors.push({
|
|
479
|
+
code: "E_INVALID_FRONTMATTER",
|
|
480
|
+
path: CONFIG_PATH,
|
|
481
|
+
detail: "specs_dir must be a string"
|
|
482
|
+
});
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
return { config: { formatVersion, specsDir }, errors };
|
|
486
|
+
}
|
|
487
|
+
var CONFIG_PATH;
|
|
488
|
+
var init_config = __esm({
|
|
489
|
+
"../core/src/config.js"() {
|
|
490
|
+
"use strict";
|
|
491
|
+
CONFIG_PATH = ".zettelgeist.yaml";
|
|
492
|
+
}
|
|
493
|
+
});
|
|
494
|
+
|
|
495
|
+
// ../core/src/index.ts
|
|
496
|
+
async function runConformance(fs11) {
|
|
497
|
+
if (!await fs11.exists(".zettelgeist.yaml")) {
|
|
498
|
+
throw new Error("not a zettelgeist repo (missing .zettelgeist.yaml)");
|
|
499
|
+
}
|
|
500
|
+
const cfg = await loadConfig(fs11);
|
|
501
|
+
const specsDir = cfg.config.specsDir;
|
|
502
|
+
const specs = await loadAllSpecs(fs11, specsDir);
|
|
503
|
+
const repoState = { claimedSpecs: /* @__PURE__ */ new Set(), mergedSpecs: /* @__PURE__ */ new Set() };
|
|
504
|
+
const validation = await validateRepo(fs11, specsDir);
|
|
505
|
+
const graph = buildGraph(specs);
|
|
506
|
+
const statuses = {};
|
|
507
|
+
for (const s of specs) statuses[s.name] = deriveStatus(s, repoState);
|
|
508
|
+
const indexPath = `${specsDir}/INDEX.md`;
|
|
509
|
+
let existingIndex = null;
|
|
510
|
+
if (await fs11.exists(indexPath)) {
|
|
511
|
+
existingIndex = await fs11.readFile(indexPath);
|
|
512
|
+
}
|
|
513
|
+
const index = regenerateIndex(specs, repoState, existingIndex);
|
|
514
|
+
const errors = [...cfg.errors, ...validation.errors].sort(compareErrors);
|
|
515
|
+
return {
|
|
516
|
+
statuses: { specs: statuses },
|
|
517
|
+
graph: { nodes: graph.nodes, edges: graph.edges, cycles: graph.cycles },
|
|
518
|
+
validation: { errors },
|
|
519
|
+
index
|
|
520
|
+
};
|
|
521
|
+
}
|
|
522
|
+
var init_src = __esm({
|
|
523
|
+
"../core/src/index.ts"() {
|
|
524
|
+
"use strict";
|
|
525
|
+
init_types();
|
|
526
|
+
init_frontmatter();
|
|
527
|
+
init_tasks();
|
|
528
|
+
init_loader();
|
|
529
|
+
init_status();
|
|
530
|
+
init_graph();
|
|
531
|
+
init_validate();
|
|
532
|
+
init_regen();
|
|
533
|
+
init_config();
|
|
534
|
+
init_loader();
|
|
535
|
+
init_status();
|
|
536
|
+
init_graph();
|
|
537
|
+
init_validate();
|
|
538
|
+
init_regen();
|
|
539
|
+
init_config();
|
|
540
|
+
}
|
|
541
|
+
});
|
|
542
|
+
|
|
543
|
+
// ../fs-adapters/src/mem.ts
|
|
544
|
+
var init_mem = __esm({
|
|
545
|
+
"../fs-adapters/src/mem.ts"() {
|
|
546
|
+
"use strict";
|
|
547
|
+
}
|
|
548
|
+
});
|
|
549
|
+
|
|
550
|
+
// ../fs-adapters/src/disk.ts
|
|
551
|
+
import { promises as fs } from "node:fs";
|
|
552
|
+
import * as path from "node:path";
|
|
553
|
+
function makeDiskFsReader(rootDir) {
|
|
554
|
+
const resolve6 = (p) => path.join(rootDir, p);
|
|
555
|
+
return {
|
|
556
|
+
async readDir(p) {
|
|
557
|
+
const entries = await fs.readdir(resolve6(p), { withFileTypes: true });
|
|
558
|
+
return entries.map((e) => ({ name: e.name, isDir: e.isDirectory() }));
|
|
559
|
+
},
|
|
560
|
+
async readFile(p) {
|
|
561
|
+
return fs.readFile(resolve6(p), "utf8");
|
|
562
|
+
},
|
|
563
|
+
async exists(p) {
|
|
564
|
+
try {
|
|
565
|
+
await fs.stat(resolve6(p));
|
|
566
|
+
return true;
|
|
567
|
+
} catch {
|
|
568
|
+
return false;
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
};
|
|
572
|
+
}
|
|
573
|
+
var init_disk = __esm({
|
|
574
|
+
"../fs-adapters/src/disk.ts"() {
|
|
575
|
+
"use strict";
|
|
576
|
+
}
|
|
577
|
+
});
|
|
578
|
+
|
|
579
|
+
// ../fs-adapters/src/index.ts
|
|
580
|
+
var init_src2 = __esm({
|
|
581
|
+
"../fs-adapters/src/index.ts"() {
|
|
582
|
+
"use strict";
|
|
583
|
+
init_mem();
|
|
584
|
+
init_disk();
|
|
585
|
+
}
|
|
586
|
+
});
|
|
587
|
+
|
|
588
|
+
// src/commands/regen.ts
|
|
589
|
+
var regen_exports = {};
|
|
590
|
+
__export(regen_exports, {
|
|
591
|
+
HELP: () => HELP,
|
|
592
|
+
regenCommand: () => regenCommand
|
|
593
|
+
});
|
|
594
|
+
import { promises as fs2 } from "node:fs";
|
|
595
|
+
import * as path2 from "node:path";
|
|
596
|
+
async function getSpecsTreeSha(repoPath, specsDir) {
|
|
597
|
+
try {
|
|
598
|
+
const hash = await hashWorkingTree(repoPath, specsDir);
|
|
599
|
+
return hash;
|
|
600
|
+
} catch {
|
|
601
|
+
return null;
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
async function hashWorkingTree(repoPath, specsDir) {
|
|
605
|
+
const root = path2.join(repoPath, specsDir);
|
|
606
|
+
const entries = [];
|
|
607
|
+
const { createHash } = await import("node:crypto");
|
|
608
|
+
await walk2(root, "");
|
|
609
|
+
entries.sort((a, b) => a.rel < b.rel ? -1 : a.rel > b.rel ? 1 : 0);
|
|
610
|
+
const h = createHash("sha1");
|
|
611
|
+
for (const e of entries) h.update(`${e.rel}\0${e.sha}
|
|
612
|
+
`);
|
|
613
|
+
return h.digest("hex");
|
|
614
|
+
async function walk2(absDir, relDir) {
|
|
615
|
+
let names;
|
|
616
|
+
try {
|
|
617
|
+
names = await fs2.readdir(absDir);
|
|
618
|
+
} catch {
|
|
619
|
+
return;
|
|
620
|
+
}
|
|
621
|
+
for (const name of names) {
|
|
622
|
+
if (name === ".git" || name === "node_modules" || name === ".claim") continue;
|
|
623
|
+
if (relDir === "" && name === "INDEX.md") continue;
|
|
624
|
+
const abs = path2.join(absDir, name);
|
|
625
|
+
const rel = relDir ? `${relDir}/${name}` : name;
|
|
626
|
+
let stat;
|
|
627
|
+
try {
|
|
628
|
+
stat = await fs2.stat(abs);
|
|
629
|
+
} catch {
|
|
630
|
+
continue;
|
|
631
|
+
}
|
|
632
|
+
if (stat.isDirectory()) {
|
|
633
|
+
await walk2(abs, rel);
|
|
634
|
+
} else if (stat.isFile()) {
|
|
635
|
+
const content = await fs2.readFile(abs);
|
|
636
|
+
const sha = createHash("sha1").update(content).digest("hex");
|
|
637
|
+
entries.push({ rel, sha });
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
async function readCache(repoPath) {
|
|
643
|
+
try {
|
|
644
|
+
const raw = await fs2.readFile(path2.join(repoPath, ".zettelgeist", "regen-cache.json"), "utf8");
|
|
645
|
+
const parsed = JSON.parse(raw);
|
|
646
|
+
if (typeof parsed.tree_sha !== "string" || typeof parsed.generated_index !== "string") return null;
|
|
647
|
+
return { tree_sha: parsed.tree_sha, generated_index: parsed.generated_index };
|
|
648
|
+
} catch {
|
|
649
|
+
return null;
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
async function writeCache(repoPath, entry) {
|
|
653
|
+
const dir = path2.join(repoPath, ".zettelgeist");
|
|
654
|
+
await fs2.mkdir(dir, { recursive: true });
|
|
655
|
+
const tmp = path2.join(dir, "regen-cache.json.tmp");
|
|
656
|
+
await fs2.writeFile(tmp, JSON.stringify(entry, null, 2) + "\n", "utf8");
|
|
657
|
+
await fs2.rename(tmp, path2.join(dir, "regen-cache.json"));
|
|
658
|
+
}
|
|
659
|
+
async function regenCommand(input) {
|
|
660
|
+
const reader = makeDiskFsReader(input.path);
|
|
661
|
+
if (!await reader.exists(".zettelgeist.yaml")) {
|
|
662
|
+
return errorEnvelope(`not a zettelgeist repo: ${input.path}`);
|
|
663
|
+
}
|
|
664
|
+
const cfg = await loadConfig(reader);
|
|
665
|
+
const specsDir = cfg.config.specsDir;
|
|
666
|
+
const indexAbsPath = path2.join(input.path, specsDir, "INDEX.md");
|
|
667
|
+
const indexRelPath = path2.posix.join(specsDir, "INDEX.md");
|
|
668
|
+
const treeSha = await getSpecsTreeSha(input.path, specsDir);
|
|
669
|
+
let generated = null;
|
|
670
|
+
let cacheHit = false;
|
|
671
|
+
if (treeSha) {
|
|
672
|
+
const cache = await readCache(input.path);
|
|
673
|
+
if (cache && cache.tree_sha === treeSha) {
|
|
674
|
+
generated = cache.generated_index;
|
|
675
|
+
cacheHit = true;
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
if (generated === null) {
|
|
679
|
+
let result;
|
|
680
|
+
try {
|
|
681
|
+
result = await runConformance(reader);
|
|
682
|
+
} catch (err) {
|
|
683
|
+
return errorEnvelope(err instanceof Error ? err.message : String(err));
|
|
684
|
+
}
|
|
685
|
+
generated = result.index;
|
|
686
|
+
if (treeSha) {
|
|
687
|
+
await writeCache(input.path, { tree_sha: treeSha, generated_index: generated });
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
let onDisk = null;
|
|
691
|
+
try {
|
|
692
|
+
onDisk = await fs2.readFile(indexAbsPath, "utf8");
|
|
693
|
+
} catch {
|
|
694
|
+
}
|
|
695
|
+
if (onDisk === generated) {
|
|
696
|
+
return okEnvelope({ changed: false, path: indexRelPath, cacheHit });
|
|
697
|
+
}
|
|
698
|
+
if (input.check) {
|
|
699
|
+
return errorEnvelope(onDisk === null ? `${indexRelPath} is missing` : `${indexRelPath} is stale`);
|
|
700
|
+
}
|
|
701
|
+
await fs2.mkdir(path2.dirname(indexAbsPath), { recursive: true });
|
|
702
|
+
const tmpPath = `${indexAbsPath}.tmp`;
|
|
703
|
+
await fs2.writeFile(tmpPath, generated, "utf8");
|
|
704
|
+
await fs2.rename(tmpPath, indexAbsPath);
|
|
705
|
+
return okEnvelope({ changed: true, path: indexRelPath, cacheHit });
|
|
706
|
+
}
|
|
707
|
+
var HELP;
|
|
708
|
+
var init_regen2 = __esm({
|
|
709
|
+
"src/commands/regen.ts"() {
|
|
710
|
+
"use strict";
|
|
711
|
+
init_src();
|
|
712
|
+
init_src2();
|
|
713
|
+
init_output();
|
|
714
|
+
HELP = `zettelgeist regen [--check] [--json]
|
|
715
|
+
|
|
716
|
+
Regenerate <specs_dir>/INDEX.md from the spec content in the current repo.
|
|
717
|
+
|
|
718
|
+
Flags:
|
|
719
|
+
--check Exit 1 if INDEX.md is stale or missing instead of writing.
|
|
720
|
+
--json Emit a machine-readable JSON envelope.
|
|
721
|
+
|
|
722
|
+
Caches generated INDEX content keyed by the git tree SHA at
|
|
723
|
+
.zettelgeist/regen-cache.json so repeat runs are fast.
|
|
724
|
+
`;
|
|
725
|
+
}
|
|
726
|
+
});
|
|
727
|
+
|
|
728
|
+
// src/commands/validate.ts
|
|
729
|
+
var validate_exports = {};
|
|
730
|
+
__export(validate_exports, {
|
|
731
|
+
HELP: () => HELP2,
|
|
732
|
+
validateCommand: () => validateCommand
|
|
733
|
+
});
|
|
734
|
+
async function validateCommand(input) {
|
|
735
|
+
const reader = makeDiskFsReader(input.path);
|
|
736
|
+
if (!await reader.exists(".zettelgeist.yaml")) {
|
|
737
|
+
return errorEnvelope(`not a zettelgeist repo: ${input.path}`);
|
|
738
|
+
}
|
|
739
|
+
const cfg = await loadConfig(reader);
|
|
740
|
+
const validation = await validateRepo(reader, cfg.config.specsDir);
|
|
741
|
+
const allErrors = [...cfg.errors, ...validation.errors];
|
|
742
|
+
if (allErrors.length === 0) return okEnvelope({ errors: [] });
|
|
743
|
+
const count = allErrors.length;
|
|
744
|
+
return errorEnvelope(
|
|
745
|
+
`${count} validation error${count === 1 ? "" : "s"}`,
|
|
746
|
+
{ errors: allErrors }
|
|
747
|
+
);
|
|
748
|
+
}
|
|
749
|
+
var HELP2;
|
|
750
|
+
var init_validate2 = __esm({
|
|
751
|
+
"src/commands/validate.ts"() {
|
|
752
|
+
"use strict";
|
|
753
|
+
init_src();
|
|
754
|
+
init_src2();
|
|
755
|
+
init_output();
|
|
756
|
+
HELP2 = `zettelgeist validate [--json]
|
|
757
|
+
|
|
758
|
+
Validate the current repo against the Zettelgeist format spec.
|
|
759
|
+
|
|
760
|
+
Reports a non-zero exit code if any validation errors are found.
|
|
761
|
+
|
|
762
|
+
Flags:
|
|
763
|
+
--json Emit a machine-readable JSON envelope (with the full
|
|
764
|
+
error list under .data.errors).
|
|
765
|
+
`;
|
|
766
|
+
}
|
|
767
|
+
});
|
|
768
|
+
|
|
769
|
+
// ../git-hook/src/install-hook.ts
|
|
770
|
+
import { promises as fs3 } from "node:fs";
|
|
771
|
+
import * as path3 from "node:path";
|
|
772
|
+
function mergeHookContent(existing) {
|
|
773
|
+
if (existing === null || existing === "") return HOOK_BLOCK + "\n";
|
|
774
|
+
const beginIdx = existing.indexOf(HOOK_MARKER_BEGIN);
|
|
775
|
+
const endIdx = existing.indexOf(HOOK_MARKER_END);
|
|
776
|
+
if (beginIdx !== -1 && endIdx !== -1 && endIdx > beginIdx) {
|
|
777
|
+
const before = existing.slice(0, beginIdx);
|
|
778
|
+
const after = existing.slice(endIdx + HOOK_MARKER_END.length);
|
|
779
|
+
return before + HOOK_BLOCK + after;
|
|
780
|
+
}
|
|
781
|
+
const shebangMatch = existing.match(SHEBANG_RE);
|
|
782
|
+
const stripped = shebangMatch ? existing.slice(shebangMatch[0].length).trim() : existing.trim();
|
|
783
|
+
if (stripped === "") {
|
|
784
|
+
return existing + HOOK_BLOCK + "\n";
|
|
785
|
+
}
|
|
786
|
+
throw new Error(
|
|
787
|
+
"pre-commit hook contains non-marker content; refuse to overwrite. Use --force to back it up to pre-commit.before-zettelgeist and replace, or merge the marker block manually."
|
|
788
|
+
);
|
|
789
|
+
}
|
|
790
|
+
async function installPreCommitHook(repoRoot, options = {}) {
|
|
791
|
+
const hookDir = path3.join(repoRoot, ".git", "hooks");
|
|
792
|
+
const hookPath = path3.join(hookDir, "pre-commit");
|
|
793
|
+
await fs3.mkdir(hookDir, { recursive: true });
|
|
794
|
+
let existing = null;
|
|
795
|
+
try {
|
|
796
|
+
existing = await fs3.readFile(hookPath, "utf8");
|
|
797
|
+
} catch {
|
|
798
|
+
}
|
|
799
|
+
let next;
|
|
800
|
+
let backup;
|
|
801
|
+
try {
|
|
802
|
+
next = mergeHookContent(existing);
|
|
803
|
+
} catch (err) {
|
|
804
|
+
if (!options.force) throw err;
|
|
805
|
+
backup = `${hookPath}.before-zettelgeist`;
|
|
806
|
+
if (existing !== null) await fs3.writeFile(backup, existing, "utf8");
|
|
807
|
+
next = HOOK_BLOCK + "\n";
|
|
808
|
+
}
|
|
809
|
+
await fs3.writeFile(hookPath, next, "utf8");
|
|
810
|
+
await fs3.chmod(hookPath, 493);
|
|
811
|
+
return backup ? { installed: true, backup } : { installed: true };
|
|
812
|
+
}
|
|
813
|
+
var HOOK_MARKER_BEGIN, HOOK_MARKER_END, HOOK_BLOCK, SHEBANG_RE;
|
|
814
|
+
var init_install_hook = __esm({
|
|
815
|
+
"../git-hook/src/install-hook.ts"() {
|
|
816
|
+
"use strict";
|
|
817
|
+
HOOK_MARKER_BEGIN = "# >>> zettelgeist >>>";
|
|
818
|
+
HOOK_MARKER_END = "# <<< zettelgeist <<<";
|
|
819
|
+
HOOK_BLOCK = HOOK_MARKER_BEGIN + '\nif command -v zettelgeist >/dev/null 2>&1; then\n zettelgeist regen --check\nelif [ -x ./node_modules/.bin/zettelgeist ]; then\n ./node_modules/.bin/zettelgeist regen --check\nelse\n echo "zettelgeist: not on PATH and not in ./node_modules/.bin \u2014 install it or remove this hook" >&2\n exit 1\nfi\n' + HOOK_MARKER_END;
|
|
820
|
+
SHEBANG_RE = /^#!\s*\/[^\n]*\n/;
|
|
821
|
+
}
|
|
822
|
+
});
|
|
823
|
+
|
|
824
|
+
// ../git-hook/src/index.ts
|
|
825
|
+
var init_src3 = __esm({
|
|
826
|
+
"../git-hook/src/index.ts"() {
|
|
827
|
+
"use strict";
|
|
828
|
+
init_install_hook();
|
|
829
|
+
}
|
|
830
|
+
});
|
|
831
|
+
|
|
832
|
+
// src/git.ts
|
|
833
|
+
import { execFile } from "node:child_process";
|
|
834
|
+
import { promisify } from "node:util";
|
|
835
|
+
async function gitRepoRoot(cwd) {
|
|
836
|
+
const { stdout } = await execFileP("git", ["rev-parse", "--show-toplevel"], { cwd });
|
|
837
|
+
return stdout.trim();
|
|
838
|
+
}
|
|
839
|
+
var execFileP;
|
|
840
|
+
var init_git = __esm({
|
|
841
|
+
"src/git.ts"() {
|
|
842
|
+
"use strict";
|
|
843
|
+
init_src3();
|
|
844
|
+
execFileP = promisify(execFile);
|
|
845
|
+
}
|
|
846
|
+
});
|
|
847
|
+
|
|
848
|
+
// src/commands/install-hook.ts
|
|
849
|
+
var install_hook_exports = {};
|
|
850
|
+
__export(install_hook_exports, {
|
|
851
|
+
HELP: () => HELP3,
|
|
852
|
+
installHookCommand: () => installHookCommand
|
|
853
|
+
});
|
|
854
|
+
async function installHookCommand(input) {
|
|
855
|
+
const reader = makeDiskFsReader(input.path);
|
|
856
|
+
if (!await reader.exists(".zettelgeist.yaml")) {
|
|
857
|
+
return errorEnvelope(`not a zettelgeist repo: ${input.path}`);
|
|
858
|
+
}
|
|
859
|
+
let repoRoot;
|
|
860
|
+
try {
|
|
861
|
+
repoRoot = await gitRepoRoot(input.path);
|
|
862
|
+
} catch {
|
|
863
|
+
return errorEnvelope(`${input.path} is not a git repo`);
|
|
864
|
+
}
|
|
865
|
+
try {
|
|
866
|
+
const result = await installPreCommitHook(repoRoot, { force: input.force });
|
|
867
|
+
return okEnvelope({ installed: true, ...result.backup ? { backup: result.backup } : {} });
|
|
868
|
+
} catch (err) {
|
|
869
|
+
return errorEnvelope(err instanceof Error ? err.message : String(err));
|
|
870
|
+
}
|
|
871
|
+
}
|
|
872
|
+
var HELP3;
|
|
873
|
+
var init_install_hook2 = __esm({
|
|
874
|
+
"src/commands/install-hook.ts"() {
|
|
875
|
+
"use strict";
|
|
876
|
+
init_src2();
|
|
877
|
+
init_output();
|
|
878
|
+
init_git();
|
|
879
|
+
HELP3 = `zettelgeist install-hook [--force] [--json]
|
|
880
|
+
|
|
881
|
+
Install the Zettelgeist pre-commit hook into the current git repo.
|
|
882
|
+
|
|
883
|
+
The hook runs \`regen --check\` before each commit so INDEX.md never
|
|
884
|
+
drifts from the specs on disk. If a pre-existing hook is found it is
|
|
885
|
+
smart-merged or backed up.
|
|
886
|
+
|
|
887
|
+
Flags:
|
|
888
|
+
--force Overwrite an existing hook (a backup is still written).
|
|
889
|
+
--json Emit a machine-readable JSON envelope.
|
|
890
|
+
`;
|
|
891
|
+
}
|
|
892
|
+
});
|
|
893
|
+
|
|
894
|
+
// src/commands/install-skill.ts
|
|
895
|
+
var install_skill_exports = {};
|
|
896
|
+
__export(install_skill_exports, {
|
|
897
|
+
HELP: () => HELP4,
|
|
898
|
+
installSkillCommand: () => installSkillCommand,
|
|
899
|
+
isScope: () => isScope,
|
|
900
|
+
stripFrontmatter: () => stripFrontmatter
|
|
901
|
+
});
|
|
902
|
+
import { promises as fs4 } from "node:fs";
|
|
903
|
+
import * as os from "node:os";
|
|
904
|
+
import * as path4 from "node:path";
|
|
905
|
+
import { fileURLToPath } from "node:url";
|
|
906
|
+
function isScope(s) {
|
|
907
|
+
return SCOPES.has(s);
|
|
908
|
+
}
|
|
909
|
+
async function locateBundledSkill() {
|
|
910
|
+
const here2 = path4.dirname(fileURLToPath(import.meta.url));
|
|
911
|
+
const candidates = [
|
|
912
|
+
path4.join(here2, "templates", "skill", "SKILL.md"),
|
|
913
|
+
path4.join(here2, "..", "templates", "skill", "SKILL.md"),
|
|
914
|
+
path4.join(here2, "..", "..", "templates", "skill", "SKILL.md")
|
|
915
|
+
];
|
|
916
|
+
for (const c of candidates) {
|
|
917
|
+
try {
|
|
918
|
+
await fs4.access(c);
|
|
919
|
+
return c;
|
|
920
|
+
} catch {
|
|
921
|
+
}
|
|
922
|
+
}
|
|
923
|
+
throw new Error("bundled SKILL.md not found \u2014 reinstall @zettelgeist/cli");
|
|
924
|
+
}
|
|
925
|
+
function resolveDest(input) {
|
|
926
|
+
if (input.scope === "user") {
|
|
927
|
+
return path4.join(
|
|
928
|
+
input.homeDir ?? os.homedir(),
|
|
929
|
+
".claude",
|
|
930
|
+
"skills",
|
|
931
|
+
"zettelgeist",
|
|
932
|
+
"SKILL.md"
|
|
933
|
+
);
|
|
934
|
+
}
|
|
935
|
+
if (input.scope === "project") {
|
|
936
|
+
return path4.join(input.cwd, ".claude", "skills", "zettelgeist", "SKILL.md");
|
|
937
|
+
}
|
|
938
|
+
return path4.join(input.cwd, "AGENTS.md");
|
|
939
|
+
}
|
|
940
|
+
function stripFrontmatter(skill) {
|
|
941
|
+
if (!skill.startsWith("---\n")) return skill;
|
|
942
|
+
const end = skill.indexOf("\n---\n", 4);
|
|
943
|
+
if (end === -1) return skill;
|
|
944
|
+
return skill.slice(end + 5).replace(/^\n+/, "");
|
|
945
|
+
}
|
|
946
|
+
function renderAgentsRegion(skillBody) {
|
|
947
|
+
return `${AGENTS_BEGIN}
|
|
948
|
+
|
|
949
|
+
${skillBody.trimEnd()}
|
|
950
|
+
|
|
951
|
+
${AGENTS_END}`;
|
|
952
|
+
}
|
|
953
|
+
function mergeAgentsMd(existing, skillBody, force) {
|
|
954
|
+
const begin = existing.indexOf(AGENTS_BEGIN);
|
|
955
|
+
const end = existing.indexOf(AGENTS_END);
|
|
956
|
+
if (begin === -1 && end === -1) {
|
|
957
|
+
const sep5 = existing.endsWith("\n") ? "\n" : "\n\n";
|
|
958
|
+
return { content: `${existing}${sep5}${renderAgentsRegion(skillBody)}
|
|
959
|
+
`, merged: true };
|
|
960
|
+
}
|
|
961
|
+
if (begin === -1 || end === -1 || end < begin) {
|
|
962
|
+
if (!force) {
|
|
963
|
+
throw new Error(
|
|
964
|
+
"AGENTS.md has a malformed ZETTELGEIST marker pair \u2014 fix manually or re-run with --force to recover"
|
|
965
|
+
);
|
|
966
|
+
}
|
|
967
|
+
let stripped = existing.split("\n").filter((line) => !line.includes(AGENTS_BEGIN) && !line.includes(AGENTS_END)).join("\n").replace(/\n{3,}/g, "\n\n").replace(/\n+$/, "");
|
|
968
|
+
if (stripped.length === 0) stripped = "";
|
|
969
|
+
const sep5 = stripped.length === 0 ? "" : "\n\n";
|
|
970
|
+
return { content: `${stripped}${sep5}${renderAgentsRegion(skillBody)}
|
|
971
|
+
`, merged: true };
|
|
972
|
+
}
|
|
973
|
+
const before = existing.slice(0, begin).replace(/\n+$/, "");
|
|
974
|
+
const after = existing.slice(end + AGENTS_END.length).replace(/^\n+/, "");
|
|
975
|
+
const middle = renderAgentsRegion(skillBody);
|
|
976
|
+
const joined = [before, middle, after].filter((s) => s.length > 0).join("\n\n");
|
|
977
|
+
return { content: `${joined}
|
|
978
|
+
`, merged: true };
|
|
979
|
+
}
|
|
980
|
+
async function installSkillCommand(input) {
|
|
981
|
+
let source;
|
|
982
|
+
try {
|
|
983
|
+
source = await locateBundledSkill();
|
|
984
|
+
} catch (err) {
|
|
985
|
+
return errorEnvelope(err instanceof Error ? err.message : String(err));
|
|
986
|
+
}
|
|
987
|
+
let skill;
|
|
988
|
+
try {
|
|
989
|
+
skill = await fs4.readFile(source, "utf8");
|
|
990
|
+
} catch (err) {
|
|
991
|
+
return errorEnvelope(err instanceof Error ? err.message : String(err));
|
|
992
|
+
}
|
|
993
|
+
const dest = resolveDest(input);
|
|
994
|
+
if (input.scope === "agents-md") {
|
|
995
|
+
let existing = null;
|
|
996
|
+
try {
|
|
997
|
+
existing = await fs4.readFile(dest, "utf8");
|
|
998
|
+
} catch {
|
|
999
|
+
}
|
|
1000
|
+
const body = stripFrontmatter(skill);
|
|
1001
|
+
try {
|
|
1002
|
+
if (existing === null) {
|
|
1003
|
+
await fs4.writeFile(dest, `${renderAgentsRegion(body)}
|
|
1004
|
+
`, "utf8");
|
|
1005
|
+
return okEnvelope({ installed: true, path: dest, scope: input.scope });
|
|
1006
|
+
}
|
|
1007
|
+
const merged = mergeAgentsMd(existing, body, input.force);
|
|
1008
|
+
await fs4.writeFile(dest, merged.content, "utf8");
|
|
1009
|
+
return okEnvelope({ installed: true, path: dest, scope: input.scope, merged: merged.merged });
|
|
1010
|
+
} catch (err) {
|
|
1011
|
+
return errorEnvelope(err instanceof Error ? err.message : String(err));
|
|
1012
|
+
}
|
|
1013
|
+
}
|
|
1014
|
+
try {
|
|
1015
|
+
await fs4.access(dest);
|
|
1016
|
+
if (!input.force) {
|
|
1017
|
+
return errorEnvelope(`${dest} already exists. Re-run with --force to overwrite.`);
|
|
1018
|
+
}
|
|
1019
|
+
} catch {
|
|
1020
|
+
}
|
|
1021
|
+
try {
|
|
1022
|
+
await fs4.mkdir(path4.dirname(dest), { recursive: true });
|
|
1023
|
+
await fs4.writeFile(dest, skill, "utf8");
|
|
1024
|
+
} catch (err) {
|
|
1025
|
+
return errorEnvelope(err instanceof Error ? err.message : String(err));
|
|
1026
|
+
}
|
|
1027
|
+
return okEnvelope({ installed: true, path: dest, scope: input.scope });
|
|
1028
|
+
}
|
|
1029
|
+
var HELP4, SCOPES, AGENTS_BEGIN, AGENTS_END;
|
|
1030
|
+
var init_install_skill = __esm({
|
|
1031
|
+
"src/commands/install-skill.ts"() {
|
|
1032
|
+
"use strict";
|
|
1033
|
+
init_output();
|
|
1034
|
+
HELP4 = `zettelgeist install-skill [--scope user|project|agents-md] [--force] [--json]
|
|
1035
|
+
|
|
1036
|
+
Install the Zettelgeist agent skill so coding agents pick it up
|
|
1037
|
+
automatically when working in this repo.
|
|
1038
|
+
|
|
1039
|
+
Scopes:
|
|
1040
|
+
user ~/.claude/skills/zettelgeist/SKILL.md
|
|
1041
|
+
Claude Code (CLI + VS Code), global. Default.
|
|
1042
|
+
project <cwd>/.claude/skills/zettelgeist/SKILL.md
|
|
1043
|
+
Claude Code, per-repo. Commit this for the whole team.
|
|
1044
|
+
agents-md <cwd>/AGENTS.md
|
|
1045
|
+
Cross-tool convention read by Codex, Copilot CLI, and
|
|
1046
|
+
Claude Code as a fallback. Smart-merge: if AGENTS.md
|
|
1047
|
+
exists, only the region between
|
|
1048
|
+
<!-- ZETTELGEIST:SKILL-BEGIN -->
|
|
1049
|
+
<!-- ZETTELGEIST:SKILL-END -->
|
|
1050
|
+
is replaced; anything else is preserved.
|
|
1051
|
+
|
|
1052
|
+
Flags:
|
|
1053
|
+
--scope SCOPE Where to install. Defaults to "user".
|
|
1054
|
+
--force For user/project: overwrite an existing SKILL.md.
|
|
1055
|
+
For agents-md: recover from a malformed marker pair
|
|
1056
|
+
(strip the orphan markers and append a clean region).
|
|
1057
|
+
Normal smart-merges happen without --force.
|
|
1058
|
+
--json Emit a machine-readable JSON envelope.
|
|
1059
|
+
|
|
1060
|
+
The skill is a workflow guide: claim \u2192 read \u2192 mutate \u2192 handoff \u2192
|
|
1061
|
+
release, plus the v0.1 format rules an agent will otherwise
|
|
1062
|
+
rediscover by trial and error.
|
|
1063
|
+
`;
|
|
1064
|
+
SCOPES = /* @__PURE__ */ new Set(["user", "project", "agents-md"]);
|
|
1065
|
+
AGENTS_BEGIN = "<!-- ZETTELGEIST:SKILL-BEGIN \u2014 managed by `zettelgeist install-skill` -->";
|
|
1066
|
+
AGENTS_END = "<!-- ZETTELGEIST:SKILL-END -->";
|
|
1067
|
+
}
|
|
1068
|
+
});
|
|
1069
|
+
|
|
1070
|
+
// src/handlers/util.ts
|
|
1071
|
+
import * as path5 from "node:path";
|
|
1072
|
+
function safeJoin(parentDir, ...segments) {
|
|
1073
|
+
const resolved = path5.resolve(parentDir, ...segments);
|
|
1074
|
+
const parent = path5.resolve(parentDir);
|
|
1075
|
+
if (resolved !== parent && !resolved.startsWith(parent + path5.sep)) {
|
|
1076
|
+
throw new PathTraversalError(`path escapes ${parent}: ${resolved}`);
|
|
1077
|
+
}
|
|
1078
|
+
return resolved;
|
|
1079
|
+
}
|
|
1080
|
+
function sendJson(res, status, body) {
|
|
1081
|
+
res.writeHead(status, { "Content-Type": "application/json; charset=utf-8" });
|
|
1082
|
+
res.end(JSON.stringify(body));
|
|
1083
|
+
}
|
|
1084
|
+
function sendText(res, status, body, contentType = "text/plain") {
|
|
1085
|
+
res.writeHead(status, { "Content-Type": `${contentType}; charset=utf-8` });
|
|
1086
|
+
res.end(body);
|
|
1087
|
+
}
|
|
1088
|
+
function sendNotFound(res) {
|
|
1089
|
+
sendJson(res, 404, { ok: false, error: { message: "not found" } });
|
|
1090
|
+
}
|
|
1091
|
+
async function readBody(req) {
|
|
1092
|
+
const chunks = [];
|
|
1093
|
+
for await (const chunk of req) chunks.push(chunk);
|
|
1094
|
+
const text = Buffer.concat(chunks).toString("utf8");
|
|
1095
|
+
if (!text) return null;
|
|
1096
|
+
try {
|
|
1097
|
+
return JSON.parse(text);
|
|
1098
|
+
} catch {
|
|
1099
|
+
return null;
|
|
1100
|
+
}
|
|
1101
|
+
}
|
|
1102
|
+
var PathTraversalError;
|
|
1103
|
+
var init_util = __esm({
|
|
1104
|
+
"src/handlers/util.ts"() {
|
|
1105
|
+
"use strict";
|
|
1106
|
+
PathTraversalError = class extends Error {
|
|
1107
|
+
constructor(message) {
|
|
1108
|
+
super(message);
|
|
1109
|
+
this.name = "PathTraversalError";
|
|
1110
|
+
}
|
|
1111
|
+
};
|
|
1112
|
+
}
|
|
1113
|
+
});
|
|
1114
|
+
|
|
1115
|
+
// src/handlers/specs.ts
|
|
1116
|
+
import { promises as fs5 } from "node:fs";
|
|
1117
|
+
import * as path6 from "node:path";
|
|
1118
|
+
import { execFile as execFile2 } from "node:child_process";
|
|
1119
|
+
import { promisify as promisify2 } from "node:util";
|
|
1120
|
+
import yaml2 from "js-yaml";
|
|
1121
|
+
import matter2 from "gray-matter";
|
|
1122
|
+
async function getContext(cwd) {
|
|
1123
|
+
const reader = makeDiskFsReader(cwd);
|
|
1124
|
+
const cfg = await loadConfig(reader);
|
|
1125
|
+
return { cwd, specsDir: cfg.config.specsDir };
|
|
1126
|
+
}
|
|
1127
|
+
async function handleSpecsRoute(req, res, cwd, pathname) {
|
|
1128
|
+
try {
|
|
1129
|
+
return await dispatchSpecsRoute(req, res, cwd, pathname);
|
|
1130
|
+
} catch (err) {
|
|
1131
|
+
if (err instanceof PathTraversalError) {
|
|
1132
|
+
sendJson(res, 403, { error: "forbidden" });
|
|
1133
|
+
return;
|
|
1134
|
+
}
|
|
1135
|
+
throw err;
|
|
1136
|
+
}
|
|
1137
|
+
}
|
|
1138
|
+
async function dispatchSpecsRoute(req, res, cwd, pathname) {
|
|
1139
|
+
const ctx = await getContext(cwd);
|
|
1140
|
+
if (pathname === "/api/specs" && req.method === "GET") {
|
|
1141
|
+
return listSpecs(res, ctx);
|
|
1142
|
+
}
|
|
1143
|
+
const m = pathname.match(/^\/api\/specs\/([^/]+)(.*)$/);
|
|
1144
|
+
if (!m) return sendNotFound(res);
|
|
1145
|
+
const name = decodeURIComponent(m[1]);
|
|
1146
|
+
const rest = m[2] ?? "";
|
|
1147
|
+
const specsRoot = path6.resolve(ctx.cwd, ctx.specsDir);
|
|
1148
|
+
safeJoin(specsRoot, name);
|
|
1149
|
+
if (rest === "" && req.method === "GET") {
|
|
1150
|
+
return readSpecDetail(res, ctx, name);
|
|
1151
|
+
}
|
|
1152
|
+
if (rest === "" && req.method === "DELETE") {
|
|
1153
|
+
return deleteSpec(res, ctx, name);
|
|
1154
|
+
}
|
|
1155
|
+
const filesMatch = rest.match(/^\/files\/(.+)$/);
|
|
1156
|
+
if (filesMatch) {
|
|
1157
|
+
const relpath = decodeURIComponent(filesMatch[1]);
|
|
1158
|
+
if (req.method === "GET") return readSpecFile(res, ctx, name, relpath);
|
|
1159
|
+
if (req.method === "PUT") return writeSpecFile(req, res, ctx, name, relpath);
|
|
1160
|
+
return sendNotFound(res);
|
|
1161
|
+
}
|
|
1162
|
+
const taskMatch = rest.match(/^\/tasks\/(\d+)\/(tick|untick)$/);
|
|
1163
|
+
if (taskMatch && req.method === "POST") {
|
|
1164
|
+
const n = parseInt(taskMatch[1], 10);
|
|
1165
|
+
const op = taskMatch[2];
|
|
1166
|
+
return tickTask(res, ctx, name, n, op === "tick");
|
|
1167
|
+
}
|
|
1168
|
+
if (rest === "/status" && req.method === "POST") {
|
|
1169
|
+
return setStatus(req, res, ctx, name);
|
|
1170
|
+
}
|
|
1171
|
+
if (rest === "/frontmatter" && req.method === "PATCH") {
|
|
1172
|
+
return patchFrontmatter(req, res, ctx, name);
|
|
1173
|
+
}
|
|
1174
|
+
if (rest === "/claim" && req.method === "POST") {
|
|
1175
|
+
return claimSpec(req, res, ctx, name);
|
|
1176
|
+
}
|
|
1177
|
+
if (rest === "/release" && req.method === "POST") {
|
|
1178
|
+
return releaseSpec(res, ctx, name);
|
|
1179
|
+
}
|
|
1180
|
+
if (rest === "/handoff" && req.method === "PUT") {
|
|
1181
|
+
return writeHandoff(req, res, ctx, name);
|
|
1182
|
+
}
|
|
1183
|
+
return sendNotFound(res);
|
|
1184
|
+
}
|
|
1185
|
+
async function listSpecs(res, ctx) {
|
|
1186
|
+
const reader = makeDiskFsReader(ctx.cwd);
|
|
1187
|
+
const specs = await loadAllSpecs(reader, ctx.specsDir);
|
|
1188
|
+
const repoState = { claimedSpecs: /* @__PURE__ */ new Set(), mergedSpecs: /* @__PURE__ */ new Set() };
|
|
1189
|
+
const out = specs.map((s) => {
|
|
1190
|
+
const counted = s.tasks.filter((t) => !t.tags.includes("#skip"));
|
|
1191
|
+
const checked = counted.filter((t) => t.checked).length;
|
|
1192
|
+
const blockedBy = stringOrNull(s.frontmatter.blocked_by);
|
|
1193
|
+
return {
|
|
1194
|
+
name: s.name,
|
|
1195
|
+
status: deriveStatus(s, repoState),
|
|
1196
|
+
progress: `${checked}/${counted.length}`,
|
|
1197
|
+
blockedBy,
|
|
1198
|
+
frontmatterStatus: statusOrNull(s.frontmatter.status),
|
|
1199
|
+
pr: stringOrNull(s.frontmatter.pr),
|
|
1200
|
+
branch: stringOrNull(s.frontmatter.branch),
|
|
1201
|
+
worktree: stringOrNull(s.frontmatter.worktree)
|
|
1202
|
+
};
|
|
1203
|
+
});
|
|
1204
|
+
sendJson(res, 200, out);
|
|
1205
|
+
}
|
|
1206
|
+
function stringOrNull(v) {
|
|
1207
|
+
return typeof v === "string" && v.trim() !== "" ? v.trim() : null;
|
|
1208
|
+
}
|
|
1209
|
+
function statusOrNull(v) {
|
|
1210
|
+
return typeof v === "string" && VALID_STATUSES2.has(v) ? v : null;
|
|
1211
|
+
}
|
|
1212
|
+
async function deleteSpec(res, ctx, name) {
|
|
1213
|
+
const specDir = safeJoin(path6.resolve(ctx.cwd, ctx.specsDir), name);
|
|
1214
|
+
const exists = await fs5.stat(specDir).then(() => true).catch(() => false);
|
|
1215
|
+
if (!exists) {
|
|
1216
|
+
sendJson(res, 404, { error: "spec not found" });
|
|
1217
|
+
return;
|
|
1218
|
+
}
|
|
1219
|
+
try {
|
|
1220
|
+
await fs5.rm(specDir, { recursive: true, force: true });
|
|
1221
|
+
} catch (err) {
|
|
1222
|
+
sendJson(res, 500, { error: err.message });
|
|
1223
|
+
return;
|
|
1224
|
+
}
|
|
1225
|
+
const specRel = path6.relative(ctx.cwd, specDir).split(path6.sep).join("/");
|
|
1226
|
+
const commit = await regenAndCommit(ctx, [specRel], `[zg] delete-spec: ${name}`);
|
|
1227
|
+
sendJson(res, 200, { commit });
|
|
1228
|
+
}
|
|
1229
|
+
async function readSpecDetail(res, ctx, name) {
|
|
1230
|
+
const reader = makeDiskFsReader(ctx.cwd);
|
|
1231
|
+
try {
|
|
1232
|
+
const spec = await loadSpec(reader, name, ctx.specsDir);
|
|
1233
|
+
if (spec.requirements === null && spec.tasks.length === 0 && spec.handoff === null && spec.lenses.size === 0) {
|
|
1234
|
+
sendJson(res, 404, { error: "spec not found" });
|
|
1235
|
+
return;
|
|
1236
|
+
}
|
|
1237
|
+
sendJson(res, 200, {
|
|
1238
|
+
name: spec.name,
|
|
1239
|
+
frontmatter: spec.frontmatter,
|
|
1240
|
+
requirements: spec.requirements,
|
|
1241
|
+
tasks: spec.tasks.map((t) => ({ index: t.index, checked: t.checked, text: t.text, tags: [...t.tags] })),
|
|
1242
|
+
handoff: spec.handoff,
|
|
1243
|
+
lenses: Object.fromEntries(spec.lenses)
|
|
1244
|
+
});
|
|
1245
|
+
} catch (err) {
|
|
1246
|
+
sendJson(res, 500, { error: err.message });
|
|
1247
|
+
}
|
|
1248
|
+
}
|
|
1249
|
+
async function readSpecFile(res, ctx, name, relpath) {
|
|
1250
|
+
const specDir = safeJoin(path6.resolve(ctx.cwd, ctx.specsDir), name);
|
|
1251
|
+
const filepath = safeJoin(specDir, relpath);
|
|
1252
|
+
try {
|
|
1253
|
+
const content = await fs5.readFile(filepath, "utf8");
|
|
1254
|
+
sendJson(res, 200, { content });
|
|
1255
|
+
} catch (err) {
|
|
1256
|
+
sendJson(res, 404, { error: err.message });
|
|
1257
|
+
}
|
|
1258
|
+
}
|
|
1259
|
+
async function writeSpecFile(req, res, ctx, name, relpath) {
|
|
1260
|
+
const body = await readBody(req);
|
|
1261
|
+
if (!body || typeof body.content !== "string") {
|
|
1262
|
+
sendJson(res, 400, { error: "body must be {content: string}" });
|
|
1263
|
+
return;
|
|
1264
|
+
}
|
|
1265
|
+
const specDir = safeJoin(path6.resolve(ctx.cwd, ctx.specsDir), name);
|
|
1266
|
+
const fileAbs = safeJoin(specDir, relpath);
|
|
1267
|
+
const fileRel = path6.relative(ctx.cwd, fileAbs).split(path6.sep).join("/");
|
|
1268
|
+
await fs5.mkdir(path6.dirname(fileAbs), { recursive: true });
|
|
1269
|
+
const tmp = `${fileAbs}.tmp`;
|
|
1270
|
+
await fs5.writeFile(tmp, body.content, "utf8");
|
|
1271
|
+
await fs5.rename(tmp, fileAbs);
|
|
1272
|
+
const commit = await regenAndCommit(ctx, [fileRel], `[zg] write: ${name}/${relpath}`);
|
|
1273
|
+
sendJson(res, 200, { commit });
|
|
1274
|
+
}
|
|
1275
|
+
async function tickTask(res, ctx, name, n, checked) {
|
|
1276
|
+
const specDir = safeJoin(path6.resolve(ctx.cwd, ctx.specsDir), name);
|
|
1277
|
+
const tasksAbs = safeJoin(specDir, "tasks.md");
|
|
1278
|
+
const tasksRel = path6.relative(ctx.cwd, tasksAbs).split(path6.sep).join("/");
|
|
1279
|
+
let body;
|
|
1280
|
+
try {
|
|
1281
|
+
body = await fs5.readFile(tasksAbs, "utf8");
|
|
1282
|
+
} catch {
|
|
1283
|
+
sendJson(res, 404, { error: "spec or tasks.md not found" });
|
|
1284
|
+
return;
|
|
1285
|
+
}
|
|
1286
|
+
const TASK_LINE2 = /^([\s>]*[-*+]\s+\[)([ xX])(\]\s+.*)$/;
|
|
1287
|
+
const lines = body.split("\n");
|
|
1288
|
+
let count = 0;
|
|
1289
|
+
let mutated = false;
|
|
1290
|
+
for (let i = 0; i < lines.length; i += 1) {
|
|
1291
|
+
const m = lines[i]?.match(TASK_LINE2);
|
|
1292
|
+
if (!m) continue;
|
|
1293
|
+
count += 1;
|
|
1294
|
+
if (count === n) {
|
|
1295
|
+
lines[i] = m[1] + (checked ? "x" : " ") + m[3];
|
|
1296
|
+
mutated = true;
|
|
1297
|
+
break;
|
|
1298
|
+
}
|
|
1299
|
+
}
|
|
1300
|
+
if (!mutated) {
|
|
1301
|
+
sendJson(res, 400, { error: `no task at index ${n}` });
|
|
1302
|
+
return;
|
|
1303
|
+
}
|
|
1304
|
+
const tmp = `${tasksAbs}.tmp`;
|
|
1305
|
+
await fs5.writeFile(tmp, lines.join("\n"), "utf8");
|
|
1306
|
+
await fs5.rename(tmp, tasksAbs);
|
|
1307
|
+
const op = checked ? "tick" : "untick";
|
|
1308
|
+
const commit = await regenAndCommit(ctx, [tasksRel], `[zg] ${op}: ${name}#${n}`);
|
|
1309
|
+
sendJson(res, 200, { commit });
|
|
1310
|
+
}
|
|
1311
|
+
async function setStatus(req, res, ctx, name) {
|
|
1312
|
+
const body = await readBody(req);
|
|
1313
|
+
if (!body || body.status !== null && !ALLOWED_STATUSES.has(body.status)) {
|
|
1314
|
+
sendJson(res, 400, {
|
|
1315
|
+
error: "body.status must be one of draft, planned, in-progress, in-review, done, blocked, cancelled, or null"
|
|
1316
|
+
});
|
|
1317
|
+
return;
|
|
1318
|
+
}
|
|
1319
|
+
const specDir = safeJoin(path6.resolve(ctx.cwd, ctx.specsDir), name);
|
|
1320
|
+
const reqAbs = safeJoin(specDir, "requirements.md");
|
|
1321
|
+
const reqRel = path6.relative(ctx.cwd, reqAbs).split(path6.sep).join("/");
|
|
1322
|
+
const raw = await fs5.readFile(reqAbs, "utf8").catch(() => "");
|
|
1323
|
+
const parsed = matter2(raw, {});
|
|
1324
|
+
const data = { ...parsed.data ?? {} };
|
|
1325
|
+
if (body.status === null) {
|
|
1326
|
+
delete data.status;
|
|
1327
|
+
delete data.blocked_by;
|
|
1328
|
+
} else {
|
|
1329
|
+
data.status = body.status;
|
|
1330
|
+
if (typeof body.reason === "string") data.blocked_by = body.reason;
|
|
1331
|
+
}
|
|
1332
|
+
const newFrontmatter = Object.keys(data).length > 0 ? `---
|
|
1333
|
+
${yaml2.dump(data)}---
|
|
1334
|
+
` : "";
|
|
1335
|
+
const content = newFrontmatter + (parsed.content.startsWith("\n") ? parsed.content.slice(1) : parsed.content);
|
|
1336
|
+
const tmp = `${reqAbs}.tmp`;
|
|
1337
|
+
await fs5.mkdir(path6.dirname(reqAbs), { recursive: true });
|
|
1338
|
+
await fs5.writeFile(tmp, content, "utf8");
|
|
1339
|
+
await fs5.rename(tmp, reqAbs);
|
|
1340
|
+
const commit = await regenAndCommit(ctx, [reqRel], `[zg] set-status: ${name}`);
|
|
1341
|
+
sendJson(res, 200, { commit });
|
|
1342
|
+
}
|
|
1343
|
+
async function patchFrontmatter(req, res, ctx, name) {
|
|
1344
|
+
const body = await readBody(req);
|
|
1345
|
+
if (!body || typeof body.patch !== "object" || body.patch === null || Array.isArray(body.patch)) {
|
|
1346
|
+
sendJson(res, 400, { error: "body must be {patch: {...}}" });
|
|
1347
|
+
return;
|
|
1348
|
+
}
|
|
1349
|
+
for (const k of Object.keys(body.patch)) {
|
|
1350
|
+
if (PATCH_FORBIDDEN_KEYS.has(k)) {
|
|
1351
|
+
sendJson(res, 400, { error: `${k} cannot be set via frontmatter patch; use POST /status instead` });
|
|
1352
|
+
return;
|
|
1353
|
+
}
|
|
1354
|
+
}
|
|
1355
|
+
const specDir = safeJoin(path6.resolve(ctx.cwd, ctx.specsDir), name);
|
|
1356
|
+
const reqAbs = safeJoin(specDir, "requirements.md");
|
|
1357
|
+
const reqRel = path6.relative(ctx.cwd, reqAbs).split(path6.sep).join("/");
|
|
1358
|
+
const raw = await fs5.readFile(reqAbs, "utf8").catch(() => "");
|
|
1359
|
+
const parsed = matter2(raw, {});
|
|
1360
|
+
const data = { ...parsed.data ?? {} };
|
|
1361
|
+
for (const [k, v] of Object.entries(body.patch)) {
|
|
1362
|
+
if (v === null) delete data[k];
|
|
1363
|
+
else data[k] = v;
|
|
1364
|
+
}
|
|
1365
|
+
const newFrontmatter = Object.keys(data).length > 0 ? `---
|
|
1366
|
+
${yaml2.dump(data)}---
|
|
1367
|
+
` : "";
|
|
1368
|
+
const content = newFrontmatter + (parsed.content.startsWith("\n") ? parsed.content.slice(1) : parsed.content);
|
|
1369
|
+
await fs5.mkdir(path6.dirname(reqAbs), { recursive: true });
|
|
1370
|
+
const tmp = `${reqAbs}.tmp`;
|
|
1371
|
+
await fs5.writeFile(tmp, content, "utf8");
|
|
1372
|
+
await fs5.rename(tmp, reqAbs);
|
|
1373
|
+
const commit = await regenAndCommit(ctx, [reqRel], `[zg] patch-frontmatter: ${name}`);
|
|
1374
|
+
sendJson(res, 200, { commit });
|
|
1375
|
+
}
|
|
1376
|
+
async function claimSpec(req, res, ctx, name) {
|
|
1377
|
+
const body = await readBody(req);
|
|
1378
|
+
const agentId = body?.agent_id ?? "agent";
|
|
1379
|
+
const dir = safeJoin(path6.resolve(ctx.cwd, ctx.specsDir), name);
|
|
1380
|
+
await fs5.mkdir(dir, { recursive: true });
|
|
1381
|
+
await fs5.writeFile(safeJoin(dir, ".claim"), `${agentId}
|
|
1382
|
+
${(/* @__PURE__ */ new Date()).toISOString()}
|
|
1383
|
+
`, "utf8");
|
|
1384
|
+
sendJson(res, 200, { acknowledged: true });
|
|
1385
|
+
}
|
|
1386
|
+
async function releaseSpec(res, ctx, name) {
|
|
1387
|
+
const specDir = safeJoin(path6.resolve(ctx.cwd, ctx.specsDir), name);
|
|
1388
|
+
const claimPath = safeJoin(specDir, ".claim");
|
|
1389
|
+
await fs5.unlink(claimPath).catch((err) => {
|
|
1390
|
+
if (err.code !== "ENOENT") throw err;
|
|
1391
|
+
});
|
|
1392
|
+
sendJson(res, 200, { acknowledged: true });
|
|
1393
|
+
}
|
|
1394
|
+
async function writeHandoff(req, res, ctx, name) {
|
|
1395
|
+
const body = await readBody(req);
|
|
1396
|
+
if (!body || typeof body.content !== "string") {
|
|
1397
|
+
sendJson(res, 400, { error: "body must be {content: string}" });
|
|
1398
|
+
return;
|
|
1399
|
+
}
|
|
1400
|
+
const specDir = safeJoin(path6.resolve(ctx.cwd, ctx.specsDir), name);
|
|
1401
|
+
const handoffAbs = safeJoin(specDir, "handoff.md");
|
|
1402
|
+
const handoffRel = path6.relative(ctx.cwd, handoffAbs).split(path6.sep).join("/");
|
|
1403
|
+
await fs5.mkdir(path6.dirname(handoffAbs), { recursive: true });
|
|
1404
|
+
const tmp = `${handoffAbs}.tmp`;
|
|
1405
|
+
await fs5.writeFile(tmp, body.content, "utf8");
|
|
1406
|
+
await fs5.rename(tmp, handoffAbs);
|
|
1407
|
+
const commit = await regenAndCommit(ctx, [handoffRel], `[zg] handoff: ${name}`);
|
|
1408
|
+
sendJson(res, 200, { commit });
|
|
1409
|
+
}
|
|
1410
|
+
async function regenAndCommit(ctx, files, message) {
|
|
1411
|
+
const { regenCommand: regenCommand2 } = await Promise.resolve().then(() => (init_regen2(), regen_exports));
|
|
1412
|
+
await regenCommand2({ path: ctx.cwd, check: false });
|
|
1413
|
+
const indexRel = path6.posix.join(ctx.specsDir, "INDEX.md");
|
|
1414
|
+
await execFileP2("git", ["add", ...files, indexRel], { cwd: ctx.cwd });
|
|
1415
|
+
try {
|
|
1416
|
+
await execFileP2("git", ["diff", "--cached", "--quiet"], { cwd: ctx.cwd });
|
|
1417
|
+
const { stdout: stdout2 } = await execFileP2("git", ["rev-parse", "HEAD"], { cwd: ctx.cwd });
|
|
1418
|
+
return stdout2.trim();
|
|
1419
|
+
} catch {
|
|
1420
|
+
}
|
|
1421
|
+
await execFileP2("git", ["commit", "-m", message], { cwd: ctx.cwd });
|
|
1422
|
+
const { stdout } = await execFileP2("git", ["rev-parse", "HEAD"], { cwd: ctx.cwd });
|
|
1423
|
+
return stdout.trim();
|
|
1424
|
+
}
|
|
1425
|
+
var execFileP2, VALID_STATUSES2, ALLOWED_STATUSES, PATCH_FORBIDDEN_KEYS;
|
|
1426
|
+
var init_specs = __esm({
|
|
1427
|
+
"src/handlers/specs.ts"() {
|
|
1428
|
+
"use strict";
|
|
1429
|
+
init_src();
|
|
1430
|
+
init_src2();
|
|
1431
|
+
init_util();
|
|
1432
|
+
execFileP2 = promisify2(execFile2);
|
|
1433
|
+
VALID_STATUSES2 = /* @__PURE__ */ new Set([
|
|
1434
|
+
"draft",
|
|
1435
|
+
"planned",
|
|
1436
|
+
"in-progress",
|
|
1437
|
+
"in-review",
|
|
1438
|
+
"done",
|
|
1439
|
+
"blocked",
|
|
1440
|
+
"cancelled"
|
|
1441
|
+
]);
|
|
1442
|
+
ALLOWED_STATUSES = /* @__PURE__ */ new Set([
|
|
1443
|
+
"draft",
|
|
1444
|
+
"planned",
|
|
1445
|
+
"in-progress",
|
|
1446
|
+
"in-review",
|
|
1447
|
+
"done",
|
|
1448
|
+
"blocked",
|
|
1449
|
+
"cancelled"
|
|
1450
|
+
]);
|
|
1451
|
+
PATCH_FORBIDDEN_KEYS = /* @__PURE__ */ new Set(["status", "blocked_by"]);
|
|
1452
|
+
}
|
|
1453
|
+
});
|
|
1454
|
+
|
|
1455
|
+
// src/handlers/docs.ts
|
|
1456
|
+
import { promises as fs6 } from "node:fs";
|
|
1457
|
+
import * as path7 from "node:path";
|
|
1458
|
+
import { execFile as execFile3 } from "node:child_process";
|
|
1459
|
+
import { promisify as promisify3 } from "node:util";
|
|
1460
|
+
async function handleDocsRoute(req, res, cwd, pathname) {
|
|
1461
|
+
if (pathname === "/api/docs" && req.method === "GET") {
|
|
1462
|
+
return listDocs(res, cwd);
|
|
1463
|
+
}
|
|
1464
|
+
const renameMatch = pathname.match(/^\/api\/docs\/(.+)\/rename$/);
|
|
1465
|
+
if (renameMatch && req.method === "POST") {
|
|
1466
|
+
return renameDoc(req, res, cwd, decodeURIComponent(renameMatch[1]));
|
|
1467
|
+
}
|
|
1468
|
+
const m = pathname.match(/^\/api\/docs\/(.+)$/);
|
|
1469
|
+
if (m) {
|
|
1470
|
+
const relpath = decodeURIComponent(m[1]);
|
|
1471
|
+
if (req.method === "GET") return readDoc(res, cwd, relpath);
|
|
1472
|
+
if (req.method === "PUT") return writeDoc(req, res, cwd, relpath);
|
|
1473
|
+
}
|
|
1474
|
+
sendNotFound(res);
|
|
1475
|
+
}
|
|
1476
|
+
async function listDocs(res, cwd) {
|
|
1477
|
+
const out = [];
|
|
1478
|
+
for (const root of DOCS_ROOTS) {
|
|
1479
|
+
const abs = path7.join(cwd, root);
|
|
1480
|
+
const stat = await fs6.stat(abs).catch(() => null);
|
|
1481
|
+
if (!stat) continue;
|
|
1482
|
+
if (stat.isDirectory()) {
|
|
1483
|
+
await walk(abs, cwd, out);
|
|
1484
|
+
} else if (stat.isFile() && abs.endsWith(".md")) {
|
|
1485
|
+
out.push({ path: root, title: await firstH1(abs) ?? root });
|
|
1486
|
+
}
|
|
1487
|
+
}
|
|
1488
|
+
sendJson(res, 200, out);
|
|
1489
|
+
}
|
|
1490
|
+
async function walk(dir, cwd, out) {
|
|
1491
|
+
const entries = await fs6.readdir(dir, { withFileTypes: true });
|
|
1492
|
+
for (const e of entries) {
|
|
1493
|
+
const full = path7.join(dir, e.name);
|
|
1494
|
+
if (e.isDirectory()) {
|
|
1495
|
+
await walk(full, cwd, out);
|
|
1496
|
+
} else if (e.isFile() && e.name.endsWith(".md")) {
|
|
1497
|
+
const rel = path7.relative(cwd, full).split(path7.sep).join("/");
|
|
1498
|
+
out.push({ path: rel, title: await firstH1(full) ?? rel });
|
|
1499
|
+
}
|
|
1500
|
+
}
|
|
1501
|
+
}
|
|
1502
|
+
async function firstH1(file) {
|
|
1503
|
+
try {
|
|
1504
|
+
const content = await fs6.readFile(file, "utf8");
|
|
1505
|
+
const m = content.match(/^#\s+(.+)$/m);
|
|
1506
|
+
return m?.[1]?.trim() ?? null;
|
|
1507
|
+
} catch {
|
|
1508
|
+
return null;
|
|
1509
|
+
}
|
|
1510
|
+
}
|
|
1511
|
+
async function readDoc(res, cwd, relpath) {
|
|
1512
|
+
const abs = guardPath(cwd, relpath);
|
|
1513
|
+
if (!abs) {
|
|
1514
|
+
sendJson(res, 403, { error: "forbidden" });
|
|
1515
|
+
return;
|
|
1516
|
+
}
|
|
1517
|
+
try {
|
|
1518
|
+
const content = await fs6.readFile(abs, "utf8");
|
|
1519
|
+
sendJson(res, 200, {
|
|
1520
|
+
source: content,
|
|
1521
|
+
metadata: { title: await firstH1(abs) ?? relpath }
|
|
1522
|
+
});
|
|
1523
|
+
} catch (err) {
|
|
1524
|
+
sendJson(res, 404, { error: err.message });
|
|
1525
|
+
}
|
|
1526
|
+
}
|
|
1527
|
+
async function writeDoc(req, res, cwd, relpath) {
|
|
1528
|
+
const body = await readBody(req);
|
|
1529
|
+
if (!body || typeof body.content !== "string") {
|
|
1530
|
+
sendJson(res, 400, { error: "body must be {content: string}" });
|
|
1531
|
+
return;
|
|
1532
|
+
}
|
|
1533
|
+
const abs = guardPath(cwd, relpath);
|
|
1534
|
+
if (!abs) {
|
|
1535
|
+
sendJson(res, 403, { error: "forbidden" });
|
|
1536
|
+
return;
|
|
1537
|
+
}
|
|
1538
|
+
await fs6.mkdir(path7.dirname(abs), { recursive: true });
|
|
1539
|
+
const tmp = `${abs}.tmp`;
|
|
1540
|
+
await fs6.writeFile(tmp, body.content, "utf8");
|
|
1541
|
+
await fs6.rename(tmp, abs);
|
|
1542
|
+
const rel = path7.relative(cwd, abs).split(path7.sep).join("/");
|
|
1543
|
+
await execFileP3("git", ["add", rel], { cwd });
|
|
1544
|
+
try {
|
|
1545
|
+
await execFileP3("git", ["diff", "--cached", "--quiet"], { cwd });
|
|
1546
|
+
const { stdout: stdout2 } = await execFileP3("git", ["rev-parse", "HEAD"], { cwd });
|
|
1547
|
+
sendJson(res, 200, { commit: stdout2.trim() });
|
|
1548
|
+
return;
|
|
1549
|
+
} catch {
|
|
1550
|
+
}
|
|
1551
|
+
await execFileP3("git", ["commit", "-m", `[zg] write-doc: ${rel}`], { cwd });
|
|
1552
|
+
const { stdout } = await execFileP3("git", ["rev-parse", "HEAD"], { cwd });
|
|
1553
|
+
sendJson(res, 200, { commit: stdout.trim() });
|
|
1554
|
+
}
|
|
1555
|
+
async function renameDoc(req, res, cwd, oldRel) {
|
|
1556
|
+
const body = await readBody(req);
|
|
1557
|
+
if (!body || typeof body.newPath !== "string" || body.newPath.trim() === "") {
|
|
1558
|
+
sendJson(res, 400, { error: "body must be {newPath: string}" });
|
|
1559
|
+
return;
|
|
1560
|
+
}
|
|
1561
|
+
const oldAbs = guardPath(cwd, oldRel);
|
|
1562
|
+
const newAbs = guardPath(cwd, body.newPath);
|
|
1563
|
+
if (!oldAbs || !newAbs) {
|
|
1564
|
+
sendJson(res, 403, { error: "forbidden" });
|
|
1565
|
+
return;
|
|
1566
|
+
}
|
|
1567
|
+
const exists = await fs6.access(newAbs).then(() => true).catch(() => false);
|
|
1568
|
+
if (exists) {
|
|
1569
|
+
sendJson(res, 409, { error: "target exists" });
|
|
1570
|
+
return;
|
|
1571
|
+
}
|
|
1572
|
+
try {
|
|
1573
|
+
await fs6.mkdir(path7.dirname(newAbs), { recursive: true });
|
|
1574
|
+
await fs6.rename(oldAbs, newAbs);
|
|
1575
|
+
} catch (err) {
|
|
1576
|
+
sendJson(res, 500, { error: err.message });
|
|
1577
|
+
return;
|
|
1578
|
+
}
|
|
1579
|
+
const oldRelClean = path7.relative(cwd, oldAbs).split(path7.sep).join("/");
|
|
1580
|
+
const newRelClean = path7.relative(cwd, newAbs).split(path7.sep).join("/");
|
|
1581
|
+
await execFileP3("git", ["add", "-A", "--", oldRelClean, newRelClean], { cwd });
|
|
1582
|
+
try {
|
|
1583
|
+
await execFileP3("git", ["diff", "--cached", "--quiet"], { cwd });
|
|
1584
|
+
const { stdout: stdout2 } = await execFileP3("git", ["rev-parse", "HEAD"], { cwd });
|
|
1585
|
+
sendJson(res, 200, { commit: stdout2.trim(), newPath: newRelClean });
|
|
1586
|
+
return;
|
|
1587
|
+
} catch {
|
|
1588
|
+
}
|
|
1589
|
+
await execFileP3("git", ["commit", "-m", `[zg] rename-doc: ${oldRelClean} \u2192 ${newRelClean}`], { cwd });
|
|
1590
|
+
const { stdout } = await execFileP3("git", ["rev-parse", "HEAD"], { cwd });
|
|
1591
|
+
sendJson(res, 200, { commit: stdout.trim(), newPath: newRelClean });
|
|
1592
|
+
}
|
|
1593
|
+
function guardPath(cwd, relpath) {
|
|
1594
|
+
const abs = path7.resolve(cwd, relpath);
|
|
1595
|
+
const root = path7.resolve(cwd);
|
|
1596
|
+
if (!abs.startsWith(root + path7.sep) && abs !== root) return null;
|
|
1597
|
+
return abs;
|
|
1598
|
+
}
|
|
1599
|
+
var execFileP3, DOCS_ROOTS;
|
|
1600
|
+
var init_docs = __esm({
|
|
1601
|
+
"src/handlers/docs.ts"() {
|
|
1602
|
+
"use strict";
|
|
1603
|
+
init_util();
|
|
1604
|
+
execFileP3 = promisify3(execFile3);
|
|
1605
|
+
DOCS_ROOTS = ["docs", "spec", "README.md"];
|
|
1606
|
+
}
|
|
1607
|
+
});
|
|
1608
|
+
|
|
1609
|
+
// src/handlers/static-assets.ts
|
|
1610
|
+
import { promises as fs7 } from "node:fs";
|
|
1611
|
+
import * as path8 from "node:path";
|
|
1612
|
+
async function handleStaticRoute(req, res, viewerBundle, pathname, _cwd) {
|
|
1613
|
+
if (req.method !== "GET") {
|
|
1614
|
+
sendNotFound(res);
|
|
1615
|
+
return;
|
|
1616
|
+
}
|
|
1617
|
+
if (pathname === "/" || pathname === "/index.html") {
|
|
1618
|
+
const indexPath = path8.join(viewerBundle, "index.html");
|
|
1619
|
+
let html;
|
|
1620
|
+
try {
|
|
1621
|
+
html = await fs7.readFile(indexPath, "utf8");
|
|
1622
|
+
} catch {
|
|
1623
|
+
sendNotFound(res);
|
|
1624
|
+
return;
|
|
1625
|
+
}
|
|
1626
|
+
const theme = "system";
|
|
1627
|
+
const inject = `<script>window.zettelgeistConfig = { theme: ${JSON.stringify(theme)} };</script>
|
|
1628
|
+
<script>${REST_BACKEND_SHIM}</script>`;
|
|
1629
|
+
html = html.replace(
|
|
1630
|
+
/<script\s+type="module"\s+src="\.\/main\.js"/i,
|
|
1631
|
+
`${inject}
|
|
1632
|
+
<script type="module" src="./main.js"`
|
|
1633
|
+
);
|
|
1634
|
+
res.writeHead(200, {
|
|
1635
|
+
"Content-Type": "text/html; charset=utf-8",
|
|
1636
|
+
"Cache-Control": "no-cache, no-store, must-revalidate",
|
|
1637
|
+
"Pragma": "no-cache",
|
|
1638
|
+
"Expires": "0"
|
|
1639
|
+
});
|
|
1640
|
+
res.end(html);
|
|
1641
|
+
return;
|
|
1642
|
+
}
|
|
1643
|
+
const rel = pathname.replace(/^\/(static\/)?/, "");
|
|
1644
|
+
const ext = path8.extname(rel).toLowerCase();
|
|
1645
|
+
if (!MIME_TYPES[ext]) {
|
|
1646
|
+
sendNotFound(res);
|
|
1647
|
+
return;
|
|
1648
|
+
}
|
|
1649
|
+
const filePath = path8.join(viewerBundle, rel);
|
|
1650
|
+
if (!filePath.startsWith(viewerBundle + path8.sep)) {
|
|
1651
|
+
sendNotFound(res);
|
|
1652
|
+
return;
|
|
1653
|
+
}
|
|
1654
|
+
try {
|
|
1655
|
+
const content = await fs7.readFile(filePath);
|
|
1656
|
+
res.writeHead(200, {
|
|
1657
|
+
"Content-Type": `${MIME_TYPES[ext]}; charset=utf-8`,
|
|
1658
|
+
"Cache-Control": "no-cache, no-store, must-revalidate"
|
|
1659
|
+
});
|
|
1660
|
+
res.end(content);
|
|
1661
|
+
} catch {
|
|
1662
|
+
sendNotFound(res);
|
|
1663
|
+
}
|
|
1664
|
+
}
|
|
1665
|
+
var REST_BACKEND_SHIM, MIME_TYPES;
|
|
1666
|
+
var init_static_assets = __esm({
|
|
1667
|
+
"src/handlers/static-assets.ts"() {
|
|
1668
|
+
"use strict";
|
|
1669
|
+
init_util();
|
|
1670
|
+
REST_BACKEND_SHIM = `
|
|
1671
|
+
(() => {
|
|
1672
|
+
const json = async (url, opts) => {
|
|
1673
|
+
const r = await fetch(url, opts);
|
|
1674
|
+
if (!r.ok) throw new Error(\`\${opts?.method || 'GET'} \${url} \u2192 \${r.status}\`);
|
|
1675
|
+
return r.json();
|
|
1676
|
+
};
|
|
1677
|
+
const post = (url, body) => json(url, {
|
|
1678
|
+
method: 'POST',
|
|
1679
|
+
headers: body !== undefined ? { 'Content-Type': 'application/json' } : {},
|
|
1680
|
+
body: body !== undefined ? JSON.stringify(body) : undefined,
|
|
1681
|
+
});
|
|
1682
|
+
const put = (url, body) => json(url, {
|
|
1683
|
+
method: 'PUT',
|
|
1684
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1685
|
+
body: JSON.stringify(body),
|
|
1686
|
+
});
|
|
1687
|
+
const patch = (url, body) => json(url, {
|
|
1688
|
+
method: 'PATCH',
|
|
1689
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1690
|
+
body: JSON.stringify(body),
|
|
1691
|
+
});
|
|
1692
|
+
const del = (url) => json(url, { method: 'DELETE' });
|
|
1693
|
+
const enc = encodeURIComponent;
|
|
1694
|
+
window.zettelgeistBackend = {
|
|
1695
|
+
listSpecs: () => json('/api/specs'),
|
|
1696
|
+
readSpec: (name) => json(\`/api/specs/\${enc(name)}\`),
|
|
1697
|
+
readSpecFile: (name, rel) => json(\`/api/specs/\${enc(name)}/files/\${rel.split('/').map(enc).join('/')}\`),
|
|
1698
|
+
validateRepo: () => json('/api/validation'),
|
|
1699
|
+
listDocs: () => json('/api/docs'),
|
|
1700
|
+
readDoc: (p) => json(\`/api/docs/\${p.split('/').map(enc).join('/')}\`),
|
|
1701
|
+
writeDoc: (p, content) => put(\`/api/docs/\${p.split('/').map(enc).join('/')}\`, { content }),
|
|
1702
|
+
renameDoc: (oldPath, newPath) => post(\`/api/docs/\${oldPath.split('/').map(enc).join('/')}/rename\`, { newPath }),
|
|
1703
|
+
writeSpecFile: (name, rel, content) => put(\`/api/specs/\${enc(name)}/files/\${rel.split('/').map(enc).join('/')}\`, { content }),
|
|
1704
|
+
tickTask: (name, n) => post(\`/api/specs/\${enc(name)}/tasks/\${n}/tick\`),
|
|
1705
|
+
untickTask: (name, n) => post(\`/api/specs/\${enc(name)}/tasks/\${n}/untick\`),
|
|
1706
|
+
setStatus: (name, status, reason) => post(\`/api/specs/\${enc(name)}/status\`, { status, reason }),
|
|
1707
|
+
patchFrontmatter: (name, fmPatch) => patch(\`/api/specs/\${enc(name)}/frontmatter\`, { patch: fmPatch }),
|
|
1708
|
+
writeHandoff: (name, content) => put(\`/api/specs/\${enc(name)}/handoff\`, { content }),
|
|
1709
|
+
deleteSpec: (name) => del(\`/api/specs/\${enc(name)}\`),
|
|
1710
|
+
regenerateIndex: () => post('/api/regenerate'),
|
|
1711
|
+
claimSpec: (name, agentId) => post(\`/api/specs/\${enc(name)}/claim\`, { agent_id: agentId }),
|
|
1712
|
+
releaseSpec: (name) => post(\`/api/specs/\${enc(name)}/release\`),
|
|
1713
|
+
};
|
|
1714
|
+
})();
|
|
1715
|
+
`;
|
|
1716
|
+
MIME_TYPES = {
|
|
1717
|
+
".html": "text/html",
|
|
1718
|
+
".js": "application/javascript",
|
|
1719
|
+
".css": "text/css",
|
|
1720
|
+
".map": "application/json",
|
|
1721
|
+
".svg": "image/svg+xml",
|
|
1722
|
+
".png": "image/png",
|
|
1723
|
+
".json": "application/json"
|
|
1724
|
+
};
|
|
1725
|
+
}
|
|
1726
|
+
});
|
|
1727
|
+
|
|
1728
|
+
// src/server.ts
|
|
1729
|
+
import { createServer } from "node:http";
|
|
1730
|
+
import * as path9 from "node:path";
|
|
1731
|
+
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
1732
|
+
import { promises as fs8 } from "node:fs";
|
|
1733
|
+
async function startServer(opts) {
|
|
1734
|
+
const viewerBundle = opts.viewerBundlePath ?? DEFAULT_VIEWER_BUNDLE;
|
|
1735
|
+
const server = createServer((req, res) => {
|
|
1736
|
+
routeRequest(req, res, opts.cwd, viewerBundle).catch((err) => {
|
|
1737
|
+
console.error("server error:", err);
|
|
1738
|
+
try {
|
|
1739
|
+
sendJson(res, 500, { ok: false, error: { message: err.message } });
|
|
1740
|
+
} catch {
|
|
1741
|
+
}
|
|
1742
|
+
});
|
|
1743
|
+
});
|
|
1744
|
+
await new Promise((resolve6, reject) => {
|
|
1745
|
+
server.once("error", reject);
|
|
1746
|
+
server.listen(opts.port, "127.0.0.1", () => resolve6());
|
|
1747
|
+
});
|
|
1748
|
+
const address = server.address();
|
|
1749
|
+
const port = typeof address === "object" && address ? address.port : opts.port;
|
|
1750
|
+
const url = `http://127.0.0.1:${port}`;
|
|
1751
|
+
return {
|
|
1752
|
+
port,
|
|
1753
|
+
url,
|
|
1754
|
+
async stop() {
|
|
1755
|
+
await new Promise((resolve6, reject) => {
|
|
1756
|
+
server.close((err) => err ? reject(err) : resolve6());
|
|
1757
|
+
});
|
|
1758
|
+
}
|
|
1759
|
+
};
|
|
1760
|
+
}
|
|
1761
|
+
async function routeRequest(req, res, cwd, viewerBundle) {
|
|
1762
|
+
const url = new URL(req.url ?? "/", "http://localhost");
|
|
1763
|
+
const pathname = url.pathname;
|
|
1764
|
+
if (pathname.startsWith("/api/specs")) {
|
|
1765
|
+
return handleSpecsRoute(req, res, cwd, pathname);
|
|
1766
|
+
}
|
|
1767
|
+
if (pathname === "/api/regenerate") {
|
|
1768
|
+
return handleRegenerate(req, res, cwd);
|
|
1769
|
+
}
|
|
1770
|
+
if (pathname === "/api/validation") {
|
|
1771
|
+
return handleValidation(req, res, cwd);
|
|
1772
|
+
}
|
|
1773
|
+
if (pathname.startsWith("/api/docs")) {
|
|
1774
|
+
return handleDocsRoute(req, res, cwd, pathname);
|
|
1775
|
+
}
|
|
1776
|
+
if (pathname === "/static/user-overrides.css") {
|
|
1777
|
+
return handleUserOverride(req, res, cwd);
|
|
1778
|
+
}
|
|
1779
|
+
return handleStaticRoute(req, res, viewerBundle, pathname, cwd);
|
|
1780
|
+
}
|
|
1781
|
+
async function handleRegenerate(_req, res, cwd) {
|
|
1782
|
+
const { regenCommand: regenCommand2 } = await Promise.resolve().then(() => (init_regen2(), regen_exports));
|
|
1783
|
+
const result = await regenCommand2({ path: cwd, check: false });
|
|
1784
|
+
sendJson(res, result.ok ? 200 : 500, result);
|
|
1785
|
+
}
|
|
1786
|
+
async function handleValidation(_req, res, cwd) {
|
|
1787
|
+
const { validateCommand: validateCommand2 } = await Promise.resolve().then(() => (init_validate2(), validate_exports));
|
|
1788
|
+
const result = await validateCommand2({ path: cwd });
|
|
1789
|
+
sendJson(res, 200, result);
|
|
1790
|
+
}
|
|
1791
|
+
async function handleUserOverride(_req, res, cwd) {
|
|
1792
|
+
const userCss = path9.join(cwd, ".zettelgeist", "render-templates", "viewer.css");
|
|
1793
|
+
try {
|
|
1794
|
+
const content = await fs8.readFile(userCss, "utf8");
|
|
1795
|
+
res.writeHead(200, { "Content-Type": "text/css; charset=utf-8" });
|
|
1796
|
+
res.end(content);
|
|
1797
|
+
} catch {
|
|
1798
|
+
sendText(res, 404, "/* no user overrides */");
|
|
1799
|
+
}
|
|
1800
|
+
}
|
|
1801
|
+
var here, DEFAULT_VIEWER_BUNDLE;
|
|
1802
|
+
var init_server = __esm({
|
|
1803
|
+
"src/server.ts"() {
|
|
1804
|
+
"use strict";
|
|
1805
|
+
init_specs();
|
|
1806
|
+
init_docs();
|
|
1807
|
+
init_static_assets();
|
|
1808
|
+
init_util();
|
|
1809
|
+
here = path9.dirname(fileURLToPath2(import.meta.url));
|
|
1810
|
+
DEFAULT_VIEWER_BUNDLE = path9.resolve(here, "viewer-bundle");
|
|
1811
|
+
}
|
|
1812
|
+
});
|
|
1813
|
+
|
|
1814
|
+
// src/commands/serve.ts
|
|
1815
|
+
var serve_exports = {};
|
|
1816
|
+
__export(serve_exports, {
|
|
1817
|
+
HELP: () => HELP5,
|
|
1818
|
+
serveCommand: () => serveCommand
|
|
1819
|
+
});
|
|
1820
|
+
import { promises as fs9 } from "node:fs";
|
|
1821
|
+
import * as path10 from "node:path";
|
|
1822
|
+
import { spawn } from "node:child_process";
|
|
1823
|
+
function openBrowser(url) {
|
|
1824
|
+
const platform = process.platform;
|
|
1825
|
+
const cmd = platform === "darwin" ? "open" : platform === "win32" ? "start" : "xdg-open";
|
|
1826
|
+
try {
|
|
1827
|
+
spawn(cmd, [url], { detached: true, stdio: "ignore" }).unref();
|
|
1828
|
+
} catch {
|
|
1829
|
+
}
|
|
1830
|
+
}
|
|
1831
|
+
async function serveCommand(input) {
|
|
1832
|
+
const exists = await fs9.access(path10.join(input.path, ".zettelgeist.yaml")).then(() => true).catch(() => false);
|
|
1833
|
+
if (!exists) return errorEnvelope(`not a zettelgeist repo: ${input.path}`);
|
|
1834
|
+
let handle;
|
|
1835
|
+
try {
|
|
1836
|
+
handle = await startServer({ cwd: input.path, port: input.port });
|
|
1837
|
+
} catch (err) {
|
|
1838
|
+
return errorEnvelope(`failed to start server: ${err.message}`);
|
|
1839
|
+
}
|
|
1840
|
+
console.error(`zettelgeist serving at ${handle.url}`);
|
|
1841
|
+
console.error("press Ctrl+C to stop.");
|
|
1842
|
+
if (!input.noOpen) {
|
|
1843
|
+
openBrowser(handle.url);
|
|
1844
|
+
}
|
|
1845
|
+
await new Promise((resolve6) => {
|
|
1846
|
+
const stopAndExit = async () => {
|
|
1847
|
+
console.error("\nshutting down\u2026");
|
|
1848
|
+
await handle.stop();
|
|
1849
|
+
resolve6();
|
|
1850
|
+
};
|
|
1851
|
+
process.once("SIGINT", stopAndExit);
|
|
1852
|
+
process.once("SIGTERM", stopAndExit);
|
|
1853
|
+
});
|
|
1854
|
+
return okEnvelope({ url: handle.url, port: handle.port });
|
|
1855
|
+
}
|
|
1856
|
+
var HELP5;
|
|
1857
|
+
var init_serve = __esm({
|
|
1858
|
+
"src/commands/serve.ts"() {
|
|
1859
|
+
"use strict";
|
|
1860
|
+
init_output();
|
|
1861
|
+
init_server();
|
|
1862
|
+
HELP5 = `zettelgeist serve [--port N] [--no-open] [--json]
|
|
1863
|
+
|
|
1864
|
+
Launch the local HTML viewer over HTTP. Defaults to
|
|
1865
|
+
http://127.0.0.1:7681 and opens the URL in the system browser.
|
|
1866
|
+
|
|
1867
|
+
The server runs until you press Ctrl+C.
|
|
1868
|
+
|
|
1869
|
+
Flags:
|
|
1870
|
+
--port N Listen on port N instead of the default 7681.
|
|
1871
|
+
--no-open Do not open the browser; just print the URL.
|
|
1872
|
+
--json Emit a machine-readable JSON envelope on shutdown.
|
|
1873
|
+
`;
|
|
1874
|
+
}
|
|
1875
|
+
});
|
|
1876
|
+
|
|
1877
|
+
// src/render.ts
|
|
1878
|
+
import { marked } from "marked";
|
|
1879
|
+
function renderMarkdownBody(markdown) {
|
|
1880
|
+
marked.setOptions({ gfm: true, breaks: false });
|
|
1881
|
+
return marked.parse(markdown);
|
|
1882
|
+
}
|
|
1883
|
+
function renderTemplate(template, context) {
|
|
1884
|
+
const placeholderRe = /\{\{([^{}]+)\}\}/g;
|
|
1885
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1886
|
+
return template.replace(placeholderRe, (match, rawKey) => {
|
|
1887
|
+
const key = rawKey.trim();
|
|
1888
|
+
seen.add(key);
|
|
1889
|
+
if (KNOWN_PLACEHOLDERS.includes(key)) {
|
|
1890
|
+
const v = context[key];
|
|
1891
|
+
if (typeof v === "string") return v;
|
|
1892
|
+
}
|
|
1893
|
+
if (key.startsWith(FRONTMATTER_PREFIX)) {
|
|
1894
|
+
const fmKey = key.slice(FRONTMATTER_PREFIX.length);
|
|
1895
|
+
const v = context.frontmatter[fmKey];
|
|
1896
|
+
return v === void 0 ? "" : String(v);
|
|
1897
|
+
}
|
|
1898
|
+
throw new Error(
|
|
1899
|
+
`unknown template placeholder "{{${key}}}". Valid placeholders: ${KNOWN_PLACEHOLDERS.join(", ")}, frontmatter.<key>`
|
|
1900
|
+
);
|
|
1901
|
+
});
|
|
1902
|
+
}
|
|
1903
|
+
function validateTemplate(template) {
|
|
1904
|
+
const placeholderRe = /\{\{([^{}]+)\}\}/g;
|
|
1905
|
+
const errors = [];
|
|
1906
|
+
let match;
|
|
1907
|
+
while ((match = placeholderRe.exec(template)) !== null) {
|
|
1908
|
+
const key = match[1]?.trim() ?? "";
|
|
1909
|
+
if (KNOWN_PLACEHOLDERS.includes(key)) continue;
|
|
1910
|
+
if (key.startsWith(FRONTMATTER_PREFIX)) continue;
|
|
1911
|
+
errors.push(`unknown placeholder: {{${key}}}`);
|
|
1912
|
+
}
|
|
1913
|
+
return errors;
|
|
1914
|
+
}
|
|
1915
|
+
var KNOWN_PLACEHOLDERS, FRONTMATTER_PREFIX;
|
|
1916
|
+
var init_render = __esm({
|
|
1917
|
+
"src/render.ts"() {
|
|
1918
|
+
"use strict";
|
|
1919
|
+
KNOWN_PLACEHOLDERS = ["content", "title", "generated_at", "tool_version"];
|
|
1920
|
+
FRONTMATTER_PREFIX = "frontmatter.";
|
|
1921
|
+
}
|
|
1922
|
+
});
|
|
1923
|
+
|
|
1924
|
+
// src/commands/export-doc.ts
|
|
1925
|
+
var export_doc_exports = {};
|
|
1926
|
+
__export(export_doc_exports, {
|
|
1927
|
+
HELP: () => HELP6,
|
|
1928
|
+
exportDocCommand: () => exportDocCommand
|
|
1929
|
+
});
|
|
1930
|
+
import { promises as fs10 } from "node:fs";
|
|
1931
|
+
import * as path11 from "node:path";
|
|
1932
|
+
import { fileURLToPath as fileURLToPath3 } from "node:url";
|
|
1933
|
+
import matter3 from "gray-matter";
|
|
1934
|
+
async function loadDefaultTemplate() {
|
|
1935
|
+
const here2 = path11.dirname(fileURLToPath3(import.meta.url));
|
|
1936
|
+
const candidates = [
|
|
1937
|
+
path11.join(here2, "templates", "export.html"),
|
|
1938
|
+
// npm install: bin.js is at dist/, sibling templates/
|
|
1939
|
+
path11.join(here2, "..", "templates", "export.html"),
|
|
1940
|
+
// workspace dev: dist/bin.js, sibling templates/ at package root
|
|
1941
|
+
path11.join(here2, "..", "..", "templates", "export.html")
|
|
1942
|
+
// tests: src/commands/, templates/ two levels up
|
|
1943
|
+
];
|
|
1944
|
+
for (const c of candidates) {
|
|
1945
|
+
try {
|
|
1946
|
+
return await fs10.readFile(c, "utf8");
|
|
1947
|
+
} catch {
|
|
1948
|
+
}
|
|
1949
|
+
}
|
|
1950
|
+
throw new Error("default export template not found");
|
|
1951
|
+
}
|
|
1952
|
+
async function exportDocCommand(input) {
|
|
1953
|
+
const sourceAbs = path11.resolve(input.cwd, input.source);
|
|
1954
|
+
let raw;
|
|
1955
|
+
try {
|
|
1956
|
+
raw = await fs10.readFile(sourceAbs, "utf8");
|
|
1957
|
+
} catch (err) {
|
|
1958
|
+
return errorEnvelope(`cannot read source: ${err.message}`);
|
|
1959
|
+
}
|
|
1960
|
+
const parsed = matter3(raw, {});
|
|
1961
|
+
const frontmatter = parsed.data ?? {};
|
|
1962
|
+
const body = parsed.content;
|
|
1963
|
+
let title;
|
|
1964
|
+
if (typeof frontmatter.title === "string") {
|
|
1965
|
+
title = frontmatter.title;
|
|
1966
|
+
} else {
|
|
1967
|
+
const h1 = body.match(/^#\s+(.+)$/m);
|
|
1968
|
+
title = h1?.[1]?.trim() ?? path11.basename(sourceAbs);
|
|
1969
|
+
}
|
|
1970
|
+
let template;
|
|
1971
|
+
if (input.templatePath) {
|
|
1972
|
+
try {
|
|
1973
|
+
template = await fs10.readFile(path11.resolve(input.cwd, input.templatePath), "utf8");
|
|
1974
|
+
} catch (err) {
|
|
1975
|
+
return errorEnvelope(`cannot read template: ${err.message}`);
|
|
1976
|
+
}
|
|
1977
|
+
} else {
|
|
1978
|
+
template = await loadDefaultTemplate();
|
|
1979
|
+
}
|
|
1980
|
+
const tmplErrors = validateTemplate(template);
|
|
1981
|
+
if (tmplErrors.length > 0) {
|
|
1982
|
+
return errorEnvelope(`template has ${tmplErrors.length} unknown placeholder(s)`, { errors: tmplErrors });
|
|
1983
|
+
}
|
|
1984
|
+
const context = {
|
|
1985
|
+
content: renderMarkdownBody(body),
|
|
1986
|
+
title,
|
|
1987
|
+
generated_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1988
|
+
tool_version: TOOL_VERSION,
|
|
1989
|
+
frontmatter
|
|
1990
|
+
};
|
|
1991
|
+
let html;
|
|
1992
|
+
try {
|
|
1993
|
+
html = renderTemplate(template, context);
|
|
1994
|
+
} catch (err) {
|
|
1995
|
+
return errorEnvelope(err.message);
|
|
1996
|
+
}
|
|
1997
|
+
const outDir = path11.join(input.cwd, ".zettelgeist", "exports");
|
|
1998
|
+
await fs10.mkdir(outDir, { recursive: true });
|
|
1999
|
+
const baseName = path11.basename(input.source, path11.extname(input.source)) + ".html";
|
|
2000
|
+
const outPath = path11.join(outDir, baseName);
|
|
2001
|
+
const tmp = `${outPath}.tmp`;
|
|
2002
|
+
await fs10.writeFile(tmp, html, "utf8");
|
|
2003
|
+
await fs10.rename(tmp, outPath);
|
|
2004
|
+
return okEnvelope({ output: path11.relative(input.cwd, outPath) });
|
|
2005
|
+
}
|
|
2006
|
+
var HELP6, TOOL_VERSION;
|
|
2007
|
+
var init_export_doc = __esm({
|
|
2008
|
+
"src/commands/export-doc.ts"() {
|
|
2009
|
+
"use strict";
|
|
2010
|
+
init_output();
|
|
2011
|
+
init_render();
|
|
2012
|
+
HELP6 = `zettelgeist export-doc <path> [--template T] [--json]
|
|
2013
|
+
|
|
2014
|
+
Render a markdown file to a standalone HTML document using the
|
|
2015
|
+
bundled (or supplied) mustache template.
|
|
2016
|
+
|
|
2017
|
+
Args:
|
|
2018
|
+
<path> Markdown source path, relative to the current repo.
|
|
2019
|
+
|
|
2020
|
+
Flags:
|
|
2021
|
+
--template T Use template file T instead of the bundled default.
|
|
2022
|
+
--json Emit a machine-readable JSON envelope.
|
|
2023
|
+
|
|
2024
|
+
Output is written under .zettelgeist/exports/<basename>.html.
|
|
2025
|
+
`;
|
|
2026
|
+
TOOL_VERSION = "0.1.0";
|
|
2027
|
+
}
|
|
2028
|
+
});
|
|
2029
|
+
|
|
2030
|
+
// src/router.ts
|
|
2031
|
+
import { parseArgs } from "node:util";
|
|
2032
|
+
var KNOWN_COMMANDS = /* @__PURE__ */ new Set([
|
|
2033
|
+
"regen",
|
|
2034
|
+
"validate",
|
|
2035
|
+
"install-hook",
|
|
2036
|
+
"install-skill",
|
|
2037
|
+
"serve",
|
|
2038
|
+
"export-doc"
|
|
2039
|
+
]);
|
|
2040
|
+
var FLAG_OPTIONS = {
|
|
2041
|
+
json: { type: "boolean" },
|
|
2042
|
+
help: { type: "boolean", short: "h" },
|
|
2043
|
+
check: { type: "boolean" },
|
|
2044
|
+
force: { type: "boolean" },
|
|
2045
|
+
port: { type: "string" },
|
|
2046
|
+
"no-open": { type: "boolean" },
|
|
2047
|
+
template: { type: "string" },
|
|
2048
|
+
scope: { type: "string" }
|
|
2049
|
+
};
|
|
2050
|
+
function parseInvocation(argv) {
|
|
2051
|
+
if (argv.length === 0) return { kind: "help", topic: null };
|
|
2052
|
+
if (argv[0] === "--help" || argv[0] === "-h") return { kind: "help", topic: null };
|
|
2053
|
+
const [first, ...rest] = argv;
|
|
2054
|
+
if (!first) return { kind: "help", topic: null };
|
|
2055
|
+
if (!KNOWN_COMMANDS.has(first)) return { kind: "unknown-command", name: first };
|
|
2056
|
+
if (rest.includes("--help") || rest.includes("-h")) {
|
|
2057
|
+
return { kind: "help", topic: first };
|
|
2058
|
+
}
|
|
2059
|
+
const { values, positionals } = parseArgs({
|
|
2060
|
+
args: rest,
|
|
2061
|
+
options: FLAG_OPTIONS,
|
|
2062
|
+
allowPositionals: true
|
|
2063
|
+
});
|
|
2064
|
+
const flags = {
|
|
2065
|
+
json: values.json ?? false,
|
|
2066
|
+
help: values.help ?? false,
|
|
2067
|
+
...values.check !== void 0 ? { check: values.check } : {},
|
|
2068
|
+
...values.force !== void 0 ? { force: values.force } : {},
|
|
2069
|
+
...values.port !== void 0 ? { port: values.port } : {},
|
|
2070
|
+
...values["no-open"] !== void 0 ? { "no-open": values["no-open"] } : {},
|
|
2071
|
+
...values.template !== void 0 ? { template: values.template } : {},
|
|
2072
|
+
...values.scope !== void 0 ? { scope: values.scope } : {}
|
|
2073
|
+
};
|
|
2074
|
+
return { kind: "command", name: first, args: positionals, flags };
|
|
2075
|
+
}
|
|
2076
|
+
|
|
2077
|
+
// src/bin.ts
|
|
2078
|
+
init_output();
|
|
2079
|
+
init_regen2();
|
|
2080
|
+
init_validate2();
|
|
2081
|
+
init_install_hook2();
|
|
2082
|
+
init_install_skill();
|
|
2083
|
+
init_serve();
|
|
2084
|
+
init_export_doc();
|
|
2085
|
+
var HELP7 = `zettelgeist v0.1
|
|
2086
|
+
|
|
2087
|
+
Usage:
|
|
2088
|
+
zettelgeist <command> [options]
|
|
2089
|
+
|
|
2090
|
+
Commands:
|
|
2091
|
+
regen [--check] regenerate specs/INDEX.md
|
|
2092
|
+
validate validate the repo against the spec
|
|
2093
|
+
install-hook [--force] install pre-commit hook
|
|
2094
|
+
install-skill [--scope S] install the agent skill (Claude Code etc.)
|
|
2095
|
+
serve [--port N] [--no-open] serve the viewer over HTTP
|
|
2096
|
+
export-doc <path> [--template T] render markdown to HTML
|
|
2097
|
+
|
|
2098
|
+
Global flags:
|
|
2099
|
+
--json emit machine-readable JSON envelope
|
|
2100
|
+
-h, --help show this help
|
|
2101
|
+
|
|
2102
|
+
Run \`zettelgeist <command> --help\` for command-specific help.
|
|
2103
|
+
`;
|
|
2104
|
+
var COMMAND_HELP = {
|
|
2105
|
+
regen: HELP,
|
|
2106
|
+
validate: HELP2,
|
|
2107
|
+
"install-hook": HELP3,
|
|
2108
|
+
"install-skill": HELP4,
|
|
2109
|
+
serve: HELP5,
|
|
2110
|
+
"export-doc": HELP6
|
|
2111
|
+
};
|
|
2112
|
+
async function main() {
|
|
2113
|
+
const inv = parseInvocation(process.argv.slice(2));
|
|
2114
|
+
if (inv.kind === "help") {
|
|
2115
|
+
const text = inv.topic && COMMAND_HELP[inv.topic] || HELP7;
|
|
2116
|
+
process.stdout.write(text);
|
|
2117
|
+
return 0;
|
|
2118
|
+
}
|
|
2119
|
+
if (inv.kind === "unknown-command") {
|
|
2120
|
+
process.stderr.write(`unknown command: ${inv.name}
|
|
2121
|
+
${HELP7}`);
|
|
2122
|
+
return 2;
|
|
2123
|
+
}
|
|
2124
|
+
const ctx = realEmitContext(inv.flags.json);
|
|
2125
|
+
const cwd = process.cwd();
|
|
2126
|
+
switch (inv.name) {
|
|
2127
|
+
case "regen": {
|
|
2128
|
+
const { regenCommand: regenCommand2 } = await Promise.resolve().then(() => (init_regen2(), regen_exports));
|
|
2129
|
+
const env = await regenCommand2({ path: cwd, check: inv.flags.check ?? false });
|
|
2130
|
+
emit(
|
|
2131
|
+
ctx,
|
|
2132
|
+
env,
|
|
2133
|
+
() => env.ok ? env.data.changed ? `regen: wrote ${env.data.path}` : `regen: ${env.data.path} up to date${env.data.cacheHit ? " (cache hit)" : ""}` : ""
|
|
2134
|
+
);
|
|
2135
|
+
return env.ok ? 0 : 1;
|
|
2136
|
+
}
|
|
2137
|
+
case "validate": {
|
|
2138
|
+
const { validateCommand: validateCommand2 } = await Promise.resolve().then(() => (init_validate2(), validate_exports));
|
|
2139
|
+
const env = await validateCommand2({ path: cwd });
|
|
2140
|
+
emit(ctx, env, () => "validate: ok");
|
|
2141
|
+
return env.ok ? 0 : 1;
|
|
2142
|
+
}
|
|
2143
|
+
case "install-hook": {
|
|
2144
|
+
const { installHookCommand: installHookCommand2 } = await Promise.resolve().then(() => (init_install_hook2(), install_hook_exports));
|
|
2145
|
+
const env = await installHookCommand2({ path: cwd, force: inv.flags.force ?? false });
|
|
2146
|
+
emit(
|
|
2147
|
+
ctx,
|
|
2148
|
+
env,
|
|
2149
|
+
() => env.ok ? `install-hook: installed${env.data.backup ? ` (backup: ${env.data.backup})` : ""}` : ""
|
|
2150
|
+
);
|
|
2151
|
+
return env.ok ? 0 : 1;
|
|
2152
|
+
}
|
|
2153
|
+
case "install-skill": {
|
|
2154
|
+
const { installSkillCommand: installSkillCommand2, isScope: isScope2 } = await Promise.resolve().then(() => (init_install_skill(), install_skill_exports));
|
|
2155
|
+
const scopeRaw = inv.flags.scope ?? "user";
|
|
2156
|
+
if (!isScope2(scopeRaw)) {
|
|
2157
|
+
process.stderr.write(
|
|
2158
|
+
`install-skill: --scope must be 'user', 'project', or 'agents-md' (got '${scopeRaw}')
|
|
2159
|
+
`
|
|
2160
|
+
);
|
|
2161
|
+
return 2;
|
|
2162
|
+
}
|
|
2163
|
+
const env = await installSkillCommand2({
|
|
2164
|
+
cwd,
|
|
2165
|
+
scope: scopeRaw,
|
|
2166
|
+
force: inv.flags.force ?? false
|
|
2167
|
+
});
|
|
2168
|
+
emit(
|
|
2169
|
+
ctx,
|
|
2170
|
+
env,
|
|
2171
|
+
() => env.ok ? `install-skill: ${env.data.merged ? "merged into" : "wrote"} ${env.data.path}` : ""
|
|
2172
|
+
);
|
|
2173
|
+
return env.ok ? 0 : 1;
|
|
2174
|
+
}
|
|
2175
|
+
case "serve": {
|
|
2176
|
+
const { serveCommand: serveCommand2 } = await Promise.resolve().then(() => (init_serve(), serve_exports));
|
|
2177
|
+
const port = inv.flags.port ? Number.parseInt(inv.flags.port, 10) : 7681;
|
|
2178
|
+
const env = await serveCommand2({ path: cwd, port, noOpen: inv.flags["no-open"] ?? false });
|
|
2179
|
+
emit(ctx, env, () => env.ok ? `serve: stopped` : "");
|
|
2180
|
+
return env.ok ? 0 : 1;
|
|
2181
|
+
}
|
|
2182
|
+
case "export-doc": {
|
|
2183
|
+
const { exportDocCommand: exportDocCommand2 } = await Promise.resolve().then(() => (init_export_doc(), export_doc_exports));
|
|
2184
|
+
const source = inv.args[0];
|
|
2185
|
+
if (!source) {
|
|
2186
|
+
process.stderr.write("export-doc: missing source path\n");
|
|
2187
|
+
return 2;
|
|
2188
|
+
}
|
|
2189
|
+
const env = await exportDocCommand2({
|
|
2190
|
+
cwd,
|
|
2191
|
+
source,
|
|
2192
|
+
...inv.flags.template ? { templatePath: inv.flags.template } : {}
|
|
2193
|
+
});
|
|
2194
|
+
emit(ctx, env, () => env.ok ? `export-doc: wrote ${env.data.output}` : "");
|
|
2195
|
+
return env.ok ? 0 : 1;
|
|
2196
|
+
}
|
|
2197
|
+
default:
|
|
2198
|
+
process.stderr.write(`unhandled command: ${inv.name}
|
|
2199
|
+
`);
|
|
2200
|
+
return 2;
|
|
2201
|
+
}
|
|
2202
|
+
}
|
|
2203
|
+
main().then(
|
|
2204
|
+
(code) => process.exit(code),
|
|
2205
|
+
(err) => {
|
|
2206
|
+
process.stderr.write(`zettelgeist: ${err instanceof Error ? err.message : String(err)}
|
|
2207
|
+
`);
|
|
2208
|
+
process.exit(1);
|
|
2209
|
+
}
|
|
2210
|
+
);
|
|
2211
|
+
//# sourceMappingURL=bin.js.map
|