@ctxr/skill-llm-wiki 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +134 -0
- package/LICENSE +21 -0
- package/README.md +484 -0
- package/SKILL.md +252 -0
- package/guide/basics/concepts.md +74 -0
- package/guide/basics/index.md +45 -0
- package/guide/basics/schema.md +140 -0
- package/guide/cli.md +256 -0
- package/guide/correctness/index.md +45 -0
- package/guide/correctness/invariants.md +89 -0
- package/guide/correctness/safety.md +96 -0
- package/guide/history/diff.md +110 -0
- package/guide/history/hidden-git.md +130 -0
- package/guide/history/index.md +52 -0
- package/guide/history/remote-sync.md +113 -0
- package/guide/index.md +134 -0
- package/guide/isolation/coexistence.md +134 -0
- package/guide/isolation/index.md +44 -0
- package/guide/isolation/scale.md +251 -0
- package/guide/layout/in-place-mode.md +97 -0
- package/guide/layout/index.md +53 -0
- package/guide/layout/layout-contract.md +131 -0
- package/guide/layout/layout-modes.md +115 -0
- package/guide/operations/index.md +76 -0
- package/guide/operations/ingest/build.md +75 -0
- package/guide/operations/ingest/extend.md +61 -0
- package/guide/operations/ingest/index.md +54 -0
- package/guide/operations/ingest/join.md +65 -0
- package/guide/operations/maintain/fix.md +66 -0
- package/guide/operations/maintain/index.md +47 -0
- package/guide/operations/maintain/rebuild.md +86 -0
- package/guide/operations/validate.md +48 -0
- package/guide/substrate/index.md +47 -0
- package/guide/substrate/operators.md +96 -0
- package/guide/substrate/tiered-ai.md +363 -0
- package/guide/ux/index.md +44 -0
- package/guide/ux/preflight.md +150 -0
- package/guide/ux/user-intent.md +135 -0
- package/package.json +55 -0
- package/scripts/cli.mjs +893 -0
- package/scripts/commands/remote.mjs +93 -0
- package/scripts/commands/review.mjs +253 -0
- package/scripts/commands/sync.mjs +84 -0
- package/scripts/lib/chunk.mjs +421 -0
- package/scripts/lib/cluster-detect.mjs +516 -0
- package/scripts/lib/decision-log.mjs +343 -0
- package/scripts/lib/draft.mjs +158 -0
- package/scripts/lib/embeddings.mjs +366 -0
- package/scripts/lib/frontmatter.mjs +497 -0
- package/scripts/lib/git-commands.mjs +155 -0
- package/scripts/lib/git.mjs +486 -0
- package/scripts/lib/gitignore.mjs +62 -0
- package/scripts/lib/history.mjs +331 -0
- package/scripts/lib/indices.mjs +510 -0
- package/scripts/lib/ingest.mjs +258 -0
- package/scripts/lib/intent.mjs +713 -0
- package/scripts/lib/interactive.mjs +99 -0
- package/scripts/lib/migrate.mjs +126 -0
- package/scripts/lib/nest-applier.mjs +260 -0
- package/scripts/lib/operators.mjs +1365 -0
- package/scripts/lib/orchestrator.mjs +718 -0
- package/scripts/lib/paths.mjs +197 -0
- package/scripts/lib/preflight.mjs +213 -0
- package/scripts/lib/provenance.mjs +672 -0
- package/scripts/lib/quality-metric.mjs +269 -0
- package/scripts/lib/query-fixture.mjs +71 -0
- package/scripts/lib/rollback.mjs +95 -0
- package/scripts/lib/shape-check.mjs +172 -0
- package/scripts/lib/similarity-cache.mjs +126 -0
- package/scripts/lib/similarity.mjs +230 -0
- package/scripts/lib/snapshot.mjs +54 -0
- package/scripts/lib/source-frontmatter.mjs +85 -0
- package/scripts/lib/tier2-protocol.mjs +470 -0
- package/scripts/lib/tiered.mjs +453 -0
- package/scripts/lib/validate.mjs +362 -0
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
// Sibling-versioned output directory conventions.
|
|
2
|
+
//
|
|
3
|
+
// For any user source `./docs`, a wiki lives at `./docs.llmwiki.v<N>/` and
|
|
4
|
+
// the current version is tracked via a plaintext file `./docs.llmwiki.current`
|
|
5
|
+
// containing a single line `v<N>`. Rollback is `echo v2 > docs.llmwiki.current`.
|
|
6
|
+
//
|
|
7
|
+
// Every operation that produces structural change writes a new vN+1 and then
|
|
8
|
+
// atomically flips the current pointer on successful commit. The previous
|
|
9
|
+
// version stays on disk until the user explicitly prunes.
|
|
10
|
+
|
|
11
|
+
import { existsSync, readFileSync, readdirSync, statSync, writeFileSync } from "node:fs";
|
|
12
|
+
import { basename, dirname, join, resolve } from "node:path";
|
|
13
|
+
|
|
14
|
+
const VERSION_RE = /^v(\d+)$/;
|
|
15
|
+
|
|
16
|
+
// Marker written into the root index.md frontmatter by this skill. The hook
|
|
17
|
+
// will ONLY react on directories whose root index carries this marker —
|
|
18
|
+
// folders that merely happen to match the `*.llmwiki.vN` naming pattern are
|
|
19
|
+
// ignored. Bump the version suffix (v1 → v2) when the wiki format changes
|
|
20
|
+
// in a way that would confuse older hook code.
|
|
21
|
+
export const WIKI_GENERATOR_MARKER = "skill-llm-wiki/v1";
|
|
22
|
+
|
|
23
|
+
export function wikiBaseName(sourcePath) {
|
|
24
|
+
return basename(resolve(sourcePath));
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function siblingRoot(sourcePath) {
|
|
28
|
+
return dirname(resolve(sourcePath));
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// The default sibling target for any source is `<source>.wiki` — NOT
|
|
32
|
+
// `<source>.llmwiki.v1`. History lives in the private git repo inside
|
|
33
|
+
// the wiki directory (see Phase 1 `snapshot.mjs`), so we never need
|
|
34
|
+
// version-numbered folder names. Trailing slashes on the source path
|
|
35
|
+
// are normalised away by `resolve`.
|
|
36
|
+
//
|
|
37
|
+
// Examples:
|
|
38
|
+
// defaultSiblingPath("./docs") → "/abs/cwd/docs.wiki"
|
|
39
|
+
// defaultSiblingPath("./docs/") → "/abs/cwd/docs.wiki"
|
|
40
|
+
// defaultSiblingPath("/tmp/fluffy") → "/tmp/fluffy.wiki"
|
|
41
|
+
// defaultSiblingPath(".") → "/abs/cwd.wiki" (basename of cwd)
|
|
42
|
+
export function defaultSiblingPath(sourcePath) {
|
|
43
|
+
const base = wikiBaseName(sourcePath);
|
|
44
|
+
return join(siblingRoot(sourcePath), `${base}.wiki`);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function currentPointerPath(sourcePath) {
|
|
48
|
+
const base = wikiBaseName(sourcePath);
|
|
49
|
+
return join(siblingRoot(sourcePath), `${base}.llmwiki.current`);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function versionDir(sourcePath, version) {
|
|
53
|
+
const base = wikiBaseName(sourcePath);
|
|
54
|
+
const tag = typeof version === "number" ? `v${version}` : version;
|
|
55
|
+
return join(siblingRoot(sourcePath), `${base}.llmwiki.${tag}`);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Return all existing `<base>.llmwiki.v<N>` directories sorted ascending by N.
|
|
59
|
+
export function listVersions(sourcePath) {
|
|
60
|
+
const base = wikiBaseName(sourcePath);
|
|
61
|
+
const parent = siblingRoot(sourcePath);
|
|
62
|
+
if (!existsSync(parent)) return [];
|
|
63
|
+
const prefix = `${base}.llmwiki.`;
|
|
64
|
+
const out = [];
|
|
65
|
+
for (const name of readdirSync(parent)) {
|
|
66
|
+
if (!name.startsWith(prefix)) continue;
|
|
67
|
+
const tag = name.slice(prefix.length);
|
|
68
|
+
const m = VERSION_RE.exec(tag);
|
|
69
|
+
if (!m) continue;
|
|
70
|
+
const full = join(parent, name);
|
|
71
|
+
try {
|
|
72
|
+
if (statSync(full).isDirectory()) {
|
|
73
|
+
out.push({ version: Number(m[1]), tag, path: full });
|
|
74
|
+
}
|
|
75
|
+
} catch {
|
|
76
|
+
/* skip */
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
out.sort((a, b) => a.version - b.version);
|
|
80
|
+
return out;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function readCurrentPointer(sourcePath) {
|
|
84
|
+
const p = currentPointerPath(sourcePath);
|
|
85
|
+
if (!existsSync(p)) return null;
|
|
86
|
+
const raw = readFileSync(p, "utf8").trim();
|
|
87
|
+
const m = VERSION_RE.exec(raw);
|
|
88
|
+
if (!m) return null;
|
|
89
|
+
return { tag: raw, version: Number(m[1]), path: versionDir(sourcePath, raw) };
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function writeCurrentPointer(sourcePath, versionTag) {
|
|
93
|
+
const p = currentPointerPath(sourcePath);
|
|
94
|
+
writeFileSync(p, `${versionTag}\n`, "utf8");
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Resolve the "live" wiki for a source, preferring the current-pointer,
|
|
98
|
+
// falling back to the highest existing version, or null if none exist.
|
|
99
|
+
export function resolveLiveWiki(sourcePath) {
|
|
100
|
+
const pointer = readCurrentPointer(sourcePath);
|
|
101
|
+
if (pointer && existsSync(pointer.path)) return pointer;
|
|
102
|
+
const versions = listVersions(sourcePath);
|
|
103
|
+
if (versions.length === 0) return null;
|
|
104
|
+
const latest = versions[versions.length - 1];
|
|
105
|
+
return { tag: latest.tag, version: latest.version, path: latest.path };
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export function nextVersionTag(sourcePath) {
|
|
109
|
+
const versions = listVersions(sourcePath);
|
|
110
|
+
const next = versions.length === 0 ? 1 : versions[versions.length - 1].version + 1;
|
|
111
|
+
return `v${next}`;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export function workDir(wikiPath) {
|
|
115
|
+
return join(wikiPath, ".work");
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export function shapeDir(wikiPath) {
|
|
119
|
+
return join(wikiPath, ".shape");
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// A directory is a wiki root iff:
|
|
123
|
+
// (a) it contains an `index.md`
|
|
124
|
+
// (b) that `index.md`'s frontmatter declares `generator: skill-llm-wiki/v<N>`
|
|
125
|
+
// (c) AND AT LEAST ONE of:
|
|
126
|
+
// - it has a `.llmwiki/git/HEAD` file (new default — private-git managed)
|
|
127
|
+
// - its name matches `*.llmwiki.v<N>` (legacy sibling-versioned mode)
|
|
128
|
+
// - it has a `.llmwiki.layout.yaml` file at its root (hosted mode)
|
|
129
|
+
//
|
|
130
|
+
// The generator marker (b) is the core safety check — it positively
|
|
131
|
+
// identifies a directory the skill itself built. Private-git presence
|
|
132
|
+
// (c-top) is the recognition the new default sibling layout `<source>.wiki/`
|
|
133
|
+
// relies on. Name matching (c-middle) keeps old sibling-versioned wikis
|
|
134
|
+
// recognisable during auto-migration. Layout contract presence (c-bottom)
|
|
135
|
+
// handles hosted-mode targets with arbitrary names like `./memory/`.
|
|
136
|
+
//
|
|
137
|
+
// Phase 1 only adds the private-git branch; Phase 2's migrate.mjs uses it
|
|
138
|
+
// to decide whether a target is already managed.
|
|
139
|
+
export function isWikiRoot(dirPath) {
|
|
140
|
+
const indexMd = join(dirPath, "index.md");
|
|
141
|
+
if (!existsSync(indexMd)) return false;
|
|
142
|
+
|
|
143
|
+
const base = basename(dirPath);
|
|
144
|
+
const hasPrivateGit = existsSync(join(dirPath, ".llmwiki", "git", "HEAD"));
|
|
145
|
+
const hasVersionedName = /\.llmwiki\.v\d+$/.test(base);
|
|
146
|
+
const hasLayoutContract = existsSync(join(dirPath, ".llmwiki.layout.yaml"));
|
|
147
|
+
|
|
148
|
+
// Must satisfy at least one structural recognition rule.
|
|
149
|
+
if (!hasPrivateGit && !hasVersionedName && !hasLayoutContract) return false;
|
|
150
|
+
|
|
151
|
+
// Frontmatter probe: cheap, bounded, no YAML parse required for the
|
|
152
|
+
// marker check — we just look for the line within the fence.
|
|
153
|
+
try {
|
|
154
|
+
const raw = readFileSync(indexMd, "utf8");
|
|
155
|
+
return hasGeneratorMarker(raw);
|
|
156
|
+
} catch {
|
|
157
|
+
return false;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Cheap existence check used by Phase 2's intent resolution to detect
|
|
162
|
+
// "this folder is already a skill-managed wiki" without parsing index.md.
|
|
163
|
+
// Unlike isWikiRoot, this returns true even when the directory has a
|
|
164
|
+
// private git repo but no index.md yet (partially-initialised state).
|
|
165
|
+
export function hasPrivateGit(dirPath) {
|
|
166
|
+
return existsSync(join(dirPath, ".llmwiki", "git", "HEAD"));
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Legacy format detection — a folder whose basename matches `*.llmwiki.v<N>`.
|
|
170
|
+
// Used by Phase 2's auto-migration to prompt the user before operating on
|
|
171
|
+
// a legacy wiki. Intentionally lightweight: no filesystem reads beyond the
|
|
172
|
+
// basename, no frontmatter probe.
|
|
173
|
+
export function isLegacyVersionedWiki(dirPath) {
|
|
174
|
+
return /\.llmwiki\.v\d+$/.test(basename(dirPath));
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function hasGeneratorMarker(raw) {
|
|
178
|
+
if (!raw.startsWith("---\n")) return false;
|
|
179
|
+
const end = raw.indexOf("\n---\n", 4);
|
|
180
|
+
if (end === -1) return false;
|
|
181
|
+
const fm = raw.slice(4, end);
|
|
182
|
+
// Accept `generator: skill-llm-wiki/v1` with or without quotes.
|
|
183
|
+
return /^\s*generator:\s*['"]?skill-llm-wiki\/v\d+['"]?\s*$/m.test(fm);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Walk upward from `startPath` to find the nearest wiki root. Returns the
|
|
187
|
+
// absolute path to the wiki root, or null if the starting path is not
|
|
188
|
+
// inside a skill-managed wiki.
|
|
189
|
+
export function findEnclosingWiki(startPath) {
|
|
190
|
+
let cur = resolve(startPath);
|
|
191
|
+
while (true) {
|
|
192
|
+
if (isWikiRoot(cur)) return cur;
|
|
193
|
+
const parent = dirname(cur);
|
|
194
|
+
if (parent === cur) return null;
|
|
195
|
+
cur = parent;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
// preflight.mjs — centralized runtime checks invoked from cli.mjs and
|
|
2
|
+
// any long-running phase that wants defence-in-depth against a broken
|
|
3
|
+
// environment.
|
|
4
|
+
//
|
|
5
|
+
// Exit codes (kept consistent with cli.mjs):
|
|
6
|
+
// 4 — Node too old / missing
|
|
7
|
+
// 5 — git missing / too old
|
|
8
|
+
// 6 — wiki present but corrupt (git fsck failed)
|
|
9
|
+
// 8 — required runtime dependency missing (DEPS_MISSING)
|
|
10
|
+
|
|
11
|
+
import { spawnSync } from "node:child_process";
|
|
12
|
+
import { existsSync } from "node:fs";
|
|
13
|
+
import { join } from "node:path";
|
|
14
|
+
import { createRequire } from "node:module";
|
|
15
|
+
import { BASE_ISOLATION_ENV, gitFsck } from "./git.mjs";
|
|
16
|
+
|
|
17
|
+
export const REQUIRED_NODE_MAJOR = 18;
|
|
18
|
+
export const REQUIRED_GIT_MAJOR = 2;
|
|
19
|
+
export const REQUIRED_GIT_MINOR = 25;
|
|
20
|
+
|
|
21
|
+
// The runtime dependencies that MUST be resolvable from `skillRoot` for
|
|
22
|
+
// any operation to run. Both are declared in package.json `dependencies`
|
|
23
|
+
// (not devDependencies) and are pulled in by `npm install` automatically.
|
|
24
|
+
// `gray-matter` is required to parse authored frontmatter in source files;
|
|
25
|
+
// `@xenova/transformers` powers the local Tier 1 embeddings model used
|
|
26
|
+
// during operator-convergence. The list is exported so tests can verify
|
|
27
|
+
// the contract without re-declaring the names.
|
|
28
|
+
export const REQUIRED_RUNTIME_DEPS = ["gray-matter", "@xenova/transformers"];
|
|
29
|
+
|
|
30
|
+
// Parse `vX.Y.Z` → { major, minor }. Returns null on malformed input.
|
|
31
|
+
function parseNodeVersion(raw) {
|
|
32
|
+
const m = /^v(\d+)\.(\d+)/.exec(raw);
|
|
33
|
+
if (!m) return null;
|
|
34
|
+
return { major: Number(m[1]), minor: Number(m[2]) };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Parse `git version 2.25.1 (Apple Git-133)` → { major, minor }.
|
|
38
|
+
function parseGitVersion(raw) {
|
|
39
|
+
const m = /git version (\d+)\.(\d+)/.exec(raw);
|
|
40
|
+
if (!m) return null;
|
|
41
|
+
return { major: Number(m[1]), minor: Number(m[2]) };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Returns { ok: true } on success, otherwise { ok: false, message, exitCode }.
|
|
45
|
+
export function preflightNode() {
|
|
46
|
+
const raw = (process && process.version) || "";
|
|
47
|
+
const v = parseNodeVersion(raw);
|
|
48
|
+
if (!v || v.major < REQUIRED_NODE_MAJOR) {
|
|
49
|
+
return {
|
|
50
|
+
ok: false,
|
|
51
|
+
exitCode: 4,
|
|
52
|
+
message:
|
|
53
|
+
`skill-llm-wiki: Node.js ${raw || "<unknown>"} is below the required ` +
|
|
54
|
+
`minimum (v${REQUIRED_NODE_MAJOR}.0.0).\n` +
|
|
55
|
+
"Please upgrade Node.js and retry. See SKILL.md " +
|
|
56
|
+
"'Preflight: verify Node.js is installed' for platform-specific " +
|
|
57
|
+
"install instructions.\n",
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
return { ok: true, version: v };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Returns { ok: true, version } when git is present and new enough, otherwise
|
|
64
|
+
// { ok: false, exitCode, message }. Spawns `git --version` with an isolated
|
|
65
|
+
// env so a user with GIT_* env vars set cannot redirect it. Reuses
|
|
66
|
+
// BASE_ISOLATION_ENV from git.mjs so this preflight probe and the main
|
|
67
|
+
// subprocess path stay in lockstep (including the Windows NUL override).
|
|
68
|
+
// The parent `process.env` is filtered to drop every `GIT_*` / `SSH_ASKPASS`
|
|
69
|
+
// key before the isolation block is applied, matching `buildGitEnv` (D3).
|
|
70
|
+
function preflightGitEnv() {
|
|
71
|
+
const out = {};
|
|
72
|
+
for (const [k, v] of Object.entries(process.env)) {
|
|
73
|
+
if (k.startsWith("GIT_")) continue;
|
|
74
|
+
if (k === "SSH_ASKPASS") continue;
|
|
75
|
+
out[k] = v;
|
|
76
|
+
}
|
|
77
|
+
return { ...out, ...BASE_ISOLATION_ENV };
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function preflightGit() {
|
|
81
|
+
const result = spawnSync("git", ["--version"], {
|
|
82
|
+
encoding: "utf8",
|
|
83
|
+
env: preflightGitEnv(),
|
|
84
|
+
});
|
|
85
|
+
if (result.error || result.status !== 0) {
|
|
86
|
+
return {
|
|
87
|
+
ok: false,
|
|
88
|
+
exitCode: 5,
|
|
89
|
+
message:
|
|
90
|
+
"skill-llm-wiki: `git` binary not found on PATH.\n" +
|
|
91
|
+
`Please install Git >= ${REQUIRED_GIT_MAJOR}.${REQUIRED_GIT_MINOR}.\n` +
|
|
92
|
+
"macOS: brew install git\n" +
|
|
93
|
+
"Debian: sudo apt-get install git\n" +
|
|
94
|
+
"Fedora: sudo dnf install git\n" +
|
|
95
|
+
"Windows: https://git-scm.com/download/win\n",
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
const raw = (result.stdout || "").trim();
|
|
99
|
+
const v = parseGitVersion(raw);
|
|
100
|
+
if (!v) {
|
|
101
|
+
return {
|
|
102
|
+
ok: false,
|
|
103
|
+
exitCode: 5,
|
|
104
|
+
message: `skill-llm-wiki: unable to parse git version from "${raw}"\n`,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
if (
|
|
108
|
+
v.major < REQUIRED_GIT_MAJOR ||
|
|
109
|
+
(v.major === REQUIRED_GIT_MAJOR && v.minor < REQUIRED_GIT_MINOR)
|
|
110
|
+
) {
|
|
111
|
+
return {
|
|
112
|
+
ok: false,
|
|
113
|
+
exitCode: 5,
|
|
114
|
+
message:
|
|
115
|
+
`skill-llm-wiki: Git ${v.major}.${v.minor} is below the required minimum ` +
|
|
116
|
+
`(${REQUIRED_GIT_MAJOR}.${REQUIRED_GIT_MINOR}).\n` +
|
|
117
|
+
"Please upgrade git and retry.\n",
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
return { ok: true, version: v };
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Optional check: when pointed at an existing wiki, run git fsck inside
|
|
124
|
+
// the private repo to catch object corruption early. If `.llmwiki/git/`
|
|
125
|
+
// does not exist, this is a no-op success — the wiki predates git tracking.
|
|
126
|
+
export function preflightWiki(wikiRoot) {
|
|
127
|
+
if (!existsSync(join(wikiRoot, ".llmwiki", "git", "HEAD"))) {
|
|
128
|
+
return { ok: true, reason: "no-private-git" };
|
|
129
|
+
}
|
|
130
|
+
const r = gitFsck(wikiRoot);
|
|
131
|
+
if (!r.ok) {
|
|
132
|
+
return {
|
|
133
|
+
ok: false,
|
|
134
|
+
exitCode: 6,
|
|
135
|
+
message:
|
|
136
|
+
`skill-llm-wiki: private git repo at ${wikiRoot}/.llmwiki/git/ ` +
|
|
137
|
+
`failed integrity check:\n${r.stderr.trim() || r.stdout.trim()}\n`,
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
return { ok: true };
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Verify that the skill's required runtime dependencies are installed and
|
|
144
|
+
// resolvable from `skillRoot`. Uses createRequire anchored at the skill's
|
|
145
|
+
// own package.json so the resolution honours the skill's local
|
|
146
|
+
// node_modules — not the caller's. This matters when the skill is loaded
|
|
147
|
+
// from a vendored install path or a non-standard layout where the parent
|
|
148
|
+
// process's resolution roots wouldn't see the deps.
|
|
149
|
+
//
|
|
150
|
+
// Tests can pass a custom `deps` array to inject a non-existent package
|
|
151
|
+
// name; production callers should always rely on the default
|
|
152
|
+
// REQUIRED_RUNTIME_DEPS list.
|
|
153
|
+
//
|
|
154
|
+
// Returns `{ ok: true, missing: [] }` on success or
|
|
155
|
+
// `{ ok: false, missing: [...], exitCode: 8, message }` on failure. The
|
|
156
|
+
// failure shape mirrors preflightNode/preflightGit so cli.mjs can treat
|
|
157
|
+
// it uniformly.
|
|
158
|
+
export function preflightDependencies(skillRoot, deps = REQUIRED_RUNTIME_DEPS) {
|
|
159
|
+
const missing = [];
|
|
160
|
+
let require;
|
|
161
|
+
try {
|
|
162
|
+
require = createRequire(join(skillRoot, "package.json"));
|
|
163
|
+
} catch (err) {
|
|
164
|
+
return {
|
|
165
|
+
ok: false,
|
|
166
|
+
exitCode: 8,
|
|
167
|
+
missing: deps.slice(),
|
|
168
|
+
message:
|
|
169
|
+
`skill-llm-wiki: cannot anchor dependency resolution at ${skillRoot}: ` +
|
|
170
|
+
`${err && err.message ? err.message : String(err)}\n`,
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
for (const dep of deps) {
|
|
174
|
+
try {
|
|
175
|
+
require.resolve(dep);
|
|
176
|
+
} catch {
|
|
177
|
+
missing.push(dep);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
if (missing.length === 0) {
|
|
181
|
+
return { ok: true, missing: [] };
|
|
182
|
+
}
|
|
183
|
+
const list = missing.map((d) => ` - ${d}`).join("\n");
|
|
184
|
+
return {
|
|
185
|
+
ok: false,
|
|
186
|
+
exitCode: 8,
|
|
187
|
+
missing,
|
|
188
|
+
message:
|
|
189
|
+
"skill-llm-wiki: required runtime dependencies are missing:\n" +
|
|
190
|
+
list +
|
|
191
|
+
"\n" +
|
|
192
|
+
"Run `npm install` in the skill directory to install them, or see " +
|
|
193
|
+
"guide/ux/preflight.md Case E.\n",
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// Run the mandatory Node + Git preflight and exit on failure. Intended to
|
|
198
|
+
// be called once at CLI startup. Split into a helper so phase-level code
|
|
199
|
+
// can call it mid-operation if it wants extra paranoia.
|
|
200
|
+
export function preflightOrExit({ requireGit = true } = {}) {
|
|
201
|
+
const n = preflightNode();
|
|
202
|
+
if (!n.ok) {
|
|
203
|
+
process.stderr.write(n.message);
|
|
204
|
+
process.exit(n.exitCode);
|
|
205
|
+
}
|
|
206
|
+
if (requireGit) {
|
|
207
|
+
const g = preflightGit();
|
|
208
|
+
if (!g.ok) {
|
|
209
|
+
process.stderr.write(g.message);
|
|
210
|
+
process.exit(g.exitCode);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|