@vimhead.dev/norn-cli 0.1.0-tip.35614826665.1 → 0.1.0-tip.35724621678.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/assets/README.md +41 -13
- package/assets/docs/README.md +16 -16
- package/assets/docs/agents.md +23 -15
- package/assets/docs/cli.md +10 -8
- package/assets/docs/composition.md +7 -7
- package/assets/docs/persistence.md +22 -22
- package/assets/docs/projects.md +6 -6
- package/assets/docs/recovery.md +10 -10
- package/assets/docs/schemas.md +42 -36
- package/assets/docs/workflows.md +39 -32
- package/assets/examples/agent-then-analysis/input.json +3 -3
- package/assets/examples/agent-then-analysis/norn.project.json +2 -2
- package/assets/examples/agent-then-analysis/plugin.ts +37 -9
- package/assets/examples/caller-owned-routing/README.md +6 -6
- package/assets/examples/caller-owned-routing/assessment.ts +18 -4
- package/assets/examples/caller-owned-routing/contracts.ts +7 -2
- package/assets/examples/caller-owned-routing/input.json +12 -12
- package/assets/examples/caller-owned-routing/norn.project.json +2 -2
- package/assets/examples/caller-owned-routing/revision.ts +7 -2
- package/assets/examples/caller-owned-routing/router.ts +14 -3
- package/assets/examples/caller-selected-continuation/caller.ts +38 -12
- package/assets/examples/caller-selected-continuation/input.json +7 -7
- package/assets/examples/caller-selected-continuation/norn.project.json +2 -2
- package/assets/examples/caller-selected-continuation/producer.ts +10 -3
- package/assets/examples/coordinating-multiple-agents/README.md +6 -6
- package/assets/examples/coordinating-multiple-agents/input.json +20 -8
- package/assets/examples/coordinating-multiple-agents/norn.project.json +2 -2
- package/assets/examples/coordinating-multiple-agents/plugin.ts +165 -46
- package/assets/examples/coordinating-multiple-agents/queue-tools.ts +30 -7
- package/assets/examples/coordinating-multiple-agents/work-queue.ts +211 -63
- package/assets/examples/getting-started/norn.project.json +2 -2
- package/assets/examples/getting-started/plugin.ts +33 -13
- package/assets/examples/minimal-workflow/norn.project.json +2 -2
- package/assets/examples/minimal-workflow/plugin.ts +12 -3
- package/assets/examples/shared-state/README.md +6 -6
- package/assets/examples/shared-state/input.json +1 -1
- package/assets/examples/shared-state/norn.project.json +2 -2
- package/assets/examples/shared-state/plugin.ts +44 -15
- package/assets/examples/shared-state/shared-state.ts +46 -13
- package/assets/examples/shared-state/state-tools.ts +68 -16
- package/assets/examples/worktree-development-loop/README.md +12 -12
- package/assets/examples/worktree-development-loop/norn.project.json +7 -7
- package/assets/examples/worktree-development-loop/plugin.ts +7 -1
- package/assets/examples/worktree-development-loop/scope.ts +4 -1
- package/assets/examples/worktree-development-loop/shared/commands.ts +6 -2
- package/assets/examples/worktree-development-loop/workflows/development-loop/execute.ts +18 -4
- package/assets/examples/worktree-development-loop/workflows/development-loop/index.ts +4 -1
- package/assets/examples/worktree-development-loop/workflows/development-loop/repository.ts +11 -1
- package/assets/examples/worktree-development-loop/workflows/development-loop/schema.ts +6 -2
- package/assets/examples/worktree-development-loop/workflows/implementation/execute.ts +40 -10
- package/assets/examples/worktree-development-loop/workflows/implementation/index.ts +5 -1
- package/assets/examples/worktree-development-loop/workflows/planning/execute.ts +1 -1
- package/assets/examples/worktree-development-loop/workflows/planning/index.ts +5 -1
- package/assets/examples/worktree-development-loop/workflows/review/execute.ts +33 -9
- package/assets/examples/worktree-development-loop/workflows/review-router/execute.ts +51 -8
- package/assets/package.json +1 -1
- package/assets/packages/cli/src/build-info.ts +4 -1
- package/assets/packages/cli/src/bun/cli.ts +21 -5
- package/assets/packages/cli/src/cli.ts +585 -170
- package/assets/packages/cli/src/client.ts +194 -36
- package/assets/packages/cli/src/documentation.ts +135 -30
- package/assets/packages/cli/src/generated-build-info.ts +2 -2
- package/assets/packages/cli/src/internal/agent-directory.ts +7 -2
- package/assets/packages/cli/src/internal/agent-response-tool.ts +57 -15
- package/assets/packages/cli/src/internal/agents.ts +248 -60
- package/assets/packages/cli/src/internal/commands.ts +82 -20
- package/assets/packages/cli/src/internal/documentation-bundle.ts +44 -13
- package/assets/packages/cli/src/internal/engine.ts +396 -87
- package/assets/packages/cli/src/internal/errors.ts +13 -5
- package/assets/packages/cli/src/internal/execution-context.ts +20 -12
- package/assets/packages/cli/src/internal/file-coordinator.ts +95 -26
- package/assets/packages/cli/src/internal/file-names.ts +4 -1
- package/assets/packages/cli/src/internal/launch-request.ts +74 -22
- package/assets/packages/cli/src/internal/logs.ts +10 -2
- package/assets/packages/cli/src/internal/metrics.ts +247 -61
- package/assets/packages/cli/src/internal/pi-assets.ts +105 -23
- package/assets/packages/cli/src/internal/run-lease.ts +72 -22
- package/assets/packages/cli/src/internal/run-log.ts +49 -20
- package/assets/packages/cli/src/internal/run-names.ts +120 -12
- package/assets/packages/cli/src/internal/run-state.ts +306 -71
- package/assets/packages/cli/src/internal/run-store.ts +216 -53
- package/assets/packages/cli/src/internal/usage.ts +23 -7
- package/assets/packages/cli/src/internal/worker-directory.ts +9 -3
- package/assets/packages/cli/src/internal/workflow-registry.ts +281 -70
- package/assets/packages/cli/src/internal/working-directory.ts +2 -1
- package/assets/packages/cli/src/workflow-loader.ts +341 -74
- package/assets/packages/core/src/atomic-files.ts +13 -4
- package/assets/packages/core/src/workflow-transition.ts +13 -2
- package/assets/packages/sdk/src/api.ts +248 -80
- package/assets/packages/sdk/src/index.ts +6 -1
- package/assets/packages/sdk/src/schema.ts +75 -21
- package/assets/tests/workflow-ref.test.ts +242 -57
- package/bin/norn.mjs +13 -4
- package/dist/cli.js +344 -90
- package/dist/client.js +100 -22
- package/dist/documentation.js +102 -25
- package/dist/generated-build-info.d.ts +2 -2
- package/dist/generated-build-info.js +2 -2
- package/dist/internal/agent-directory.js +3 -1
- package/dist/internal/agent-response-tool.js +42 -11
- package/dist/internal/agents.js +149 -37
- package/dist/internal/commands.js +65 -16
- package/dist/internal/documentation-bundle.js +23 -11
- package/dist/internal/engine.js +304 -66
- package/dist/internal/errors.js +4 -2
- package/dist/internal/execution-context.js +2 -1
- package/dist/internal/file-coordinator.js +59 -19
- package/dist/internal/launch-request.js +32 -14
- package/dist/internal/metrics.js +138 -38
- package/dist/internal/pi-assets.js +70 -19
- package/dist/internal/run-lease.js +29 -9
- package/dist/internal/run-log.js +24 -12
- package/dist/internal/run-state.js +172 -48
- package/dist/internal/run-store.js +157 -45
- package/dist/internal/worker-directory.js +6 -2
- package/dist/internal/workflow-registry.js +123 -32
- package/dist/internal/working-directory.js +2 -1
- package/dist/workflow-loader.js +198 -41
- package/package.json +2 -2
package/dist/internal/agents.js
CHANGED
|
@@ -22,12 +22,18 @@ import {
|
|
|
22
22
|
} from "./agent-response-tool.js";
|
|
23
23
|
import { errorMessage } from "./errors.js";
|
|
24
24
|
import { safeFileName } from "./file-names.js";
|
|
25
|
-
import {
|
|
25
|
+
import {
|
|
26
|
+
agentUsageFromValue,
|
|
27
|
+
emptyAgentUsage,
|
|
28
|
+
totalAgentUsage
|
|
29
|
+
} from "./usage.js";
|
|
26
30
|
var DEFAULT_AGENT_ATTEMPTS = 3;
|
|
27
31
|
var NornAgentRunner = class {
|
|
28
32
|
constructor(input) {
|
|
29
33
|
this.input = input;
|
|
30
|
-
this.responseToolFactory = new NornAgentResponseToolFactory(
|
|
34
|
+
this.responseToolFactory = new NornAgentResponseToolFactory(
|
|
35
|
+
input.responseCollector
|
|
36
|
+
);
|
|
31
37
|
}
|
|
32
38
|
input;
|
|
33
39
|
responseToolFactory;
|
|
@@ -49,7 +55,9 @@ var NornAgentRunner = class {
|
|
|
49
55
|
const loader = services.resourceLoader;
|
|
50
56
|
const errors = [
|
|
51
57
|
...services.diagnostics.filter((diagnostic) => diagnostic.type === "error").map((diagnostic) => diagnostic.message),
|
|
52
|
-
...loader.getExtensions().errors.map(
|
|
58
|
+
...loader.getExtensions().errors.map(
|
|
59
|
+
(error) => `Failed to load extension ${error.path}: ${error.error}`
|
|
60
|
+
)
|
|
53
61
|
];
|
|
54
62
|
if (errors.length > 0) throw new Error(errors.join("\n"));
|
|
55
63
|
const customTools = agentInput.customTools ?? [];
|
|
@@ -80,7 +88,11 @@ var NornAgentRunner = class {
|
|
|
80
88
|
}));
|
|
81
89
|
await agentInput.beforeSessionStart?.({ events: eventBus });
|
|
82
90
|
await session.bindExtensions({});
|
|
83
|
-
await this.input.logger.record({
|
|
91
|
+
await this.input.logger.record({
|
|
92
|
+
type: "agent.spawned",
|
|
93
|
+
label: agentInput.label,
|
|
94
|
+
cwd
|
|
95
|
+
});
|
|
84
96
|
return new CreatedNornAgentSession({
|
|
85
97
|
...this.input,
|
|
86
98
|
label: agentInput.label,
|
|
@@ -95,7 +107,8 @@ var NornAgentRunner = class {
|
|
|
95
107
|
} catch (cleanupError) {
|
|
96
108
|
errors2.push(cleanupError);
|
|
97
109
|
}
|
|
98
|
-
if (errors2.length > 1)
|
|
110
|
+
if (errors2.length > 1)
|
|
111
|
+
throw new AggregateError(errors2, "Agent creation and cleanup failed");
|
|
99
112
|
throw error;
|
|
100
113
|
}
|
|
101
114
|
}
|
|
@@ -132,22 +145,42 @@ var CreatedNornAgentSession = class {
|
|
|
132
145
|
return (await this.promptWithResult(agentInput)).response;
|
|
133
146
|
}
|
|
134
147
|
async promptWithResult(agentInput) {
|
|
135
|
-
if (this.isDisposed)
|
|
136
|
-
|
|
148
|
+
if (this.isDisposed)
|
|
149
|
+
throw new Error(`Workflow agent session is disposed: ${this.label}`);
|
|
150
|
+
const maxAttempts = Math.max(
|
|
151
|
+
1,
|
|
152
|
+
Math.floor(agentInput.maxAttempts ?? DEFAULT_AGENT_ATTEMPTS)
|
|
153
|
+
);
|
|
137
154
|
const attempts = [];
|
|
138
155
|
const startedAtMs = Date.now();
|
|
139
156
|
let isTerminalEventRecorded = false;
|
|
140
|
-
await this.input.logger.record({
|
|
157
|
+
await this.input.logger.record({
|
|
158
|
+
type: "agent.started",
|
|
159
|
+
label: this.label,
|
|
160
|
+
cwd: this.cwd,
|
|
161
|
+
maxAttempts
|
|
162
|
+
});
|
|
141
163
|
try {
|
|
142
164
|
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
143
|
-
await this.input.logger.record({
|
|
144
|
-
|
|
165
|
+
await this.input.logger.record({
|
|
166
|
+
type: "agent.attempt.started",
|
|
167
|
+
label: this.label,
|
|
168
|
+
attempt
|
|
169
|
+
});
|
|
170
|
+
const raw = await this.runAttempt(
|
|
171
|
+
agentInput,
|
|
172
|
+
attempt,
|
|
173
|
+
attempts.at(-1)?.error
|
|
174
|
+
);
|
|
145
175
|
const rawAttempt = { attempt, ...raw };
|
|
146
176
|
attempts.push(rawAttempt);
|
|
147
177
|
try {
|
|
148
|
-
if (!raw.responseToolCalled)
|
|
178
|
+
if (!raw.responseToolCalled)
|
|
179
|
+
throw new Error(`Agent did not call ${AGENT_RESPONSE_TOOL_NAME}`);
|
|
149
180
|
const response = raw.toolResponse;
|
|
150
|
-
const usage = totalAgentUsage(
|
|
181
|
+
const usage = totalAgentUsage(
|
|
182
|
+
attempts.map((candidate) => candidate.usage)
|
|
183
|
+
);
|
|
151
184
|
const result = {
|
|
152
185
|
label: this.label,
|
|
153
186
|
cwd: this.cwd,
|
|
@@ -155,22 +188,49 @@ var CreatedNornAgentSession = class {
|
|
|
155
188
|
usage,
|
|
156
189
|
raw: { ...raw, usage, attempts }
|
|
157
190
|
};
|
|
158
|
-
await this.input.logs.write(
|
|
191
|
+
await this.input.logs.write(
|
|
192
|
+
`agents/${safeFileName(this.label)}.json`,
|
|
193
|
+
JSON.stringify(result, null, 2)
|
|
194
|
+
);
|
|
159
195
|
isTerminalEventRecorded = true;
|
|
160
|
-
await this.input.logger.record({
|
|
196
|
+
await this.input.logger.record({
|
|
197
|
+
type: "agent.completed",
|
|
198
|
+
label: this.label,
|
|
199
|
+
attempts: attempt,
|
|
200
|
+
durationMs: Date.now() - startedAtMs,
|
|
201
|
+
usage
|
|
202
|
+
});
|
|
161
203
|
return result;
|
|
162
204
|
} catch (error) {
|
|
163
205
|
const message = errorMessage(error);
|
|
164
206
|
attempts[attempts.length - 1] = { ...rawAttempt, error: message };
|
|
165
207
|
await this.input.logs.write(
|
|
166
208
|
`agents/${safeFileName(this.label)}.attempt-${attempt}.raw.json`,
|
|
167
|
-
JSON.stringify(
|
|
209
|
+
JSON.stringify(
|
|
210
|
+
{ label: this.label, cwd: this.cwd, raw: attempts.at(-1) },
|
|
211
|
+
null,
|
|
212
|
+
2
|
|
213
|
+
)
|
|
168
214
|
);
|
|
169
|
-
await this.input.logger.record({
|
|
215
|
+
await this.input.logger.record({
|
|
216
|
+
type: "agent.attempt.failed",
|
|
217
|
+
label: this.label,
|
|
218
|
+
attempt,
|
|
219
|
+
error: message
|
|
220
|
+
});
|
|
170
221
|
if (attempt === maxAttempts) {
|
|
171
|
-
const usage = totalAgentUsage(
|
|
222
|
+
const usage = totalAgentUsage(
|
|
223
|
+
attempts.map((candidate) => candidate.usage)
|
|
224
|
+
);
|
|
172
225
|
isTerminalEventRecorded = true;
|
|
173
|
-
await this.input.logger.record({
|
|
226
|
+
await this.input.logger.record({
|
|
227
|
+
type: "agent.failed",
|
|
228
|
+
label: this.label,
|
|
229
|
+
attempts: attempt,
|
|
230
|
+
durationMs: Date.now() - startedAtMs,
|
|
231
|
+
usage,
|
|
232
|
+
error: message
|
|
233
|
+
});
|
|
174
234
|
throw new Error(
|
|
175
235
|
`Agent ${this.label} did not return a valid structured response after ${attempt} attempt(s). Raw output saved to logs/agents/${safeFileName(this.label)}.attempt-${attempt}.raw.json: ${message}`
|
|
176
236
|
);
|
|
@@ -206,35 +266,61 @@ var CreatedNornAgentSession = class {
|
|
|
206
266
|
} catch (error) {
|
|
207
267
|
errors.push(error);
|
|
208
268
|
}
|
|
209
|
-
if (errors.length)
|
|
210
|
-
|
|
269
|
+
if (errors.length)
|
|
270
|
+
throw new AggregateError(errors, "Agent disposal failed");
|
|
271
|
+
await this.input.logger.record({
|
|
272
|
+
type: "agent.disposed",
|
|
273
|
+
label: this.label
|
|
274
|
+
});
|
|
211
275
|
}
|
|
212
276
|
async runAttempt(agentInput, attempt, previousError) {
|
|
213
277
|
const responseRunId = `${this.input.id}:${attempt}:${randomUUID()}`;
|
|
214
278
|
const messages = [];
|
|
215
279
|
let text = "";
|
|
216
|
-
const releaseResponseSlot = this.input.responseCollector.begin(
|
|
280
|
+
const releaseResponseSlot = this.input.responseCollector.begin(
|
|
281
|
+
responseRunId,
|
|
282
|
+
this.label,
|
|
283
|
+
agentInput.response
|
|
284
|
+
);
|
|
217
285
|
const unsubscribe = this.input.session.subscribe((event) => {
|
|
218
286
|
if (event.type === "message_update" && event.assistantMessageEvent.type === "text_delta") {
|
|
219
287
|
text += event.assistantMessageEvent.delta;
|
|
220
288
|
}
|
|
221
|
-
if (event.type === "message_end" && event.message)
|
|
289
|
+
if (event.type === "message_end" && event.message)
|
|
290
|
+
messages.push(event.message);
|
|
222
291
|
});
|
|
223
292
|
const abortNestedSession = () => {
|
|
224
293
|
void this.input.session.abort();
|
|
225
294
|
};
|
|
226
|
-
this.input.signal?.addEventListener("abort", abortNestedSession, {
|
|
295
|
+
this.input.signal?.addEventListener("abort", abortNestedSession, {
|
|
296
|
+
once: true
|
|
297
|
+
});
|
|
227
298
|
let capturedResponse = { called: false };
|
|
228
299
|
try {
|
|
229
300
|
this.input.signal?.throwIfAborted();
|
|
230
|
-
await this.input.session.prompt(
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
301
|
+
await this.input.session.prompt(
|
|
302
|
+
withResponseToolInstruction(
|
|
303
|
+
agentInput.prompt,
|
|
304
|
+
agentInput.response,
|
|
305
|
+
this.label,
|
|
306
|
+
responseRunId,
|
|
307
|
+
attempt,
|
|
308
|
+
previousError
|
|
309
|
+
),
|
|
310
|
+
{
|
|
311
|
+
...agentInput.options,
|
|
312
|
+
source: agentInput.options?.source ?? "extension"
|
|
313
|
+
}
|
|
314
|
+
);
|
|
234
315
|
this.input.signal?.throwIfAborted();
|
|
235
316
|
capturedResponse = this.input.responseCollector.get(responseRunId);
|
|
236
317
|
if (!capturedResponse.called) {
|
|
237
|
-
await this.promptForStructuredResponse(
|
|
318
|
+
await this.promptForStructuredResponse(
|
|
319
|
+
agentInput,
|
|
320
|
+
responseRunId,
|
|
321
|
+
attempt,
|
|
322
|
+
previousError
|
|
323
|
+
);
|
|
238
324
|
this.input.signal?.throwIfAborted();
|
|
239
325
|
capturedResponse = this.input.responseCollector.get(responseRunId);
|
|
240
326
|
}
|
|
@@ -254,16 +340,38 @@ var CreatedNornAgentSession = class {
|
|
|
254
340
|
}
|
|
255
341
|
async promptForStructuredResponse(agentInput, responseRunId, attempt, previousError) {
|
|
256
342
|
const activeToolNames = this.input.session.getActiveToolNames();
|
|
257
|
-
await this.input.logger.record({
|
|
343
|
+
await this.input.logger.record({
|
|
344
|
+
type: "agent.response-finalization.started",
|
|
345
|
+
label: this.label,
|
|
346
|
+
attempt
|
|
347
|
+
});
|
|
258
348
|
this.input.session.setActiveToolsByName([AGENT_RESPONSE_TOOL_NAME]);
|
|
259
349
|
try {
|
|
260
|
-
await this.input.session.prompt(
|
|
261
|
-
|
|
262
|
-
|
|
350
|
+
await this.input.session.prompt(
|
|
351
|
+
withResponseToolFinalizationInstruction(
|
|
352
|
+
agentInput.response,
|
|
353
|
+
this.label,
|
|
354
|
+
responseRunId,
|
|
355
|
+
attempt,
|
|
356
|
+
previousError
|
|
357
|
+
),
|
|
358
|
+
{
|
|
359
|
+
...agentInput.options,
|
|
360
|
+
source: agentInput.options?.source ?? "extension"
|
|
361
|
+
}
|
|
362
|
+
);
|
|
363
|
+
await this.input.logger.record({
|
|
364
|
+
type: "agent.response-finalization.completed",
|
|
365
|
+
label: this.label,
|
|
366
|
+
attempt
|
|
263
367
|
});
|
|
264
|
-
await this.input.logger.record({ type: "agent.response-finalization.completed", label: this.label, attempt });
|
|
265
368
|
} catch (error) {
|
|
266
|
-
await this.input.logger.record({
|
|
369
|
+
await this.input.logger.record({
|
|
370
|
+
type: "agent.response-finalization.failed",
|
|
371
|
+
label: this.label,
|
|
372
|
+
attempt,
|
|
373
|
+
error: errorMessage(error)
|
|
374
|
+
});
|
|
267
375
|
throw error;
|
|
268
376
|
} finally {
|
|
269
377
|
this.input.session.setActiveToolsByName(activeToolNames);
|
|
@@ -271,10 +379,13 @@ var CreatedNornAgentSession = class {
|
|
|
271
379
|
}
|
|
272
380
|
};
|
|
273
381
|
function usageFromMessages(messages) {
|
|
274
|
-
return totalAgentUsage(
|
|
382
|
+
return totalAgentUsage(
|
|
383
|
+
messages.map(usageFromMessage).filter((usage) => usage !== void 0)
|
|
384
|
+
);
|
|
275
385
|
}
|
|
276
386
|
function usageFromMessage(message) {
|
|
277
|
-
if (!message || typeof message !== "object" || !("usage" in message))
|
|
387
|
+
if (!message || typeof message !== "object" || !("usage" in message))
|
|
388
|
+
return void 0;
|
|
278
389
|
return agentUsageFromValue(message.usage) ?? emptyAgentUsage();
|
|
279
390
|
}
|
|
280
391
|
function withResponseToolInstruction(prompt, responseSchema, label, responseRunId, attempt, previousError) {
|
|
@@ -311,7 +422,8 @@ function withAgentResponseTool(tools) {
|
|
|
311
422
|
function assertCustomToolNames(input) {
|
|
312
423
|
const names = new Set(input.reservedNames);
|
|
313
424
|
for (const tool of input.tools) {
|
|
314
|
-
if (names.has(tool.name))
|
|
425
|
+
if (names.has(tool.name))
|
|
426
|
+
throw new Error(`Custom tool name collision: ${tool.name}`);
|
|
315
427
|
names.add(tool.name);
|
|
316
428
|
}
|
|
317
429
|
}
|
|
@@ -13,9 +13,25 @@ var NornCommandRunner = class {
|
|
|
13
13
|
const cwd = requireAbsoluteWorkingDirectory(commandInput.cwd);
|
|
14
14
|
const startedAtMs = Date.now();
|
|
15
15
|
const invocationId = randomUUID();
|
|
16
|
-
const stdoutRef = commandLog({
|
|
17
|
-
|
|
18
|
-
|
|
16
|
+
const stdoutRef = commandLog({
|
|
17
|
+
label: commandInput.label,
|
|
18
|
+
invocationId,
|
|
19
|
+
stream: "stdout"
|
|
20
|
+
});
|
|
21
|
+
const stderrRef = commandLog({
|
|
22
|
+
label: commandInput.label,
|
|
23
|
+
invocationId,
|
|
24
|
+
stream: "stderr"
|
|
25
|
+
});
|
|
26
|
+
await this.input.logger.record({
|
|
27
|
+
type: "command.started",
|
|
28
|
+
invocationId,
|
|
29
|
+
label: commandInput.label,
|
|
30
|
+
command: commandInput.command,
|
|
31
|
+
cwd,
|
|
32
|
+
stdoutLogId: stdoutRef.id,
|
|
33
|
+
stderrLogId: stderrRef.id
|
|
34
|
+
});
|
|
19
35
|
let result;
|
|
20
36
|
const stdoutLog = await this.input.logs.createWriteStream(stdoutRef);
|
|
21
37
|
const stderrLog = await this.input.logs.createWriteStream(stderrRef);
|
|
@@ -30,7 +46,13 @@ var NornCommandRunner = class {
|
|
|
30
46
|
stderrStream: stderrLog.stream
|
|
31
47
|
});
|
|
32
48
|
} catch (error) {
|
|
33
|
-
await this.input.logger.record({
|
|
49
|
+
await this.input.logger.record({
|
|
50
|
+
type: "command.failed",
|
|
51
|
+
invocationId,
|
|
52
|
+
label: commandInput.label,
|
|
53
|
+
durationMs: Date.now() - startedAtMs,
|
|
54
|
+
error: errorMessage(error)
|
|
55
|
+
});
|
|
34
56
|
throw error;
|
|
35
57
|
}
|
|
36
58
|
const commandResult = {
|
|
@@ -64,7 +86,11 @@ async function spawnCommand(input) {
|
|
|
64
86
|
const stdout = new BoundedTextBuffer();
|
|
65
87
|
const stderr = new BoundedTextBuffer();
|
|
66
88
|
return new Promise((resolvePromise, reject) => {
|
|
67
|
-
const child = spawn(command, args, {
|
|
89
|
+
const child = spawn(command, args, {
|
|
90
|
+
cwd: input.cwd,
|
|
91
|
+
env: input.env,
|
|
92
|
+
shell: false
|
|
93
|
+
});
|
|
68
94
|
let timeout;
|
|
69
95
|
let escalation;
|
|
70
96
|
let isSettled = false;
|
|
@@ -77,24 +103,32 @@ async function spawnCommand(input) {
|
|
|
77
103
|
if (isSettled) return;
|
|
78
104
|
isSettled = true;
|
|
79
105
|
cleanup();
|
|
80
|
-
void closeStreams(input.stdoutStream, input.stderrStream).then(
|
|
106
|
+
void closeStreams(input.stdoutStream, input.stderrStream).then(
|
|
107
|
+
() => resolvePromise(result),
|
|
108
|
+
reject
|
|
109
|
+
);
|
|
81
110
|
};
|
|
82
111
|
const fail = (error) => {
|
|
83
112
|
if (isSettled) return;
|
|
84
113
|
isSettled = true;
|
|
85
114
|
cleanup();
|
|
86
|
-
void closeStreams(input.stdoutStream, input.stderrStream).then(
|
|
115
|
+
void closeStreams(input.stdoutStream, input.stderrStream).then(
|
|
116
|
+
() => reject(error),
|
|
117
|
+
reject
|
|
118
|
+
);
|
|
87
119
|
};
|
|
88
120
|
const killChild = () => {
|
|
89
121
|
if (isSettled || killed) return;
|
|
90
122
|
killed = true;
|
|
91
123
|
child.kill("SIGTERM");
|
|
92
124
|
escalation = setTimeout(() => {
|
|
93
|
-
if (child.exitCode === null && child.signalCode === null)
|
|
125
|
+
if (child.exitCode === null && child.signalCode === null)
|
|
126
|
+
child.kill("SIGKILL");
|
|
94
127
|
}, 5e3);
|
|
95
128
|
escalation.unref?.();
|
|
96
129
|
};
|
|
97
|
-
if (input.timeoutMs !== void 0)
|
|
130
|
+
if (input.timeoutMs !== void 0)
|
|
131
|
+
timeout = setTimeout(killChild, input.timeoutMs);
|
|
98
132
|
if (input.signal?.aborted) killChild();
|
|
99
133
|
input.signal?.addEventListener("abort", killChild, { once: true });
|
|
100
134
|
child.stdout?.on("data", (chunk) => {
|
|
@@ -108,11 +142,21 @@ async function spawnCommand(input) {
|
|
|
108
142
|
input.stderrStream.write(text);
|
|
109
143
|
});
|
|
110
144
|
child.on("error", fail);
|
|
111
|
-
child.on(
|
|
145
|
+
child.on(
|
|
146
|
+
"close",
|
|
147
|
+
(code) => finish({
|
|
148
|
+
exitCode: code,
|
|
149
|
+
stdoutTail: stdout.value(),
|
|
150
|
+
stderrTail: stderr.value(),
|
|
151
|
+
killed
|
|
152
|
+
})
|
|
153
|
+
);
|
|
112
154
|
});
|
|
113
155
|
}
|
|
114
156
|
function commandLog(input) {
|
|
115
|
-
return {
|
|
157
|
+
return {
|
|
158
|
+
id: `commands/${safeFileName(input.label)}-${input.invocationId}.${input.stream}`
|
|
159
|
+
};
|
|
116
160
|
}
|
|
117
161
|
var BoundedTextBuffer = class {
|
|
118
162
|
constructor(maxChars = 128e3) {
|
|
@@ -122,17 +166,22 @@ var BoundedTextBuffer = class {
|
|
|
122
166
|
text = "";
|
|
123
167
|
append(value) {
|
|
124
168
|
this.text += value;
|
|
125
|
-
if (this.text.length > this.maxChars)
|
|
169
|
+
if (this.text.length > this.maxChars)
|
|
170
|
+
this.text = this.text.slice(this.text.length - this.maxChars);
|
|
126
171
|
}
|
|
127
172
|
value() {
|
|
128
173
|
return this.text;
|
|
129
174
|
}
|
|
130
175
|
};
|
|
131
176
|
async function closeStreams(...streams) {
|
|
132
|
-
await Promise.all(
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
177
|
+
await Promise.all(
|
|
178
|
+
streams.map(
|
|
179
|
+
(stream) => new Promise((resolvePromise, reject) => {
|
|
180
|
+
stream.on("error", reject);
|
|
181
|
+
stream.end(resolvePromise);
|
|
182
|
+
})
|
|
183
|
+
)
|
|
184
|
+
);
|
|
136
185
|
}
|
|
137
186
|
export {
|
|
138
187
|
NornCommandRunner
|
|
@@ -7,33 +7,45 @@ var DOCUMENTATION_PATHS = {
|
|
|
7
7
|
examples: "examples"
|
|
8
8
|
};
|
|
9
9
|
function validateDocumentationBundle(bundle) {
|
|
10
|
-
if (typeof bundle.version !== "string" || bundle.version.length === 0)
|
|
10
|
+
if (typeof bundle.version !== "string" || bundle.version.length === 0)
|
|
11
|
+
throw new Error("Documentation bundle version must not be empty");
|
|
11
12
|
const paths = /* @__PURE__ */ new Set();
|
|
12
13
|
for (const file of bundle.files) {
|
|
13
|
-
if (typeof file.content !== "string")
|
|
14
|
-
|
|
14
|
+
if (typeof file.content !== "string")
|
|
15
|
+
throw new Error(`Documentation asset must contain text: ${file.path}`);
|
|
16
|
+
if (typeof file.path !== "string" || !/^[a-zA-Z0-9._/-]+$/.test(file.path) || file.path.split("/").some(
|
|
17
|
+
(part) => part === "" || part === "." || part === ".." || part.endsWith(".") || /^(con|prn|aux|nul|com[1-9]|lpt[1-9])(?:\.|$)/i.test(part)
|
|
18
|
+
)) {
|
|
15
19
|
throw new Error(`Invalid documentation asset path: ${file.path}`);
|
|
16
20
|
}
|
|
17
21
|
const portablePath = file.path.toLowerCase();
|
|
18
|
-
if (paths.has(portablePath))
|
|
22
|
+
if (paths.has(portablePath))
|
|
23
|
+
throw new Error(`Duplicate documentation asset path: ${file.path}`);
|
|
19
24
|
paths.add(portablePath);
|
|
20
25
|
}
|
|
21
26
|
for (const path of paths) {
|
|
22
27
|
const parts = path.split("/");
|
|
23
28
|
while (parts.pop() && parts.length > 0) {
|
|
24
|
-
if (paths.has(parts.join("/")))
|
|
29
|
+
if (paths.has(parts.join("/")))
|
|
30
|
+
throw new Error(`Documentation file/directory collision: ${path}`);
|
|
25
31
|
}
|
|
26
32
|
}
|
|
27
33
|
for (const path of [DOCUMENTATION_PATHS.readme, DOCUMENTATION_PATHS.index]) {
|
|
28
|
-
if (!bundle.files.some((file) => file.path === path))
|
|
34
|
+
if (!bundle.files.some((file) => file.path === path))
|
|
35
|
+
throw new Error(`Missing documentation asset: ${path}`);
|
|
29
36
|
}
|
|
30
|
-
if (!bundle.files.some((file) => file.path.startsWith("examples/")))
|
|
37
|
+
if (!bundle.files.some((file) => file.path.startsWith("examples/")))
|
|
38
|
+
throw new Error("Missing documentation examples");
|
|
31
39
|
}
|
|
32
40
|
function hashDocumentationBundle(bundle) {
|
|
33
|
-
return createHash("sha256").update(
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
41
|
+
return createHash("sha256").update(
|
|
42
|
+
JSON.stringify({
|
|
43
|
+
version: bundle.version,
|
|
44
|
+
files: bundle.files.map((file) => ({ path: file.path, content: file.content })).sort(
|
|
45
|
+
(left, right) => left.path < right.path ? -1 : left.path > right.path ? 1 : 0
|
|
46
|
+
)
|
|
47
|
+
})
|
|
48
|
+
).digest("hex");
|
|
37
49
|
}
|
|
38
50
|
export {
|
|
39
51
|
DOCUMENTATION_PATHS,
|