@cr8rcho/alkahest 0.1.91 → 0.1.92
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/package.json
CHANGED
|
@@ -4,6 +4,11 @@ Why each shipped file changed, newest first. `alkahest preset update` prints the
|
|
|
4
4
|
than the version an installed copy came from, so whoever merges knows what the change is for.
|
|
5
5
|
One `## <version>` per release that touched the preset; each bullet names its file.
|
|
6
6
|
|
|
7
|
+
## 0.1.92
|
|
8
|
+
- `sync-docs-maps.mjs`: removes its staging dir after a successful sync (it is this run's own
|
|
9
|
+
since 0.1.90, so nothing else is touched); kept on `--stage-only` and on failure, with the path
|
|
10
|
+
printed.
|
|
11
|
+
|
|
7
12
|
## 0.1.91
|
|
8
13
|
- `CLAUDE-snippet.md`: closes with an end marker (`<!-- /alkahest as-built preset -->`) so
|
|
9
14
|
`alkahest preset update` can find the block's boundary; the header names the update command.
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Reference implementation (alkahest as-built preset) — mirror this repo's docs into the
|
|
3
|
+
// project's note maps:
|
|
4
|
+
// docs/decisions/NNN-*.md → note map `adr` (flat)
|
|
5
|
+
// docs/{system,components,features,modules}/*.md → note map `as-built` (folder = category)
|
|
6
|
+
//
|
|
7
|
+
// Staging transforms (the importer's rules: filename = title, frontmatter passes through):
|
|
8
|
+
// - title from the first H1 ("ADR-NNN: rest — tail" → "ADR-NNN rest"; as-built strips a
|
|
9
|
+
// "System|Component|Feature|Module — " class prefix and a " — " tail), sanitized;
|
|
10
|
+
// - the H1 line is dropped from the body (the note title renders it);
|
|
11
|
+
// - relative .md links WITHIN each set become [[wikilinks]] so the graph connects
|
|
12
|
+
// (cross-set links stay plain markdown);
|
|
13
|
+
// - the ORIGINAL repo path rides in as `source_path:` frontmatter — the staged filename
|
|
14
|
+
// is title-derived, so this is what keeps identity through a retitle (the importer
|
|
15
|
+
// matches source_path before title and renames the note in place).
|
|
16
|
+
//
|
|
17
|
+
// Re-running is safe: import is idempotent by source_path, then title.
|
|
18
|
+
// This script belongs to the repo it lives in — adapt titles/sets/maps to local conventions.
|
|
19
|
+
// Usage: node scripts/sync-docs-maps.mjs [--stage-only]
|
|
20
|
+
import { readdirSync, readFileSync, mkdirSync, writeFileSync, mkdtempSync } from "node:fs";
|
|
21
|
+
import { spawnSync } from "node:child_process";
|
|
22
|
+
import { join, dirname, basename } from "node:path";
|
|
23
|
+
import { fileURLToPath } from "node:url";
|
|
24
|
+
import { tmpdir } from "node:os";
|
|
25
|
+
|
|
26
|
+
const ROOT = join(dirname(fileURLToPath(import.meta.url)), "..");
|
|
27
|
+
const DOCS = join(ROOT, "docs");
|
|
28
|
+
// A fresh dir per run: a fixed shared path let another repo's copy of this script, running at the
|
|
29
|
+
// same time, restage its own docs under us, and our import pushed them into this project's maps.
|
|
30
|
+
const OUT = mkdtempSync(join(tmpdir(), "alkahest-docs-staging-"));
|
|
31
|
+
|
|
32
|
+
const sanitize = (t) =>
|
|
33
|
+
t.replace(/\(\//g, "(").replace(/[/\\:]/g, "-").replace(/`/g, "").replace(/\s+/g, " ").trim();
|
|
34
|
+
const firstH1 = (src) => (src.match(/^#\s+(.+)$/m) ?? [null, ""])[1].trim();
|
|
35
|
+
// Frontmatter rides through to the staged file — the importer strips it from the stored body
|
|
36
|
+
// and harvests its keys into note props (`tags:` is the reserved one). Split it off first so
|
|
37
|
+
// stripH1 still sees the H1 at the head of what's left.
|
|
38
|
+
const splitFm = (src) => {
|
|
39
|
+
const m = src.match(/^---\r?\n[\s\S]*?\r?\n---\r?\n+/);
|
|
40
|
+
return m ? [m[0], src.slice(m[0].length)] : ["", src];
|
|
41
|
+
};
|
|
42
|
+
const stripH1 = (src) => {
|
|
43
|
+
const [fm, body] = splitFm(src);
|
|
44
|
+
return fm + body.replace(/^#\s+.+\n+/, "");
|
|
45
|
+
};
|
|
46
|
+
// Rename-safe identity: inject the ORIGINAL repo path as source_path: frontmatter (merged
|
|
47
|
+
// into an existing block, or a new block is minted).
|
|
48
|
+
const withSource = (staged, rel) => {
|
|
49
|
+
const [fm, body] = splitFm(staged);
|
|
50
|
+
const line = `source_path: ${rel}\n`;
|
|
51
|
+
return fm ? fm.replace(/---\r?\n*$/, (close) => line + close) + body : `---\n${line}---\n\n${staged}`;
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
// ---- ADRs ------------------------------------------------------------------
|
|
55
|
+
const adrDir = join(DOCS, "decisions");
|
|
56
|
+
const adrFiles = readdirSync(adrDir).filter((f) => /^\d{3}-.*\.md$/.test(f));
|
|
57
|
+
const adrTitle = {};
|
|
58
|
+
for (const f of adrFiles) {
|
|
59
|
+
const h1 = firstH1(readFileSync(join(adrDir, f), "utf8"));
|
|
60
|
+
// Convention "ADR-NNN: title — tail"; the em-dash variant "ADR-NNN — title" also parses.
|
|
61
|
+
const m = h1.match(/^ADR-(\d{3})(?::|\s+—)\s*(.+)$/);
|
|
62
|
+
const num = m ? m[1] : f.slice(0, 3);
|
|
63
|
+
adrTitle[num] = sanitize(`ADR-${num} ${(m ? m[2] : h1).split(" — ")[0].trim()}`);
|
|
64
|
+
}
|
|
65
|
+
const linkAdrs = (body) =>
|
|
66
|
+
body.replace(/\[([^\]]*)\]\((?:\.\/)?(\d{3})-[^)#\s]*\.md(?:#[^)]*)?\)/g, (all, _label, num) =>
|
|
67
|
+
adrTitle[num] ? `[[${adrTitle[num]}]]` : all);
|
|
68
|
+
mkdirSync(join(OUT, "adr"), { recursive: true });
|
|
69
|
+
for (const f of adrFiles) {
|
|
70
|
+
const t = adrTitle[f.slice(0, 3)];
|
|
71
|
+
const staged = linkAdrs(stripH1(readFileSync(join(adrDir, f), "utf8")));
|
|
72
|
+
writeFileSync(join(OUT, "adr", `${t}.md`), withSource(staged, `docs/decisions/${f}`));
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// ---- as-built --------------------------------------------------------------
|
|
76
|
+
const cats = ["system", "components", "features", "modules"];
|
|
77
|
+
const abTitle = {};
|
|
78
|
+
const abSrc = {};
|
|
79
|
+
for (const cat of cats) {
|
|
80
|
+
for (const f of readdirSync(join(DOCS, cat)).filter((x) => x.endsWith(".md"))) {
|
|
81
|
+
const src = readFileSync(join(DOCS, cat, f), "utf8");
|
|
82
|
+
let t = firstH1(src).replace(/^(System|Component|Feature|Module)\s*—\s*/i, "");
|
|
83
|
+
t = t.split(" — ")[0].trim() || firstH1(src);
|
|
84
|
+
abTitle[basename(f, ".md")] = sanitize(t);
|
|
85
|
+
abSrc[basename(f, ".md")] = { cat, src };
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
const linkAb = (body) =>
|
|
89
|
+
body.replace(/\[([^\]]*)\]\((?:\.\.\/(?:system|components|features|modules)\/|\.\/)?([a-z0-9-]+)\.md(?:#[^)]*)?\)/g,
|
|
90
|
+
(all, _label, name) => (abTitle[name] ? `[[${abTitle[name]}]]` : all));
|
|
91
|
+
for (const [name, { cat, src }] of Object.entries(abSrc)) {
|
|
92
|
+
mkdirSync(join(OUT, "as-built", cat), { recursive: true });
|
|
93
|
+
writeFileSync(join(OUT, "as-built", cat, `${abTitle[name]}.md`), withSource(linkAb(stripH1(src)), `docs/${cat}/${name}.md`));
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
console.log(`staged ${adrFiles.length} ADRs + ${Object.keys(abSrc).length} as-built docs → ${OUT}`);
|
|
97
|
+
if (process.argv.includes("--stage-only")) process.exit(0);
|
|
98
|
+
|
|
99
|
+
// A full mirror is many sequential POSTs, so a transient failure is a question of when, not
|
|
100
|
+
// if. The CLI handles it per file (keeps going, prints `✗ <file>: <message>`, exits 1) — act
|
|
101
|
+
// on the exit code and retry the whole set: rows that already landed come back as updates.
|
|
102
|
+
const ATTEMPTS = 3;
|
|
103
|
+
const failed = [];
|
|
104
|
+
|
|
105
|
+
for (const [dir, map] of [["adr", "adr"], ["as-built", "as-built"]]) {
|
|
106
|
+
let ok = false;
|
|
107
|
+
for (let attempt = 1; attempt <= ATTEMPTS && !ok; attempt++) {
|
|
108
|
+
console.log(`\n== notes import ${dir} → map ${map}${attempt > 1 ? ` (retry ${attempt - 1})` : ""} ==`);
|
|
109
|
+
const run = spawnSync("alkahest", ["notes", "import", join(OUT, dir), "--map", map, "--path", ROOT], {
|
|
110
|
+
stdio: "inherit",
|
|
111
|
+
});
|
|
112
|
+
if (run.error) console.error(`[sync] could not run alkahest: ${run.error.message}`);
|
|
113
|
+
ok = !run.error && run.status === 0;
|
|
114
|
+
if (!ok && attempt < ATTEMPTS) Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 5000);
|
|
115
|
+
}
|
|
116
|
+
if (!ok) failed.push(`${dir} → ${map}`);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (failed.length) {
|
|
120
|
+
console.error(`\n[sync] FAILED after ${ATTEMPTS} attempts: ${failed.join(", ")}`);
|
|
121
|
+
console.error("[sync] scroll up for the per-file '✗ <file>: <message>' lines from the importer.");
|
|
122
|
+
console.error(`[sync] staged files kept for inspection: ${OUT}`);
|
|
123
|
+
process.exit(1);
|
|
124
|
+
}
|
|
125
|
+
// The staging dir is this run's own (mkdtempSync), so removing it touches no other sync.
|
|
126
|
+
// Kept on --stage-only (that's what it's for) and on failure (above). Imported here rather
|
|
127
|
+
// than at the top so `alkahest preset update` can merge this into copies that reorganised
|
|
128
|
+
// their import block.
|
|
129
|
+
(await import("node:fs")).rmSync(OUT, { recursive: true, force: true });
|
|
130
|
+
console.log("\n[sync] both maps up to date.");
|
|
@@ -119,6 +119,12 @@ for (const [dir, map] of [["adr", "adr"], ["as-built", "as-built"]]) {
|
|
|
119
119
|
if (failed.length) {
|
|
120
120
|
console.error(`\n[sync] FAILED after ${ATTEMPTS} attempts: ${failed.join(", ")}`);
|
|
121
121
|
console.error("[sync] scroll up for the per-file '✗ <file>: <message>' lines from the importer.");
|
|
122
|
+
console.error(`[sync] staged files kept for inspection: ${OUT}`);
|
|
122
123
|
process.exit(1);
|
|
123
124
|
}
|
|
125
|
+
// The staging dir is this run's own (mkdtempSync), so removing it touches no other sync.
|
|
126
|
+
// Kept on --stage-only (that's what it's for) and on failure (above). Imported here rather
|
|
127
|
+
// than at the top so `alkahest preset update` can merge this into copies that reorganised
|
|
128
|
+
// their import block.
|
|
129
|
+
(await import("node:fs")).rmSync(OUT, { recursive: true, force: true });
|
|
124
130
|
console.log("\n[sync] both maps up to date.");
|