@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.
Files changed (119) hide show
  1. package/assets/README.md +41 -13
  2. package/assets/docs/README.md +16 -16
  3. package/assets/docs/agents.md +23 -15
  4. package/assets/docs/cli.md +10 -8
  5. package/assets/docs/composition.md +7 -7
  6. package/assets/docs/persistence.md +22 -22
  7. package/assets/docs/projects.md +6 -6
  8. package/assets/docs/recovery.md +10 -10
  9. package/assets/docs/schemas.md +42 -36
  10. package/assets/docs/workflows.md +39 -32
  11. package/assets/examples/agent-then-analysis/input.json +3 -3
  12. package/assets/examples/agent-then-analysis/norn.project.json +2 -2
  13. package/assets/examples/agent-then-analysis/plugin.ts +37 -9
  14. package/assets/examples/caller-owned-routing/README.md +6 -6
  15. package/assets/examples/caller-owned-routing/assessment.ts +18 -4
  16. package/assets/examples/caller-owned-routing/contracts.ts +7 -2
  17. package/assets/examples/caller-owned-routing/input.json +12 -12
  18. package/assets/examples/caller-owned-routing/norn.project.json +2 -2
  19. package/assets/examples/caller-owned-routing/revision.ts +7 -2
  20. package/assets/examples/caller-owned-routing/router.ts +14 -3
  21. package/assets/examples/caller-selected-continuation/caller.ts +38 -12
  22. package/assets/examples/caller-selected-continuation/input.json +7 -7
  23. package/assets/examples/caller-selected-continuation/norn.project.json +2 -2
  24. package/assets/examples/caller-selected-continuation/producer.ts +10 -3
  25. package/assets/examples/coordinating-multiple-agents/README.md +6 -6
  26. package/assets/examples/coordinating-multiple-agents/input.json +20 -8
  27. package/assets/examples/coordinating-multiple-agents/norn.project.json +2 -2
  28. package/assets/examples/coordinating-multiple-agents/plugin.ts +165 -46
  29. package/assets/examples/coordinating-multiple-agents/queue-tools.ts +30 -7
  30. package/assets/examples/coordinating-multiple-agents/work-queue.ts +211 -63
  31. package/assets/examples/getting-started/norn.project.json +2 -2
  32. package/assets/examples/getting-started/plugin.ts +33 -13
  33. package/assets/examples/minimal-workflow/norn.project.json +2 -2
  34. package/assets/examples/minimal-workflow/plugin.ts +12 -3
  35. package/assets/examples/shared-state/README.md +6 -6
  36. package/assets/examples/shared-state/input.json +1 -1
  37. package/assets/examples/shared-state/norn.project.json +2 -2
  38. package/assets/examples/shared-state/plugin.ts +44 -15
  39. package/assets/examples/shared-state/shared-state.ts +46 -13
  40. package/assets/examples/shared-state/state-tools.ts +68 -16
  41. package/assets/examples/worktree-development-loop/README.md +12 -12
  42. package/assets/examples/worktree-development-loop/norn.project.json +7 -7
  43. package/assets/examples/worktree-development-loop/plugin.ts +7 -1
  44. package/assets/examples/worktree-development-loop/scope.ts +4 -1
  45. package/assets/examples/worktree-development-loop/shared/commands.ts +6 -2
  46. package/assets/examples/worktree-development-loop/workflows/development-loop/execute.ts +18 -4
  47. package/assets/examples/worktree-development-loop/workflows/development-loop/index.ts +4 -1
  48. package/assets/examples/worktree-development-loop/workflows/development-loop/repository.ts +11 -1
  49. package/assets/examples/worktree-development-loop/workflows/development-loop/schema.ts +6 -2
  50. package/assets/examples/worktree-development-loop/workflows/implementation/execute.ts +40 -10
  51. package/assets/examples/worktree-development-loop/workflows/implementation/index.ts +5 -1
  52. package/assets/examples/worktree-development-loop/workflows/planning/execute.ts +1 -1
  53. package/assets/examples/worktree-development-loop/workflows/planning/index.ts +5 -1
  54. package/assets/examples/worktree-development-loop/workflows/review/execute.ts +33 -9
  55. package/assets/examples/worktree-development-loop/workflows/review-router/execute.ts +51 -8
  56. package/assets/package.json +1 -1
  57. package/assets/packages/cli/src/build-info.ts +4 -1
  58. package/assets/packages/cli/src/bun/cli.ts +21 -5
  59. package/assets/packages/cli/src/cli.ts +585 -170
  60. package/assets/packages/cli/src/client.ts +194 -36
  61. package/assets/packages/cli/src/documentation.ts +135 -30
  62. package/assets/packages/cli/src/generated-build-info.ts +2 -2
  63. package/assets/packages/cli/src/internal/agent-directory.ts +7 -2
  64. package/assets/packages/cli/src/internal/agent-response-tool.ts +57 -15
  65. package/assets/packages/cli/src/internal/agents.ts +248 -60
  66. package/assets/packages/cli/src/internal/commands.ts +82 -20
  67. package/assets/packages/cli/src/internal/documentation-bundle.ts +44 -13
  68. package/assets/packages/cli/src/internal/engine.ts +396 -87
  69. package/assets/packages/cli/src/internal/errors.ts +13 -5
  70. package/assets/packages/cli/src/internal/execution-context.ts +20 -12
  71. package/assets/packages/cli/src/internal/file-coordinator.ts +95 -26
  72. package/assets/packages/cli/src/internal/file-names.ts +4 -1
  73. package/assets/packages/cli/src/internal/launch-request.ts +74 -22
  74. package/assets/packages/cli/src/internal/logs.ts +10 -2
  75. package/assets/packages/cli/src/internal/metrics.ts +247 -61
  76. package/assets/packages/cli/src/internal/pi-assets.ts +105 -23
  77. package/assets/packages/cli/src/internal/run-lease.ts +72 -22
  78. package/assets/packages/cli/src/internal/run-log.ts +49 -20
  79. package/assets/packages/cli/src/internal/run-names.ts +120 -12
  80. package/assets/packages/cli/src/internal/run-state.ts +306 -71
  81. package/assets/packages/cli/src/internal/run-store.ts +216 -53
  82. package/assets/packages/cli/src/internal/usage.ts +23 -7
  83. package/assets/packages/cli/src/internal/worker-directory.ts +9 -3
  84. package/assets/packages/cli/src/internal/workflow-registry.ts +281 -70
  85. package/assets/packages/cli/src/internal/working-directory.ts +2 -1
  86. package/assets/packages/cli/src/workflow-loader.ts +341 -74
  87. package/assets/packages/core/src/atomic-files.ts +13 -4
  88. package/assets/packages/core/src/workflow-transition.ts +13 -2
  89. package/assets/packages/sdk/src/api.ts +248 -80
  90. package/assets/packages/sdk/src/index.ts +6 -1
  91. package/assets/packages/sdk/src/schema.ts +75 -21
  92. package/assets/tests/workflow-ref.test.ts +242 -57
  93. package/bin/norn.mjs +13 -4
  94. package/dist/cli.js +344 -90
  95. package/dist/client.js +100 -22
  96. package/dist/documentation.js +102 -25
  97. package/dist/generated-build-info.d.ts +2 -2
  98. package/dist/generated-build-info.js +2 -2
  99. package/dist/internal/agent-directory.js +3 -1
  100. package/dist/internal/agent-response-tool.js +42 -11
  101. package/dist/internal/agents.js +149 -37
  102. package/dist/internal/commands.js +65 -16
  103. package/dist/internal/documentation-bundle.js +23 -11
  104. package/dist/internal/engine.js +304 -66
  105. package/dist/internal/errors.js +4 -2
  106. package/dist/internal/execution-context.js +2 -1
  107. package/dist/internal/file-coordinator.js +59 -19
  108. package/dist/internal/launch-request.js +32 -14
  109. package/dist/internal/metrics.js +138 -38
  110. package/dist/internal/pi-assets.js +70 -19
  111. package/dist/internal/run-lease.js +29 -9
  112. package/dist/internal/run-log.js +24 -12
  113. package/dist/internal/run-state.js +172 -48
  114. package/dist/internal/run-store.js +157 -45
  115. package/dist/internal/worker-directory.js +6 -2
  116. package/dist/internal/workflow-registry.js +123 -32
  117. package/dist/internal/working-directory.js +2 -1
  118. package/dist/workflow-loader.js +198 -41
  119. 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?: { readonly all?: boolean }): Promise<NornWorkflowCatalogInfo>;
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: { readonly workflowId: string; readonly args: unknown; readonly configOverride?: unknown }): Promise<NornRunInfo>;
36
- resume(input: { readonly run: string; readonly args?: unknown }): Promise<NornRunInfo>;
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(run: string, options?: { readonly follow?: boolean }): AsyncIterable<Record<string, unknown>>;
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 () => processRunner.readJson<NornProjectInspection>(["project", "inspect"]),
67
+ inspect: async () =>
68
+ processRunner.readJson<NornProjectInspection>(["project", "inspect"]),
56
69
  },
57
70
  workflows: {
58
- list: async (options) => processRunner.readJson<NornWorkflowCatalogInfo>(["workflows", "list", ...(options?.all ? ["--all"] : [])]),
59
- inspect: async (workflowId) => processRunner.readJson<NornWorkflowInspection>(["workflows", "inspect", workflowId]),
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) => (await processRunner.readJson<{ run: NornRunInfo }>(["runs", "start", startInput.workflowId], JSON.stringify(runStartInput(startInput)))).run,
64
- resume: async (resumeInput) => (await processRunner.readJson<{ run: NornRunInfo }>(["runs", "resume", resumeInput.run], runResumeStdin(resumeInput))).run,
65
- wait: async (run) => (await processRunner.readJson<{ run: NornRunInfo }>(["runs", "wait", run])).run,
66
- list: async () => (await processRunner.readJson<{ runs: NornRunInfo[] }>(["runs", "list"])).runs,
67
- inspect: async (run) => (await processRunner.readJson<{ run: NornRunInfo }>(["runs", "inspect", run])).run,
68
- checkpoints: async (run) => (await processRunner.readJson<{ checkpoints: NornRunCheckpoint[] }>(["runs", "checkpoints", run])).checkpoints,
69
- metrics: async (run) => (await processRunner.readJson<{ metrics: NornRunMetrics }>(["runs", "metrics", run])).metrics,
70
- rollback: async (run, checkpointId) => (await processRunner.readJson<{ run: NornRunInfo }>(["runs", "rollback", run, checkpointId])).run,
71
- stop: async (run) => (await processRunner.readJson<{ run: NornRunInfo }>(["runs", "stop", run])).run,
72
- kill: async (run) => (await processRunner.readJson<{ run: NornRunInfo }>(["runs", "kill", run])).run,
73
- delete: async (run) => (await processRunner.readJson<{ deleted: DeletedNornRunInfo }>(["runs", "delete", run])).deleted,
74
- logs: (run, options) => processRunner.readJsonLines(["runs", "logs", run, ...(options?.follow ? ["--follow"] : [])]),
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 = input.executablePath ?? fileURLToPath(new URL("../bin/norn.mjs", import.meta.url));
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 T | { error?: { code?: string; message?: string; diagnostics?: NornWorkflowDiagnostic[] } };
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 (parsed.error?.code === "NORN_PROJECT_INVALID" && Array.isArray(parsed.error.diagnostics)) throw new NornProjectLoadError({ diagnostics: parsed.error.diagnostics });
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) throw new Error(result.stderr || `Norn exited with code ${result.exitCode}`);
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(args: readonly string[]): AsyncIterable<Record<string, unknown>> {
113
- const child = spawn(process.execPath, [this.executablePath, ...args], { cwd: this.input.spawnCwd, stdio: ["ignore", "pipe", "pipe"] });
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) throw new Error(String((parsed.error as { message?: unknown }).message ?? "Norn stream failed"));
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) => child.on("close", resolvePromise));
126
- if (exitCode !== 0) throw new Error(stderr || `Norn exited with code ${exitCode}`);
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(args: readonly string[], stdin?: string): Promise<{ readonly stdout: string; readonly stderr: string; readonly exitCode: number | null }> {
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], { cwd: this.input.spawnCwd, stdio: [stdin === undefined ? "ignore" : "pipe", "pipe", "pipe"] });
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) => resolvePromise({ stdout, stderr, exitCode }));
289
+ child.on("close", (exitCode) =>
290
+ resolvePromise({ stdout, stderr, exitCode }),
291
+ );
143
292
  });
144
293
  }
145
294
  }
146
295
 
147
- function runStartInput(input: { readonly args: unknown; readonly configOverride?: unknown }): { readonly args: unknown; readonly config?: unknown } {
148
- return input.configOverride === undefined ? { args: input.args } : { args: input.args, config: input.configOverride };
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: { readonly args?: unknown }): string | undefined {
152
- return input.args === undefined ? undefined : JSON.stringify({ args: input.args });
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 { lstat, mkdir, mkdtemp, readFile, readdir, rename, rm, writeFile } from "node:fs/promises";
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 { DOCUMENTATION_PATHS, hashDocumentationBundle, validateDocumentationBundle, type NornDocumentationBundle } from "./internal/documentation-bundle.ts";
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: { readonly root: string; readonly readme: string; readonly index: string; readonly docs: string; readonly examples: string };
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") return join(input.environment.LOCALAPPDATA || join(input.home, "AppData", "Local"), "norn", "docs");
30
- if (input.platform === "darwin") return join(input.home, "Library", "Caches", "norn", "docs");
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(xdgCache?.startsWith("/") ? xdgCache : join(input.home, ".cache"), "norn", "docs");
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(await readFile(join(root, "package.json"), "utf8")) as { version?: unknown };
46
- if (typeof packageInfo.version !== "string" || packageInfo.version.length === 0) throw new Error("Invalid documentation package version");
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) throw new Error("Local documentation version does not match the runtime; rebuild or reinstall this CLI package");
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) throw new Error("Embedded documentation version does not match the binary build");
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").update(JSON.stringify({ format: 1, assetDigest, version, commit: input.build.commit })).digest("hex");
56
- root = await new NornDocumentationCache({ cacheRoot: resolve(input.cacheRoot), cacheKey, bundle: input.source.bundle }).materialize();
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 = key === "docs" || key === "examples" ? stat.isDirectory() : stat.isFile();
77
- if (!isExpectedType) throw new Error(`Invalid local documentation path: ${join(root, path)}`);
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(private readonly input: { readonly cacheRoot: string; readonly cacheKey: string; readonly bundle: NornDocumentationBundle }) {
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(input.bundle.files.map(file => [file.path, file.content]));
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()) throw new Error(`Documentation cache root must be a real directory: ${this.input.cacheRoot}`);
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)), { recursive: true, mode: 0o700 });
100
- await writeFile(join(staging, ...parts), file.content, { encoding: "utf8", flag: "wx", mode: 0o600 });
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 (!isNodeError(error) || !["EEXIST", "ENOTEMPTY", "EPERM", "EACCES"].includes(error.code ?? "")) throw error;
106
- if (!await this.validateExistingCache()) throw error;
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()) throw new Error(`Documentation cache disappeared after extraction: ${this.root}`);
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()) throw this.invalidCacheError();
205
+ if (!stat.isDirectory() || stat.isSymbolicLink())
206
+ throw this.invalidCacheError();
124
207
  const seenFiles = new Set<string>();
125
- await this.validateDirectory({ directory: this.root, prefix: "", seenFiles });
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: { readonly directory: string; readonly prefix: string; readonly seenFiles: Set<string> }): Promise<void> {
131
- for (const entry of await readdir(input.directory, { withFileTypes: true })) {
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}/`))) throw this.invalidCacheError();
136
- await this.validateDirectory({ directory: absolutePath, prefix: path, seenFiles: input.seenFiles });
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 (!(await readFile(absolutePath)).equals(Buffer.from(this.files.get(path)!, "utf8"))) throw this.invalidCacheError();
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(`Documentation cache is incomplete or modified: ${this.root}. Remove this cache entry and retry; copy examples elsewhere before editing them.`);
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.35614826665.1",
6
- "commit": "286e68533b1d26b5afbb0cba9d75f857fb85dc53",
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: { home: string; environment: NodeJS.ProcessEnv }): string {
4
- return resolve(input.environment.NORN_AGENT_DIR || join(input.home, ".norn", "agent"));
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
  }