@vimhead.dev/norn-cli 0.1.0-tip.35614826665.1 → 0.1.0-tip.35724621678.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/assets/README.md +41 -13
- package/assets/docs/README.md +16 -16
- package/assets/docs/agents.md +23 -15
- package/assets/docs/cli.md +10 -8
- package/assets/docs/composition.md +7 -7
- package/assets/docs/persistence.md +22 -22
- package/assets/docs/projects.md +6 -6
- package/assets/docs/recovery.md +10 -10
- package/assets/docs/schemas.md +42 -36
- package/assets/docs/workflows.md +39 -32
- package/assets/examples/agent-then-analysis/input.json +3 -3
- package/assets/examples/agent-then-analysis/norn.project.json +2 -2
- package/assets/examples/agent-then-analysis/plugin.ts +37 -9
- package/assets/examples/caller-owned-routing/README.md +6 -6
- package/assets/examples/caller-owned-routing/assessment.ts +18 -4
- package/assets/examples/caller-owned-routing/contracts.ts +7 -2
- package/assets/examples/caller-owned-routing/input.json +12 -12
- package/assets/examples/caller-owned-routing/norn.project.json +2 -2
- package/assets/examples/caller-owned-routing/revision.ts +7 -2
- package/assets/examples/caller-owned-routing/router.ts +14 -3
- package/assets/examples/caller-selected-continuation/caller.ts +38 -12
- package/assets/examples/caller-selected-continuation/input.json +7 -7
- package/assets/examples/caller-selected-continuation/norn.project.json +2 -2
- package/assets/examples/caller-selected-continuation/producer.ts +10 -3
- package/assets/examples/coordinating-multiple-agents/README.md +6 -6
- package/assets/examples/coordinating-multiple-agents/input.json +20 -8
- package/assets/examples/coordinating-multiple-agents/norn.project.json +2 -2
- package/assets/examples/coordinating-multiple-agents/plugin.ts +165 -46
- package/assets/examples/coordinating-multiple-agents/queue-tools.ts +30 -7
- package/assets/examples/coordinating-multiple-agents/work-queue.ts +211 -63
- package/assets/examples/getting-started/norn.project.json +2 -2
- package/assets/examples/getting-started/plugin.ts +33 -13
- package/assets/examples/minimal-workflow/norn.project.json +2 -2
- package/assets/examples/minimal-workflow/plugin.ts +12 -3
- package/assets/examples/shared-state/README.md +6 -6
- package/assets/examples/shared-state/input.json +1 -1
- package/assets/examples/shared-state/norn.project.json +2 -2
- package/assets/examples/shared-state/plugin.ts +44 -15
- package/assets/examples/shared-state/shared-state.ts +46 -13
- package/assets/examples/shared-state/state-tools.ts +68 -16
- package/assets/examples/worktree-development-loop/README.md +12 -12
- package/assets/examples/worktree-development-loop/norn.project.json +7 -7
- package/assets/examples/worktree-development-loop/plugin.ts +7 -1
- package/assets/examples/worktree-development-loop/scope.ts +4 -1
- package/assets/examples/worktree-development-loop/shared/commands.ts +6 -2
- package/assets/examples/worktree-development-loop/workflows/development-loop/execute.ts +18 -4
- package/assets/examples/worktree-development-loop/workflows/development-loop/index.ts +4 -1
- package/assets/examples/worktree-development-loop/workflows/development-loop/repository.ts +11 -1
- package/assets/examples/worktree-development-loop/workflows/development-loop/schema.ts +6 -2
- package/assets/examples/worktree-development-loop/workflows/implementation/execute.ts +40 -10
- package/assets/examples/worktree-development-loop/workflows/implementation/index.ts +5 -1
- package/assets/examples/worktree-development-loop/workflows/planning/execute.ts +1 -1
- package/assets/examples/worktree-development-loop/workflows/planning/index.ts +5 -1
- package/assets/examples/worktree-development-loop/workflows/review/execute.ts +33 -9
- package/assets/examples/worktree-development-loop/workflows/review-router/execute.ts +51 -8
- package/assets/package.json +1 -1
- package/assets/packages/cli/src/build-info.ts +4 -1
- package/assets/packages/cli/src/bun/cli.ts +21 -5
- package/assets/packages/cli/src/cli.ts +585 -170
- package/assets/packages/cli/src/client.ts +194 -36
- package/assets/packages/cli/src/documentation.ts +135 -30
- package/assets/packages/cli/src/generated-build-info.ts +2 -2
- package/assets/packages/cli/src/internal/agent-directory.ts +7 -2
- package/assets/packages/cli/src/internal/agent-response-tool.ts +57 -15
- package/assets/packages/cli/src/internal/agents.ts +248 -60
- package/assets/packages/cli/src/internal/commands.ts +82 -20
- package/assets/packages/cli/src/internal/documentation-bundle.ts +44 -13
- package/assets/packages/cli/src/internal/engine.ts +396 -87
- package/assets/packages/cli/src/internal/errors.ts +13 -5
- package/assets/packages/cli/src/internal/execution-context.ts +20 -12
- package/assets/packages/cli/src/internal/file-coordinator.ts +95 -26
- package/assets/packages/cli/src/internal/file-names.ts +4 -1
- package/assets/packages/cli/src/internal/launch-request.ts +74 -22
- package/assets/packages/cli/src/internal/logs.ts +10 -2
- package/assets/packages/cli/src/internal/metrics.ts +247 -61
- package/assets/packages/cli/src/internal/pi-assets.ts +105 -23
- package/assets/packages/cli/src/internal/run-lease.ts +72 -22
- package/assets/packages/cli/src/internal/run-log.ts +49 -20
- package/assets/packages/cli/src/internal/run-names.ts +120 -12
- package/assets/packages/cli/src/internal/run-state.ts +306 -71
- package/assets/packages/cli/src/internal/run-store.ts +216 -53
- package/assets/packages/cli/src/internal/usage.ts +23 -7
- package/assets/packages/cli/src/internal/worker-directory.ts +9 -3
- package/assets/packages/cli/src/internal/workflow-registry.ts +281 -70
- package/assets/packages/cli/src/internal/working-directory.ts +2 -1
- package/assets/packages/cli/src/workflow-loader.ts +341 -74
- package/assets/packages/core/src/atomic-files.ts +13 -4
- package/assets/packages/core/src/workflow-transition.ts +13 -2
- package/assets/packages/sdk/src/api.ts +248 -80
- package/assets/packages/sdk/src/index.ts +6 -1
- package/assets/packages/sdk/src/schema.ts +75 -21
- package/assets/tests/workflow-ref.test.ts +242 -57
- package/bin/norn.mjs +13 -4
- package/dist/cli.js +344 -90
- package/dist/client.js +100 -22
- package/dist/documentation.js +102 -25
- package/dist/generated-build-info.d.ts +2 -2
- package/dist/generated-build-info.js +2 -2
- package/dist/internal/agent-directory.js +3 -1
- package/dist/internal/agent-response-tool.js +42 -11
- package/dist/internal/agents.js +149 -37
- package/dist/internal/commands.js +65 -16
- package/dist/internal/documentation-bundle.js +23 -11
- package/dist/internal/engine.js +304 -66
- package/dist/internal/errors.js +4 -2
- package/dist/internal/execution-context.js +2 -1
- package/dist/internal/file-coordinator.js +59 -19
- package/dist/internal/launch-request.js +32 -14
- package/dist/internal/metrics.js +138 -38
- package/dist/internal/pi-assets.js +70 -19
- package/dist/internal/run-lease.js +29 -9
- package/dist/internal/run-log.js +24 -12
- package/dist/internal/run-state.js +172 -48
- package/dist/internal/run-store.js +157 -45
- package/dist/internal/worker-directory.js +6 -2
- package/dist/internal/workflow-registry.js +123 -32
- package/dist/internal/working-directory.js +2 -1
- package/dist/workflow-loader.js +198 -41
- package/package.json +2 -2
|
@@ -27,13 +27,22 @@ export type NornClient = {
|
|
|
27
27
|
inspect(): Promise<NornProjectInspection>;
|
|
28
28
|
};
|
|
29
29
|
readonly workflows: {
|
|
30
|
-
list(options?: {
|
|
30
|
+
list(options?: {
|
|
31
|
+
readonly all?: boolean;
|
|
32
|
+
}): Promise<NornWorkflowCatalogInfo>;
|
|
31
33
|
inspect(workflowId: string): Promise<NornWorkflowInspection>;
|
|
32
34
|
entries(): Promise<readonly NornRegisteredWorkflow[]>;
|
|
33
35
|
};
|
|
34
36
|
readonly runs: {
|
|
35
|
-
start(input: {
|
|
36
|
-
|
|
37
|
+
start(input: {
|
|
38
|
+
readonly workflowId: string;
|
|
39
|
+
readonly args: unknown;
|
|
40
|
+
readonly configOverride?: unknown;
|
|
41
|
+
}): Promise<NornRunInfo>;
|
|
42
|
+
resume(input: {
|
|
43
|
+
readonly run: string;
|
|
44
|
+
readonly args?: unknown;
|
|
45
|
+
}): Promise<NornRunInfo>;
|
|
37
46
|
wait(run: string): Promise<NornRunInfo>;
|
|
38
47
|
list(): Promise<NornRunInfo[]>;
|
|
39
48
|
inspect(run: string): Promise<NornRunInfo>;
|
|
@@ -43,7 +52,10 @@ export type NornClient = {
|
|
|
43
52
|
stop(run: string): Promise<NornRunInfo>;
|
|
44
53
|
kill(run: string): Promise<NornRunInfo>;
|
|
45
54
|
delete(run: string): Promise<DeletedNornRunInfo>;
|
|
46
|
-
logs(
|
|
55
|
+
logs(
|
|
56
|
+
run: string,
|
|
57
|
+
options?: { readonly follow?: boolean },
|
|
58
|
+
): AsyncIterable<Record<string, unknown>>;
|
|
47
59
|
};
|
|
48
60
|
};
|
|
49
61
|
|
|
@@ -52,26 +64,118 @@ export function createNornClient(input: NornClientInput = {}): NornClient {
|
|
|
52
64
|
const catalog = new NornWorkflowCatalog(input.spawnCwd ?? process.cwd());
|
|
53
65
|
const client: NornClient = {
|
|
54
66
|
project: {
|
|
55
|
-
inspect: async () =>
|
|
67
|
+
inspect: async () =>
|
|
68
|
+
processRunner.readJson<NornProjectInspection>(["project", "inspect"]),
|
|
56
69
|
},
|
|
57
70
|
workflows: {
|
|
58
|
-
list: async (options) =>
|
|
59
|
-
|
|
71
|
+
list: async (options) =>
|
|
72
|
+
processRunner.readJson<NornWorkflowCatalogInfo>([
|
|
73
|
+
"workflows",
|
|
74
|
+
"list",
|
|
75
|
+
...(options?.all ? ["--all"] : []),
|
|
76
|
+
]),
|
|
77
|
+
inspect: async (workflowId) =>
|
|
78
|
+
processRunner.readJson<NornWorkflowInspection>([
|
|
79
|
+
"workflows",
|
|
80
|
+
"inspect",
|
|
81
|
+
workflowId,
|
|
82
|
+
]),
|
|
60
83
|
entries: async () => (await catalog.load()).workflows,
|
|
61
84
|
},
|
|
62
85
|
runs: {
|
|
63
|
-
start: async (startInput) =>
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
86
|
+
start: async (startInput) =>
|
|
87
|
+
(
|
|
88
|
+
await processRunner.readJson<{ run: NornRunInfo }>(
|
|
89
|
+
["runs", "start", startInput.workflowId],
|
|
90
|
+
JSON.stringify(runStartInput(startInput)),
|
|
91
|
+
)
|
|
92
|
+
).run,
|
|
93
|
+
resume: async (resumeInput) =>
|
|
94
|
+
(
|
|
95
|
+
await processRunner.readJson<{ run: NornRunInfo }>(
|
|
96
|
+
["runs", "resume", resumeInput.run],
|
|
97
|
+
runResumeStdin(resumeInput),
|
|
98
|
+
)
|
|
99
|
+
).run,
|
|
100
|
+
wait: async (run) =>
|
|
101
|
+
(
|
|
102
|
+
await processRunner.readJson<{ run: NornRunInfo }>([
|
|
103
|
+
"runs",
|
|
104
|
+
"wait",
|
|
105
|
+
run,
|
|
106
|
+
])
|
|
107
|
+
).run,
|
|
108
|
+
list: async () =>
|
|
109
|
+
(
|
|
110
|
+
await processRunner.readJson<{ runs: NornRunInfo[] }>([
|
|
111
|
+
"runs",
|
|
112
|
+
"list",
|
|
113
|
+
])
|
|
114
|
+
).runs,
|
|
115
|
+
inspect: async (run) =>
|
|
116
|
+
(
|
|
117
|
+
await processRunner.readJson<{ run: NornRunInfo }>([
|
|
118
|
+
"runs",
|
|
119
|
+
"inspect",
|
|
120
|
+
run,
|
|
121
|
+
])
|
|
122
|
+
).run,
|
|
123
|
+
checkpoints: async (run) =>
|
|
124
|
+
(
|
|
125
|
+
await processRunner.readJson<{ checkpoints: NornRunCheckpoint[] }>([
|
|
126
|
+
"runs",
|
|
127
|
+
"checkpoints",
|
|
128
|
+
run,
|
|
129
|
+
])
|
|
130
|
+
).checkpoints,
|
|
131
|
+
metrics: async (run) =>
|
|
132
|
+
(
|
|
133
|
+
await processRunner.readJson<{ metrics: NornRunMetrics }>([
|
|
134
|
+
"runs",
|
|
135
|
+
"metrics",
|
|
136
|
+
run,
|
|
137
|
+
])
|
|
138
|
+
).metrics,
|
|
139
|
+
rollback: async (run, checkpointId) =>
|
|
140
|
+
(
|
|
141
|
+
await processRunner.readJson<{ run: NornRunInfo }>([
|
|
142
|
+
"runs",
|
|
143
|
+
"rollback",
|
|
144
|
+
run,
|
|
145
|
+
checkpointId,
|
|
146
|
+
])
|
|
147
|
+
).run,
|
|
148
|
+
stop: async (run) =>
|
|
149
|
+
(
|
|
150
|
+
await processRunner.readJson<{ run: NornRunInfo }>([
|
|
151
|
+
"runs",
|
|
152
|
+
"stop",
|
|
153
|
+
run,
|
|
154
|
+
])
|
|
155
|
+
).run,
|
|
156
|
+
kill: async (run) =>
|
|
157
|
+
(
|
|
158
|
+
await processRunner.readJson<{ run: NornRunInfo }>([
|
|
159
|
+
"runs",
|
|
160
|
+
"kill",
|
|
161
|
+
run,
|
|
162
|
+
])
|
|
163
|
+
).run,
|
|
164
|
+
delete: async (run) =>
|
|
165
|
+
(
|
|
166
|
+
await processRunner.readJson<{ deleted: DeletedNornRunInfo }>([
|
|
167
|
+
"runs",
|
|
168
|
+
"delete",
|
|
169
|
+
run,
|
|
170
|
+
])
|
|
171
|
+
).deleted,
|
|
172
|
+
logs: (run, options) =>
|
|
173
|
+
processRunner.readJsonLines([
|
|
174
|
+
"runs",
|
|
175
|
+
"logs",
|
|
176
|
+
run,
|
|
177
|
+
...(options?.follow ? ["--follow"] : []),
|
|
178
|
+
]),
|
|
75
179
|
},
|
|
76
180
|
};
|
|
77
181
|
return client;
|
|
@@ -83,7 +187,7 @@ class NornWorkflowCatalog {
|
|
|
83
187
|
constructor(private readonly cwd: string) {}
|
|
84
188
|
|
|
85
189
|
load(): ReturnType<typeof loadNornProject> {
|
|
86
|
-
this.loaded ??= loadNornProject(this.cwd).catch(error => {
|
|
190
|
+
this.loaded ??= loadNornProject(this.cwd).catch((error) => {
|
|
87
191
|
this.loaded = undefined;
|
|
88
192
|
throw error;
|
|
89
193
|
});
|
|
@@ -95,22 +199,46 @@ class NornProcessRunner {
|
|
|
95
199
|
private readonly executablePath: string;
|
|
96
200
|
|
|
97
201
|
constructor(private readonly input: NornClientInput) {
|
|
98
|
-
this.executablePath =
|
|
202
|
+
this.executablePath =
|
|
203
|
+
input.executablePath ??
|
|
204
|
+
fileURLToPath(new URL("../bin/norn.mjs", import.meta.url));
|
|
99
205
|
}
|
|
100
206
|
|
|
101
207
|
async readJson<T>(args: readonly string[], stdin?: string): Promise<T> {
|
|
102
208
|
const result = await this.spawn(args, stdin);
|
|
103
|
-
const parsed = JSON.parse(result.stdout) as
|
|
209
|
+
const parsed = JSON.parse(result.stdout) as
|
|
210
|
+
| T
|
|
211
|
+
| {
|
|
212
|
+
error?: {
|
|
213
|
+
code?: string;
|
|
214
|
+
message?: string;
|
|
215
|
+
diagnostics?: NornWorkflowDiagnostic[];
|
|
216
|
+
};
|
|
217
|
+
};
|
|
104
218
|
if (parsed && typeof parsed === "object" && "error" in parsed) {
|
|
105
|
-
if (
|
|
219
|
+
if (
|
|
220
|
+
parsed.error?.code === "NORN_PROJECT_INVALID" &&
|
|
221
|
+
Array.isArray(parsed.error.diagnostics)
|
|
222
|
+
)
|
|
223
|
+
throw new NornProjectLoadError({
|
|
224
|
+
diagnostics: parsed.error.diagnostics,
|
|
225
|
+
});
|
|
106
226
|
throw new Error(parsed.error?.message ?? "Norn command failed");
|
|
107
227
|
}
|
|
108
|
-
if (result.exitCode !== 0)
|
|
228
|
+
if (result.exitCode !== 0)
|
|
229
|
+
throw new Error(
|
|
230
|
+
result.stderr || `Norn exited with code ${result.exitCode}`,
|
|
231
|
+
);
|
|
109
232
|
return parsed as T;
|
|
110
233
|
}
|
|
111
234
|
|
|
112
|
-
async *readJsonLines(
|
|
113
|
-
|
|
235
|
+
async *readJsonLines(
|
|
236
|
+
args: readonly string[],
|
|
237
|
+
): AsyncIterable<Record<string, unknown>> {
|
|
238
|
+
const child = spawn(process.execPath, [this.executablePath, ...args], {
|
|
239
|
+
cwd: this.input.spawnCwd,
|
|
240
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
241
|
+
});
|
|
114
242
|
let stderr = "";
|
|
115
243
|
child.stderr?.on("data", (chunk) => {
|
|
116
244
|
stderr += chunk.toString();
|
|
@@ -119,16 +247,35 @@ class NornProcessRunner {
|
|
|
119
247
|
for await (const line of lines) {
|
|
120
248
|
if (line.trim().length === 0) continue;
|
|
121
249
|
const parsed = JSON.parse(line) as Record<string, unknown>;
|
|
122
|
-
if (parsed.error)
|
|
250
|
+
if (parsed.error)
|
|
251
|
+
throw new Error(
|
|
252
|
+
String(
|
|
253
|
+
(parsed.error as { message?: unknown }).message ??
|
|
254
|
+
"Norn stream failed",
|
|
255
|
+
),
|
|
256
|
+
);
|
|
123
257
|
yield parsed;
|
|
124
258
|
}
|
|
125
|
-
const exitCode = await new Promise<number | null>((resolvePromise) =>
|
|
126
|
-
|
|
259
|
+
const exitCode = await new Promise<number | null>((resolvePromise) =>
|
|
260
|
+
child.on("close", resolvePromise),
|
|
261
|
+
);
|
|
262
|
+
if (exitCode !== 0)
|
|
263
|
+
throw new Error(stderr || `Norn exited with code ${exitCode}`);
|
|
127
264
|
}
|
|
128
265
|
|
|
129
|
-
private async spawn(
|
|
266
|
+
private async spawn(
|
|
267
|
+
args: readonly string[],
|
|
268
|
+
stdin?: string,
|
|
269
|
+
): Promise<{
|
|
270
|
+
readonly stdout: string;
|
|
271
|
+
readonly stderr: string;
|
|
272
|
+
readonly exitCode: number | null;
|
|
273
|
+
}> {
|
|
130
274
|
return new Promise((resolvePromise, reject) => {
|
|
131
|
-
const child = spawn(process.execPath, [this.executablePath, ...args], {
|
|
275
|
+
const child = spawn(process.execPath, [this.executablePath, ...args], {
|
|
276
|
+
cwd: this.input.spawnCwd,
|
|
277
|
+
stdio: [stdin === undefined ? "ignore" : "pipe", "pipe", "pipe"],
|
|
278
|
+
});
|
|
132
279
|
let stdout = "";
|
|
133
280
|
let stderr = "";
|
|
134
281
|
child.stdout?.on("data", (chunk) => {
|
|
@@ -139,15 +286,26 @@ class NornProcessRunner {
|
|
|
139
286
|
});
|
|
140
287
|
child.on("error", reject);
|
|
141
288
|
if (stdin !== undefined) child.stdin?.end(stdin);
|
|
142
|
-
child.on("close", (exitCode) =>
|
|
289
|
+
child.on("close", (exitCode) =>
|
|
290
|
+
resolvePromise({ stdout, stderr, exitCode }),
|
|
291
|
+
);
|
|
143
292
|
});
|
|
144
293
|
}
|
|
145
294
|
}
|
|
146
295
|
|
|
147
|
-
function runStartInput(input: {
|
|
148
|
-
|
|
296
|
+
function runStartInput(input: {
|
|
297
|
+
readonly args: unknown;
|
|
298
|
+
readonly configOverride?: unknown;
|
|
299
|
+
}): { readonly args: unknown; readonly config?: unknown } {
|
|
300
|
+
return input.configOverride === undefined
|
|
301
|
+
? { args: input.args }
|
|
302
|
+
: { args: input.args, config: input.configOverride };
|
|
149
303
|
}
|
|
150
304
|
|
|
151
|
-
function runResumeStdin(input: {
|
|
152
|
-
|
|
305
|
+
function runResumeStdin(input: {
|
|
306
|
+
readonly args?: unknown;
|
|
307
|
+
}): string | undefined {
|
|
308
|
+
return input.args === undefined
|
|
309
|
+
? undefined
|
|
310
|
+
: JSON.stringify({ args: input.args });
|
|
153
311
|
}
|
|
@@ -1,8 +1,22 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
lstat,
|
|
4
|
+
mkdir,
|
|
5
|
+
mkdtemp,
|
|
6
|
+
readFile,
|
|
7
|
+
readdir,
|
|
8
|
+
rename,
|
|
9
|
+
rm,
|
|
10
|
+
writeFile,
|
|
11
|
+
} from "node:fs/promises";
|
|
3
12
|
import { join, resolve } from "node:path";
|
|
4
13
|
import type { NornBuildInfo } from "./build-info.ts";
|
|
5
|
-
import {
|
|
14
|
+
import {
|
|
15
|
+
DOCUMENTATION_PATHS,
|
|
16
|
+
hashDocumentationBundle,
|
|
17
|
+
validateDocumentationBundle,
|
|
18
|
+
type NornDocumentationBundle,
|
|
19
|
+
} from "./internal/documentation-bundle.ts";
|
|
6
20
|
import { isNodeError } from "@vimhead.dev/norn-core/errors";
|
|
7
21
|
|
|
8
22
|
export { renderNornDocumentationIntro } from "./documentation-intro.ts";
|
|
@@ -16,7 +30,13 @@ export type NornDocumentationLocation = {
|
|
|
16
30
|
readonly version: string;
|
|
17
31
|
readonly commit: string | null;
|
|
18
32
|
readonly assetDigest: string | null;
|
|
19
|
-
readonly paths: {
|
|
33
|
+
readonly paths: {
|
|
34
|
+
readonly root: string;
|
|
35
|
+
readonly readme: string;
|
|
36
|
+
readonly index: string;
|
|
37
|
+
readonly docs: string;
|
|
38
|
+
readonly examples: string;
|
|
39
|
+
};
|
|
20
40
|
};
|
|
21
41
|
|
|
22
42
|
export function resolveDocumentationCacheRoot(input: {
|
|
@@ -26,10 +46,20 @@ export function resolveDocumentationCacheRoot(input: {
|
|
|
26
46
|
}): string {
|
|
27
47
|
const override = input.environment.NORN_DOCS_CACHE_DIR;
|
|
28
48
|
if (override) return resolve(override);
|
|
29
|
-
if (input.platform === "win32")
|
|
30
|
-
|
|
49
|
+
if (input.platform === "win32")
|
|
50
|
+
return join(
|
|
51
|
+
input.environment.LOCALAPPDATA || join(input.home, "AppData", "Local"),
|
|
52
|
+
"norn",
|
|
53
|
+
"docs",
|
|
54
|
+
);
|
|
55
|
+
if (input.platform === "darwin")
|
|
56
|
+
return join(input.home, "Library", "Caches", "norn", "docs");
|
|
31
57
|
const xdgCache = input.environment.XDG_CACHE_HOME;
|
|
32
|
-
return join(
|
|
58
|
+
return join(
|
|
59
|
+
xdgCache?.startsWith("/") ? xdgCache : join(input.home, ".cache"),
|
|
60
|
+
"norn",
|
|
61
|
+
"docs",
|
|
62
|
+
);
|
|
33
63
|
}
|
|
34
64
|
|
|
35
65
|
export async function resolveNornDocumentation(input: {
|
|
@@ -42,18 +72,43 @@ export async function resolveNornDocumentation(input: {
|
|
|
42
72
|
let assetDigest: string | null = null;
|
|
43
73
|
if (input.source.kind === "local") {
|
|
44
74
|
root = resolve(input.source.root);
|
|
45
|
-
const packageInfo = JSON.parse(
|
|
46
|
-
|
|
75
|
+
const packageInfo = JSON.parse(
|
|
76
|
+
await readFile(join(root, "package.json"), "utf8"),
|
|
77
|
+
) as { version?: unknown };
|
|
78
|
+
if (
|
|
79
|
+
typeof packageInfo.version !== "string" ||
|
|
80
|
+
packageInfo.version.length === 0
|
|
81
|
+
)
|
|
82
|
+
throw new Error("Invalid documentation package version");
|
|
47
83
|
version = packageInfo.version;
|
|
48
|
-
if (version !== input.build.version)
|
|
84
|
+
if (version !== input.build.version)
|
|
85
|
+
throw new Error(
|
|
86
|
+
"Local documentation version does not match the runtime; rebuild or reinstall this CLI package",
|
|
87
|
+
);
|
|
49
88
|
await validateLocalDocumentation(root);
|
|
50
89
|
} else {
|
|
51
90
|
validateDocumentationBundle(input.source.bundle);
|
|
52
91
|
version = input.source.bundle.version;
|
|
53
|
-
if (version !== input.build.version)
|
|
92
|
+
if (version !== input.build.version)
|
|
93
|
+
throw new Error(
|
|
94
|
+
"Embedded documentation version does not match the binary build",
|
|
95
|
+
);
|
|
54
96
|
assetDigest = hashDocumentationBundle(input.source.bundle);
|
|
55
|
-
const cacheKey = createHash("sha256")
|
|
56
|
-
|
|
97
|
+
const cacheKey = createHash("sha256")
|
|
98
|
+
.update(
|
|
99
|
+
JSON.stringify({
|
|
100
|
+
format: 1,
|
|
101
|
+
assetDigest,
|
|
102
|
+
version,
|
|
103
|
+
commit: input.build.commit,
|
|
104
|
+
}),
|
|
105
|
+
)
|
|
106
|
+
.digest("hex");
|
|
107
|
+
root = await new NornDocumentationCache({
|
|
108
|
+
cacheRoot: resolve(input.cacheRoot),
|
|
109
|
+
cacheKey,
|
|
110
|
+
bundle: input.source.bundle,
|
|
111
|
+
}).materialize();
|
|
57
112
|
}
|
|
58
113
|
return {
|
|
59
114
|
storage: input.source.kind === "local" ? "installation" : "cache",
|
|
@@ -73,8 +128,10 @@ export async function resolveNornDocumentation(input: {
|
|
|
73
128
|
async function validateLocalDocumentation(root: string): Promise<void> {
|
|
74
129
|
for (const [key, path] of Object.entries(DOCUMENTATION_PATHS)) {
|
|
75
130
|
const stat = await lstat(join(root, path));
|
|
76
|
-
const isExpectedType =
|
|
77
|
-
|
|
131
|
+
const isExpectedType =
|
|
132
|
+
key === "docs" || key === "examples" ? stat.isDirectory() : stat.isFile();
|
|
133
|
+
if (!isExpectedType)
|
|
134
|
+
throw new Error(`Invalid local documentation path: ${join(root, path)}`);
|
|
78
135
|
}
|
|
79
136
|
}
|
|
80
137
|
|
|
@@ -82,30 +139,55 @@ class NornDocumentationCache {
|
|
|
82
139
|
private readonly root: string;
|
|
83
140
|
private readonly files: ReadonlyMap<string, string>;
|
|
84
141
|
|
|
85
|
-
constructor(
|
|
142
|
+
constructor(
|
|
143
|
+
private readonly input: {
|
|
144
|
+
readonly cacheRoot: string;
|
|
145
|
+
readonly cacheKey: string;
|
|
146
|
+
readonly bundle: NornDocumentationBundle;
|
|
147
|
+
},
|
|
148
|
+
) {
|
|
86
149
|
this.root = join(input.cacheRoot, `v1-${input.cacheKey}`);
|
|
87
|
-
this.files = new Map(
|
|
150
|
+
this.files = new Map(
|
|
151
|
+
input.bundle.files.map((file) => [file.path, file.content]),
|
|
152
|
+
);
|
|
88
153
|
}
|
|
89
154
|
|
|
90
155
|
async materialize(): Promise<string> {
|
|
91
156
|
await mkdir(this.input.cacheRoot, { recursive: true, mode: 0o700 });
|
|
92
157
|
const cacheStat = await lstat(this.input.cacheRoot);
|
|
93
|
-
if (!cacheStat.isDirectory() || cacheStat.isSymbolicLink())
|
|
158
|
+
if (!cacheStat.isDirectory() || cacheStat.isSymbolicLink())
|
|
159
|
+
throw new Error(
|
|
160
|
+
`Documentation cache root must be a real directory: ${this.input.cacheRoot}`,
|
|
161
|
+
);
|
|
94
162
|
if (await this.validateExistingCache()) return this.root;
|
|
95
163
|
const staging = await mkdtemp(join(this.input.cacheRoot, ".extract-"));
|
|
96
164
|
try {
|
|
97
165
|
for (const file of this.input.bundle.files) {
|
|
98
166
|
const parts = file.path.split("/");
|
|
99
|
-
await mkdir(join(staging, ...parts.slice(0, -1)), {
|
|
100
|
-
|
|
167
|
+
await mkdir(join(staging, ...parts.slice(0, -1)), {
|
|
168
|
+
recursive: true,
|
|
169
|
+
mode: 0o700,
|
|
170
|
+
});
|
|
171
|
+
await writeFile(join(staging, ...parts), file.content, {
|
|
172
|
+
encoding: "utf8",
|
|
173
|
+
flag: "wx",
|
|
174
|
+
mode: 0o600,
|
|
175
|
+
});
|
|
101
176
|
}
|
|
102
177
|
try {
|
|
103
178
|
await rename(staging, this.root);
|
|
104
179
|
} catch (error) {
|
|
105
|
-
if (
|
|
106
|
-
|
|
180
|
+
if (
|
|
181
|
+
!isNodeError(error) ||
|
|
182
|
+
!["EEXIST", "ENOTEMPTY", "EPERM", "EACCES"].includes(error.code ?? "")
|
|
183
|
+
)
|
|
184
|
+
throw error;
|
|
185
|
+
if (!(await this.validateExistingCache())) throw error;
|
|
107
186
|
}
|
|
108
|
-
if (!await this.validateExistingCache())
|
|
187
|
+
if (!(await this.validateExistingCache()))
|
|
188
|
+
throw new Error(
|
|
189
|
+
`Documentation cache disappeared after extraction: ${this.root}`,
|
|
190
|
+
);
|
|
109
191
|
return this.root;
|
|
110
192
|
} finally {
|
|
111
193
|
await rm(staging, { recursive: true, force: true });
|
|
@@ -120,22 +202,43 @@ class NornDocumentationCache {
|
|
|
120
202
|
if (isNodeError(error) && error.code === "ENOENT") return false;
|
|
121
203
|
throw error;
|
|
122
204
|
}
|
|
123
|
-
if (!stat.isDirectory() || stat.isSymbolicLink())
|
|
205
|
+
if (!stat.isDirectory() || stat.isSymbolicLink())
|
|
206
|
+
throw this.invalidCacheError();
|
|
124
207
|
const seenFiles = new Set<string>();
|
|
125
|
-
await this.validateDirectory({
|
|
208
|
+
await this.validateDirectory({
|
|
209
|
+
directory: this.root,
|
|
210
|
+
prefix: "",
|
|
211
|
+
seenFiles,
|
|
212
|
+
});
|
|
126
213
|
if (seenFiles.size !== this.files.size) throw this.invalidCacheError();
|
|
127
214
|
return true;
|
|
128
215
|
}
|
|
129
216
|
|
|
130
|
-
private async validateDirectory(input: {
|
|
131
|
-
|
|
217
|
+
private async validateDirectory(input: {
|
|
218
|
+
readonly directory: string;
|
|
219
|
+
readonly prefix: string;
|
|
220
|
+
readonly seenFiles: Set<string>;
|
|
221
|
+
}): Promise<void> {
|
|
222
|
+
for (const entry of await readdir(input.directory, {
|
|
223
|
+
withFileTypes: true,
|
|
224
|
+
})) {
|
|
132
225
|
const path = input.prefix ? `${input.prefix}/${entry.name}` : entry.name;
|
|
133
226
|
const absolutePath = join(input.directory, entry.name);
|
|
134
227
|
if (entry.isDirectory()) {
|
|
135
|
-
if (![...this.files.keys()].some(file => file.startsWith(`${path}/`)))
|
|
136
|
-
|
|
228
|
+
if (![...this.files.keys()].some((file) => file.startsWith(`${path}/`)))
|
|
229
|
+
throw this.invalidCacheError();
|
|
230
|
+
await this.validateDirectory({
|
|
231
|
+
directory: absolutePath,
|
|
232
|
+
prefix: path,
|
|
233
|
+
seenFiles: input.seenFiles,
|
|
234
|
+
});
|
|
137
235
|
} else if (entry.isFile() && this.files.has(path)) {
|
|
138
|
-
if (
|
|
236
|
+
if (
|
|
237
|
+
!(await readFile(absolutePath)).equals(
|
|
238
|
+
Buffer.from(this.files.get(path)!, "utf8"),
|
|
239
|
+
)
|
|
240
|
+
)
|
|
241
|
+
throw this.invalidCacheError();
|
|
139
242
|
input.seenFiles.add(path);
|
|
140
243
|
} else {
|
|
141
244
|
throw this.invalidCacheError();
|
|
@@ -144,6 +247,8 @@ class NornDocumentationCache {
|
|
|
144
247
|
}
|
|
145
248
|
|
|
146
249
|
private invalidCacheError(): Error {
|
|
147
|
-
return new Error(
|
|
250
|
+
return new Error(
|
|
251
|
+
`Documentation cache is incomplete or modified: ${this.root}. Remove this cache entry and retry; copy examples elsewhere before editing them.`,
|
|
252
|
+
);
|
|
148
253
|
}
|
|
149
254
|
}
|
|
@@ -2,8 +2,8 @@ import type { NornBuildInfo } from "./build-info.ts";
|
|
|
2
2
|
|
|
3
3
|
export const NORN_GENERATED_BUILD_INFO = {
|
|
4
4
|
"kind": "npm-registry",
|
|
5
|
-
"version": "0.1.0-tip.
|
|
6
|
-
"commit": "
|
|
5
|
+
"version": "0.1.0-tip.35724621678.1",
|
|
6
|
+
"commit": "51ea925604755506d4cde05b9224eacb48189ffd",
|
|
7
7
|
"packageSpec": "@vimhead.dev/norn-cli@tip",
|
|
8
8
|
"upgrade": {
|
|
9
9
|
"supported": false,
|
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
import { join, resolve } from "node:path";
|
|
2
2
|
|
|
3
|
-
export function resolveNornAgentDirectory(input: {
|
|
4
|
-
|
|
3
|
+
export function resolveNornAgentDirectory(input: {
|
|
4
|
+
home: string;
|
|
5
|
+
environment: NodeJS.ProcessEnv;
|
|
6
|
+
}): string {
|
|
7
|
+
return resolve(
|
|
8
|
+
input.environment.NORN_AGENT_DIR || join(input.home, ".norn", "agent"),
|
|
9
|
+
);
|
|
5
10
|
}
|