@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 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 NornWorkflowIsolationMode = "runWorkspace" | "project";
12
- export type NornWorkflowIsolation<Mode extends NornWorkflowIsolationMode = NornWorkflowIsolationMode> = {
13
- readonly mode: Mode;
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, IsolationMode extends NornWorkflowIsolationMode = NornWorkflowIsolationMode, ConfigSchema extends TSchema | undefined = TSchema | undefined, Scope extends NornWorkflowScopeInfo | undefined = NornWorkflowScopeInfo | undefined> = {
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, IsolationMode, ConfigSchema, Scope>): MaybePromise<string>;
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, IsolationMode, ConfigSchema, Scope>): MaybePromise<WorkflowResult>;
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, Mode extends NornWorkflowIsolationMode = NornWorkflowIsolationMode, ConfigSchema extends TSchema | undefined = TSchema | undefined, Scope extends NornWorkflowScopeInfo | undefined = NornWorkflowScopeInfo | undefined> = {
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 run: RunForIsolation<Mode>;
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<Id extends string = string, ArgsSchema extends TSchema = TSchema, IsolationMode extends NornWorkflowIsolationMode = NornWorkflowIsolationMode, ConfigSchema extends TSchema | undefined = undefined, Scope extends NornWorkflowScopeInfo | undefined = undefined> = {
65
- readonly id: Id;
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, IsolationMode, ConfigSchema, Scope>): MaybePromise<string>;
73
+ describe?(context: NornWorkflowContext<ArgsSchema, ConfigSchema, Scope>): MaybePromise<string>;
71
74
  };
72
- execute(context: NornWorkflowContext<ArgsSchema, IsolationMode, ConfigSchema, Scope>): MaybePromise<WorkflowResult>;
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<Id extends string, ConfigSchema extends TSchema | undefined> = {
79
- readonly id: Id;
81
+ export type NornWorkflowScope<Name extends string, ConfigSchema extends TSchema | undefined> = {
82
+ readonly id: Name;
80
83
  readonly config: ConfigSchema;
81
- workflow<const LocalId extends string, ArgsSchema extends TSchema, const Mode extends NornWorkflowIsolationMode = "runWorkspace", LocalConfig extends TSchema | undefined = undefined>(definition: NornWorkflowDefinition<LocalId, ArgsSchema, Mode, LocalConfig, {
82
- readonly id: 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<`${Id}.${LocalId}`, ArgsSchema, Mode, LocalConfig, {
85
- readonly id: 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 Id extends string, ArgsSchema extends TSchema, const Mode extends NornWorkflowIsolationMode = "runWorkspace", ConfigSchema extends TSchema | undefined = undefined>(definition: NornWorkflowDefinition<Id, ArgsSchema, Mode, ConfigSchema>): NornWorkflowDeclaration<Id, ArgsSchema, Mode, ConfigSchema, undefined>;
90
- export declare function workflowScope<const Id extends string, ConfigSchema extends TSchema | undefined = undefined>(input: {
91
- readonly id: Id;
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<Id, ConfigSchema>;
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?: string;
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 resourceAdapters?: readonly import("./agent-resource-adapter.ts").NornAgentResourceAdapter[];
249
+ readonly customTools?: readonly ToolDefinition[];
256
250
  readonly label: string;
257
- readonly cwd?: string;
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 = NornRunBase;
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
- resources: import("./resources.ts").NornResources;
385
- artifacts: {
386
- write(path: string, content: string): Promise<NornArtifactRef>;
387
- read(ref: NornArtifactRef): Promise<string>;
388
- };
389
- logs: {
390
- read(log: NornLogRef): Promise<string>;
391
- };
392
- commands: {
393
- run(input: NornCommandRunInput): Promise<NornCommandRunResult>;
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
- assertDeclarationId(definition.id);
20
- const callable = (args) => createWorkflowTransition({ workflowId: definition.id, args });
21
- const result = Object.assign(callable, definition, {
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
- scope,
24
- isolation: definition.isolation ?? { mode: "runWorkspace" }
23
+ id,
24
+ scope
25
25
  });
26
- if (!isWorkflowDeclaration(result)) throw new Error(`Invalid workflow definition: ${definition.id}`);
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
- assertDeclarationId(input.id);
32
- const scope = { id: input.id, config: input.config };
31
+ assertDeclarationName(input.name);
32
+ const scope = { id: input.name, config: input.config };
33
33
  return {
34
- id: input.id,
35
- config: input.config,
34
+ id: scope.id,
35
+ config: scope.config,
36
36
  workflow(definition) {
37
- assertDeclarationId(definition.id);
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 assertDeclarationId(id) {
64
- if (typeof id !== "string" || id.trim().length === 0 || id.split(".").some((segment) => segment.length === 0)) throw new Error(`Invalid workflow or scope id: ${id}`);
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" && (candidate.isolation?.mode === "runWorkspace" || candidate.isolation?.mode === "project");
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
@@ -1,8 +1,2 @@
1
1
  // src/index.ts
2
2
  export * from "./api.js";
3
- import { NornFileCoordinator } from "./files.js";
4
- export * from "./resources.js";
5
- export * from "./agent-resource-adapter.js";
6
- export {
7
- NornFileCoordinator
8
- };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vimhead.dev/norn",
3
- "version": "0.1.0-tip.35390859630.1",
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
- };
@@ -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