@vimhead.dev/norn-cli 0.1.0-tip.35240723931.1 → 0.1.0-tip.35343816255.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 +2 -24
- package/assets/docs/README.md +3 -1
- package/assets/docs/agents.md +3 -3
- package/assets/docs/cli.md +7 -7
- package/assets/docs/composition.md +64 -34
- package/assets/docs/persistence.md +4 -2
- package/assets/docs/projects.md +3 -12
- package/assets/{setup → docs}/providers.md +4 -3
- package/assets/docs/resources.md +7 -7
- package/assets/docs/schemas.md +130 -0
- package/assets/docs/workflows.md +4 -3
- package/assets/examples/agent-then-analysis/README.md +1 -1
- package/assets/examples/agent-then-analysis/plugin.ts +16 -15
- package/assets/examples/caller-selected-continuation/README.md +75 -0
- package/assets/examples/caller-selected-continuation/caller.ts +47 -0
- package/assets/examples/caller-selected-continuation/input.json +9 -0
- package/assets/examples/caller-selected-continuation/norn.project.json +4 -0
- package/assets/examples/caller-selected-continuation/producer.ts +32 -0
- package/assets/examples/coordinating-multiple-agents/README.md +1 -1
- package/assets/examples/coordinating-multiple-agents/plugin.ts +10 -11
- package/assets/examples/coordinating-multiple-agents/queue-adapter.ts +3 -4
- package/assets/examples/coordinating-multiple-agents/work-queue.ts +18 -18
- package/assets/examples/minimal-workflow/plugin.ts +2 -2
- package/assets/examples/shared-state/README.md +1 -1
- package/assets/examples/shared-state/plugin.ts +6 -6
- package/assets/examples/worktree-development-loop/README.md +105 -44
- package/assets/examples/worktree-development-loop/norn.project.json +1 -1
- package/assets/examples/worktree-development-loop/state.ts +6 -6
- package/assets/examples/worktree-development-loop/workflows/development-loop/execute.ts +1 -1
- package/assets/examples/worktree-development-loop/workflows/development-loop/schema.ts +9 -9
- package/assets/examples/worktree-development-loop/workflows/implementation/execute.ts +1 -1
- package/assets/examples/worktree-development-loop/workflows/implementation/schema.ts +7 -7
- package/assets/examples/worktree-development-loop/workflows/planning/execute.ts +1 -1
- package/assets/examples/worktree-development-loop/workflows/planning/schema.ts +7 -7
- package/assets/examples/worktree-development-loop/workflows/review/execute.ts +1 -1
- package/assets/examples/worktree-development-loop/workflows/review/schema.ts +11 -11
- package/assets/examples/worktree-development-loop/workflows/review-router/execute.ts +1 -1
- package/assets/examples/worktree-development-loop/workflows/review-router/schema.ts +5 -5
- package/assets/package.json +1 -1
- package/assets/packages/cli/src/cli.ts +15 -32
- package/assets/packages/cli/src/client.ts +0 -7
- package/assets/packages/cli/src/generated-build-info.ts +2 -2
- package/assets/packages/cli/src/internal/agent-response-tool.ts +9 -10
- package/assets/packages/cli/src/internal/agents.ts +17 -16
- package/assets/packages/cli/src/internal/engine.ts +20 -18
- package/assets/packages/cli/src/internal/errors.ts +4 -10
- package/assets/packages/cli/src/internal/run-state.ts +4 -0
- package/assets/packages/cli/src/internal/run.ts +4 -7
- package/assets/packages/cli/src/internal/state-store.ts +22 -14
- package/assets/packages/cli/src/internal/workflow-registry.ts +27 -21
- package/assets/packages/cli/src/plugin-loader.ts +33 -45
- package/assets/packages/cli/src/resources.ts +14 -12
- package/assets/packages/core/src/workflow-transition.ts +4 -0
- package/assets/packages/sdk/src/api.ts +87 -131
- package/assets/packages/sdk/src/files.ts +11 -10
- package/assets/packages/sdk/src/index.ts +0 -1
- package/assets/packages/sdk/src/resources.ts +2 -2
- package/assets/packages/sdk/src/schema.ts +53 -29
- package/assets/packages/sdk/src/state-adapter.ts +4 -4
- package/assets/tests/workflow-ref.test.ts +121 -77
- package/dist/cli.js +20 -36
- package/dist/client.d.ts +0 -4
- package/dist/client.js +0 -3
- package/dist/generated-build-info.d.ts +2 -2
- package/dist/generated-build-info.js +2 -2
- package/dist/internal/agent-response-tool.d.ts +2 -2
- package/dist/internal/agent-response-tool.js +7 -7
- package/dist/internal/agents.d.ts +2 -2
- package/dist/internal/agents.js +10 -10
- package/dist/internal/engine.d.ts +1 -1
- package/dist/internal/engine.js +14 -12
- package/dist/internal/errors.d.ts +2 -1
- package/dist/internal/errors.js +5 -8
- package/dist/internal/run-state.js +4 -0
- package/dist/internal/run.d.ts +2 -2
- package/dist/internal/run.js +10 -6
- package/dist/internal/state-store.d.ts +8 -7
- package/dist/internal/state-store.js +15 -9
- package/dist/internal/workflow-registry.d.ts +14 -6
- package/dist/internal/workflow-registry.js +20 -15
- package/dist/plugin-loader.d.ts +15 -21
- package/dist/plugin-loader.js +35 -47
- package/dist/resources.d.ts +1 -1
- package/dist/resources.js +15 -13
- package/package.json +3 -4
- package/assets/packages/sdk/src/seer/config.ts +0 -62
- package/assets/packages/sdk/src/seer/index.ts +0 -7
- package/assets/setup/releases.md +0 -76
|
@@ -7,6 +7,8 @@ import { getRunLeaseHealth } from "./run-lease.ts";
|
|
|
7
7
|
import { writeJsonAtomically } from "@vimhead.dev/norn-core/atomic-files";
|
|
8
8
|
import { createRunFileCoordinator, type NornFileCoordinator } from "@vimhead.dev/norn/files";
|
|
9
9
|
import { runCurrentRoot } from "./run-store.ts";
|
|
10
|
+
import { jsonValueSchema } from "@vimhead.dev/norn/schema";
|
|
11
|
+
import { Value } from "typebox/value";
|
|
10
12
|
|
|
11
13
|
export const RUN_STATE_FILE_NAME = "run-state.json";
|
|
12
14
|
const LEGACY_RUN_STATE_FILE_NAME = "runtime-state.json";
|
|
@@ -208,12 +210,14 @@ export class NornRunStateStore {
|
|
|
208
210
|
await this.files.withExclusiveLock(this.path, async (path) => {
|
|
209
211
|
const current = parseNornRunState(await readRunStateFile({ path, legacyPath: join(dirname(this.path), LEGACY_RUN_STATE_FILE_NAME) }));
|
|
210
212
|
const next = { ...current, ...patch, updatedAt: new Date().toISOString() };
|
|
213
|
+
if (next.current?.params !== undefined) Value.Assert(jsonValueSchema, next.current.params);
|
|
211
214
|
await writeJsonAtomically(path, next);
|
|
212
215
|
this.state = next;
|
|
213
216
|
});
|
|
214
217
|
}
|
|
215
218
|
|
|
216
219
|
private async write(): Promise<void> {
|
|
220
|
+
if (this.state.current?.params !== undefined) Value.Assert(jsonValueSchema, this.state.current.params);
|
|
217
221
|
await mkdir(dirname(this.path), { recursive: true });
|
|
218
222
|
await this.files.withExclusiveLock(this.path, (path) => writeJsonAtomically(path, this.state));
|
|
219
223
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { isAbsolute, relative, resolve, sep } from "node:path";
|
|
2
2
|
import type { CreateAgentSessionOptions } from "@earendil-works/pi-coding-agent";
|
|
3
|
-
import type { NornAnyWorkflowDeclaration, NornProjectRun, NornRunOutcomeMetadata, NornWorkflowIsolationMode,
|
|
3
|
+
import type { NornAnyWorkflowDeclaration, NornProjectRun, NornRunOutcomeMetadata, NornWorkflowIsolationMode, NornRun } from "@vimhead.dev/norn";
|
|
4
|
+
import { createWorkflowTransition } from "@vimhead.dev/norn-core/workflow-transition";
|
|
4
5
|
import type { NornAgentResponseCollector } from "./agent-response-tool.ts";
|
|
5
6
|
import type { NornArtifacts } from "./artifacts.ts";
|
|
6
7
|
import { NornAgentRunner } from "./agents.ts";
|
|
@@ -106,12 +107,8 @@ export class NornRunContext implements NornProjectRun {
|
|
|
106
107
|
return this.resolveFromRoot(this.projectRoot, this.projectRoot, relativePath);
|
|
107
108
|
}
|
|
108
109
|
|
|
109
|
-
next
|
|
110
|
-
return {
|
|
111
|
-
type: "next",
|
|
112
|
-
workflowId: typeof workflow === "string" ? workflow : workflow.id,
|
|
113
|
-
params,
|
|
114
|
-
};
|
|
110
|
+
next(workflowId: string, params: unknown): ReturnType<NornRun["next"]> {
|
|
111
|
+
return createWorkflowTransition({ workflowId, params });
|
|
115
112
|
}
|
|
116
113
|
|
|
117
114
|
complete(metadata?: NornRunOutcomeMetadata): ReturnType<NornRun["complete"]> {
|
|
@@ -1,25 +1,33 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import type { NornWorkflowStateDefinition, NornWorkflowState } from "@vimhead.dev/norn";
|
|
3
|
-
import type { NornFileCoordinator } from "@vimhead.dev/norn/files";
|
|
4
|
-
import { isNodeError } from "@vimhead.dev/norn-core/errors";
|
|
1
|
+
import type { NornWorkflowState, NornWorkflowStateDefinition } from "@vimhead.dev/norn";
|
|
5
2
|
import { writeJsonAtomically } from "@vimhead.dev/norn-core/atomic-files";
|
|
3
|
+
import { isNodeError } from "@vimhead.dev/norn-core/errors";
|
|
4
|
+
import type { NornFileCoordinator } from "@vimhead.dev/norn/files";
|
|
5
|
+
import { jsonValueSchema } from "@vimhead.dev/norn/schema";
|
|
6
|
+
import { readFile } from "node:fs/promises";
|
|
7
|
+
import type { Static, TSchema } from "typebox";
|
|
8
|
+
import { Value } from "typebox/value";
|
|
9
|
+
|
|
10
|
+
function copyStateValue<Schema extends TSchema>(schema: Schema, value: unknown): Static<Schema> {
|
|
11
|
+
Value.Assert(jsonValueSchema, value);
|
|
12
|
+
return structuredClone(Value.Parse(schema, value));
|
|
13
|
+
}
|
|
6
14
|
|
|
7
15
|
export class NornMemoryWorkflowState implements NornWorkflowState {
|
|
8
16
|
private readonly data = new Map<string, unknown>();
|
|
9
17
|
|
|
10
|
-
async get<
|
|
18
|
+
async get<Schema extends TSchema>(state: NornWorkflowStateDefinition<Schema>): Promise<Static<Schema>> {
|
|
11
19
|
const value = await this.getOptional(state);
|
|
12
20
|
if (value === undefined) throw new Error(`Missing workflow state: ${state.id}`);
|
|
13
21
|
return value;
|
|
14
22
|
}
|
|
15
23
|
|
|
16
|
-
async getOptional<
|
|
24
|
+
async getOptional<Schema extends TSchema>(state: NornWorkflowStateDefinition<Schema>): Promise<Static<Schema> | undefined> {
|
|
17
25
|
if (!this.data.has(state.id)) return undefined;
|
|
18
|
-
return state.schema
|
|
26
|
+
return copyStateValue(state.schema, this.data.get(state.id));
|
|
19
27
|
}
|
|
20
28
|
|
|
21
|
-
async set<
|
|
22
|
-
this.data.set(state.id, state.schema
|
|
29
|
+
async set<Schema extends TSchema>(state: NornWorkflowStateDefinition<Schema>, value: NoInfer<Static<Schema>>): Promise<void> {
|
|
30
|
+
this.data.set(state.id, copyStateValue(state.schema, value));
|
|
23
31
|
}
|
|
24
32
|
}
|
|
25
33
|
|
|
@@ -45,20 +53,20 @@ export class NornJsonWorkflowState implements NornWorkflowState {
|
|
|
45
53
|
});
|
|
46
54
|
}
|
|
47
55
|
|
|
48
|
-
async get<
|
|
56
|
+
async get<Schema extends TSchema>(state: NornWorkflowStateDefinition<Schema>): Promise<Static<Schema>> {
|
|
49
57
|
const value = await this.getOptional(state);
|
|
50
58
|
if (value === undefined) throw new Error(`Missing workflow state: ${state.id}`);
|
|
51
59
|
return value;
|
|
52
60
|
}
|
|
53
61
|
|
|
54
|
-
async getOptional<
|
|
62
|
+
async getOptional<Schema extends TSchema>(state: NornWorkflowStateDefinition<Schema>): Promise<Static<Schema> | undefined> {
|
|
55
63
|
const data = await this.input.files.withExclusiveLock(this.stateFile, (path) => this.readStateFile(path));
|
|
56
64
|
if (!Object.prototype.hasOwnProperty.call(data, state.id)) return undefined;
|
|
57
|
-
return state.schema
|
|
65
|
+
return copyStateValue(state.schema, data[state.id]);
|
|
58
66
|
}
|
|
59
67
|
|
|
60
|
-
async set<
|
|
61
|
-
const parsedValue = state.schema
|
|
68
|
+
async set<Schema extends TSchema>(state: NornWorkflowStateDefinition<Schema>, value: NoInfer<Static<Schema>>): Promise<void> {
|
|
69
|
+
const parsedValue = copyStateValue(state.schema, value);
|
|
62
70
|
await this.input.coordinateMutation(this.stateFile, () => this.input.files.withExclusiveLock(this.stateFile, async (path) => {
|
|
63
71
|
const data = await this.readStateFile(path);
|
|
64
72
|
Object.defineProperty(data, state.id, { value: parsedValue, enumerable: true, configurable: true, writable: true });
|
|
@@ -1,15 +1,26 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
1
|
+
import { isWorkflowComplete, isWorkflowFail, isWorkflowNext, type NornAnyWorkflowDeclaration, type NornDispose, type NornInspectedWorkflowInfo, type NornRegisteredWorkflowInfo, type NornRunComplete, type NornRunFail, type NornRunFor, type NornRunNext, type NornWorkflowGateInfo, type NornWorkflowImplementation, type NornWorkflowParams, type NornWorkflowPluginInfo } from "@vimhead.dev/norn";
|
|
2
|
+
import { assertWorkflowMetadata, inspectSchema, isPlainObject, schemaShape, schemaType, unwrapSchema } from "@vimhead.dev/norn/schema";
|
|
3
|
+
import { type TSchema } from "typebox";
|
|
4
|
+
import { Value } from "typebox/value";
|
|
4
5
|
|
|
5
6
|
export type NornRegisteredWorkflow = {
|
|
6
7
|
workflow: NornAnyWorkflowDeclaration;
|
|
7
8
|
implementation: NornWorkflowImplementation<NornAnyWorkflowDeclaration, unknown>;
|
|
8
|
-
|
|
9
|
-
config: unknown;
|
|
9
|
+
configuration?: NornRegisteredConfiguration;
|
|
10
10
|
plugin?: NornWorkflowPluginInfo;
|
|
11
11
|
};
|
|
12
12
|
|
|
13
|
+
export type NornRegisteredConfiguration = { readonly schema: TSchema; readonly input: unknown; readonly value: unknown };
|
|
14
|
+
|
|
15
|
+
export function decodePluginConfiguration(input: { readonly pluginId: string; readonly schema: TSchema | undefined; readonly value: unknown }): NornRegisteredConfiguration | undefined {
|
|
16
|
+
if (!input.schema) {
|
|
17
|
+
if (input.value !== undefined) throw new Error(`Norn config provided for plugin without config schema: ${input.pluginId}`);
|
|
18
|
+
return undefined;
|
|
19
|
+
}
|
|
20
|
+
const value = defaultConfigInput(input.schema, input.value);
|
|
21
|
+
return { schema: input.schema, input: value, value: Value.Decode(input.schema, value) };
|
|
22
|
+
}
|
|
23
|
+
|
|
13
24
|
export type NornWorkflowStepResult =
|
|
14
25
|
| NornRunNext
|
|
15
26
|
| { readonly type: "complete"; readonly workflow: NornAnyWorkflowDeclaration; readonly metadata?: NornRunComplete["metadata"] }
|
|
@@ -21,15 +32,14 @@ export class NornWorkflowRegistry {
|
|
|
21
32
|
register<TWorkflow extends NornAnyWorkflowDeclaration>(
|
|
22
33
|
workflow: TWorkflow,
|
|
23
34
|
implementation: NornWorkflowImplementation<TWorkflow, unknown>,
|
|
24
|
-
metadata: { readonly plugin?: NornWorkflowPluginInfo; readonly
|
|
35
|
+
metadata: { readonly plugin?: NornWorkflowPluginInfo; readonly configuration?: NornRegisteredConfiguration } = {},
|
|
25
36
|
): NornDispose {
|
|
26
37
|
if (this.entries.has(workflow.id)) throw new Error(`Workflow already registered: ${workflow.id}`);
|
|
27
38
|
|
|
28
39
|
const entry: NornRegisteredWorkflow = {
|
|
29
40
|
workflow,
|
|
30
41
|
implementation: implementation as NornWorkflowImplementation<NornAnyWorkflowDeclaration, unknown>,
|
|
31
|
-
|
|
32
|
-
config: metadata.configSchema ? metadata.configSchema.parse(defaultConfigInput(metadata.configSchema, metadata.config)) : undefined,
|
|
42
|
+
configuration: metadata.configuration,
|
|
33
43
|
plugin: metadata.plugin,
|
|
34
44
|
};
|
|
35
45
|
assertWorkflowMetadata(workflow);
|
|
@@ -68,7 +78,7 @@ export class NornWorkflowRegistry {
|
|
|
68
78
|
const entry = this.entries.get(workflow.id);
|
|
69
79
|
if (!entry) throw new Error(`Unknown workflow: ${workflow.id}`);
|
|
70
80
|
if (!workflow.gate) throw new Error(`Workflow is not gated: ${workflow.id}`);
|
|
71
|
-
const parsedParams = workflow.params
|
|
81
|
+
const parsedParams = Value.Decode(workflow.params, params) as NornWorkflowParams<TWorkflow>;
|
|
72
82
|
const parsedConfig = parseExecutionConfig(entry, configOverride);
|
|
73
83
|
const description = await (entry.implementation as NornWorkflowImplementation<TWorkflow, unknown>).gate?.describe(run, parsedParams, parsedConfig);
|
|
74
84
|
return validateGateDescription(description ?? workflow.id, workflow.id);
|
|
@@ -83,7 +93,7 @@ export class NornWorkflowRegistry {
|
|
|
83
93
|
const entry = this.entries.get(workflow.id);
|
|
84
94
|
if (!entry) throw new Error(`Unknown workflow: ${workflow.id}`);
|
|
85
95
|
|
|
86
|
-
const parsedParams = workflow.params
|
|
96
|
+
const parsedParams = Value.Decode(workflow.params, params) as NornWorkflowParams<TWorkflow>;
|
|
87
97
|
const parsedConfig = parseExecutionConfig(entry, configOverride);
|
|
88
98
|
const result = await (entry.implementation as NornWorkflowImplementation<TWorkflow, unknown>).execute(run, parsedParams, parsedConfig);
|
|
89
99
|
if (isWorkflowNext(result)) return result;
|
|
@@ -97,22 +107,22 @@ export class NornWorkflowRegistry {
|
|
|
97
107
|
}
|
|
98
108
|
}
|
|
99
109
|
|
|
100
|
-
function defaultConfigInput(configSchema:
|
|
110
|
+
function defaultConfigInput(configSchema: TSchema, config: unknown): unknown {
|
|
101
111
|
if (config !== undefined) return config;
|
|
102
112
|
return schemaType(unwrapSchema(configSchema)) === "object" ? {} : undefined;
|
|
103
113
|
}
|
|
104
114
|
|
|
105
115
|
function parseExecutionConfig(entry: NornRegisteredWorkflow, configOverride: unknown): unknown {
|
|
106
116
|
const pluginConfigOverride = pluginConfigOverrideInput(entry, configOverride);
|
|
107
|
-
if (!entry.
|
|
117
|
+
if (!entry.configuration) {
|
|
108
118
|
if (pluginConfigOverride !== undefined) throw new Error(`Run config override provided for plugin without config schema: ${entry.plugin?.id ?? entry.workflow.id}`);
|
|
109
119
|
return undefined;
|
|
110
120
|
}
|
|
111
|
-
if (pluginConfigOverride === undefined) return entry.
|
|
112
|
-
const rawConfig = isPlainObject(entry.
|
|
113
|
-
? deepMerge(entry.
|
|
121
|
+
if (pluginConfigOverride === undefined) return entry.configuration.value;
|
|
122
|
+
const rawConfig = isPlainObject(entry.configuration.input) && isPlainObject(pluginConfigOverride)
|
|
123
|
+
? deepMerge(entry.configuration.input, pluginConfigOverride)
|
|
114
124
|
: pluginConfigOverride;
|
|
115
|
-
return entry.
|
|
125
|
+
return Value.Decode(entry.configuration.schema, rawConfig);
|
|
116
126
|
}
|
|
117
127
|
|
|
118
128
|
function pluginConfigOverrideInput(entry: NornRegisteredWorkflow, configOverride: unknown): unknown {
|
|
@@ -152,7 +162,7 @@ function validateGateDescription(description: string, workflowId: string): strin
|
|
|
152
162
|
function inspectedWorkflowInfo(entry: NornRegisteredWorkflow): NornInspectedWorkflowInfo {
|
|
153
163
|
return {
|
|
154
164
|
...workflowInfo(entry),
|
|
155
|
-
paramsSchema:
|
|
165
|
+
paramsSchema: inspectSchema(entry.workflow.params),
|
|
156
166
|
gate: gateInfo(entry.workflow),
|
|
157
167
|
};
|
|
158
168
|
}
|
|
@@ -170,7 +180,3 @@ function workflowInfo(entry: NornRegisteredWorkflow): NornRegisteredWorkflowInfo
|
|
|
170
180
|
function gateInfo(workflow: NornAnyWorkflowDeclaration): NornWorkflowGateInfo | null {
|
|
171
181
|
return workflow.gate ? { enabled: true, fields: workflow.gate.fields } : null;
|
|
172
182
|
}
|
|
173
|
-
|
|
174
|
-
function jsonSchema(schema: z.ZodType): NornJsonSchema {
|
|
175
|
-
return z.toJSONSchema(schema, { io: "input" }) as NornJsonSchema;
|
|
176
|
-
}
|
|
@@ -1,39 +1,37 @@
|
|
|
1
|
-
import { access, readFile, readdir } from "node:fs/promises";
|
|
2
|
-
import { dirname, isAbsolute, join, resolve, sep } from "node:path";
|
|
3
|
-
import { pathToFileURL } from "node:url";
|
|
4
|
-
import { createJiti } from "jiti/static";
|
|
5
|
-
import * as typeboxModule from "typebox";
|
|
6
|
-
import { z } from "zod";
|
|
7
|
-
import * as zodModule from "zod";
|
|
8
1
|
import * as nornModule from "@vimhead.dev/norn";
|
|
2
|
+
import { isWorkflowPlugin, type NornDispose, type NornPluginDiagnostic, type NornProjectInfo, type NornProjectInspection, type NornProjectPluginInfo, type NornWorkflowCatalogInfo, type NornWorkflowInspection, type NornWorkflowPlugin, type NornWorkflowPluginInfo } from "@vimhead.dev/norn";
|
|
3
|
+
import { isNodeError } from "@vimhead.dev/norn-core/errors";
|
|
9
4
|
import * as nornFilesModule from "@vimhead.dev/norn/files";
|
|
10
5
|
import * as nornSchemaModule from "@vimhead.dev/norn/schema";
|
|
11
|
-
import
|
|
12
|
-
import {
|
|
6
|
+
import { inspectSchema } from "@vimhead.dev/norn/schema";
|
|
7
|
+
import { createJiti } from "jiti/static";
|
|
8
|
+
import { access, readdir, readFile } from "node:fs/promises";
|
|
9
|
+
import { dirname, isAbsolute, join, resolve, sep } from "node:path";
|
|
10
|
+
import { pathToFileURL } from "node:url";
|
|
11
|
+
import * as typeboxModule from "typebox";
|
|
12
|
+
import { Type, type StaticDecode } from "typebox";
|
|
13
|
+
import * as typeboxCompileModule from "typebox/compile";
|
|
14
|
+
import * as typeboxSchemaModule from "typebox/schema";
|
|
15
|
+
import * as typeboxValueModule from "typebox/value";
|
|
16
|
+
import { AssertError, Value } from "typebox/value";
|
|
13
17
|
import { errorMessage, NornProjectLoadError } from "./internal/errors.ts";
|
|
14
|
-
import { isNodeError } from "@vimhead.dev/norn-core/errors";
|
|
15
18
|
import { NornMemoryWorkflowState } from "./internal/state-store.ts";
|
|
16
|
-
import { NornWorkflowRegistry, type NornRegisteredWorkflow } from "./internal/workflow-registry.ts";
|
|
17
|
-
import { schemaType, unwrapSchema } from "@vimhead.dev/norn/schema";
|
|
18
|
-
import { resolveSeerModeConfig, type NornResolvedSeerModeConfig } from "@vimhead.dev/norn/seer";
|
|
19
|
+
import { decodePluginConfiguration, NornWorkflowRegistry, type NornRegisteredWorkflow } from "./internal/workflow-registry.ts";
|
|
19
20
|
|
|
20
21
|
export const NORN_PROJECT_FILE_NAME = "norn.project.json";
|
|
21
22
|
|
|
22
|
-
const
|
|
23
|
-
|
|
23
|
+
const nornConfigSchema = Type.Object({
|
|
24
|
+
plugins: Type.Array(Type.String({ minLength: 1 }), { default: [] }),
|
|
25
|
+
includes: Type.Array(Type.String({ minLength: 1 }), { default: [] }),
|
|
26
|
+
config: Type.Record(Type.String(), Type.Unknown(), { default: {} }),
|
|
24
27
|
});
|
|
25
|
-
const
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
config: z.record(z.string(), z.unknown()).default({}),
|
|
29
|
-
});
|
|
30
|
-
const nornProjectConfigSchema = nornConfigSchema.extend({
|
|
31
|
-
version: z.literal(1).default(1),
|
|
32
|
-
seerMode: seerModeConfigSchema.optional(),
|
|
28
|
+
const nornProjectConfigSchema = Type.Object({
|
|
29
|
+
...nornConfigSchema.properties,
|
|
30
|
+
version: Type.Literal(1, { default: 1 }),
|
|
33
31
|
});
|
|
34
32
|
|
|
35
|
-
type NornConfig =
|
|
36
|
-
type NornProjectConfig =
|
|
33
|
+
type NornConfig = StaticDecode<typeof nornConfigSchema>;
|
|
34
|
+
type NornProjectConfig = StaticDecode<typeof nornProjectConfigSchema>;
|
|
37
35
|
|
|
38
36
|
type NornConfigFile = {
|
|
39
37
|
readonly path: string;
|
|
@@ -50,7 +48,6 @@ export type NornProject = {
|
|
|
50
48
|
readonly config: NornProjectConfig;
|
|
51
49
|
readonly configFiles: readonly NornConfigFile[];
|
|
52
50
|
readonly projectConfig: Record<string, unknown>;
|
|
53
|
-
readonly seerMode?: NornResolvedSeerModeConfig;
|
|
54
51
|
};
|
|
55
52
|
|
|
56
53
|
export type NornLoadedProject = NornProject & {
|
|
@@ -101,7 +98,6 @@ function buildProjectInfo(project: NornLoadedProject): NornProjectInfo {
|
|
|
101
98
|
cwd: project.cwd, projectPath: project.projectPath, projectRoot: project.projectRoot,
|
|
102
99
|
configPath: project.configPath, configRoot: project.configRoot,
|
|
103
100
|
configFiles: project.configFiles.map(file => file.path), plugins: project.pluginInfos,
|
|
104
|
-
seerMode: project.seerMode ?? null,
|
|
105
101
|
};
|
|
106
102
|
}
|
|
107
103
|
|
|
@@ -118,7 +114,6 @@ export async function findNornProject(cwd: string): Promise<NornProject> {
|
|
|
118
114
|
config: projectRootConfig.config,
|
|
119
115
|
configFiles,
|
|
120
116
|
projectConfig: mergeProjectConfig(configFiles),
|
|
121
|
-
seerMode: resolveSeerModeConfig({ configPath: projectRootConfig.path, configRoot: projectRootConfig.root, seerMode: projectRootConfig.config.seerMode }),
|
|
122
117
|
};
|
|
123
118
|
}
|
|
124
119
|
|
|
@@ -165,12 +160,12 @@ async function loadNornConfigTree(configFile: NornConfigFile, visitedPaths: Set<
|
|
|
165
160
|
}
|
|
166
161
|
|
|
167
162
|
async function readNornProjectConfigFile(path: string): Promise<NornProjectConfigFile> {
|
|
168
|
-
const config = nornProjectConfigSchema.
|
|
163
|
+
const config = Value.Parse(nornProjectConfigSchema, Value.Default(nornProjectConfigSchema, JSON.parse(await readFile(path, "utf8"))));
|
|
169
164
|
return { path, root: dirname(path), config };
|
|
170
165
|
}
|
|
171
166
|
|
|
172
167
|
async function readNornConfigFile(path: string): Promise<NornConfigFile> {
|
|
173
|
-
const config = nornConfigSchema.
|
|
168
|
+
const config = Value.Parse(nornConfigSchema, Value.Default(nornConfigSchema, JSON.parse(await readFile(path, "utf8"))));
|
|
174
169
|
return { path, root: dirname(path), config };
|
|
175
170
|
}
|
|
176
171
|
|
|
@@ -275,12 +270,11 @@ function registerProjectPlugin(input: {
|
|
|
275
270
|
let stage: NornPluginDiagnostic["stage"] = "config";
|
|
276
271
|
try {
|
|
277
272
|
const configInput = input.project.projectConfig[plugin.manifest.id];
|
|
278
|
-
|
|
279
|
-
const config = plugin.manifest.config ? plugin.manifest.config.parse(defaultConfigInput(plugin.manifest.config, configInput)) : undefined;
|
|
273
|
+
const configuration = decodePluginConfiguration({ pluginId: plugin.manifest.id, schema: plugin.manifest.config, value: configInput });
|
|
280
274
|
stage = "schema";
|
|
281
275
|
const info: NornProjectPluginInfo = {
|
|
282
276
|
id: plugin.manifest.id, path: source.pluginPath, configPath: source.configPath,
|
|
283
|
-
configSchema: plugin.manifest.config ?
|
|
277
|
+
configSchema: plugin.manifest.config ? inspectSchema(plugin.manifest.config) : null, config: configuration?.value,
|
|
284
278
|
};
|
|
285
279
|
stage = "implementation";
|
|
286
280
|
const implementation = typeof plugin.implementation === "function" ? plugin.implementation({ cwd: input.project.cwd, state: input.state }) : plugin.implementation;
|
|
@@ -292,9 +286,9 @@ function registerProjectPlugin(input: {
|
|
|
292
286
|
stage = "duplicate";
|
|
293
287
|
if (input.registry.workflowById(workflow.id)) throw new Error(`Workflow already registered: ${workflow.id}`);
|
|
294
288
|
stage = "declaration";
|
|
295
|
-
unregister.push(input.registry.register(workflow, workflowImplementation, { plugin: workflowPluginInfo(info),
|
|
289
|
+
unregister.push(input.registry.register(workflow, workflowImplementation, { plugin: workflowPluginInfo(info), configuration }));
|
|
296
290
|
} catch (error) {
|
|
297
|
-
diagnostics.push(createPluginDiagnostic({ source, workflowId: workflow.id, stage
|
|
291
|
+
diagnostics.push(createPluginDiagnostic({ source, workflowId: workflow.id, stage, error }));
|
|
298
292
|
}
|
|
299
293
|
}
|
|
300
294
|
if (diagnostics.length === 0) return { loaded: { plugin, info }, diagnostics };
|
|
@@ -313,9 +307,7 @@ function createPluginDiagnostic(input: {
|
|
|
313
307
|
}): NornPluginDiagnostic {
|
|
314
308
|
return {
|
|
315
309
|
...input.source, workflowId: input.workflowId, stage: input.stage, message: errorMessage(input.error),
|
|
316
|
-
issues: input.error instanceof
|
|
317
|
-
path: issue.path.map(part => typeof part === "symbol" ? String(part) : part), code: issue.code, message: issue.message,
|
|
318
|
-
})) : [],
|
|
310
|
+
issues: input.error instanceof AssertError ? input.error.cause.errors : [],
|
|
319
311
|
};
|
|
320
312
|
}
|
|
321
313
|
|
|
@@ -328,9 +320,10 @@ function nornWorkflowVirtualModules(): Record<string, unknown> {
|
|
|
328
320
|
"@vimhead.dev/norn": nornModule,
|
|
329
321
|
"@vimhead.dev/norn/files": nornFilesModule,
|
|
330
322
|
"@vimhead.dev/norn/schema": nornSchemaModule,
|
|
331
|
-
"@vimhead.dev/norn/seer": nornSeerModule,
|
|
332
323
|
typebox: typeboxModule,
|
|
333
|
-
|
|
324
|
+
"typebox/value": typeboxValueModule,
|
|
325
|
+
"typebox/compile": typeboxCompileModule,
|
|
326
|
+
"typebox/schema": typeboxSchemaModule,
|
|
334
327
|
};
|
|
335
328
|
}
|
|
336
329
|
|
|
@@ -373,11 +366,6 @@ function isPlainObject(value: unknown): value is Record<string, unknown> {
|
|
|
373
366
|
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
374
367
|
}
|
|
375
368
|
|
|
376
|
-
function defaultConfigInput(configSchema: z.ZodType, config: unknown): unknown {
|
|
377
|
-
if (config !== undefined) return config;
|
|
378
|
-
return schemaType(unwrapSchema(configSchema)) === "object" ? {} : undefined;
|
|
379
|
-
}
|
|
380
|
-
|
|
381
369
|
async function expandIncludePath(configRoot: string, includePath: string): Promise<string[]> {
|
|
382
370
|
const resolvedIncludePath = resolveConfigPath(configRoot, includePath);
|
|
383
371
|
if (!hasGlobSegment(includePath)) return [resolvedIncludePath];
|
|
@@ -1,18 +1,20 @@
|
|
|
1
|
+
import type { NornResourceDefinition, NornResources } from "@vimhead.dev/norn";
|
|
2
|
+
import { writeJsonAtomically } from "@vimhead.dev/norn-core/atomic-files";
|
|
3
|
+
import { isNodeError } from "@vimhead.dev/norn-core/errors";
|
|
4
|
+
import { createRunFileCoordinator, type NornFileCoordinator } from "@vimhead.dev/norn/files";
|
|
5
|
+
import { jsonValueSchema } from "@vimhead.dev/norn/schema";
|
|
1
6
|
import { mkdir, readFile } from "node:fs/promises";
|
|
2
7
|
import { join, resolve } from "node:path";
|
|
3
8
|
import { isDeepStrictEqual } from "node:util";
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import type { NornResourceDefinition, NornResources } from "@vimhead.dev/norn";
|
|
7
|
-
import { isNodeError } from "@vimhead.dev/norn-core/errors";
|
|
8
|
-
import { writeJsonAtomically } from "@vimhead.dev/norn-core/atomic-files";
|
|
9
|
+
import { Type, type StaticDecode } from "typebox";
|
|
10
|
+
import { Value } from "typebox/value";
|
|
9
11
|
|
|
10
|
-
const definitionSchema =
|
|
11
|
-
const recordSchema =
|
|
12
|
+
const definitionSchema = Type.Object({ name: Type.String(), kind: Type.String({ minLength: 1 }), configuration: jsonValueSchema }, { additionalProperties: false });
|
|
13
|
+
const recordSchema = Type.Object({ ...definitionSchema.properties, isInitialized: Type.Boolean() }, { additionalProperties: false });
|
|
12
14
|
|
|
13
15
|
export class NornRunResources implements NornResources {
|
|
14
16
|
readonly files: NornFileCoordinator;
|
|
15
|
-
private readonly entries = new Map<string, { readonly definition:
|
|
17
|
+
private readonly entries = new Map<string, { readonly definition: StaticDecode<typeof definitionSchema>; readonly value: Promise<unknown> }>();
|
|
16
18
|
|
|
17
19
|
private constructor(private readonly runRoot: string) {
|
|
18
20
|
this.files = createRunFileCoordinator(runRoot);
|
|
@@ -26,7 +28,7 @@ export class NornRunResources implements NornResources {
|
|
|
26
28
|
|
|
27
29
|
async ensure<T>(definition: NornResourceDefinition<T>): Promise<T> {
|
|
28
30
|
if (!/^[a-zA-Z0-9][a-zA-Z0-9_-]*$/.test(definition.name)) throw new Error(`Invalid resource name: ${definition.name}`);
|
|
29
|
-
const identity =
|
|
31
|
+
const identity = structuredClone(Value.Parse(definitionSchema, { name: definition.name, kind: definition.kind, configuration: definition.configuration }));
|
|
30
32
|
const existing = this.entries.get(definition.name);
|
|
31
33
|
if (existing) {
|
|
32
34
|
if (!isDeepStrictEqual(existing.definition, identity)) throw new Error(`Incompatible resource definition: ${definition.name}`);
|
|
@@ -43,13 +45,13 @@ export class NornRunResources implements NornResources {
|
|
|
43
45
|
}
|
|
44
46
|
}
|
|
45
47
|
|
|
46
|
-
private async initializeResource<T>(definition: NornResourceDefinition<T>, identity:
|
|
48
|
+
private async initializeResource<T>(definition: NornResourceDefinition<T>, identity: StaticDecode<typeof definitionSchema>): Promise<T> {
|
|
47
49
|
const directory = join(this.runRoot, "current", "resources", definition.name);
|
|
48
50
|
await mkdir(directory, { recursive: true });
|
|
49
51
|
return this.files.withExclusiveLock(join(directory, "definition.json"), async (path) => {
|
|
50
|
-
let stored:
|
|
52
|
+
let stored: StaticDecode<typeof recordSchema> | undefined;
|
|
51
53
|
try {
|
|
52
|
-
stored =
|
|
54
|
+
stored = Value.Parse(recordSchema, JSON.parse(await readFile(path, "utf8")));
|
|
53
55
|
} catch (error) {
|
|
54
56
|
if (!isNodeError(error) || error.code !== "ENOENT") throw error;
|
|
55
57
|
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export function createWorkflowTransition(input: { readonly workflowId: string; readonly params: unknown }): { readonly type: "next"; readonly workflowId: string; readonly params: unknown } {
|
|
2
|
+
if (typeof input.workflowId !== "string" || input.workflowId.trim().length === 0) throw new Error("Workflow transition requires a nonempty workflow ID");
|
|
3
|
+
return { type: "next", workflowId: input.workflowId, params: input.params };
|
|
4
|
+
}
|