@vimhead.dev/norn-cli 0.1.0-tip.35359392805.1 → 0.1.0-tip.35436871363.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 (146) hide show
  1. package/assets/README.md +22 -27
  2. package/assets/docs/README.md +5 -5
  3. package/assets/docs/agents.md +48 -9
  4. package/assets/docs/cli.md +7 -7
  5. package/assets/docs/composition.md +25 -25
  6. package/assets/docs/persistence.md +34 -23
  7. package/assets/docs/projects.md +30 -18
  8. package/assets/docs/providers.md +3 -3
  9. package/assets/docs/recovery.md +13 -9
  10. package/assets/docs/schemas.md +1 -1
  11. package/assets/docs/workflows.md +105 -12
  12. package/assets/examples/agent-then-analysis/README.md +10 -10
  13. package/assets/examples/agent-then-analysis/input.json +1 -1
  14. package/assets/examples/agent-then-analysis/norn.project.json +1 -1
  15. package/assets/examples/agent-then-analysis/plugin.ts +57 -68
  16. package/assets/examples/caller-selected-continuation/README.md +15 -14
  17. package/assets/examples/caller-selected-continuation/caller.ts +37 -38
  18. package/assets/examples/caller-selected-continuation/input.json +2 -2
  19. package/assets/examples/caller-selected-continuation/norn.project.json +1 -1
  20. package/assets/examples/caller-selected-continuation/producer.ts +19 -24
  21. package/assets/examples/coordinating-multiple-agents/README.md +27 -17
  22. package/assets/examples/coordinating-multiple-agents/input.json +1 -1
  23. package/assets/examples/coordinating-multiple-agents/norn.project.json +1 -1
  24. package/assets/examples/coordinating-multiple-agents/plugin.ts +79 -60
  25. package/assets/examples/coordinating-multiple-agents/queue-tools.ts +43 -0
  26. package/assets/examples/coordinating-multiple-agents/work-queue.ts +60 -65
  27. package/assets/examples/getting-started/README.md +2 -2
  28. package/assets/examples/getting-started/norn.project.json +1 -1
  29. package/assets/examples/getting-started/plugin.ts +20 -26
  30. package/assets/examples/minimal-workflow/README.md +13 -12
  31. package/assets/examples/minimal-workflow/norn.project.json +1 -1
  32. package/assets/examples/minimal-workflow/plugin.ts +14 -25
  33. package/assets/examples/shared-state/README.md +12 -4
  34. package/assets/examples/shared-state/input.json +1 -1
  35. package/assets/examples/shared-state/norn.project.json +1 -1
  36. package/assets/examples/shared-state/plugin.ts +49 -41
  37. package/assets/examples/shared-state/shared-state.ts +56 -0
  38. package/assets/examples/shared-state/state-tools.ts +68 -0
  39. package/assets/examples/worktree-development-loop/README.md +18 -17
  40. package/assets/examples/worktree-development-loop/norn.project.json +1 -1
  41. package/assets/examples/worktree-development-loop/plugin.ts +6 -26
  42. package/assets/examples/worktree-development-loop/scope.ts +4 -0
  43. package/assets/examples/worktree-development-loop/workflows/development-loop/execute.ts +14 -15
  44. package/assets/examples/worktree-development-loop/workflows/development-loop/index.ts +3 -4
  45. package/assets/examples/worktree-development-loop/workflows/development-loop/repository.ts +5 -3
  46. package/assets/examples/worktree-development-loop/workflows/development-loop/schema.ts +2 -2
  47. package/assets/examples/worktree-development-loop/workflows/implementation/execute.ts +35 -33
  48. package/assets/examples/worktree-development-loop/workflows/implementation/index.ts +2 -3
  49. package/assets/examples/worktree-development-loop/workflows/implementation/schema.ts +6 -3
  50. package/assets/examples/worktree-development-loop/workflows/planning/execute.ts +27 -16
  51. package/assets/examples/worktree-development-loop/workflows/planning/index.ts +2 -3
  52. package/assets/examples/worktree-development-loop/workflows/planning/schema.ts +4 -2
  53. package/assets/examples/worktree-development-loop/workflows/review/execute.ts +43 -31
  54. package/assets/examples/worktree-development-loop/workflows/review/index.ts +3 -4
  55. package/assets/examples/worktree-development-loop/workflows/review/schema.ts +6 -6
  56. package/assets/examples/worktree-development-loop/workflows/review-router/execute.ts +50 -44
  57. package/assets/examples/worktree-development-loop/workflows/review-router/index.ts +2 -3
  58. package/assets/examples/worktree-development-loop/workflows/review-router/schema.ts +5 -5
  59. package/assets/package.json +1 -1
  60. package/assets/packages/cli/src/cli.ts +44 -37
  61. package/assets/packages/cli/src/client.ts +9 -16
  62. package/assets/packages/cli/src/documentation-intro.ts +1 -1
  63. package/assets/packages/cli/src/generated-build-info.ts +2 -2
  64. package/assets/packages/cli/src/internal/agent-response-tool.ts +3 -3
  65. package/assets/packages/cli/src/internal/agents.ts +24 -32
  66. package/assets/packages/cli/src/internal/commands.ts +2 -14
  67. package/assets/packages/cli/src/internal/engine.ts +63 -101
  68. package/assets/packages/cli/src/internal/errors.ts +4 -4
  69. package/assets/packages/cli/src/internal/launch-request.ts +6 -6
  70. package/assets/packages/cli/src/internal/logs.ts +1 -1
  71. package/assets/packages/cli/src/internal/run-log.ts +1 -1
  72. package/assets/packages/cli/src/internal/run-state.ts +38 -23
  73. package/assets/packages/cli/src/internal/run.ts +4 -62
  74. package/assets/packages/cli/src/internal/worker-directory.ts +17 -0
  75. package/assets/packages/cli/src/internal/workflow-registry.ts +126 -111
  76. package/assets/packages/cli/src/internal/working-directory.ts +6 -0
  77. package/assets/packages/cli/src/workflow-loader.ts +213 -0
  78. package/assets/packages/core/src/workflow-transition.ts +2 -2
  79. package/assets/packages/sdk/src/api.ts +141 -329
  80. package/assets/packages/sdk/src/index.ts +1 -4
  81. package/assets/packages/sdk/src/schema.ts +3 -3
  82. package/assets/tests/workflow-ref.test.ts +53 -55
  83. package/dist/cli.js +41 -35
  84. package/dist/client.d.ts +3 -4
  85. package/dist/client.js +3 -8
  86. package/dist/documentation-intro.js +1 -1
  87. package/dist/generated-build-info.d.ts +2 -2
  88. package/dist/generated-build-info.js +2 -2
  89. package/dist/internal/agent-response-tool.js +3 -3
  90. package/dist/internal/agents.d.ts +0 -4
  91. package/dist/internal/agents.js +30 -46
  92. package/dist/internal/commands.d.ts +0 -4
  93. package/dist/internal/commands.js +2 -10
  94. package/dist/internal/engine.d.ts +4 -8
  95. package/dist/internal/engine.js +57 -84
  96. package/dist/internal/errors.d.ts +3 -3
  97. package/dist/internal/errors.js +1 -1
  98. package/dist/internal/file-coordinator.d.ts +17 -0
  99. package/dist/internal/file-coordinator.js +162 -0
  100. package/dist/internal/launch-request.d.ts +4 -4
  101. package/dist/internal/launch-request.js +2 -2
  102. package/dist/internal/logs.d.ts +1 -1
  103. package/dist/internal/run-log.d.ts +1 -1
  104. package/dist/internal/run-state.d.ts +11 -8
  105. package/dist/internal/run-state.js +34 -22
  106. package/dist/internal/run.d.ts +3 -23
  107. package/dist/internal/run.js +4 -50
  108. package/dist/internal/worker-directory.d.ts +1 -0
  109. package/dist/internal/worker-directory.js +26 -0
  110. package/dist/internal/workflow-registry.d.ts +35 -17
  111. package/dist/internal/workflow-registry.js +110 -73
  112. package/dist/internal/working-directory.d.ts +1 -0
  113. package/dist/internal/working-directory.js +9 -0
  114. package/dist/{plugin-loader.d.ts → workflow-loader.d.ts} +7 -8
  115. package/dist/workflow-loader.js +234 -0
  116. package/package.json +2 -2
  117. package/assets/docs/resources.md +0 -61
  118. package/assets/examples/coordinating-multiple-agents/queue-adapter.ts +0 -51
  119. package/assets/examples/worktree-development-loop/manifest.ts +0 -26
  120. package/assets/examples/worktree-development-loop/state.ts +0 -23
  121. package/assets/examples/worktree-development-loop/workflows/development-loop/declaration.ts +0 -8
  122. package/assets/examples/worktree-development-loop/workflows/implementation/declaration.ts +0 -8
  123. package/assets/examples/worktree-development-loop/workflows/planning/declaration.ts +0 -8
  124. package/assets/examples/worktree-development-loop/workflows/review/declaration.ts +0 -8
  125. package/assets/examples/worktree-development-loop/workflows/review-router/declaration.ts +0 -12
  126. package/assets/packages/cli/src/internal/artifacts.ts +0 -26
  127. package/assets/packages/cli/src/internal/resource-bindings.ts +0 -35
  128. package/assets/packages/cli/src/internal/run-resources.ts +0 -23
  129. package/assets/packages/cli/src/internal/state-store.ts +0 -83
  130. package/assets/packages/cli/src/plugin-loader.ts +0 -400
  131. package/assets/packages/cli/src/resources.ts +0 -69
  132. package/assets/packages/sdk/src/agent-resource-adapter.ts +0 -11
  133. package/assets/packages/sdk/src/resources.ts +0 -20
  134. package/assets/packages/sdk/src/state-adapter.ts +0 -75
  135. package/dist/internal/artifacts.d.ts +0 -10
  136. package/dist/internal/artifacts.js +0 -29
  137. package/dist/internal/resource-bindings.d.ts +0 -13
  138. package/dist/internal/resource-bindings.js +0 -34
  139. package/dist/internal/run-resources.d.ts +0 -6
  140. package/dist/internal/run-resources.js +0 -26
  141. package/dist/internal/state-store.d.ts +0 -23
  142. package/dist/internal/state-store.js +0 -103
  143. package/dist/plugin-loader.js +0 -341
  144. package/dist/resources.d.ts +0 -11
  145. package/dist/resources.js +0 -100
  146. /package/assets/packages/{sdk/src/files.ts → cli/src/internal/file-coordinator.ts} +0 -0
@@ -1,26 +0,0 @@
1
- // src/internal/run-resources.ts
2
- import { join, resolve } from "node:path";
3
- import { withFileMutationQueue } from "@earendil-works/pi-coding-agent";
4
- import { NornRunResources } from "../resources.js";
5
- import { NornJsonWorkflowState } from "./state-store.js";
6
- async function initializeRunResources(runRoot) {
7
- const resources = await NornRunResources.initialize(runRoot);
8
- const state = await resources.ensure({
9
- name: "workflow-state",
10
- kind: "norn.state",
11
- configuration: { format: 1, path: "state.json" },
12
- async initialize({ files, mode }) {
13
- const state2 = new NornJsonWorkflowState({
14
- stateFile: join(resolve(runRoot), "current", "state.json"),
15
- files,
16
- coordinateMutation: withFileMutationQueue
17
- });
18
- await state2.initialize(mode);
19
- return state2;
20
- }
21
- });
22
- return { resources, state };
23
- }
24
- export {
25
- initializeRunResources
26
- };
@@ -1,23 +0,0 @@
1
- import type { NornWorkflowState, NornWorkflowStateDefinition } from "@vimhead.dev/norn";
2
- import type { NornFileCoordinator } from "@vimhead.dev/norn/files";
3
- import type { Static, TSchema } from "typebox";
4
- export declare class NornMemoryWorkflowState implements NornWorkflowState {
5
- private readonly data;
6
- get<Schema extends TSchema>(state: NornWorkflowStateDefinition<Schema>): Promise<Static<Schema>>;
7
- getOptional<Schema extends TSchema>(state: NornWorkflowStateDefinition<Schema>): Promise<Static<Schema> | undefined>;
8
- set<Schema extends TSchema>(state: NornWorkflowStateDefinition<Schema>, value: NoInfer<Static<Schema>>): Promise<void>;
9
- }
10
- export declare class NornJsonWorkflowState implements NornWorkflowState {
11
- private readonly input;
12
- readonly stateFile: string;
13
- constructor(input: {
14
- readonly stateFile: string;
15
- readonly files: NornFileCoordinator;
16
- readonly coordinateMutation: <T>(path: string, operation: () => Promise<T>) => Promise<T>;
17
- });
18
- initialize(mode: "create" | "open"): Promise<void>;
19
- get<Schema extends TSchema>(state: NornWorkflowStateDefinition<Schema>): Promise<Static<Schema>>;
20
- getOptional<Schema extends TSchema>(state: NornWorkflowStateDefinition<Schema>): Promise<Static<Schema> | undefined>;
21
- set<Schema extends TSchema>(state: NornWorkflowStateDefinition<Schema>, value: NoInfer<Static<Schema>>): Promise<void>;
22
- private readStateFile;
23
- }
@@ -1,103 +0,0 @@
1
- // ../core/src/atomic-files.ts
2
- import { randomUUID } from "node:crypto";
3
- import { chmod, mkdir, rename, rm, stat, writeFile } from "node:fs/promises";
4
- import { dirname } from "node:path";
5
-
6
- // ../core/src/errors.ts
7
- function isNodeError(error) {
8
- return error instanceof Error && "code" in error;
9
- }
10
-
11
- // ../core/src/atomic-files.ts
12
- async function writeJsonAtomically(path, value) {
13
- await writeTextAtomically(path, `${JSON.stringify(value, null, 2)}
14
- `);
15
- }
16
- async function writeTextAtomically(path, content) {
17
- await mkdir(dirname(path), { recursive: true });
18
- let existingMode;
19
- try {
20
- existingMode = (await stat(path)).mode & 511;
21
- } catch (error) {
22
- if (!isNodeError(error) || error.code !== "ENOENT") throw error;
23
- }
24
- const tmpPath = `${path}.${randomUUID()}.tmp`;
25
- try {
26
- await writeFile(tmpPath, content, { encoding: "utf8", mode: existingMode });
27
- if (existingMode !== void 0) await chmod(tmpPath, existingMode);
28
- await rename(tmpPath, path);
29
- } catch (error) {
30
- await rm(tmpPath, { force: true }).catch(() => void 0);
31
- throw error;
32
- }
33
- }
34
-
35
- // src/internal/state-store.ts
36
- import { jsonValueSchema } from "@vimhead.dev/norn/schema";
37
- import { readFile } from "node:fs/promises";
38
- import { Value } from "typebox/value";
39
- function copyStateValue(schema, value) {
40
- Value.Assert(jsonValueSchema, value);
41
- return structuredClone(Value.Parse(schema, value));
42
- }
43
- var NornMemoryWorkflowState = class {
44
- data = /* @__PURE__ */ new Map();
45
- async get(state) {
46
- const value = await this.getOptional(state);
47
- if (value === void 0) throw new Error(`Missing workflow state: ${state.id}`);
48
- return value;
49
- }
50
- async getOptional(state) {
51
- if (!this.data.has(state.id)) return void 0;
52
- return copyStateValue(state.schema, this.data.get(state.id));
53
- }
54
- async set(state, value) {
55
- this.data.set(state.id, copyStateValue(state.schema, value));
56
- }
57
- };
58
- var NornJsonWorkflowState = class {
59
- constructor(input) {
60
- this.input = input;
61
- this.stateFile = input.stateFile;
62
- }
63
- input;
64
- stateFile;
65
- async initialize(mode) {
66
- await this.input.files.withExclusiveLock(this.stateFile, async (path) => {
67
- try {
68
- await this.readStateFile(path);
69
- } catch (error) {
70
- if (mode !== "create" || !isNodeError(error) || error.code !== "ENOENT") throw error;
71
- await writeJsonAtomically(path, {});
72
- }
73
- });
74
- }
75
- async get(state) {
76
- const value = await this.getOptional(state);
77
- if (value === void 0) throw new Error(`Missing workflow state: ${state.id}`);
78
- return value;
79
- }
80
- async getOptional(state) {
81
- const data = await this.input.files.withExclusiveLock(this.stateFile, (path) => this.readStateFile(path));
82
- if (!Object.prototype.hasOwnProperty.call(data, state.id)) return void 0;
83
- return copyStateValue(state.schema, data[state.id]);
84
- }
85
- async set(state, value) {
86
- const parsedValue = copyStateValue(state.schema, value);
87
- await this.input.coordinateMutation(this.stateFile, () => this.input.files.withExclusiveLock(this.stateFile, async (path) => {
88
- const data = await this.readStateFile(path);
89
- Object.defineProperty(data, state.id, { value: parsedValue, enumerable: true, configurable: true, writable: true });
90
- await writeJsonAtomically(path, data);
91
- }));
92
- }
93
- async readStateFile(path) {
94
- const content = await readFile(path, "utf8");
95
- const parsed = JSON.parse(content);
96
- if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) return parsed;
97
- throw new Error(`Invalid workflow state document: ${path}`);
98
- }
99
- };
100
- export {
101
- NornJsonWorkflowState,
102
- NornMemoryWorkflowState
103
- };
@@ -1,341 +0,0 @@
1
- // src/plugin-loader.ts
2
- import * as nornModule from "@vimhead.dev/norn";
3
- import { isWorkflowPlugin } from "@vimhead.dev/norn";
4
-
5
- // ../core/src/errors.ts
6
- function isNodeError(error) {
7
- return error instanceof Error && "code" in error;
8
- }
9
-
10
- // src/plugin-loader.ts
11
- import * as nornFilesModule from "@vimhead.dev/norn/files";
12
- import * as nornSchemaModule from "@vimhead.dev/norn/schema";
13
- import { inspectSchema } from "@vimhead.dev/norn/schema";
14
- import { createJiti } from "jiti/static";
15
- import { access, readdir, readFile } from "node:fs/promises";
16
- import { dirname, isAbsolute, join, resolve, sep } from "node:path";
17
- import { pathToFileURL } from "node:url";
18
- import * as typeboxModule from "typebox";
19
- import { Type } from "typebox";
20
- import * as typeboxCompileModule from "typebox/compile";
21
- import * as typeboxSchemaModule from "typebox/schema";
22
- import * as typeboxValueModule from "typebox/value";
23
- import { AssertError, Value } from "typebox/value";
24
- import { errorMessage, NornProjectLoadError } from "./internal/errors.js";
25
- import { NornMemoryWorkflowState } from "./internal/state-store.js";
26
- import { decodePluginConfiguration, NornWorkflowRegistry } from "./internal/workflow-registry.js";
27
- var NORN_PROJECT_FILE_NAME = "norn.project.json";
28
- var nornConfigSchema = Type.Object({
29
- plugins: Type.Array(Type.String({ minLength: 1 }), { default: [] }),
30
- includes: Type.Array(Type.String({ minLength: 1 }), { default: [] }),
31
- config: Type.Record(Type.String(), Type.Unknown(), { default: {} })
32
- });
33
- var nornProjectConfigSchema = Type.Object({
34
- ...nornConfigSchema.properties,
35
- version: Type.Literal(1, { default: 1 })
36
- });
37
- async function loadNornProject(cwd) {
38
- const { project, diagnostics } = await collectNornProject(cwd);
39
- if (diagnostics.length > 0) throw new NornProjectLoadError({ diagnostics });
40
- return project;
41
- }
42
- async function discoverNornProject(cwd) {
43
- const { project, diagnostics } = await collectNornProject(cwd);
44
- return { project: buildProjectInfo(project), workflows: project.registry.list(), isComplete: diagnostics.length === 0, diagnostics };
45
- }
46
- async function inspectNornWorkflow(input) {
47
- const { project, diagnostics } = await collectNornProject(input.cwd);
48
- const summary = project.registry.list().find((workflow) => workflow.id === input.workflowId);
49
- if (!summary) {
50
- if (diagnostics.length === 0) throw new Error(`Unknown workflow: ${input.workflowId}`);
51
- return { workflow: null, isComplete: false, diagnostics };
52
- }
53
- const info = summary.plugin;
54
- if (!info?.path || !info.configPath) throw new Error(`Loaded workflow lacks plugin source: ${input.workflowId}`);
55
- try {
56
- return { workflow: project.registry.inspect(input.workflowId) ?? null, isComplete: diagnostics.length === 0, diagnostics };
57
- } catch (error) {
58
- return { workflow: null, isComplete: false, diagnostics: [...diagnostics, createPluginDiagnostic({
59
- source: { configPath: info.configPath, pluginPath: info.path, pluginId: info.id },
60
- workflowId: input.workflowId,
61
- stage: "schema",
62
- error
63
- })] };
64
- }
65
- }
66
- function buildProjectInfo(project) {
67
- return {
68
- cwd: project.cwd,
69
- projectPath: project.projectPath,
70
- projectRoot: project.projectRoot,
71
- configPath: project.configPath,
72
- configRoot: project.configRoot,
73
- configFiles: project.configFiles.map((file) => file.path),
74
- plugins: project.pluginInfos
75
- };
76
- }
77
- async function findNornProject(cwd) {
78
- const projectPath = await findNearestNornProject(cwd);
79
- const projectRootConfig = await readNornProjectConfigFile(projectPath);
80
- const configFiles = await loadIncludedNornConfigFiles(projectRootConfig, /* @__PURE__ */ new Set([projectRootConfig.path]));
81
- return {
82
- cwd: resolve(cwd),
83
- projectPath: projectRootConfig.path,
84
- projectRoot: projectRootConfig.root,
85
- configPath: projectRootConfig.path,
86
- configRoot: projectRootConfig.root,
87
- config: projectRootConfig.config,
88
- configFiles,
89
- projectConfig: mergeProjectConfig(configFiles)
90
- };
91
- }
92
- async function findNearestNornProject(cwd) {
93
- let current = resolve(cwd);
94
- while (true) {
95
- const projectPath = join(current, NORN_PROJECT_FILE_NAME);
96
- try {
97
- await access(projectPath);
98
- return projectPath;
99
- } catch (error) {
100
- if (!isNodeError(error) || error.code !== "ENOENT") throw error;
101
- }
102
- const parent = dirname(current);
103
- if (parent === current) throw new Error(`Could not find ${NORN_PROJECT_FILE_NAME} from ${cwd}`);
104
- current = parent;
105
- }
106
- }
107
- async function loadIncludedNornConfigFiles(projectFile, visitedPaths) {
108
- const includedConfigFiles = await Promise.all(projectFile.config.includes.map(async (includePath) => {
109
- const configPaths = await expandIncludePath(projectFile.root, includePath);
110
- return Promise.all(configPaths.map(readNornConfigFile));
111
- }));
112
- const descendants = await Promise.all(includedConfigFiles.flat().map((includedConfigFile) => loadNornConfigTree(includedConfigFile, visitedPaths)));
113
- return [createProjectConfigEntry(projectFile), ...descendants.flat()];
114
- }
115
- async function loadNornConfigTree(configFile, visitedPaths) {
116
- if (visitedPaths.has(configFile.path)) return [];
117
- visitedPaths.add(configFile.path);
118
- const includedConfigFiles = await Promise.all(configFile.config.includes.map(async (includePath) => {
119
- const configPaths = await expandIncludePath(configFile.root, includePath);
120
- return Promise.all(configPaths.map(readNornConfigFile));
121
- }));
122
- const descendants = await Promise.all(includedConfigFiles.flat().map((includedConfigFile) => loadNornConfigTree(includedConfigFile, visitedPaths)));
123
- return [configFile, ...descendants.flat()];
124
- }
125
- async function readNornProjectConfigFile(path) {
126
- const config = Value.Parse(nornProjectConfigSchema, Value.Default(nornProjectConfigSchema, JSON.parse(await readFile(path, "utf8"))));
127
- return { path, root: dirname(path), config };
128
- }
129
- async function readNornConfigFile(path) {
130
- const config = Value.Parse(nornConfigSchema, Value.Default(nornConfigSchema, JSON.parse(await readFile(path, "utf8"))));
131
- return { path, root: dirname(path), config };
132
- }
133
- function createProjectConfigEntry(projectFile) {
134
- return {
135
- path: projectFile.path,
136
- root: projectFile.root,
137
- config: {
138
- plugins: projectFile.config.plugins,
139
- includes: projectFile.config.includes,
140
- config: projectFile.config.config
141
- }
142
- };
143
- }
144
- async function collectNornProject(cwd) {
145
- const project = await findNornProject(cwd);
146
- const imported = await importWorkflowPlugins(project);
147
- const diagnostics = [...imported.diagnostics];
148
- const registry = new NornWorkflowRegistry();
149
- const state = new NornMemoryWorkflowState();
150
- const loaded = [];
151
- const conflicts = collectPluginConflicts(imported.plugins);
152
- for (const candidate of imported.plugins) {
153
- const pluginConflicts = conflicts.get(candidate);
154
- if (pluginConflicts) {
155
- diagnostics.push(...pluginConflicts);
156
- continue;
157
- }
158
- const result = registerProjectPlugin({ candidate, project, registry, state });
159
- diagnostics.push(...result.diagnostics);
160
- if (result.loaded) loaded.push(result.loaded);
161
- }
162
- return {
163
- project: { ...project, plugins: loaded.map((entry) => entry.plugin), pluginInfos: loaded.map((entry) => entry.info), registry, workflows: registry.launchableEntries(), state },
164
- diagnostics
165
- };
166
- }
167
- function collectPluginConflicts(plugins) {
168
- const byPluginId = /* @__PURE__ */ new Map();
169
- const byWorkflowId = /* @__PURE__ */ new Map();
170
- for (const candidate of plugins) {
171
- const id = candidate.plugin.manifest.id;
172
- byPluginId.set(id, [...byPluginId.get(id) ?? [], candidate]);
173
- for (const workflowId of new Set(Object.values(candidate.plugin.manifest.workflows).map((workflow) => workflow.id))) {
174
- byWorkflowId.set(workflowId, [...byWorkflowId.get(workflowId) ?? [], candidate]);
175
- }
176
- }
177
- const conflicts = /* @__PURE__ */ new Map();
178
- for (const [pluginId, candidates] of byPluginId) {
179
- if (candidates.length < 2) continue;
180
- for (const candidate of candidates) conflicts.set(candidate, [createPluginDiagnostic({
181
- source: candidate.source,
182
- workflowId: null,
183
- stage: "duplicate",
184
- error: new Error(`Duplicate Norn plugin id: ${pluginId} (${candidates.map((other) => other.source.pluginPath).join(", ")})`)
185
- })]);
186
- }
187
- for (const [workflowId, candidates] of byWorkflowId) {
188
- const distinctPlugins = candidates.filter((candidate) => byPluginId.get(candidate.plugin.manifest.id)?.length === 1);
189
- if (distinctPlugins.length < 2) continue;
190
- for (const candidate of distinctPlugins) conflicts.set(candidate, [...conflicts.get(candidate) ?? [], createPluginDiagnostic({
191
- source: candidate.source,
192
- workflowId,
193
- stage: "duplicate",
194
- error: new Error(`Workflow already registered: ${workflowId} (${distinctPlugins.map((other) => other.source.pluginPath).join(", ")})`)
195
- })]);
196
- }
197
- return conflicts;
198
- }
199
- async function importWorkflowPlugins(project) {
200
- const plugins = [];
201
- const diagnostics = [];
202
- for (const configFile of project.configFiles) {
203
- const jiti = createJiti(pathToFileURL(configFile.path).href, { moduleCache: false, virtualModules: nornWorkflowVirtualModules() });
204
- for (const pluginPath of configFile.config.plugins) {
205
- const source = { configPath: configFile.path, pluginPath: resolveConfigPath(configFile.root, pluginPath), pluginId: null };
206
- let stage = "import";
207
- try {
208
- const module = await jiti.import(pathToFileURL(source.pluginPath).href);
209
- stage = "declaration";
210
- if (!isWorkflowPlugin(module?.default)) throw new Error(`Norn plugin must be the default export: ${source.pluginPath}`);
211
- plugins.push({ plugin: module.default, source: { ...source, pluginId: module.default.manifest.id } });
212
- } catch (error) {
213
- diagnostics.push(createPluginDiagnostic({ source, workflowId: null, stage, error }));
214
- }
215
- }
216
- }
217
- return { plugins, diagnostics };
218
- }
219
- function registerProjectPlugin(input) {
220
- const { plugin, source } = input.candidate;
221
- const diagnostics = [];
222
- const unregister = [];
223
- let stage = "config";
224
- try {
225
- const configInput = input.project.projectConfig[plugin.manifest.id];
226
- const configuration = decodePluginConfiguration({ pluginId: plugin.manifest.id, schema: plugin.manifest.config, value: configInput });
227
- stage = "schema";
228
- const info = {
229
- id: plugin.manifest.id,
230
- path: source.pluginPath,
231
- configPath: source.configPath,
232
- configSchema: plugin.manifest.config ? inspectSchema(plugin.manifest.config) : null,
233
- config: configuration?.value
234
- };
235
- stage = "implementation";
236
- const implementation = typeof plugin.implementation === "function" ? plugin.implementation({ cwd: input.project.cwd, state: input.state }) : plugin.implementation;
237
- for (const [key, workflow] of Object.entries(plugin.manifest.workflows)) {
238
- stage = "implementation";
239
- try {
240
- const workflowImplementation = implementation.workflows?.[key];
241
- if (typeof workflowImplementation?.execute !== "function") throw new Error(`Missing implementation for workflow ${workflow.id}`);
242
- stage = "duplicate";
243
- if (input.registry.workflowById(workflow.id)) throw new Error(`Workflow already registered: ${workflow.id}`);
244
- stage = "declaration";
245
- unregister.push(input.registry.register(workflow, workflowImplementation, { plugin: workflowPluginInfo(info), configuration }));
246
- } catch (error) {
247
- diagnostics.push(createPluginDiagnostic({ source, workflowId: workflow.id, stage, error }));
248
- }
249
- }
250
- if (diagnostics.length === 0) return { loaded: { plugin, info }, diagnostics };
251
- } catch (error) {
252
- diagnostics.push(createPluginDiagnostic({ source, workflowId: null, stage, error }));
253
- }
254
- for (const dispose of unregister) dispose();
255
- return { loaded: null, diagnostics };
256
- }
257
- function createPluginDiagnostic(input) {
258
- return {
259
- ...input.source,
260
- workflowId: input.workflowId,
261
- stage: input.stage,
262
- message: errorMessage(input.error),
263
- issues: input.error instanceof AssertError ? input.error.cause.errors : []
264
- };
265
- }
266
- function workflowPluginInfo(info) {
267
- return { id: info.id, path: info.path, configPath: info.configPath };
268
- }
269
- function nornWorkflowVirtualModules() {
270
- return {
271
- "@vimhead.dev/norn": nornModule,
272
- "@vimhead.dev/norn/files": nornFilesModule,
273
- "@vimhead.dev/norn/schema": nornSchemaModule,
274
- typebox: typeboxModule,
275
- "typebox/value": typeboxValueModule,
276
- "typebox/compile": typeboxCompileModule,
277
- "typebox/schema": typeboxSchemaModule
278
- };
279
- }
280
- function mergeProjectConfig(configFiles) {
281
- const [projectFile, ...reusableConfigFiles] = configFiles;
282
- let reusableConfig = {};
283
- for (const configFile of reusableConfigFiles) {
284
- reusableConfig = mergeReusableConfigObjects(reusableConfig, configFile.config.config, ["config"]);
285
- }
286
- return mergeProjectConfigObjects(reusableConfig, projectFile?.config.config ?? {});
287
- }
288
- function mergeReusableConfigObjects(base, override, path) {
289
- const result = { ...base };
290
- for (const [key, overrideValue] of Object.entries(override)) {
291
- result[key] = Object.prototype.hasOwnProperty.call(result, key) ? mergeReusableConfigValue(result[key], overrideValue, [...path, key]) : overrideValue;
292
- }
293
- return result;
294
- }
295
- function mergeReusableConfigValue(base, override, path) {
296
- if (isPlainObject(base) && isPlainObject(override)) return mergeReusableConfigObjects(base, override, path);
297
- if (JSON.stringify(base) === JSON.stringify(override)) return base;
298
- throw new Error(`Conflicting Norn reusable config at ${path.join(".")}`);
299
- }
300
- function mergeProjectConfigObjects(base, override) {
301
- const result = { ...base };
302
- for (const [key, overrideValue] of Object.entries(override)) {
303
- result[key] = isPlainObject(result[key]) && isPlainObject(overrideValue) ? mergeProjectConfigObjects(result[key], overrideValue) : overrideValue;
304
- }
305
- return result;
306
- }
307
- function isPlainObject(value) {
308
- return Boolean(value) && typeof value === "object" && !Array.isArray(value);
309
- }
310
- async function expandIncludePath(configRoot, includePath) {
311
- const resolvedIncludePath = resolveConfigPath(configRoot, includePath);
312
- if (!hasGlobSegment(includePath)) return [resolvedIncludePath];
313
- return expandGlobSegments(isAbsolute(includePath) ? sep : configRoot, splitPathSegments(includePath));
314
- }
315
- async function expandGlobSegments(root, segments) {
316
- if (segments.length === 0) return [root];
317
- const [segment, ...remainingSegments] = segments;
318
- if (segment === "*") {
319
- const entries = await readdir(root, { withFileTypes: true });
320
- const expanded = await Promise.all(entries.filter((entry) => entry.isDirectory()).sort((left, right) => left.name.localeCompare(right.name)).map((entry) => expandGlobSegments(join(root, entry.name), remainingSegments)));
321
- return expanded.flat();
322
- }
323
- return expandGlobSegments(join(root, segment), remainingSegments);
324
- }
325
- function resolveConfigPath(configRoot, path) {
326
- if (path.length === 0) throw new Error("Norn path must not be empty");
327
- return isAbsolute(path) ? path : resolve(configRoot, path);
328
- }
329
- function hasGlobSegment(path) {
330
- return splitPathSegments(path).includes("*");
331
- }
332
- function splitPathSegments(path) {
333
- return path.split(/[\\/]+/).filter((segment) => segment.length > 0 && segment !== ".");
334
- }
335
- export {
336
- NORN_PROJECT_FILE_NAME,
337
- discoverNornProject,
338
- findNornProject,
339
- inspectNornWorkflow,
340
- loadNornProject
341
- };
@@ -1,11 +0,0 @@
1
- import type { NornResourceDefinition, NornResources } from "@vimhead.dev/norn";
2
- import { type NornFileCoordinator } from "@vimhead.dev/norn/files";
3
- export declare class NornRunResources implements NornResources {
4
- private readonly runRoot;
5
- readonly files: NornFileCoordinator;
6
- private readonly entries;
7
- private constructor();
8
- static initialize(runRoot: string): Promise<NornRunResources>;
9
- ensure<T>(definition: NornResourceDefinition<T>): Promise<T>;
10
- private initializeResource;
11
- }
package/dist/resources.js DELETED
@@ -1,100 +0,0 @@
1
- // ../core/src/atomic-files.ts
2
- import { randomUUID } from "node:crypto";
3
- import { chmod, mkdir, rename, rm, stat, writeFile } from "node:fs/promises";
4
- import { dirname } from "node:path";
5
-
6
- // ../core/src/errors.ts
7
- function isNodeError(error) {
8
- return error instanceof Error && "code" in error;
9
- }
10
-
11
- // ../core/src/atomic-files.ts
12
- async function writeJsonAtomically(path, value) {
13
- await writeTextAtomically(path, `${JSON.stringify(value, null, 2)}
14
- `);
15
- }
16
- async function writeTextAtomically(path, content) {
17
- await mkdir(dirname(path), { recursive: true });
18
- let existingMode;
19
- try {
20
- existingMode = (await stat(path)).mode & 511;
21
- } catch (error) {
22
- if (!isNodeError(error) || error.code !== "ENOENT") throw error;
23
- }
24
- const tmpPath = `${path}.${randomUUID()}.tmp`;
25
- try {
26
- await writeFile(tmpPath, content, { encoding: "utf8", mode: existingMode });
27
- if (existingMode !== void 0) await chmod(tmpPath, existingMode);
28
- await rename(tmpPath, path);
29
- } catch (error) {
30
- await rm(tmpPath, { force: true }).catch(() => void 0);
31
- throw error;
32
- }
33
- }
34
-
35
- // src/resources.ts
36
- import { createRunFileCoordinator } from "@vimhead.dev/norn/files";
37
- import { jsonValueSchema } from "@vimhead.dev/norn/schema";
38
- import { mkdir as mkdir2, readFile } from "node:fs/promises";
39
- import { join, resolve } from "node:path";
40
- import { isDeepStrictEqual } from "node:util";
41
- import { Type } from "typebox";
42
- import { Value } from "typebox/value";
43
- var definitionSchema = Type.Object({ name: Type.String(), kind: Type.String({ minLength: 1 }), configuration: jsonValueSchema }, { additionalProperties: false });
44
- var recordSchema = Type.Object({ ...definitionSchema.properties, isInitialized: Type.Boolean() }, { additionalProperties: false });
45
- var NornRunResources = class _NornRunResources {
46
- constructor(runRoot) {
47
- this.runRoot = runRoot;
48
- this.files = createRunFileCoordinator(runRoot);
49
- }
50
- runRoot;
51
- files;
52
- entries = /* @__PURE__ */ new Map();
53
- static async initialize(runRoot) {
54
- const resources = new _NornRunResources(resolve(runRoot));
55
- await mkdir2(join(resources.runRoot, "current", "resources"), { recursive: true });
56
- return resources;
57
- }
58
- async ensure(definition) {
59
- if (!/^[a-zA-Z0-9][a-zA-Z0-9_-]*$/.test(definition.name)) throw new Error(`Invalid resource name: ${definition.name}`);
60
- const identity = structuredClone(Value.Parse(definitionSchema, { name: definition.name, kind: definition.kind, configuration: definition.configuration }));
61
- const existing = this.entries.get(definition.name);
62
- if (existing) {
63
- if (!isDeepStrictEqual(existing.definition, identity)) throw new Error(`Incompatible resource definition: ${definition.name}`);
64
- return existing.value;
65
- }
66
- const value = this.initializeResource(definition, identity);
67
- const entry = { definition: identity, value };
68
- this.entries.set(definition.name, entry);
69
- try {
70
- return await value;
71
- } catch (error) {
72
- if (this.entries.get(definition.name) === entry) this.entries.delete(definition.name);
73
- throw error;
74
- }
75
- }
76
- async initializeResource(definition, identity) {
77
- const directory = join(this.runRoot, "current", "resources", definition.name);
78
- await mkdir2(directory, { recursive: true });
79
- return this.files.withExclusiveLock(join(directory, "definition.json"), async (path) => {
80
- let stored;
81
- try {
82
- stored = Value.Parse(recordSchema, JSON.parse(await readFile(path, "utf8")));
83
- } catch (error) {
84
- if (!isNodeError(error) || error.code !== "ENOENT") throw error;
85
- }
86
- if (stored) {
87
- const { isInitialized: _isInitialized, ...storedIdentity } = stored;
88
- if (!isDeepStrictEqual(storedIdentity, identity)) throw new Error(`Incompatible resource definition: ${definition.name}`);
89
- } else {
90
- await writeJsonAtomically(path, { ...identity, isInitialized: false });
91
- }
92
- const value = await definition.initialize({ directory, files: this.files, mode: stored?.isInitialized ? "open" : "create" });
93
- if (!stored?.isInitialized) await writeJsonAtomically(path, { ...identity, isInitialized: true });
94
- return value;
95
- });
96
- }
97
- };
98
- export {
99
- NornRunResources
100
- };