@vimhead.dev/norn 0.1.0-tip.35390859630.1 → 0.1.0-tip.35568631545.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/dist/api.d.ts +44 -64
- package/dist/api.js +16 -21
- package/dist/index.d.ts +0 -3
- package/dist/index.js +0 -6
- package/package.json +1 -5
- package/dist/agent-resource-adapter.d.ts +0 -12
- package/dist/agent-resource-adapter.js +0 -0
- package/dist/files.d.ts +0 -17
- package/dist/files.js +0 -162
- package/dist/resources.d.ts +0 -17
- package/dist/resources.js +0 -0
package/dist/api.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { CreateAgentSessionOptions, EventBus, PromptOptions } from "@earendil-works/pi-coding-agent";
|
|
1
|
+
import type { CreateAgentSessionOptions, EventBus, PromptOptions, ToolDefinition } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import { Type, type StaticDecode, type StaticEncode, type TCodec, type TSchema } from "typebox";
|
|
3
3
|
import type { TLocalizedValidationError } from "typebox/error";
|
|
4
4
|
declare const WORKFLOW_DECLARATION_KIND = "norn.workflow";
|
|
@@ -8,11 +8,13 @@ export type NornWorkflowAnyGate = {
|
|
|
8
8
|
readonly enabled: true;
|
|
9
9
|
readonly fields?: readonly string[];
|
|
10
10
|
};
|
|
11
|
-
export type
|
|
12
|
-
|
|
13
|
-
readonly
|
|
11
|
+
export type NornWorkflowPaths = {
|
|
12
|
+
/** Absolute project root. Excluded from run checkpoints and rollback. */
|
|
13
|
+
readonly project: string;
|
|
14
|
+
/** Absolute, initially empty workspace. Saved in run checkpoints and restored on rollback. */
|
|
15
|
+
readonly workspace: string;
|
|
14
16
|
};
|
|
15
|
-
export type NornWorkflowDeclaration<Id extends string = string, ArgsSchema extends TSchema = TSchema,
|
|
17
|
+
export type NornWorkflowDeclaration<Id extends string = string, ArgsSchema extends TSchema = TSchema, ConfigSchema extends TSchema | undefined = TSchema | undefined, Scope extends NornWorkflowScopeInfo | undefined = NornWorkflowScopeInfo | undefined> = {
|
|
16
18
|
(args: StaticEncode<ArgsSchema>): NornRunNext;
|
|
17
19
|
readonly kind: typeof WORKFLOW_DECLARATION_KIND;
|
|
18
20
|
readonly id: Id;
|
|
@@ -20,12 +22,11 @@ export type NornWorkflowDeclaration<Id extends string = string, ArgsSchema exten
|
|
|
20
22
|
readonly instructions?: string;
|
|
21
23
|
readonly args: ArgsSchema;
|
|
22
24
|
readonly gate?: NornWorkflowGate<ArgsSchema> & {
|
|
23
|
-
describe?(context: NornWorkflowContext<ArgsSchema,
|
|
25
|
+
describe?(context: NornWorkflowContext<ArgsSchema, ConfigSchema, Scope>): MaybePromise<string>;
|
|
24
26
|
};
|
|
25
|
-
readonly isolation: NornWorkflowIsolation<IsolationMode>;
|
|
26
27
|
readonly config?: ConfigSchema;
|
|
27
28
|
readonly scope: Scope;
|
|
28
|
-
execute(context: NornWorkflowContext<ArgsSchema,
|
|
29
|
+
execute(context: NornWorkflowContext<ArgsSchema, ConfigSchema, Scope>): MaybePromise<WorkflowResult>;
|
|
29
30
|
};
|
|
30
31
|
export type NornAnyWorkflowDeclaration = Pick<NornWorkflowDeclaration, keyof NornWorkflowDeclaration> & ((args: never) => NornRunNext);
|
|
31
32
|
export type NornWorkflowRefSchemaOptions<ArgsSchema extends TSchema = TSchema> = {
|
|
@@ -49,48 +50,50 @@ type NornWorkflowInstructions = {
|
|
|
49
50
|
readonly isEntrypoint: false;
|
|
50
51
|
readonly instructions?: string;
|
|
51
52
|
};
|
|
52
|
-
type RunForIsolation<Mode extends NornWorkflowIsolationMode> = Mode extends "project" ? NornProjectRun : NornRun;
|
|
53
53
|
type WorkflowConfig<Schema extends TSchema | undefined> = Schema extends TSchema ? StaticDecode<Schema> : undefined;
|
|
54
|
-
export type NornWorkflowContext<ArgsSchema extends TSchema = TSchema,
|
|
54
|
+
export type NornWorkflowContext<ArgsSchema extends TSchema = TSchema, ConfigSchema extends TSchema | undefined = TSchema | undefined, Scope extends NornWorkflowScopeInfo | undefined = NornWorkflowScopeInfo | undefined> = {
|
|
55
55
|
readonly args: StaticDecode<ArgsSchema>;
|
|
56
56
|
readonly config: WorkflowConfig<ConfigSchema>;
|
|
57
|
-
readonly
|
|
57
|
+
readonly paths: NornWorkflowPaths;
|
|
58
|
+
readonly agents: NornAgents;
|
|
59
|
+
readonly commands: NornCommands;
|
|
60
|
+
readonly logs: NornLogs;
|
|
61
|
+
readonly run: NornRun;
|
|
58
62
|
} & (Scope extends NornWorkflowScopeInfo ? {
|
|
59
63
|
readonly scope: {
|
|
60
64
|
readonly id: Scope["id"];
|
|
61
65
|
readonly config: WorkflowConfig<Scope["config"]>;
|
|
62
66
|
};
|
|
63
67
|
} : {});
|
|
64
|
-
export type NornWorkflowDefinition<
|
|
65
|
-
readonly
|
|
68
|
+
export type NornWorkflowDefinition<Name extends string = string, ArgsSchema extends TSchema = TSchema, ConfigSchema extends TSchema | undefined = undefined, Scope extends NornWorkflowScopeInfo | undefined = undefined> = {
|
|
69
|
+
readonly name: Name;
|
|
66
70
|
readonly args: ArgsSchema;
|
|
67
71
|
readonly config?: ConfigSchema;
|
|
68
|
-
readonly isolation?: NornWorkflowIsolation<IsolationMode>;
|
|
69
72
|
readonly gate?: NornWorkflowGate<ArgsSchema> & {
|
|
70
|
-
describe?(context: NornWorkflowContext<ArgsSchema,
|
|
73
|
+
describe?(context: NornWorkflowContext<ArgsSchema, ConfigSchema, Scope>): MaybePromise<string>;
|
|
71
74
|
};
|
|
72
|
-
execute(context: NornWorkflowContext<ArgsSchema,
|
|
75
|
+
execute(context: NornWorkflowContext<ArgsSchema, ConfigSchema, Scope>): MaybePromise<WorkflowResult>;
|
|
73
76
|
} & NornWorkflowInstructions;
|
|
74
77
|
export type NornWorkflowScopeInfo = {
|
|
75
78
|
readonly id: string;
|
|
76
79
|
readonly config?: TSchema;
|
|
77
80
|
};
|
|
78
|
-
export type NornWorkflowScope<
|
|
79
|
-
readonly id:
|
|
81
|
+
export type NornWorkflowScope<Name extends string, ConfigSchema extends TSchema | undefined> = {
|
|
82
|
+
readonly id: Name;
|
|
80
83
|
readonly config: ConfigSchema;
|
|
81
|
-
workflow<const
|
|
82
|
-
readonly id:
|
|
84
|
+
workflow<const LocalName extends string, ArgsSchema extends TSchema, LocalConfig extends TSchema | undefined = undefined>(definition: NornWorkflowDefinition<LocalName, ArgsSchema, LocalConfig, {
|
|
85
|
+
readonly id: Name;
|
|
83
86
|
readonly config: ConfigSchema;
|
|
84
|
-
}>): NornWorkflowDeclaration<`${
|
|
85
|
-
readonly id:
|
|
87
|
+
}>): NornWorkflowDeclaration<`${Name}.${LocalName}`, ArgsSchema, LocalConfig, {
|
|
88
|
+
readonly id: Name;
|
|
86
89
|
readonly config: ConfigSchema;
|
|
87
90
|
}>;
|
|
88
91
|
};
|
|
89
|
-
export declare function workflow<const
|
|
90
|
-
export declare function workflowScope<const
|
|
91
|
-
readonly
|
|
92
|
+
export declare function workflow<const Name extends string, ArgsSchema extends TSchema, ConfigSchema extends TSchema | undefined = undefined>(definition: NornWorkflowDefinition<Name, ArgsSchema, ConfigSchema>): NornWorkflowDeclaration<Name, ArgsSchema, ConfigSchema, undefined>;
|
|
93
|
+
export declare function workflowScope<const Name extends string, ConfigSchema extends TSchema | undefined = undefined>(input: {
|
|
94
|
+
readonly name: Name;
|
|
92
95
|
readonly config?: ConfigSchema;
|
|
93
|
-
}): NornWorkflowScope<
|
|
96
|
+
}): NornWorkflowScope<Name, ConfigSchema>;
|
|
94
97
|
export type NornRunNext = {
|
|
95
98
|
readonly type: "next";
|
|
96
99
|
readonly workflowId: string;
|
|
@@ -98,7 +101,6 @@ export type NornRunNext = {
|
|
|
98
101
|
};
|
|
99
102
|
export type NornRunOutcomeMetadata = {
|
|
100
103
|
readonly summary?: string;
|
|
101
|
-
readonly artifacts?: Record<string, NornArtifactRef>;
|
|
102
104
|
readonly logs?: Record<string, NornLogRef>;
|
|
103
105
|
readonly data?: Record<string, unknown>;
|
|
104
106
|
};
|
|
@@ -113,11 +115,6 @@ export type NornRunFail = {
|
|
|
113
115
|
};
|
|
114
116
|
};
|
|
115
117
|
export type WorkflowResult = NornRunNext | NornRunComplete | NornRunFail;
|
|
116
|
-
export type NornRunFor<TWorkflow extends NornAnyWorkflowDeclaration> = TWorkflow extends {
|
|
117
|
-
readonly isolation: {
|
|
118
|
-
readonly mode: "project";
|
|
119
|
-
};
|
|
120
|
-
} ? NornProjectRun : NornRun;
|
|
121
118
|
export type NornRunStartOptions = {
|
|
122
119
|
readonly id?: string;
|
|
123
120
|
readonly name?: string;
|
|
@@ -195,6 +192,7 @@ export type NornRunInfo = {
|
|
|
195
192
|
readonly id: string;
|
|
196
193
|
readonly name: string;
|
|
197
194
|
readonly path: string;
|
|
195
|
+
readonly paths: NornWorkflowPaths;
|
|
198
196
|
readonly entrypointWorkflowId: string;
|
|
199
197
|
readonly currentWorkflowId?: string;
|
|
200
198
|
readonly status: NornRunStatus;
|
|
@@ -220,14 +218,10 @@ export type NornRunCheckpoint = {
|
|
|
220
218
|
export type NornCommandRunInput = {
|
|
221
219
|
readonly label: string;
|
|
222
220
|
readonly command: string | readonly [string, ...string[]];
|
|
223
|
-
readonly cwd
|
|
221
|
+
readonly cwd: string;
|
|
224
222
|
readonly env?: Record<string, string>;
|
|
225
223
|
readonly timeoutMs?: number;
|
|
226
224
|
};
|
|
227
|
-
export declare const artifactRefSchema: Type.TObject<{
|
|
228
|
-
path: Type.TString;
|
|
229
|
-
}>;
|
|
230
|
-
export type NornArtifactRef = StaticDecode<typeof artifactRefSchema>;
|
|
231
225
|
declare const emptyWorkflowRefArgsSchema: Type.TObject<{}>;
|
|
232
226
|
declare const workflowReferenceInputSchema: Type.TUnion<[Type.TString, Type.TObject<{
|
|
233
227
|
workflow: Type.TString;
|
|
@@ -252,9 +246,9 @@ export type NornAgentBeforeSessionStartContext = {
|
|
|
252
246
|
readonly events: EventBus;
|
|
253
247
|
};
|
|
254
248
|
export type NornAgentCreateSessionInput = {
|
|
255
|
-
readonly
|
|
249
|
+
readonly customTools?: readonly ToolDefinition[];
|
|
256
250
|
readonly label: string;
|
|
257
|
-
readonly cwd
|
|
251
|
+
readonly cwd: string;
|
|
258
252
|
readonly tools?: string[];
|
|
259
253
|
readonly beforeSessionStart?: (context: NornAgentBeforeSessionStartContext) => MaybePromise<void>;
|
|
260
254
|
readonly model?: CreateAgentSessionOptions["model"];
|
|
@@ -366,36 +360,23 @@ export type NornAgentSession = {
|
|
|
366
360
|
prompt<ResponseSchema extends TSchema>(input: NornAgentPromptInput<ResponseSchema>): Promise<StaticDecode<ResponseSchema>>;
|
|
367
361
|
dispose(): Promise<void>;
|
|
368
362
|
};
|
|
369
|
-
export type NornRun =
|
|
370
|
-
export type NornProjectRun = NornRunBase & {
|
|
371
|
-
projectRoot: string;
|
|
372
|
-
projectPath(relativePath: string): string;
|
|
373
|
-
};
|
|
374
|
-
type NornRunBase = {
|
|
363
|
+
export type NornRun = {
|
|
375
364
|
id: string;
|
|
376
|
-
workspace: string;
|
|
377
|
-
cwd: string;
|
|
378
|
-
path(relativePath: string): string;
|
|
379
365
|
next(workflowId: string, args: unknown): NornRunNext;
|
|
380
366
|
complete(metadata?: NornRunOutcomeMetadata): NornRunComplete;
|
|
381
367
|
fail(metadata: NornRunOutcomeMetadata & {
|
|
382
368
|
readonly summary: string;
|
|
383
369
|
}): NornRunFail;
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
};
|
|
395
|
-
agents: {
|
|
396
|
-
createSession(input: NornAgentCreateSessionInput): Promise<NornAgentSession>;
|
|
397
|
-
prompt<ResponseSchema extends TSchema>(input: NornAgentSinglePromptInput<ResponseSchema>): Promise<StaticDecode<ResponseSchema>>;
|
|
398
|
-
};
|
|
370
|
+
};
|
|
371
|
+
export type NornLogs = {
|
|
372
|
+
read(log: NornLogRef): Promise<string>;
|
|
373
|
+
};
|
|
374
|
+
export type NornCommands = {
|
|
375
|
+
run(input: NornCommandRunInput): Promise<NornCommandRunResult>;
|
|
376
|
+
};
|
|
377
|
+
export type NornAgents = {
|
|
378
|
+
createSession(input: NornAgentCreateSessionInput): Promise<NornAgentSession>;
|
|
379
|
+
prompt<ResponseSchema extends TSchema>(input: NornAgentSinglePromptInput<ResponseSchema>): Promise<StaticDecode<ResponseSchema>>;
|
|
399
380
|
};
|
|
400
381
|
export type NornWorkflowSource = {
|
|
401
382
|
readonly path: string;
|
|
@@ -410,7 +391,6 @@ export type NornRegisteredWorkflowInfo = {
|
|
|
410
391
|
readonly id: string;
|
|
411
392
|
readonly instructions?: string;
|
|
412
393
|
readonly isEntrypoint: boolean;
|
|
413
|
-
readonly isolation: NornWorkflowIsolation;
|
|
414
394
|
readonly scope?: {
|
|
415
395
|
readonly id: string;
|
|
416
396
|
};
|
package/dist/api.js
CHANGED
|
@@ -12,36 +12,32 @@ import { Value } from "typebox/value";
|
|
|
12
12
|
import { assertWorkflowMetadata, isPlainObject, jsonValueSchema } from "./schema.js";
|
|
13
13
|
var WORKFLOW_DECLARATION_KIND = "norn.workflow";
|
|
14
14
|
function workflow(definition) {
|
|
15
|
-
return createWorkflow({ definition, scope: void 0 });
|
|
15
|
+
return createWorkflow({ definition, id: definition.name, scope: void 0 });
|
|
16
16
|
}
|
|
17
17
|
function createWorkflow(input) {
|
|
18
|
-
const { definition, scope } = input;
|
|
19
|
-
|
|
20
|
-
const callable = (args) => createWorkflowTransition({ workflowId:
|
|
21
|
-
const result = Object.assign(callable,
|
|
18
|
+
const { definition: { name, ...properties }, id, scope } = input;
|
|
19
|
+
assertDeclarationName(name);
|
|
20
|
+
const callable = (args) => createWorkflowTransition({ workflowId: id, args });
|
|
21
|
+
const result = Object.assign(callable, properties, {
|
|
22
22
|
kind: WORKFLOW_DECLARATION_KIND,
|
|
23
|
-
|
|
24
|
-
|
|
23
|
+
id,
|
|
24
|
+
scope
|
|
25
25
|
});
|
|
26
|
-
if (!isWorkflowDeclaration(result)) throw new Error(`Invalid workflow definition: ${
|
|
26
|
+
if (!isWorkflowDeclaration(result)) throw new Error(`Invalid workflow definition: ${id}`);
|
|
27
27
|
assertWorkflowMetadata(result);
|
|
28
28
|
return result;
|
|
29
29
|
}
|
|
30
30
|
function workflowScope(input) {
|
|
31
|
-
|
|
32
|
-
const scope = { id: input.
|
|
31
|
+
assertDeclarationName(input.name);
|
|
32
|
+
const scope = { id: input.name, config: input.config };
|
|
33
33
|
return {
|
|
34
|
-
id:
|
|
35
|
-
config:
|
|
34
|
+
id: scope.id,
|
|
35
|
+
config: scope.config,
|
|
36
36
|
workflow(definition) {
|
|
37
|
-
|
|
38
|
-
return createWorkflow({ definition: { ...definition, id: `${input.id}.${definition.id}` }, scope });
|
|
37
|
+
return createWorkflow({ definition, id: `${scope.id}.${definition.name}`, scope });
|
|
39
38
|
}
|
|
40
39
|
};
|
|
41
40
|
}
|
|
42
|
-
var artifactRefSchema = Type.Object({
|
|
43
|
-
path: Type.String()
|
|
44
|
-
});
|
|
45
41
|
var emptyWorkflowRefArgsSchema = Type.Object({});
|
|
46
42
|
var workflowReferenceInputSchema = Type.Union([
|
|
47
43
|
Type.String({ minLength: 1 }),
|
|
@@ -60,13 +56,13 @@ function workflowRefSchema(options) {
|
|
|
60
56
|
};
|
|
61
57
|
}), { "x-norn-workflow-ref": { contributedArgsSchema: contributionSchema } });
|
|
62
58
|
}
|
|
63
|
-
function
|
|
64
|
-
if (typeof
|
|
59
|
+
function assertDeclarationName(name) {
|
|
60
|
+
if (typeof name !== "string" || name.trim().length === 0 || name.includes(".")) throw new Error(`Invalid workflow or scope name: ${name}; expected a nonempty name without dots`);
|
|
65
61
|
}
|
|
66
62
|
function isWorkflowDeclaration(value) {
|
|
67
63
|
if (typeof value !== "function") return false;
|
|
68
64
|
const candidate = value;
|
|
69
|
-
return candidate.kind === WORKFLOW_DECLARATION_KIND && typeof candidate.id === "string" && candidate.id.length > 0 && (candidate.instructions === void 0 || typeof candidate.instructions === "string") && typeof candidate.isEntrypoint === "boolean" && Boolean(candidate.args) && typeof candidate.execute === "function"
|
|
65
|
+
return candidate.kind === WORKFLOW_DECLARATION_KIND && typeof candidate.id === "string" && candidate.id.length > 0 && (candidate.instructions === void 0 || typeof candidate.instructions === "string") && typeof candidate.isEntrypoint === "boolean" && Boolean(candidate.args) && typeof candidate.execute === "function";
|
|
70
66
|
}
|
|
71
67
|
function isWorkflowNext(value) {
|
|
72
68
|
if (!value || typeof value !== "object") return false;
|
|
@@ -84,7 +80,6 @@ function isWorkflowFail(value) {
|
|
|
84
80
|
return candidate.type === "fail" && typeof candidate.metadata?.summary === "string" && candidate.metadata.summary.length > 0;
|
|
85
81
|
}
|
|
86
82
|
export {
|
|
87
|
-
artifactRefSchema,
|
|
88
83
|
isWorkflowComplete,
|
|
89
84
|
isWorkflowDeclaration,
|
|
90
85
|
isWorkflowFail,
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,2 @@
|
|
|
1
1
|
export type { CreateAgentSessionOptions, EventBus, PromptOptions, ToolDefinition } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
export * from "./api.ts";
|
|
3
|
-
export { NornFileCoordinator } from "./files.ts";
|
|
4
|
-
export * from "./resources.ts";
|
|
5
|
-
export * from "./agent-resource-adapter.ts";
|
package/dist/index.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vimhead.dev/norn",
|
|
3
|
-
"version": "0.1.0-tip.
|
|
3
|
+
"version": "0.1.0-tip.35568631545.1",
|
|
4
4
|
"description": "TypeScript SDK for building agent-driven and code-driven Norn workflows.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -26,10 +26,6 @@
|
|
|
26
26
|
"types": "./dist/index.d.ts",
|
|
27
27
|
"import": "./dist/index.js"
|
|
28
28
|
},
|
|
29
|
-
"./files": {
|
|
30
|
-
"types": "./dist/files.d.ts",
|
|
31
|
-
"import": "./dist/files.js"
|
|
32
|
-
},
|
|
33
29
|
"./schema": {
|
|
34
30
|
"types": "./dist/schema.d.ts",
|
|
35
31
|
"import": "./dist/schema.js"
|
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
import type { ToolDefinition } from "@earendil-works/pi-coding-agent";
|
|
2
|
-
export type NornAgentResourceBinding = {
|
|
3
|
-
readonly tools: readonly ToolDefinition[];
|
|
4
|
-
dispose(): Promise<void>;
|
|
5
|
-
};
|
|
6
|
-
export type NornAgentResourceAdapter = {
|
|
7
|
-
readonly name: string;
|
|
8
|
-
bind(context: {
|
|
9
|
-
readonly runId: string;
|
|
10
|
-
readonly label: string;
|
|
11
|
-
}): Promise<NornAgentResourceBinding>;
|
|
12
|
-
};
|
|
File without changes
|
package/dist/files.d.ts
DELETED
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
export declare class NornFileCoordinator {
|
|
2
|
-
private readonly input;
|
|
3
|
-
private readonly lockRoot;
|
|
4
|
-
constructor(input: {
|
|
5
|
-
readonly lockRoot: string;
|
|
6
|
-
readonly waitTimeoutMs: number;
|
|
7
|
-
});
|
|
8
|
-
withExclusiveLock<T>(path: string, operation: (lockedPath: string) => Promise<T>): Promise<T>;
|
|
9
|
-
readText(path: string): Promise<string>;
|
|
10
|
-
writeText(path: string, content: string): Promise<void>;
|
|
11
|
-
private withCleanup;
|
|
12
|
-
private resolveTarget;
|
|
13
|
-
private acquire;
|
|
14
|
-
private reclaimDeadOwner;
|
|
15
|
-
private removeEmptyLock;
|
|
16
|
-
}
|
|
17
|
-
export declare function createRunFileCoordinator(runRoot: string): NornFileCoordinator;
|
package/dist/files.js
DELETED
|
@@ -1,162 +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 writeTextAtomically(path, content) {
|
|
13
|
-
await mkdir(dirname(path), { recursive: true });
|
|
14
|
-
let existingMode;
|
|
15
|
-
try {
|
|
16
|
-
existingMode = (await stat(path)).mode & 511;
|
|
17
|
-
} catch (error) {
|
|
18
|
-
if (!isNodeError(error) || error.code !== "ENOENT") throw error;
|
|
19
|
-
}
|
|
20
|
-
const tmpPath = `${path}.${randomUUID()}.tmp`;
|
|
21
|
-
try {
|
|
22
|
-
await writeFile(tmpPath, content, { encoding: "utf8", mode: existingMode });
|
|
23
|
-
if (existingMode !== void 0) await chmod(tmpPath, existingMode);
|
|
24
|
-
await rename(tmpPath, path);
|
|
25
|
-
} catch (error) {
|
|
26
|
-
await rm(tmpPath, { force: true }).catch(() => void 0);
|
|
27
|
-
throw error;
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
// src/files.ts
|
|
32
|
-
import { createHash, randomUUID as randomUUID2 } from "node:crypto";
|
|
33
|
-
import { lstat, mkdir as mkdir2, mkdtemp, readFile, readdir, realpath, rename as rename2, rm as rm2, rmdir, unlink, writeFile as writeFile2 } from "node:fs/promises";
|
|
34
|
-
import { hostname } from "node:os";
|
|
35
|
-
import { basename, dirname as dirname2, join, resolve } from "node:path";
|
|
36
|
-
import { setTimeout as delay } from "node:timers/promises";
|
|
37
|
-
import { Type } from "typebox";
|
|
38
|
-
import { Value } from "typebox/value";
|
|
39
|
-
var ownerSchema = Type.Object({
|
|
40
|
-
token: Type.String({ format: "uuid" }),
|
|
41
|
-
pid: Type.Integer({ exclusiveMinimum: 0 }),
|
|
42
|
-
host: Type.String(),
|
|
43
|
-
target: Type.String()
|
|
44
|
-
}, { additionalProperties: false });
|
|
45
|
-
var NornFileCoordinator = class {
|
|
46
|
-
constructor(input) {
|
|
47
|
-
this.input = input;
|
|
48
|
-
if (!Number.isSafeInteger(input.waitTimeoutMs) || input.waitTimeoutMs <= 0) throw new Error("Lock wait timeout must be a positive integer");
|
|
49
|
-
this.lockRoot = resolve(input.lockRoot);
|
|
50
|
-
}
|
|
51
|
-
input;
|
|
52
|
-
lockRoot;
|
|
53
|
-
async withExclusiveLock(path, operation) {
|
|
54
|
-
const target = await this.resolveTarget(path);
|
|
55
|
-
await mkdir2(this.lockRoot, { recursive: true, mode: 448 });
|
|
56
|
-
const key = createHash("sha256").update(target).digest("hex");
|
|
57
|
-
const lockPath = join(this.lockRoot, `${key}.lock`);
|
|
58
|
-
const stagingPath = await mkdtemp(join(this.lockRoot, `${key}.pending-`));
|
|
59
|
-
const token = randomUUID2();
|
|
60
|
-
const marker = `${token}.json`;
|
|
61
|
-
return this.withCleanup({
|
|
62
|
-
operation: async () => {
|
|
63
|
-
await writeFile2(join(stagingPath, marker), JSON.stringify({ token, pid: process.pid, host: hostname(), target }), { mode: 384 });
|
|
64
|
-
await this.acquire({ stagingPath, lockPath, target });
|
|
65
|
-
return this.withCleanup({
|
|
66
|
-
operation: () => operation(target),
|
|
67
|
-
cleanup: async () => {
|
|
68
|
-
await unlink(join(lockPath, marker));
|
|
69
|
-
await this.removeEmptyLock(lockPath);
|
|
70
|
-
}
|
|
71
|
-
});
|
|
72
|
-
},
|
|
73
|
-
cleanup: () => rm2(stagingPath, { recursive: true, force: true })
|
|
74
|
-
});
|
|
75
|
-
}
|
|
76
|
-
async readText(path) {
|
|
77
|
-
return this.withExclusiveLock(path, (lockedPath) => readFile(lockedPath, "utf8"));
|
|
78
|
-
}
|
|
79
|
-
async writeText(path, content) {
|
|
80
|
-
await mkdir2(dirname2(path), { recursive: true });
|
|
81
|
-
await this.withExclusiveLock(path, (lockedPath) => writeTextAtomically(lockedPath, content));
|
|
82
|
-
}
|
|
83
|
-
async withCleanup(input) {
|
|
84
|
-
let result;
|
|
85
|
-
try {
|
|
86
|
-
result = await input.operation();
|
|
87
|
-
} catch (error) {
|
|
88
|
-
try {
|
|
89
|
-
await input.cleanup();
|
|
90
|
-
} catch (cleanupError) {
|
|
91
|
-
throw new AggregateError([error, cleanupError], "File operation and lock cleanup failed");
|
|
92
|
-
}
|
|
93
|
-
throw error;
|
|
94
|
-
}
|
|
95
|
-
await input.cleanup();
|
|
96
|
-
return result;
|
|
97
|
-
}
|
|
98
|
-
async resolveTarget(path) {
|
|
99
|
-
try {
|
|
100
|
-
return await realpath(path);
|
|
101
|
-
} catch (error) {
|
|
102
|
-
if (!isNodeError(error) || error.code !== "ENOENT") throw error;
|
|
103
|
-
const entry = await lstat(path).catch((error2) => {
|
|
104
|
-
if (isNodeError(error2) && error2.code === "ENOENT") return void 0;
|
|
105
|
-
throw error2;
|
|
106
|
-
});
|
|
107
|
-
if (entry?.isSymbolicLink()) throw new Error(`Cannot lock a dangling symbolic link: ${path}`);
|
|
108
|
-
return join(await realpath(dirname2(path)), basename(path));
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
async acquire(input) {
|
|
112
|
-
const deadline = Date.now() + this.input.waitTimeoutMs;
|
|
113
|
-
while (true) {
|
|
114
|
-
try {
|
|
115
|
-
await rename2(input.stagingPath, input.lockPath);
|
|
116
|
-
return;
|
|
117
|
-
} catch (error) {
|
|
118
|
-
if (!isNodeError(error) || !["ENOTEMPTY", "EEXIST"].includes(error.code ?? "")) throw error;
|
|
119
|
-
}
|
|
120
|
-
await this.reclaimDeadOwner(input);
|
|
121
|
-
if (Date.now() >= deadline) throw new Error(`Timed out waiting for file lock: ${input.target}`);
|
|
122
|
-
await delay(10);
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
async reclaimDeadOwner(input) {
|
|
126
|
-
try {
|
|
127
|
-
const entries = await readdir(input.lockPath);
|
|
128
|
-
if (entries.length === 0) return;
|
|
129
|
-
if (entries.length !== 1) throw new Error(`Invalid file lock ownership: ${input.lockPath}`);
|
|
130
|
-
const marker = entries[0];
|
|
131
|
-
const owner = Value.Parse(ownerSchema, JSON.parse(await readFile(join(input.lockPath, marker), "utf8")));
|
|
132
|
-
if (marker !== `${owner.token}.json` || owner.target !== input.target || owner.host !== hostname()) {
|
|
133
|
-
throw new Error(`Incompatible file lock ownership: ${input.lockPath}`);
|
|
134
|
-
}
|
|
135
|
-
try {
|
|
136
|
-
process.kill(owner.pid, 0);
|
|
137
|
-
return;
|
|
138
|
-
} catch (error) {
|
|
139
|
-
if (isNodeError(error) && error.code === "EPERM") return;
|
|
140
|
-
if (!isNodeError(error) || error.code !== "ESRCH") throw error;
|
|
141
|
-
}
|
|
142
|
-
await unlink(join(input.lockPath, marker));
|
|
143
|
-
await this.removeEmptyLock(input.lockPath);
|
|
144
|
-
} catch (error) {
|
|
145
|
-
if (!isNodeError(error) || error.code !== "ENOENT") throw error;
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
async removeEmptyLock(path) {
|
|
149
|
-
try {
|
|
150
|
-
await rmdir(path);
|
|
151
|
-
} catch (error) {
|
|
152
|
-
if (!isNodeError(error) || !["ENOENT", "ENOTEMPTY", "EEXIST"].includes(error.code ?? "")) throw error;
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
};
|
|
156
|
-
function createRunFileCoordinator(runRoot) {
|
|
157
|
-
return new NornFileCoordinator({ lockRoot: join(runRoot, "locks"), waitTimeoutMs: 3e4 });
|
|
158
|
-
}
|
|
159
|
-
export {
|
|
160
|
-
NornFileCoordinator,
|
|
161
|
-
createRunFileCoordinator
|
|
162
|
-
};
|
package/dist/resources.d.ts
DELETED
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
import type { NornFileCoordinator } from "./files.ts";
|
|
2
|
-
import type { NornJsonValue } from "./schema.ts";
|
|
3
|
-
export type NornResourceContext = {
|
|
4
|
-
readonly mode: "create" | "open";
|
|
5
|
-
readonly directory: string;
|
|
6
|
-
readonly files: NornFileCoordinator;
|
|
7
|
-
};
|
|
8
|
-
export type NornResourceDefinition<T> = {
|
|
9
|
-
readonly name: string;
|
|
10
|
-
readonly kind: string;
|
|
11
|
-
readonly configuration: NornJsonValue;
|
|
12
|
-
initialize(context: NornResourceContext): Promise<T>;
|
|
13
|
-
};
|
|
14
|
-
export type NornResources = {
|
|
15
|
-
readonly files: NornFileCoordinator;
|
|
16
|
-
ensure<T>(definition: NornResourceDefinition<T>): Promise<T>;
|
|
17
|
-
};
|
package/dist/resources.js
DELETED
|
File without changes
|