@hasna/loops 0.1.0 → 0.2.0
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/README.md +67 -2
- package/dist/cli/index.js +928 -33
- package/dist/daemon/index.js +779 -23
- package/dist/index.d.ts +3 -1
- package/dist/index.js +785 -22
- package/dist/lib/accounts.d.ts +4 -0
- package/dist/lib/executor.d.ts +12 -1
- package/dist/lib/format.d.ts +5 -1
- package/dist/lib/store.d.ts +37 -2
- package/dist/lib/store.js +489 -5
- package/dist/lib/workflow-runner.d.ts +11 -0
- package/dist/lib/workflow-spec.d.ts +5 -0
- package/dist/sdk/index.js +780 -22
- package/dist/types.d.ts +87 -2
- package/docs/USAGE.md +68 -3
- package/package.json +2 -2
package/dist/cli/index.js
CHANGED
|
@@ -236,6 +236,100 @@ function parseDuration(input) {
|
|
|
236
236
|
return Math.round(value * multiplier);
|
|
237
237
|
}
|
|
238
238
|
|
|
239
|
+
// src/lib/workflow-spec.ts
|
|
240
|
+
function assertObject(value, label) {
|
|
241
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
242
|
+
throw new Error(`${label} must be an object`);
|
|
243
|
+
}
|
|
244
|
+
function assertString(value, label) {
|
|
245
|
+
if (typeof value !== "string" || value.trim() === "")
|
|
246
|
+
throw new Error(`${label} must be a non-empty string`);
|
|
247
|
+
}
|
|
248
|
+
function validateTarget(value, label) {
|
|
249
|
+
assertObject(value, label);
|
|
250
|
+
if (value.type === "command") {
|
|
251
|
+
assertString(value.command, `${label}.command`);
|
|
252
|
+
return value;
|
|
253
|
+
}
|
|
254
|
+
if (value.type === "agent") {
|
|
255
|
+
assertString(value.provider, `${label}.provider`);
|
|
256
|
+
assertString(value.prompt, `${label}.prompt`);
|
|
257
|
+
const providers = ["claude", "cursor", "codewith", "aicopilot", "opencode", "codex"];
|
|
258
|
+
if (!providers.includes(value.provider))
|
|
259
|
+
throw new Error(`${label}.provider must be one of ${providers.join(", ")}`);
|
|
260
|
+
return value;
|
|
261
|
+
}
|
|
262
|
+
throw new Error(`${label}.type must be command or agent`);
|
|
263
|
+
}
|
|
264
|
+
function normalizeCreateWorkflowInput(input) {
|
|
265
|
+
assertString(input.name, "workflow.name");
|
|
266
|
+
if (!Array.isArray(input.steps) || input.steps.length === 0)
|
|
267
|
+
throw new Error("workflow.steps must contain at least one step");
|
|
268
|
+
const seen = new Set;
|
|
269
|
+
const steps = input.steps.map((step, index) => {
|
|
270
|
+
assertObject(step, `workflow.steps[${index}]`);
|
|
271
|
+
assertString(step.id, `workflow.steps[${index}].id`);
|
|
272
|
+
if (seen.has(step.id))
|
|
273
|
+
throw new Error(`duplicate workflow step id: ${step.id}`);
|
|
274
|
+
seen.add(step.id);
|
|
275
|
+
return {
|
|
276
|
+
...step,
|
|
277
|
+
id: step.id,
|
|
278
|
+
target: validateTarget(step.target, `workflow.steps[${index}].target`),
|
|
279
|
+
dependsOn: step.dependsOn ?? [],
|
|
280
|
+
continueOnFailure: step.continueOnFailure ?? false
|
|
281
|
+
};
|
|
282
|
+
});
|
|
283
|
+
for (const step of steps) {
|
|
284
|
+
for (const dependency of step.dependsOn ?? []) {
|
|
285
|
+
if (!seen.has(dependency))
|
|
286
|
+
throw new Error(`step ${step.id} depends on missing step ${dependency}`);
|
|
287
|
+
if (dependency === step.id)
|
|
288
|
+
throw new Error(`step ${step.id} cannot depend on itself`);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
workflowExecutionOrder({ steps });
|
|
292
|
+
return { ...input, name: input.name.trim(), version: input.version ?? 1, steps };
|
|
293
|
+
}
|
|
294
|
+
function workflowExecutionOrder(workflow) {
|
|
295
|
+
const byId = new Map(workflow.steps.map((step) => [step.id, step]));
|
|
296
|
+
const visiting = new Set;
|
|
297
|
+
const visited = new Set;
|
|
298
|
+
const order = [];
|
|
299
|
+
function visit(step) {
|
|
300
|
+
if (visited.has(step.id))
|
|
301
|
+
return;
|
|
302
|
+
if (visiting.has(step.id))
|
|
303
|
+
throw new Error(`workflow dependency cycle includes step ${step.id}`);
|
|
304
|
+
visiting.add(step.id);
|
|
305
|
+
for (const dependencyId of step.dependsOn ?? []) {
|
|
306
|
+
const dependency = byId.get(dependencyId);
|
|
307
|
+
if (!dependency)
|
|
308
|
+
throw new Error(`step ${step.id} depends on missing step ${dependencyId}`);
|
|
309
|
+
visit(dependency);
|
|
310
|
+
}
|
|
311
|
+
visiting.delete(step.id);
|
|
312
|
+
visited.add(step.id);
|
|
313
|
+
order.push(step);
|
|
314
|
+
}
|
|
315
|
+
for (const step of workflow.steps)
|
|
316
|
+
visit(step);
|
|
317
|
+
return order;
|
|
318
|
+
}
|
|
319
|
+
function workflowBodyFromJson(value, fallbackName) {
|
|
320
|
+
assertObject(value, "workflow file");
|
|
321
|
+
const rawName = fallbackName ?? value.name;
|
|
322
|
+
assertString(rawName, "workflow.name");
|
|
323
|
+
if (!Array.isArray(value.steps))
|
|
324
|
+
throw new Error("workflow.steps must be an array");
|
|
325
|
+
return normalizeCreateWorkflowInput({
|
|
326
|
+
name: rawName,
|
|
327
|
+
description: typeof value.description === "string" ? value.description : undefined,
|
|
328
|
+
version: typeof value.version === "number" ? value.version : undefined,
|
|
329
|
+
steps: value.steps
|
|
330
|
+
});
|
|
331
|
+
}
|
|
332
|
+
|
|
239
333
|
// src/lib/store.ts
|
|
240
334
|
function rowToLoop(row) {
|
|
241
335
|
return {
|
|
@@ -280,6 +374,67 @@ function rowToRun(row) {
|
|
|
280
374
|
updatedAt: row.updated_at
|
|
281
375
|
};
|
|
282
376
|
}
|
|
377
|
+
function rowToWorkflow(row) {
|
|
378
|
+
return {
|
|
379
|
+
id: row.id,
|
|
380
|
+
name: row.name,
|
|
381
|
+
description: row.description ?? undefined,
|
|
382
|
+
version: row.version,
|
|
383
|
+
status: row.status,
|
|
384
|
+
steps: JSON.parse(row.steps_json),
|
|
385
|
+
createdAt: row.created_at,
|
|
386
|
+
updatedAt: row.updated_at
|
|
387
|
+
};
|
|
388
|
+
}
|
|
389
|
+
function rowToWorkflowRun(row) {
|
|
390
|
+
return {
|
|
391
|
+
id: row.id,
|
|
392
|
+
workflowId: row.workflow_id,
|
|
393
|
+
workflowName: row.workflow_name,
|
|
394
|
+
loopId: row.loop_id ?? undefined,
|
|
395
|
+
loopRunId: row.loop_run_id ?? undefined,
|
|
396
|
+
scheduledFor: row.scheduled_for ?? undefined,
|
|
397
|
+
idempotencyKey: row.idempotency_key ?? undefined,
|
|
398
|
+
status: row.status,
|
|
399
|
+
startedAt: row.started_at ?? undefined,
|
|
400
|
+
finishedAt: row.finished_at ?? undefined,
|
|
401
|
+
durationMs: row.duration_ms ?? undefined,
|
|
402
|
+
error: row.error ?? undefined,
|
|
403
|
+
createdAt: row.created_at,
|
|
404
|
+
updatedAt: row.updated_at
|
|
405
|
+
};
|
|
406
|
+
}
|
|
407
|
+
function rowToWorkflowStepRun(row) {
|
|
408
|
+
return {
|
|
409
|
+
id: row.id,
|
|
410
|
+
workflowRunId: row.workflow_run_id,
|
|
411
|
+
stepId: row.step_id,
|
|
412
|
+
sequence: row.sequence,
|
|
413
|
+
status: row.status,
|
|
414
|
+
startedAt: row.started_at ?? undefined,
|
|
415
|
+
finishedAt: row.finished_at ?? undefined,
|
|
416
|
+
exitCode: row.exit_code ?? undefined,
|
|
417
|
+
durationMs: row.duration_ms ?? undefined,
|
|
418
|
+
stdout: row.stdout ?? undefined,
|
|
419
|
+
stderr: row.stderr ?? undefined,
|
|
420
|
+
error: row.error ?? undefined,
|
|
421
|
+
accountProfile: row.account_profile ?? undefined,
|
|
422
|
+
accountTool: row.account_tool ?? undefined,
|
|
423
|
+
createdAt: row.created_at,
|
|
424
|
+
updatedAt: row.updated_at
|
|
425
|
+
};
|
|
426
|
+
}
|
|
427
|
+
function rowToWorkflowEvent(row) {
|
|
428
|
+
return {
|
|
429
|
+
id: row.id,
|
|
430
|
+
workflowRunId: row.workflow_run_id,
|
|
431
|
+
sequence: row.sequence,
|
|
432
|
+
eventType: row.event_type,
|
|
433
|
+
stepId: row.step_id ?? undefined,
|
|
434
|
+
payload: row.payload_json ? JSON.parse(row.payload_json) : undefined,
|
|
435
|
+
createdAt: row.created_at
|
|
436
|
+
};
|
|
437
|
+
}
|
|
283
438
|
function rowToLease(row) {
|
|
284
439
|
return {
|
|
285
440
|
id: row.id,
|
|
@@ -305,6 +460,11 @@ class Store {
|
|
|
305
460
|
}
|
|
306
461
|
migrate() {
|
|
307
462
|
this.db.exec(`
|
|
463
|
+
CREATE TABLE IF NOT EXISTS schema_migrations (
|
|
464
|
+
id TEXT PRIMARY KEY,
|
|
465
|
+
applied_at TEXT NOT NULL
|
|
466
|
+
);
|
|
467
|
+
|
|
308
468
|
CREATE TABLE IF NOT EXISTS loops (
|
|
309
469
|
id TEXT PRIMARY KEY,
|
|
310
470
|
name TEXT NOT NULL,
|
|
@@ -361,7 +521,78 @@ class Store {
|
|
|
361
521
|
created_at TEXT NOT NULL,
|
|
362
522
|
updated_at TEXT NOT NULL
|
|
363
523
|
);
|
|
524
|
+
|
|
525
|
+
CREATE TABLE IF NOT EXISTS workflow_specs (
|
|
526
|
+
id TEXT PRIMARY KEY,
|
|
527
|
+
name TEXT NOT NULL,
|
|
528
|
+
description TEXT,
|
|
529
|
+
version INTEGER NOT NULL,
|
|
530
|
+
status TEXT NOT NULL,
|
|
531
|
+
steps_json TEXT NOT NULL,
|
|
532
|
+
created_at TEXT NOT NULL,
|
|
533
|
+
updated_at TEXT NOT NULL
|
|
534
|
+
);
|
|
535
|
+
CREATE INDEX IF NOT EXISTS idx_workflows_status_name ON workflow_specs(status, name);
|
|
536
|
+
CREATE UNIQUE INDEX IF NOT EXISTS idx_workflows_name_active ON workflow_specs(name) WHERE status = 'active';
|
|
537
|
+
|
|
538
|
+
CREATE TABLE IF NOT EXISTS workflow_runs (
|
|
539
|
+
id TEXT PRIMARY KEY,
|
|
540
|
+
workflow_id TEXT NOT NULL REFERENCES workflow_specs(id) ON DELETE CASCADE,
|
|
541
|
+
workflow_name TEXT NOT NULL,
|
|
542
|
+
loop_id TEXT REFERENCES loops(id) ON DELETE SET NULL,
|
|
543
|
+
loop_run_id TEXT REFERENCES loop_runs(id) ON DELETE SET NULL,
|
|
544
|
+
scheduled_for TEXT,
|
|
545
|
+
idempotency_key TEXT,
|
|
546
|
+
status TEXT NOT NULL,
|
|
547
|
+
started_at TEXT,
|
|
548
|
+
finished_at TEXT,
|
|
549
|
+
duration_ms INTEGER,
|
|
550
|
+
error TEXT,
|
|
551
|
+
created_at TEXT NOT NULL,
|
|
552
|
+
updated_at TEXT NOT NULL
|
|
553
|
+
);
|
|
554
|
+
CREATE UNIQUE INDEX IF NOT EXISTS idx_workflow_runs_idempotency
|
|
555
|
+
ON workflow_runs(workflow_id, idempotency_key)
|
|
556
|
+
WHERE idempotency_key IS NOT NULL;
|
|
557
|
+
CREATE INDEX IF NOT EXISTS idx_workflow_runs_workflow_created ON workflow_runs(workflow_id, created_at DESC);
|
|
558
|
+
CREATE INDEX IF NOT EXISTS idx_workflow_runs_loop_run ON workflow_runs(loop_run_id);
|
|
559
|
+
CREATE INDEX IF NOT EXISTS idx_workflow_runs_status ON workflow_runs(status);
|
|
560
|
+
|
|
561
|
+
CREATE TABLE IF NOT EXISTS workflow_step_runs (
|
|
562
|
+
id TEXT PRIMARY KEY,
|
|
563
|
+
workflow_run_id TEXT NOT NULL REFERENCES workflow_runs(id) ON DELETE CASCADE,
|
|
564
|
+
step_id TEXT NOT NULL,
|
|
565
|
+
sequence INTEGER NOT NULL,
|
|
566
|
+
status TEXT NOT NULL,
|
|
567
|
+
started_at TEXT,
|
|
568
|
+
finished_at TEXT,
|
|
569
|
+
exit_code INTEGER,
|
|
570
|
+
duration_ms INTEGER,
|
|
571
|
+
stdout TEXT,
|
|
572
|
+
stderr TEXT,
|
|
573
|
+
error TEXT,
|
|
574
|
+
account_profile TEXT,
|
|
575
|
+
account_tool TEXT,
|
|
576
|
+
created_at TEXT NOT NULL,
|
|
577
|
+
updated_at TEXT NOT NULL,
|
|
578
|
+
UNIQUE(workflow_run_id, step_id)
|
|
579
|
+
);
|
|
580
|
+
CREATE INDEX IF NOT EXISTS idx_workflow_step_runs_run_sequence ON workflow_step_runs(workflow_run_id, sequence);
|
|
581
|
+
CREATE INDEX IF NOT EXISTS idx_workflow_step_runs_status ON workflow_step_runs(status);
|
|
582
|
+
|
|
583
|
+
CREATE TABLE IF NOT EXISTS workflow_events (
|
|
584
|
+
id TEXT PRIMARY KEY,
|
|
585
|
+
workflow_run_id TEXT NOT NULL REFERENCES workflow_runs(id) ON DELETE CASCADE,
|
|
586
|
+
sequence INTEGER NOT NULL,
|
|
587
|
+
event_type TEXT NOT NULL,
|
|
588
|
+
step_id TEXT,
|
|
589
|
+
payload_json TEXT,
|
|
590
|
+
created_at TEXT NOT NULL,
|
|
591
|
+
UNIQUE(workflow_run_id, sequence)
|
|
592
|
+
);
|
|
593
|
+
CREATE INDEX IF NOT EXISTS idx_workflow_events_run_sequence ON workflow_events(workflow_run_id, sequence);
|
|
364
594
|
`);
|
|
595
|
+
this.db.query("INSERT OR IGNORE INTO schema_migrations (id, applied_at) VALUES (?, ?)").run("0001_initial_and_workflows", nowIso());
|
|
365
596
|
}
|
|
366
597
|
createLoop(input, from = new Date) {
|
|
367
598
|
const now = nowIso();
|
|
@@ -453,6 +684,244 @@ class Store {
|
|
|
453
684
|
const res = this.db.query("DELETE FROM loops WHERE id = ?").run(loop.id);
|
|
454
685
|
return res.changes > 0;
|
|
455
686
|
}
|
|
687
|
+
createWorkflow(input) {
|
|
688
|
+
const normalized = normalizeCreateWorkflowInput(input);
|
|
689
|
+
const now = nowIso();
|
|
690
|
+
const workflow = {
|
|
691
|
+
id: genId(),
|
|
692
|
+
name: normalized.name,
|
|
693
|
+
description: normalized.description,
|
|
694
|
+
version: normalized.version ?? 1,
|
|
695
|
+
status: "active",
|
|
696
|
+
steps: normalized.steps,
|
|
697
|
+
createdAt: now,
|
|
698
|
+
updatedAt: now
|
|
699
|
+
};
|
|
700
|
+
this.db.query(`INSERT INTO workflow_specs (id, name, description, version, status, steps_json, created_at, updated_at)
|
|
701
|
+
VALUES ($id, $name, $description, $version, $status, $steps, $created, $updated)`).run({
|
|
702
|
+
$id: workflow.id,
|
|
703
|
+
$name: workflow.name,
|
|
704
|
+
$description: workflow.description ?? null,
|
|
705
|
+
$version: workflow.version,
|
|
706
|
+
$status: workflow.status,
|
|
707
|
+
$steps: JSON.stringify(workflow.steps),
|
|
708
|
+
$created: workflow.createdAt,
|
|
709
|
+
$updated: workflow.updatedAt
|
|
710
|
+
});
|
|
711
|
+
return workflow;
|
|
712
|
+
}
|
|
713
|
+
getWorkflow(id) {
|
|
714
|
+
const row = this.db.query("SELECT * FROM workflow_specs WHERE id = ?").get(id);
|
|
715
|
+
return row ? rowToWorkflow(row) : undefined;
|
|
716
|
+
}
|
|
717
|
+
findWorkflowByName(name) {
|
|
718
|
+
const row = this.db.query("SELECT * FROM workflow_specs WHERE name = ? AND status = 'active' ORDER BY updated_at DESC LIMIT 1").get(name);
|
|
719
|
+
return row ? rowToWorkflow(row) : undefined;
|
|
720
|
+
}
|
|
721
|
+
requireWorkflow(idOrName) {
|
|
722
|
+
return this.getWorkflow(idOrName) ?? this.findWorkflowByName(idOrName) ?? (() => {
|
|
723
|
+
throw new Error(`workflow not found: ${idOrName}`);
|
|
724
|
+
})();
|
|
725
|
+
}
|
|
726
|
+
listWorkflows(opts = {}) {
|
|
727
|
+
const limit = opts.limit ?? 200;
|
|
728
|
+
const rows = opts.status ? this.db.query("SELECT * FROM workflow_specs WHERE status = ? ORDER BY updated_at DESC LIMIT ?").all(opts.status, limit) : this.db.query("SELECT * FROM workflow_specs ORDER BY status ASC, updated_at DESC LIMIT ?").all(limit);
|
|
729
|
+
return rows.map(rowToWorkflow);
|
|
730
|
+
}
|
|
731
|
+
archiveWorkflow(idOrName) {
|
|
732
|
+
const workflow = this.requireWorkflow(idOrName);
|
|
733
|
+
const updated = nowIso();
|
|
734
|
+
this.db.query("UPDATE workflow_specs SET status='archived', updated_at=? WHERE id=?").run(updated, workflow.id);
|
|
735
|
+
const archived = this.getWorkflow(workflow.id);
|
|
736
|
+
if (!archived)
|
|
737
|
+
throw new Error(`workflow not found after archive: ${workflow.id}`);
|
|
738
|
+
return archived;
|
|
739
|
+
}
|
|
740
|
+
createWorkflowRun(input) {
|
|
741
|
+
const now = nowIso();
|
|
742
|
+
if (input.idempotencyKey) {
|
|
743
|
+
const existing = this.db.query("SELECT * FROM workflow_runs WHERE workflow_id = ? AND idempotency_key = ? LIMIT 1").get(input.workflow.id, input.idempotencyKey);
|
|
744
|
+
if (existing)
|
|
745
|
+
return rowToWorkflowRun(existing);
|
|
746
|
+
}
|
|
747
|
+
this.db.exec("BEGIN IMMEDIATE");
|
|
748
|
+
try {
|
|
749
|
+
if (input.idempotencyKey) {
|
|
750
|
+
const existing = this.db.query("SELECT * FROM workflow_runs WHERE workflow_id = ? AND idempotency_key = ? LIMIT 1").get(input.workflow.id, input.idempotencyKey);
|
|
751
|
+
if (existing) {
|
|
752
|
+
this.db.exec("COMMIT");
|
|
753
|
+
return rowToWorkflowRun(existing);
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
const runId = genId();
|
|
757
|
+
this.db.query(`INSERT INTO workflow_runs (id, workflow_id, workflow_name, loop_id, loop_run_id, scheduled_for, idempotency_key,
|
|
758
|
+
status, started_at, finished_at, duration_ms, error, created_at, updated_at)
|
|
759
|
+
VALUES ($id, $workflowId, $workflowName, $loopId, $loopRunId, $scheduledFor, $idempotencyKey,
|
|
760
|
+
'running', $started, NULL, NULL, NULL, $created, $updated)`).run({
|
|
761
|
+
$id: runId,
|
|
762
|
+
$workflowId: input.workflow.id,
|
|
763
|
+
$workflowName: input.workflow.name,
|
|
764
|
+
$loopId: input.loop?.id ?? null,
|
|
765
|
+
$loopRunId: input.loopRun?.id ?? null,
|
|
766
|
+
$scheduledFor: input.scheduledFor ?? input.loopRun?.scheduledFor ?? null,
|
|
767
|
+
$idempotencyKey: input.idempotencyKey ?? null,
|
|
768
|
+
$started: now,
|
|
769
|
+
$created: now,
|
|
770
|
+
$updated: now
|
|
771
|
+
});
|
|
772
|
+
input.workflow.steps.forEach((step, sequence) => {
|
|
773
|
+
const account = step.account ?? step.target.account;
|
|
774
|
+
this.db.query(`INSERT INTO workflow_step_runs (id, workflow_run_id, step_id, sequence, status, started_at, finished_at,
|
|
775
|
+
exit_code, duration_ms, stdout, stderr, error, account_profile, account_tool, created_at, updated_at)
|
|
776
|
+
VALUES ($id, $workflowRunId, $stepId, $sequence, 'pending', NULL, NULL, NULL, NULL, NULL, NULL, NULL,
|
|
777
|
+
$accountProfile, $accountTool, $created, $updated)`).run({
|
|
778
|
+
$id: genId(),
|
|
779
|
+
$workflowRunId: runId,
|
|
780
|
+
$stepId: step.id,
|
|
781
|
+
$sequence: sequence,
|
|
782
|
+
$accountProfile: account?.profile ?? null,
|
|
783
|
+
$accountTool: account?.tool ?? null,
|
|
784
|
+
$created: now,
|
|
785
|
+
$updated: now
|
|
786
|
+
});
|
|
787
|
+
});
|
|
788
|
+
this.db.query(`INSERT INTO workflow_events (id, workflow_run_id, sequence, event_type, step_id, payload_json, created_at)
|
|
789
|
+
VALUES ($id, $workflowRunId, 1, 'created', NULL, $payload, $created)`).run({
|
|
790
|
+
$id: genId(),
|
|
791
|
+
$workflowRunId: runId,
|
|
792
|
+
$payload: JSON.stringify({
|
|
793
|
+
workflowId: input.workflow.id,
|
|
794
|
+
workflowName: input.workflow.name,
|
|
795
|
+
stepCount: input.workflow.steps.length,
|
|
796
|
+
loopId: input.loop?.id,
|
|
797
|
+
loopRunId: input.loopRun?.id
|
|
798
|
+
}),
|
|
799
|
+
$created: now
|
|
800
|
+
});
|
|
801
|
+
this.db.exec("COMMIT");
|
|
802
|
+
const run = this.getWorkflowRun(runId);
|
|
803
|
+
if (!run)
|
|
804
|
+
throw new Error(`workflow run not found after create: ${runId}`);
|
|
805
|
+
return run;
|
|
806
|
+
} catch (error) {
|
|
807
|
+
try {
|
|
808
|
+
this.db.exec("ROLLBACK");
|
|
809
|
+
} catch {}
|
|
810
|
+
throw error;
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
getWorkflowRun(id) {
|
|
814
|
+
const row = this.db.query("SELECT * FROM workflow_runs WHERE id = ?").get(id);
|
|
815
|
+
return row ? rowToWorkflowRun(row) : undefined;
|
|
816
|
+
}
|
|
817
|
+
listWorkflowRuns(opts = {}) {
|
|
818
|
+
const limit = opts.limit ?? 100;
|
|
819
|
+
let rows;
|
|
820
|
+
if (opts.workflowId) {
|
|
821
|
+
rows = this.db.query("SELECT * FROM workflow_runs WHERE workflow_id = ? ORDER BY created_at DESC LIMIT ?").all(opts.workflowId, limit);
|
|
822
|
+
} else if (opts.loopRunId) {
|
|
823
|
+
rows = this.db.query("SELECT * FROM workflow_runs WHERE loop_run_id = ? ORDER BY created_at DESC LIMIT ?").all(opts.loopRunId, limit);
|
|
824
|
+
} else {
|
|
825
|
+
rows = this.db.query("SELECT * FROM workflow_runs ORDER BY created_at DESC LIMIT ?").all(limit);
|
|
826
|
+
}
|
|
827
|
+
return rows.map(rowToWorkflowRun);
|
|
828
|
+
}
|
|
829
|
+
listWorkflowStepRuns(workflowRunId) {
|
|
830
|
+
const rows = this.db.query("SELECT * FROM workflow_step_runs WHERE workflow_run_id = ? ORDER BY sequence ASC").all(workflowRunId);
|
|
831
|
+
return rows.map(rowToWorkflowStepRun);
|
|
832
|
+
}
|
|
833
|
+
getWorkflowStepRun(workflowRunId, stepId) {
|
|
834
|
+
const row = this.db.query("SELECT * FROM workflow_step_runs WHERE workflow_run_id = ? AND step_id = ?").get(workflowRunId, stepId);
|
|
835
|
+
return row ? rowToWorkflowStepRun(row) : undefined;
|
|
836
|
+
}
|
|
837
|
+
startWorkflowStepRun(workflowRunId, stepId) {
|
|
838
|
+
const now = nowIso();
|
|
839
|
+
this.db.query(`UPDATE workflow_step_runs
|
|
840
|
+
SET status='running', started_at=$started, finished_at=NULL, exit_code=NULL, duration_ms=NULL,
|
|
841
|
+
stdout=NULL, stderr=NULL, error=NULL, updated_at=$updated
|
|
842
|
+
WHERE workflow_run_id=$workflowRunId AND step_id=$stepId AND status IN ('pending', 'running', 'failed', 'timed_out')`).run({ $workflowRunId: workflowRunId, $stepId: stepId, $started: now, $updated: now });
|
|
843
|
+
this.appendWorkflowEvent(workflowRunId, "step_started", stepId);
|
|
844
|
+
const run = this.getWorkflowStepRun(workflowRunId, stepId);
|
|
845
|
+
if (!run)
|
|
846
|
+
throw new Error(`workflow step run not found: ${workflowRunId}/${stepId}`);
|
|
847
|
+
return run;
|
|
848
|
+
}
|
|
849
|
+
finalizeWorkflowStepRun(workflowRunId, stepId, patch) {
|
|
850
|
+
const finishedAt = patch.finishedAt ?? nowIso();
|
|
851
|
+
this.db.query(`UPDATE workflow_step_runs SET status=$status, finished_at=$finished, exit_code=$exitCode, duration_ms=$durationMs,
|
|
852
|
+
stdout=$stdout, stderr=$stderr, error=$error, updated_at=$updated
|
|
853
|
+
WHERE workflow_run_id=$workflowRunId AND step_id=$stepId`).run({
|
|
854
|
+
$workflowRunId: workflowRunId,
|
|
855
|
+
$stepId: stepId,
|
|
856
|
+
$status: patch.status,
|
|
857
|
+
$finished: finishedAt,
|
|
858
|
+
$exitCode: patch.exitCode ?? null,
|
|
859
|
+
$durationMs: patch.durationMs ?? null,
|
|
860
|
+
$stdout: patch.stdout ?? null,
|
|
861
|
+
$stderr: patch.stderr ?? null,
|
|
862
|
+
$error: patch.error ?? null,
|
|
863
|
+
$updated: finishedAt
|
|
864
|
+
});
|
|
865
|
+
this.appendWorkflowEvent(workflowRunId, `step_${patch.status}`, stepId, {
|
|
866
|
+
exitCode: patch.exitCode,
|
|
867
|
+
error: patch.error
|
|
868
|
+
});
|
|
869
|
+
const run = this.getWorkflowStepRun(workflowRunId, stepId);
|
|
870
|
+
if (!run)
|
|
871
|
+
throw new Error(`workflow step run not found after finalize: ${workflowRunId}/${stepId}`);
|
|
872
|
+
return run;
|
|
873
|
+
}
|
|
874
|
+
skipWorkflowStepRun(workflowRunId, stepId, reason) {
|
|
875
|
+
const now = nowIso();
|
|
876
|
+
this.db.query(`UPDATE workflow_step_runs SET status='skipped', finished_at=$finished, error=$error, updated_at=$updated
|
|
877
|
+
WHERE workflow_run_id=$workflowRunId AND step_id=$stepId`).run({ $workflowRunId: workflowRunId, $stepId: stepId, $finished: now, $error: reason, $updated: now });
|
|
878
|
+
this.appendWorkflowEvent(workflowRunId, "step_skipped", stepId, { reason });
|
|
879
|
+
const run = this.getWorkflowStepRun(workflowRunId, stepId);
|
|
880
|
+
if (!run)
|
|
881
|
+
throw new Error(`workflow step run not found after skip: ${workflowRunId}/${stepId}`);
|
|
882
|
+
return run;
|
|
883
|
+
}
|
|
884
|
+
finalizeWorkflowRun(workflowRunId, status, patch = {}) {
|
|
885
|
+
const finishedAt = patch.finishedAt ?? nowIso();
|
|
886
|
+
this.db.query(`UPDATE workflow_runs SET status=$status, finished_at=$finished, duration_ms=$durationMs, error=$error, updated_at=$updated
|
|
887
|
+
WHERE id=$id`).run({
|
|
888
|
+
$id: workflowRunId,
|
|
889
|
+
$status: status,
|
|
890
|
+
$finished: finishedAt,
|
|
891
|
+
$durationMs: patch.durationMs ?? null,
|
|
892
|
+
$error: patch.error ?? null,
|
|
893
|
+
$updated: finishedAt
|
|
894
|
+
});
|
|
895
|
+
this.appendWorkflowEvent(workflowRunId, status, undefined, { error: patch.error });
|
|
896
|
+
const run = this.getWorkflowRun(workflowRunId);
|
|
897
|
+
if (!run)
|
|
898
|
+
throw new Error(`workflow run not found after finalize: ${workflowRunId}`);
|
|
899
|
+
return run;
|
|
900
|
+
}
|
|
901
|
+
appendWorkflowEvent(workflowRunId, eventType, stepId, payload) {
|
|
902
|
+
const now = nowIso();
|
|
903
|
+
const current = this.db.query("SELECT MAX(sequence) AS sequence FROM workflow_events WHERE workflow_run_id = ?").get(workflowRunId);
|
|
904
|
+
const sequence = (current?.sequence ?? 0) + 1;
|
|
905
|
+
const id = genId();
|
|
906
|
+
this.db.query(`INSERT INTO workflow_events (id, workflow_run_id, sequence, event_type, step_id, payload_json, created_at)
|
|
907
|
+
VALUES ($id, $workflowRunId, $sequence, $eventType, $stepId, $payload, $created)`).run({
|
|
908
|
+
$id: id,
|
|
909
|
+
$workflowRunId: workflowRunId,
|
|
910
|
+
$sequence: sequence,
|
|
911
|
+
$eventType: eventType,
|
|
912
|
+
$stepId: stepId ?? null,
|
|
913
|
+
$payload: payload ? JSON.stringify(payload) : null,
|
|
914
|
+
$created: now
|
|
915
|
+
});
|
|
916
|
+
const event = this.db.query("SELECT * FROM workflow_events WHERE id = ?").get(id);
|
|
917
|
+
if (!event)
|
|
918
|
+
throw new Error(`workflow event not found after append: ${id}`);
|
|
919
|
+
return rowToWorkflowEvent(event);
|
|
920
|
+
}
|
|
921
|
+
listWorkflowEvents(workflowRunId, limit = 200) {
|
|
922
|
+
const rows = this.db.query("SELECT * FROM workflow_events WHERE workflow_run_id = ? ORDER BY sequence ASC LIMIT ?").all(workflowRunId, limit);
|
|
923
|
+
return rows.map(rowToWorkflowEvent);
|
|
924
|
+
}
|
|
456
925
|
hasRunningRun(loopId) {
|
|
457
926
|
const row = this.db.query("SELECT COUNT(*) AS count FROM loop_runs WHERE loop_id = ? AND status = 'running'").get(loopId);
|
|
458
927
|
return (row?.count ?? 0) > 0;
|
|
@@ -573,10 +1042,9 @@ class Store {
|
|
|
573
1042
|
throw error;
|
|
574
1043
|
}
|
|
575
1044
|
}
|
|
576
|
-
finalizeRun(id, patch) {
|
|
1045
|
+
finalizeRun(id, patch, opts = {}) {
|
|
577
1046
|
const finishedAt = patch.finishedAt ?? nowIso();
|
|
578
|
-
|
|
579
|
-
duration_ms=$durationMs, stdout=$stdout, stderr=$stderr, error=$error, updated_at=$updated WHERE id=$id`).run({
|
|
1047
|
+
const params = {
|
|
580
1048
|
$id: id,
|
|
581
1049
|
$status: patch.status,
|
|
582
1050
|
$finished: finishedAt,
|
|
@@ -586,13 +1054,29 @@ class Store {
|
|
|
586
1054
|
$stdout: patch.stdout ?? null,
|
|
587
1055
|
$stderr: patch.stderr ?? null,
|
|
588
1056
|
$error: patch.error ?? null,
|
|
589
|
-
$updated: finishedAt
|
|
590
|
-
|
|
1057
|
+
$updated: finishedAt,
|
|
1058
|
+
$claimedBy: opts.claimedBy ?? null,
|
|
1059
|
+
$now: (opts.now ?? new Date).toISOString()
|
|
1060
|
+
};
|
|
1061
|
+
const res = opts.claimedBy ? this.db.query(`UPDATE loop_runs SET status=$status, finished_at=$finished, lease_expires_at=NULL, pid=$pid, exit_code=$exitCode,
|
|
1062
|
+
duration_ms=$durationMs, stdout=$stdout, stderr=$stderr, error=$error, updated_at=$updated
|
|
1063
|
+
WHERE id=$id AND status='running' AND claimed_by=$claimedBy AND lease_expires_at > $now`).run(params) : this.db.query(`UPDATE loop_runs SET status=$status, finished_at=$finished, lease_expires_at=NULL, pid=$pid, exit_code=$exitCode,
|
|
1064
|
+
duration_ms=$durationMs, stdout=$stdout, stderr=$stderr, error=$error, updated_at=$updated WHERE id=$id`).run(params);
|
|
591
1065
|
const run = this.getRun(id);
|
|
592
1066
|
if (!run)
|
|
593
1067
|
throw new Error(`run not found after finalize: ${id}`);
|
|
1068
|
+
if (opts.claimedBy && res.changes !== 1)
|
|
1069
|
+
return run;
|
|
594
1070
|
return run;
|
|
595
1071
|
}
|
|
1072
|
+
heartbeatRunLease(id, claimedBy, leaseMs, now = new Date) {
|
|
1073
|
+
const expiresAt = new Date(now.getTime() + leaseMs).toISOString();
|
|
1074
|
+
const res = this.db.query(`UPDATE loop_runs SET lease_expires_at=$expires, updated_at=$updated
|
|
1075
|
+
WHERE id=$id AND status='running' AND claimed_by=$claimedBy`).run({ $id: id, $claimedBy: claimedBy, $expires: expiresAt, $updated: now.toISOString() });
|
|
1076
|
+
if (res.changes !== 1)
|
|
1077
|
+
return;
|
|
1078
|
+
return this.getRun(id);
|
|
1079
|
+
}
|
|
596
1080
|
listRuns(opts = {}) {
|
|
597
1081
|
const limit = opts.limit ?? 100;
|
|
598
1082
|
let rows;
|
|
@@ -684,7 +1168,7 @@ class Store {
|
|
|
684
1168
|
}
|
|
685
1169
|
|
|
686
1170
|
// src/cli/index.ts
|
|
687
|
-
import { existsSync as
|
|
1171
|
+
import { existsSync as existsSync3, readFileSync as readFileSync2 } from "fs";
|
|
688
1172
|
import { Command } from "commander";
|
|
689
1173
|
|
|
690
1174
|
// src/lib/format.ts
|
|
@@ -696,7 +1180,7 @@ function redact(value, visible = 80) {
|
|
|
696
1180
|
return `${value.slice(0, visible)}... [redacted ${value.length - visible} chars]`;
|
|
697
1181
|
}
|
|
698
1182
|
function publicLoop(loop) {
|
|
699
|
-
const target = loop.target.type === "command" ? { ...loop.target, env: loop.target.env ? "[redacted]" : undefined } : { ...loop.target, prompt: redact(loop.target.prompt) };
|
|
1183
|
+
const target = loop.target.type === "command" ? { ...loop.target, env: loop.target.env ? "[redacted]" : undefined } : loop.target.type === "agent" ? { ...loop.target, prompt: redact(loop.target.prompt) } : loop.target;
|
|
700
1184
|
return {
|
|
701
1185
|
...loop,
|
|
702
1186
|
target
|
|
@@ -709,12 +1193,129 @@ function publicRun(run, showOutput = false) {
|
|
|
709
1193
|
stderr: showOutput ? run.stderr : run.stderr ? `[redacted ${run.stderr.length} chars]` : undefined
|
|
710
1194
|
};
|
|
711
1195
|
}
|
|
1196
|
+
function publicWorkflow(workflow) {
|
|
1197
|
+
return {
|
|
1198
|
+
...workflow,
|
|
1199
|
+
steps: workflow.steps.map((step) => ({
|
|
1200
|
+
...step,
|
|
1201
|
+
target: step.target.type === "agent" ? { ...step.target, prompt: redact(step.target.prompt) } : step.target.type === "command" && step.target.env ? { ...step.target, env: "[redacted]" } : step.target
|
|
1202
|
+
}))
|
|
1203
|
+
};
|
|
1204
|
+
}
|
|
1205
|
+
function publicWorkflowRun(run) {
|
|
1206
|
+
return { ...run };
|
|
1207
|
+
}
|
|
1208
|
+
function publicWorkflowStepRun(run, showOutput = false) {
|
|
1209
|
+
return {
|
|
1210
|
+
...run,
|
|
1211
|
+
stdout: showOutput ? run.stdout : run.stdout ? `[redacted ${run.stdout.length} chars]` : undefined,
|
|
1212
|
+
stderr: showOutput ? run.stderr : run.stderr ? `[redacted ${run.stderr.length} chars]` : undefined
|
|
1213
|
+
};
|
|
1214
|
+
}
|
|
1215
|
+
function publicWorkflowEvent(event) {
|
|
1216
|
+
return { ...event };
|
|
1217
|
+
}
|
|
712
1218
|
|
|
713
1219
|
// src/lib/executor.ts
|
|
714
1220
|
import { spawn } from "child_process";
|
|
715
1221
|
import { once } from "events";
|
|
1222
|
+
|
|
1223
|
+
// src/lib/accounts.ts
|
|
1224
|
+
import { spawnSync } from "child_process";
|
|
1225
|
+
import { existsSync } from "fs";
|
|
1226
|
+
var EXPORT_RE = /^export\s+([A-Za-z_][A-Za-z0-9_]*)=(.*)$/;
|
|
1227
|
+
function accountToolForProvider(provider) {
|
|
1228
|
+
switch (provider) {
|
|
1229
|
+
case "claude":
|
|
1230
|
+
return "claude";
|
|
1231
|
+
case "cursor":
|
|
1232
|
+
return "cursor";
|
|
1233
|
+
case "codewith":
|
|
1234
|
+
return "codewith";
|
|
1235
|
+
case "aicopilot":
|
|
1236
|
+
return "aicopilot";
|
|
1237
|
+
case "opencode":
|
|
1238
|
+
return "opencode";
|
|
1239
|
+
case "codex":
|
|
1240
|
+
return "codex";
|
|
1241
|
+
}
|
|
1242
|
+
}
|
|
1243
|
+
function parseExportValue(raw) {
|
|
1244
|
+
try {
|
|
1245
|
+
return JSON.parse(raw);
|
|
1246
|
+
} catch {
|
|
1247
|
+
return raw.replace(/^['"]|['"]$/g, "");
|
|
1248
|
+
}
|
|
1249
|
+
}
|
|
1250
|
+
function parseAccountExportLines(output) {
|
|
1251
|
+
const env = {};
|
|
1252
|
+
for (const line of output.split(/\r?\n/)) {
|
|
1253
|
+
const match = EXPORT_RE.exec(line.trim());
|
|
1254
|
+
if (!match)
|
|
1255
|
+
continue;
|
|
1256
|
+
env[match[1]] = parseExportValue(match[2]);
|
|
1257
|
+
}
|
|
1258
|
+
return env;
|
|
1259
|
+
}
|
|
1260
|
+
function primaryAccountDir(output) {
|
|
1261
|
+
for (const line of output.split(/\r?\n/)) {
|
|
1262
|
+
const match = EXPORT_RE.exec(line.trim());
|
|
1263
|
+
if (!match)
|
|
1264
|
+
continue;
|
|
1265
|
+
return parseExportValue(match[2]);
|
|
1266
|
+
}
|
|
1267
|
+
return;
|
|
1268
|
+
}
|
|
1269
|
+
function resolveAccountEnv(account, toolHint, env) {
|
|
1270
|
+
if (!account)
|
|
1271
|
+
return {};
|
|
1272
|
+
const tool = account.tool ?? toolHint;
|
|
1273
|
+
if (!tool)
|
|
1274
|
+
throw new Error("account.tool is required when no provider tool can be inferred");
|
|
1275
|
+
const result = spawnSync("accounts", ["env", account.profile, "--tool", tool], {
|
|
1276
|
+
encoding: "utf8",
|
|
1277
|
+
env,
|
|
1278
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
1279
|
+
});
|
|
1280
|
+
if (result.error) {
|
|
1281
|
+
throw new Error(`failed to run accounts env for ${account.profile}/${tool}: ${result.error.message}`);
|
|
1282
|
+
}
|
|
1283
|
+
if ((result.status ?? 0) !== 0) {
|
|
1284
|
+
const stderr = result.stderr.trim();
|
|
1285
|
+
throw new Error(`accounts env failed for ${account.profile}/${tool}${stderr ? `: ${stderr}` : ""}`);
|
|
1286
|
+
}
|
|
1287
|
+
const profileDir = primaryAccountDir(result.stdout);
|
|
1288
|
+
if (!profileDir)
|
|
1289
|
+
throw new Error(`accounts env returned no profile directory for ${account.profile}/${tool}`);
|
|
1290
|
+
if (!existsSync(profileDir))
|
|
1291
|
+
throw new Error(`account profile directory does not exist for ${account.profile}/${tool}: ${profileDir}`);
|
|
1292
|
+
return {
|
|
1293
|
+
...parseAccountExportLines(result.stdout),
|
|
1294
|
+
LOOPS_ACCOUNT_PROFILE: account.profile,
|
|
1295
|
+
LOOPS_ACCOUNT_TOOL: tool
|
|
1296
|
+
};
|
|
1297
|
+
}
|
|
1298
|
+
|
|
1299
|
+
// src/lib/executor.ts
|
|
716
1300
|
var DEFAULT_TIMEOUT_MS = 30 * 60000;
|
|
717
1301
|
var DEFAULT_MAX_OUTPUT_BYTES = 256 * 1024;
|
|
1302
|
+
var AUTH_ENV_KEYS = [
|
|
1303
|
+
"CLAUDE_CONFIG_DIR",
|
|
1304
|
+
"CODEWITH_HOME",
|
|
1305
|
+
"CODEX_HOME",
|
|
1306
|
+
"CURSOR_CONFIG_DIR",
|
|
1307
|
+
"OPENCODE_CONFIG_DIR",
|
|
1308
|
+
"AICOPILOT_CONFIG_DIR",
|
|
1309
|
+
"ANTHROPIC_API_KEY",
|
|
1310
|
+
"OPENAI_API_KEY",
|
|
1311
|
+
"OPENROUTER_API_KEY",
|
|
1312
|
+
"GITHUB_TOKEN",
|
|
1313
|
+
"GH_TOKEN",
|
|
1314
|
+
"XDG_CONFIG_HOME",
|
|
1315
|
+
"XDG_DATA_HOME",
|
|
1316
|
+
"XDG_STATE_HOME",
|
|
1317
|
+
"XDG_CACHE_HOME"
|
|
1318
|
+
];
|
|
718
1319
|
function appendBounded(current, chunk, maxBytes) {
|
|
719
1320
|
const next = current + chunk.toString("utf8");
|
|
720
1321
|
if (Buffer.byteLength(next, "utf8") <= maxBytes)
|
|
@@ -753,6 +1354,8 @@ function providerCommand(provider) {
|
|
|
753
1354
|
return "aicopilot";
|
|
754
1355
|
case "opencode":
|
|
755
1356
|
return "opencode";
|
|
1357
|
+
case "codex":
|
|
1358
|
+
return "codex";
|
|
756
1359
|
}
|
|
757
1360
|
}
|
|
758
1361
|
function agentArgs(target) {
|
|
@@ -789,6 +1392,16 @@ function agentArgs(target) {
|
|
|
789
1392
|
args.push("--agent", target.agent);
|
|
790
1393
|
args.push(...target.extraArgs ?? [], target.prompt);
|
|
791
1394
|
return args;
|
|
1395
|
+
case "codex":
|
|
1396
|
+
args.push("exec", "--json", "--ephemeral", "--ask-for-approval", "never", "--sandbox", "workspace-write");
|
|
1397
|
+
if (isolation === "safe")
|
|
1398
|
+
args.push("--ignore-rules");
|
|
1399
|
+
if (target.cwd)
|
|
1400
|
+
args.push("--cd", target.cwd);
|
|
1401
|
+
if (target.model)
|
|
1402
|
+
args.push("--model", target.model);
|
|
1403
|
+
args.push(...target.extraArgs ?? [], target.prompt);
|
|
1404
|
+
return args;
|
|
792
1405
|
case "aicopilot":
|
|
793
1406
|
args.push("run", "--format", "json");
|
|
794
1407
|
if (isolation === "safe")
|
|
@@ -815,8 +1428,7 @@ function agentArgs(target) {
|
|
|
815
1428
|
return args;
|
|
816
1429
|
}
|
|
817
1430
|
}
|
|
818
|
-
function commandSpec(
|
|
819
|
-
const target = loop.target;
|
|
1431
|
+
function commandSpec(target) {
|
|
820
1432
|
if (target.type === "command") {
|
|
821
1433
|
const commandTarget = target;
|
|
822
1434
|
return {
|
|
@@ -825,7 +1437,9 @@ function commandSpec(loop) {
|
|
|
825
1437
|
cwd: commandTarget.cwd,
|
|
826
1438
|
shell: commandTarget.shell,
|
|
827
1439
|
env: commandTarget.env,
|
|
828
|
-
timeoutMs: commandTarget.timeoutMs ?? DEFAULT_TIMEOUT_MS
|
|
1440
|
+
timeoutMs: commandTarget.timeoutMs ?? DEFAULT_TIMEOUT_MS,
|
|
1441
|
+
account: commandTarget.account,
|
|
1442
|
+
accountTool: commandTarget.account?.tool
|
|
829
1443
|
};
|
|
830
1444
|
}
|
|
831
1445
|
const agentTarget = target;
|
|
@@ -833,11 +1447,40 @@ function commandSpec(loop) {
|
|
|
833
1447
|
command: providerCommand(agentTarget.provider),
|
|
834
1448
|
args: agentArgs(agentTarget),
|
|
835
1449
|
cwd: agentTarget.cwd,
|
|
836
|
-
timeoutMs: agentTarget.timeoutMs ?? DEFAULT_TIMEOUT_MS
|
|
1450
|
+
timeoutMs: agentTarget.timeoutMs ?? DEFAULT_TIMEOUT_MS,
|
|
1451
|
+
account: agentTarget.account,
|
|
1452
|
+
accountTool: agentTarget.account?.tool ?? accountToolForProvider(agentTarget.provider)
|
|
837
1453
|
};
|
|
838
1454
|
}
|
|
839
|
-
|
|
840
|
-
const
|
|
1455
|
+
function executionEnv(spec, metadata, opts) {
|
|
1456
|
+
const env = { ...opts.env ?? process.env };
|
|
1457
|
+
if (spec.account) {
|
|
1458
|
+
const accountEnv = resolveAccountEnv(spec.account, spec.accountTool, env);
|
|
1459
|
+
for (const key of AUTH_ENV_KEYS)
|
|
1460
|
+
delete env[key];
|
|
1461
|
+
Object.assign(env, accountEnv);
|
|
1462
|
+
}
|
|
1463
|
+
Object.assign(env, spec.env ?? {});
|
|
1464
|
+
if (metadata.loopId)
|
|
1465
|
+
env.LOOPS_LOOP_ID = metadata.loopId;
|
|
1466
|
+
if (metadata.loopName)
|
|
1467
|
+
env.LOOPS_LOOP_NAME = metadata.loopName;
|
|
1468
|
+
if (metadata.runId)
|
|
1469
|
+
env.LOOPS_RUN_ID = metadata.runId;
|
|
1470
|
+
if (metadata.scheduledFor)
|
|
1471
|
+
env.LOOPS_SCHEDULED_FOR = metadata.scheduledFor;
|
|
1472
|
+
if (metadata.workflowId)
|
|
1473
|
+
env.LOOPS_WORKFLOW_ID = metadata.workflowId;
|
|
1474
|
+
if (metadata.workflowName)
|
|
1475
|
+
env.LOOPS_WORKFLOW_NAME = metadata.workflowName;
|
|
1476
|
+
if (metadata.workflowRunId)
|
|
1477
|
+
env.LOOPS_WORKFLOW_RUN_ID = metadata.workflowRunId;
|
|
1478
|
+
if (metadata.workflowStepId)
|
|
1479
|
+
env.LOOPS_WORKFLOW_STEP_ID = metadata.workflowStepId;
|
|
1480
|
+
return env;
|
|
1481
|
+
}
|
|
1482
|
+
async function executeTarget(target, metadata = {}, opts = {}) {
|
|
1483
|
+
const spec = commandSpec(target);
|
|
841
1484
|
const maxOutputBytes = opts.maxOutputBytes ?? DEFAULT_MAX_OUTPUT_BYTES;
|
|
842
1485
|
const startedAt = nowIso();
|
|
843
1486
|
let stdout = "";
|
|
@@ -845,14 +1488,7 @@ async function executeLoop(loop, run, opts = {}) {
|
|
|
845
1488
|
let timedOut = false;
|
|
846
1489
|
let exitCode;
|
|
847
1490
|
let error;
|
|
848
|
-
const env =
|
|
849
|
-
...opts.env ?? process.env,
|
|
850
|
-
...spec.env ?? {},
|
|
851
|
-
LOOPS_LOOP_ID: loop.id,
|
|
852
|
-
LOOPS_LOOP_NAME: loop.name,
|
|
853
|
-
LOOPS_RUN_ID: run.id,
|
|
854
|
-
LOOPS_SCHEDULED_FOR: run.scheduledFor
|
|
855
|
-
};
|
|
1491
|
+
const env = executionEnv(spec, metadata, opts);
|
|
856
1492
|
const child = spawn(spec.command, spec.args, {
|
|
857
1493
|
cwd: spec.cwd,
|
|
858
1494
|
env,
|
|
@@ -922,12 +1558,137 @@ async function executeLoop(loop, run, opts = {}) {
|
|
|
922
1558
|
durationMs
|
|
923
1559
|
};
|
|
924
1560
|
}
|
|
1561
|
+
async function executeLoop(loop, run, opts = {}) {
|
|
1562
|
+
if (loop.target.type === "workflow") {
|
|
1563
|
+
throw new Error("workflow loop targets must be executed with executeLoopTarget");
|
|
1564
|
+
}
|
|
1565
|
+
return executeTarget(loop.target, {
|
|
1566
|
+
loopId: loop.id,
|
|
1567
|
+
loopName: loop.name,
|
|
1568
|
+
runId: run.id,
|
|
1569
|
+
scheduledFor: run.scheduledFor
|
|
1570
|
+
}, opts);
|
|
1571
|
+
}
|
|
1572
|
+
|
|
1573
|
+
// src/lib/workflow-runner.ts
|
|
1574
|
+
function targetWithStepAccount(step) {
|
|
1575
|
+
const account = step.account ?? step.target.account;
|
|
1576
|
+
if (!account)
|
|
1577
|
+
return step.target;
|
|
1578
|
+
return { ...step.target, account };
|
|
1579
|
+
}
|
|
1580
|
+
function workflowResult(workflowRun, status, startedAt, finishedAt, stdout, error) {
|
|
1581
|
+
return {
|
|
1582
|
+
status,
|
|
1583
|
+
exitCode: status === "succeeded" ? 0 : 1,
|
|
1584
|
+
stdout,
|
|
1585
|
+
stderr: "",
|
|
1586
|
+
error,
|
|
1587
|
+
startedAt,
|
|
1588
|
+
finishedAt,
|
|
1589
|
+
durationMs: new Date(finishedAt).getTime() - new Date(startedAt).getTime()
|
|
1590
|
+
};
|
|
1591
|
+
}
|
|
1592
|
+
async function executeWorkflow(store, workflow, opts = {}) {
|
|
1593
|
+
const run = store.createWorkflowRun({
|
|
1594
|
+
workflow,
|
|
1595
|
+
loop: opts.loop,
|
|
1596
|
+
loopRun: opts.loopRun,
|
|
1597
|
+
scheduledFor: opts.scheduledFor,
|
|
1598
|
+
idempotencyKey: opts.idempotencyKey
|
|
1599
|
+
});
|
|
1600
|
+
const startedAt = run.startedAt ?? nowIso();
|
|
1601
|
+
if (run.status === "succeeded" || run.status === "failed" || run.status === "timed_out") {
|
|
1602
|
+
const steps2 = store.listWorkflowStepRuns(run.id);
|
|
1603
|
+
return workflowResult(run, run.status, startedAt, run.finishedAt ?? nowIso(), JSON.stringify({ workflowRun: run, steps: steps2 }, null, 2), run.error);
|
|
1604
|
+
}
|
|
1605
|
+
const ordered = workflowExecutionOrder(workflow);
|
|
1606
|
+
const byId = new Map(workflow.steps.map((step) => [step.id, step]));
|
|
1607
|
+
let blockingError;
|
|
1608
|
+
let terminalStatus = "succeeded";
|
|
1609
|
+
for (const step of ordered) {
|
|
1610
|
+
const existing = store.getWorkflowStepRun(run.id, step.id);
|
|
1611
|
+
if (existing?.status === "succeeded" || existing?.status === "skipped")
|
|
1612
|
+
continue;
|
|
1613
|
+
const blockedBy = (step.dependsOn ?? []).find((dependencyId) => {
|
|
1614
|
+
const dependencyRun = store.getWorkflowStepRun(run.id, dependencyId);
|
|
1615
|
+
const dependencyStep = byId.get(dependencyId);
|
|
1616
|
+
if (dependencyRun?.status === "succeeded")
|
|
1617
|
+
return false;
|
|
1618
|
+
return !dependencyStep?.continueOnFailure;
|
|
1619
|
+
});
|
|
1620
|
+
if (blockedBy) {
|
|
1621
|
+
store.skipWorkflowStepRun(run.id, step.id, `dependency did not succeed: ${blockedBy}`);
|
|
1622
|
+
blockingError ??= `step ${step.id} blocked by dependency ${blockedBy}`;
|
|
1623
|
+
terminalStatus = "failed";
|
|
1624
|
+
continue;
|
|
1625
|
+
}
|
|
1626
|
+
store.startWorkflowStepRun(run.id, step.id);
|
|
1627
|
+
const result = await executeTarget(targetWithStepAccount(step), {
|
|
1628
|
+
loopId: opts.loop?.id,
|
|
1629
|
+
loopName: opts.loop?.name,
|
|
1630
|
+
runId: opts.loopRun?.id,
|
|
1631
|
+
scheduledFor: opts.loopRun?.scheduledFor ?? opts.scheduledFor,
|
|
1632
|
+
workflowId: workflow.id,
|
|
1633
|
+
workflowName: workflow.name,
|
|
1634
|
+
workflowRunId: run.id,
|
|
1635
|
+
workflowStepId: step.id
|
|
1636
|
+
}, opts);
|
|
1637
|
+
store.finalizeWorkflowStepRun(run.id, step.id, {
|
|
1638
|
+
status: result.status,
|
|
1639
|
+
finishedAt: result.finishedAt,
|
|
1640
|
+
durationMs: result.durationMs,
|
|
1641
|
+
stdout: result.stdout,
|
|
1642
|
+
stderr: result.stderr,
|
|
1643
|
+
exitCode: result.exitCode,
|
|
1644
|
+
error: result.error
|
|
1645
|
+
});
|
|
1646
|
+
if (result.status !== "succeeded" && !step.continueOnFailure) {
|
|
1647
|
+
terminalStatus = result.status;
|
|
1648
|
+
blockingError = `step ${step.id} ${result.status}${result.error ? `: ${result.error}` : ""}`;
|
|
1649
|
+
break;
|
|
1650
|
+
}
|
|
1651
|
+
}
|
|
1652
|
+
if (terminalStatus !== "succeeded") {
|
|
1653
|
+
for (const step of ordered) {
|
|
1654
|
+
const existing = store.getWorkflowStepRun(run.id, step.id);
|
|
1655
|
+
if (existing?.status === "pending" || existing?.status === "running") {
|
|
1656
|
+
store.skipWorkflowStepRun(run.id, step.id, blockingError ?? "workflow stopped before step could run");
|
|
1657
|
+
}
|
|
1658
|
+
}
|
|
1659
|
+
}
|
|
1660
|
+
const finishedAt = nowIso();
|
|
1661
|
+
const finalRun = store.finalizeWorkflowRun(run.id, terminalStatus, {
|
|
1662
|
+
finishedAt,
|
|
1663
|
+
durationMs: new Date(finishedAt).getTime() - new Date(startedAt).getTime(),
|
|
1664
|
+
error: blockingError
|
|
1665
|
+
});
|
|
1666
|
+
const steps = store.listWorkflowStepRuns(run.id);
|
|
1667
|
+
return workflowResult(finalRun, terminalStatus, startedAt, finishedAt, JSON.stringify({ workflowRun: finalRun, steps }, null, 2), blockingError);
|
|
1668
|
+
}
|
|
1669
|
+
async function executeLoopTarget(store, loop, run, opts = {}) {
|
|
1670
|
+
if (loop.target.type !== "workflow")
|
|
1671
|
+
return executeLoop(loop, run, opts);
|
|
1672
|
+
const workflow = store.requireWorkflow(loop.target.workflowId);
|
|
1673
|
+
return executeWorkflow(store, workflow, {
|
|
1674
|
+
...opts,
|
|
1675
|
+
loop,
|
|
1676
|
+
loopRun: run,
|
|
1677
|
+
scheduledFor: run.scheduledFor,
|
|
1678
|
+
idempotencyKey: `${loop.id}:${run.scheduledFor}`
|
|
1679
|
+
});
|
|
1680
|
+
}
|
|
925
1681
|
|
|
926
1682
|
// src/lib/scheduler.ts
|
|
927
1683
|
function nextAfterRetry(loop, now) {
|
|
928
1684
|
return new Date(now.getTime() + loop.retryDelayMs).toISOString();
|
|
929
1685
|
}
|
|
930
1686
|
function advanceLoop(store, loop, run, finishedAt, succeeded) {
|
|
1687
|
+
if (run.status === "running")
|
|
1688
|
+
return;
|
|
1689
|
+
const current = store.getLoop(loop.id);
|
|
1690
|
+
if (!current || current.status !== "active")
|
|
1691
|
+
return;
|
|
931
1692
|
const shouldRetry = !succeeded && run.attempt < loop.maxAttempts;
|
|
932
1693
|
if (shouldRetry) {
|
|
933
1694
|
store.updateLoop(loop.id, {
|
|
@@ -956,8 +1717,14 @@ async function runSlot(deps, loop, scheduledFor) {
|
|
|
956
1717
|
if (!claim)
|
|
957
1718
|
return;
|
|
958
1719
|
deps.onRun?.(claim.run);
|
|
1720
|
+
let heartbeat;
|
|
1721
|
+
const heartbeatEveryMs = Math.max(1000, Math.min(60000, Math.floor(claim.loop.leaseMs / 3)));
|
|
1722
|
+
heartbeat = setInterval(() => {
|
|
1723
|
+
deps.store.heartbeatRunLease(claim.run.id, deps.runnerId, claim.loop.leaseMs);
|
|
1724
|
+
}, heartbeatEveryMs);
|
|
1725
|
+
heartbeat.unref();
|
|
959
1726
|
try {
|
|
960
|
-
const result = await (deps.execute ??
|
|
1727
|
+
const result = await (deps.execute ?? ((loop2, run) => executeLoopTarget(deps.store, loop2, run)))(claim.loop, claim.run);
|
|
961
1728
|
const finalRun = deps.store.finalizeRun(claim.run.id, {
|
|
962
1729
|
status: result.status,
|
|
963
1730
|
finishedAt: result.finishedAt,
|
|
@@ -967,8 +1734,11 @@ async function runSlot(deps, loop, scheduledFor) {
|
|
|
967
1734
|
exitCode: result.exitCode,
|
|
968
1735
|
error: result.error,
|
|
969
1736
|
pid: result.pid
|
|
1737
|
+
}, {
|
|
1738
|
+
claimedBy: deps.runnerId,
|
|
1739
|
+
now: deps.now?.() ?? new Date(result.finishedAt)
|
|
970
1740
|
});
|
|
971
|
-
advanceLoop(deps.store, claim.loop, finalRun, new Date(result.finishedAt),
|
|
1741
|
+
advanceLoop(deps.store, claim.loop, finalRun, new Date(result.finishedAt), finalRun.status === "succeeded");
|
|
972
1742
|
deps.onRun?.(finalRun);
|
|
973
1743
|
return finalRun;
|
|
974
1744
|
} catch (err) {
|
|
@@ -981,10 +1751,16 @@ async function runSlot(deps, loop, scheduledFor) {
|
|
|
981
1751
|
stdout: "",
|
|
982
1752
|
stderr: "",
|
|
983
1753
|
error: err instanceof Error ? err.message : String(err)
|
|
1754
|
+
}, {
|
|
1755
|
+
claimedBy: deps.runnerId,
|
|
1756
|
+
now: deps.now?.() ?? finishedAt
|
|
984
1757
|
});
|
|
985
1758
|
advanceLoop(deps.store, claim.loop, finalRun, finishedAt, false);
|
|
986
1759
|
deps.onRun?.(finalRun);
|
|
987
1760
|
return finalRun;
|
|
1761
|
+
} finally {
|
|
1762
|
+
if (heartbeat)
|
|
1763
|
+
clearInterval(heartbeat);
|
|
988
1764
|
}
|
|
989
1765
|
}
|
|
990
1766
|
async function tick(deps) {
|
|
@@ -1011,13 +1787,15 @@ async function tick(deps) {
|
|
|
1011
1787
|
skipped.push(run);
|
|
1012
1788
|
else
|
|
1013
1789
|
completed.push(run);
|
|
1790
|
+
if (["failed", "timed_out", "abandoned"].includes(run.status) && run.attempt < loop.maxAttempts)
|
|
1791
|
+
break;
|
|
1014
1792
|
}
|
|
1015
1793
|
}
|
|
1016
1794
|
return { claimed, completed, skipped, recovered, expired };
|
|
1017
1795
|
}
|
|
1018
1796
|
|
|
1019
1797
|
// src/daemon/control.ts
|
|
1020
|
-
import { existsSync, mkdirSync as mkdirSync3, readFileSync, rmSync, writeFileSync } from "fs";
|
|
1798
|
+
import { existsSync as existsSync2, mkdirSync as mkdirSync3, readFileSync, rmSync, writeFileSync } from "fs";
|
|
1021
1799
|
import { hostname } from "os";
|
|
1022
1800
|
import { dirname as dirname2 } from "path";
|
|
1023
1801
|
|
|
@@ -1045,7 +1823,7 @@ async function runLoop(opts) {
|
|
|
1045
1823
|
|
|
1046
1824
|
// src/daemon/control.ts
|
|
1047
1825
|
function readPid(path = pidFilePath()) {
|
|
1048
|
-
if (!
|
|
1826
|
+
if (!existsSync2(path))
|
|
1049
1827
|
return;
|
|
1050
1828
|
try {
|
|
1051
1829
|
const pid = Number(readFileSync(path, "utf8").trim());
|
|
@@ -1330,7 +2108,7 @@ ${args.map((arg) => ` <string>${arg}</string>`).join(`
|
|
|
1330
2108
|
|
|
1331
2109
|
// src/cli/index.ts
|
|
1332
2110
|
var program = new Command;
|
|
1333
|
-
program.name("loops").description("Persistent local loops for commands and headless coding agents").version("0.
|
|
2111
|
+
program.name("loops").description("Persistent local loops for commands and headless coding agents").version("0.2.0");
|
|
1334
2112
|
program.option("-j, --json", "print JSON");
|
|
1335
2113
|
function isJson() {
|
|
1336
2114
|
return Boolean(program.opts().json);
|
|
@@ -1412,8 +2190,16 @@ function baseCreateInput(name, opts, target) {
|
|
|
1412
2190
|
function addScheduleOptions(command) {
|
|
1413
2191
|
return command.option("--at <time>", "run once at an absolute time").option("--every <duration>", "run at a fixed interval, e.g. 15m, 1h, 30s").option("--cron <expr>", "run on a 5-field cron expression").option("--dynamic", "run on the default dynamic one-minute cadence").option("--catch-up <policy>", "none, latest, or all", "latest").option("--catch-up-limit <n>", "maximum missed slots to run when --catch-up all").option("--overlap <policy>", "skip or allow", "skip").option("--attempts <n>", "max attempts per scheduled slot").option("--retry-delay <duration>", "delay between retries", "1m").option("--lease <duration>", "running lease timeout", "30m").option("--expires-at <time>", "stop scheduling after this time").option("-d, --description <text>", "description");
|
|
1414
2192
|
}
|
|
2193
|
+
function addAccountOptions(command) {
|
|
2194
|
+
return command.option("--account <profile>", "OpenAccounts profile name for this target").option("--account-tool <tool>", "OpenAccounts tool id; defaults from provider for agents");
|
|
2195
|
+
}
|
|
2196
|
+
function accountFromOpts(opts) {
|
|
2197
|
+
if (!opts.account && opts.accountTool)
|
|
2198
|
+
throw new Error("--account-tool requires --account");
|
|
2199
|
+
return opts.account ? { profile: opts.account, tool: opts.accountTool } : undefined;
|
|
2200
|
+
}
|
|
1415
2201
|
var create = program.command("create").description("create loops");
|
|
1416
|
-
addScheduleOptions(create.command("command <name>").description("create a deterministic shell command loop").requiredOption("--cmd <command>", "command string to execute").option("--cwd <dir>", "working directory").option("--timeout <duration>", "run timeout").option("--no-shell", "execute without a shell")).action((name, opts) => {
|
|
2202
|
+
addAccountOptions(addScheduleOptions(create.command("command <name>").description("create a deterministic shell command loop").requiredOption("--cmd <command>", "command string to execute").option("--cwd <dir>", "working directory").option("--timeout <duration>", "run timeout").option("--no-shell", "execute without a shell"))).action((name, opts) => {
|
|
1417
2203
|
const store = new Store;
|
|
1418
2204
|
try {
|
|
1419
2205
|
const target = {
|
|
@@ -1421,7 +2207,8 @@ addScheduleOptions(create.command("command <name>").description("create a determ
|
|
|
1421
2207
|
command: opts.cmd,
|
|
1422
2208
|
cwd: opts.cwd,
|
|
1423
2209
|
shell: opts.shell,
|
|
1424
|
-
timeoutMs: opts.timeout ? parseDuration(opts.timeout) : undefined
|
|
2210
|
+
timeoutMs: opts.timeout ? parseDuration(opts.timeout) : undefined,
|
|
2211
|
+
account: accountFromOpts(opts)
|
|
1425
2212
|
};
|
|
1426
2213
|
const loop = store.createLoop(baseCreateInput(name, opts, target));
|
|
1427
2214
|
print(publicLoop(loop), `created loop ${loop.id} (${loop.name}) next=${loop.nextRunAt}`);
|
|
@@ -1429,9 +2216,9 @@ addScheduleOptions(create.command("command <name>").description("create a determ
|
|
|
1429
2216
|
store.close();
|
|
1430
2217
|
}
|
|
1431
2218
|
});
|
|
1432
|
-
addScheduleOptions(create.command("agent <name>").description("create a headless coding-agent loop").requiredOption("--provider <provider>", "claude, cursor, codewith, aicopilot, or
|
|
2219
|
+
addAccountOptions(addScheduleOptions(create.command("agent <name>").description("create a headless coding-agent loop").requiredOption("--provider <provider>", "claude, cursor, codewith, aicopilot, opencode, or codex").requiredOption("--prompt <prompt>", "agent prompt").option("--cwd <dir>", "working directory").option("--model <model>", "model").option("--agent <agent>", "provider-specific agent").option("--timeout <duration>", "run timeout").option("--config-isolation <mode>", "safe or none", "safe"))).action((name, opts) => {
|
|
1433
2220
|
const provider = opts.provider;
|
|
1434
|
-
if (!["claude", "cursor", "codewith", "aicopilot", "opencode"].includes(provider)) {
|
|
2221
|
+
if (!["claude", "cursor", "codewith", "aicopilot", "opencode", "codex"].includes(provider)) {
|
|
1435
2222
|
throw new Error("unsupported provider");
|
|
1436
2223
|
}
|
|
1437
2224
|
if (!["safe", "none"].includes(opts.configIsolation)) {
|
|
@@ -1447,7 +2234,8 @@ addScheduleOptions(create.command("agent <name>").description("create a headless
|
|
|
1447
2234
|
model: opts.model,
|
|
1448
2235
|
agent: opts.agent,
|
|
1449
2236
|
timeoutMs: opts.timeout ? parseDuration(opts.timeout) : undefined,
|
|
1450
|
-
configIsolation: opts.configIsolation
|
|
2237
|
+
configIsolation: opts.configIsolation,
|
|
2238
|
+
account: accountFromOpts(opts)
|
|
1451
2239
|
};
|
|
1452
2240
|
const loop = store.createLoop(baseCreateInput(name, opts, target));
|
|
1453
2241
|
print(publicLoop(loop), `created loop ${loop.id} (${loop.name}) next=${loop.nextRunAt}`);
|
|
@@ -1455,6 +2243,111 @@ addScheduleOptions(create.command("agent <name>").description("create a headless
|
|
|
1455
2243
|
store.close();
|
|
1456
2244
|
}
|
|
1457
2245
|
});
|
|
2246
|
+
addScheduleOptions(create.command("workflow <name>").description("schedule a stored workflow").requiredOption("--workflow <idOrName>", "workflow id or name")).action((name, opts) => {
|
|
2247
|
+
const store = new Store;
|
|
2248
|
+
try {
|
|
2249
|
+
const workflow = store.requireWorkflow(opts.workflow);
|
|
2250
|
+
const target = {
|
|
2251
|
+
type: "workflow",
|
|
2252
|
+
workflowId: workflow.id
|
|
2253
|
+
};
|
|
2254
|
+
const loop = store.createLoop(baseCreateInput(name, opts, target));
|
|
2255
|
+
print(publicLoop(loop), `created workflow loop ${loop.id} (${loop.name}) workflow=${workflow.name} next=${loop.nextRunAt}`);
|
|
2256
|
+
} finally {
|
|
2257
|
+
store.close();
|
|
2258
|
+
}
|
|
2259
|
+
});
|
|
2260
|
+
var workflows = program.command("workflows").alias("workflow").description("manage workflow specs and runs");
|
|
2261
|
+
workflows.command("create <file>").description("validate and store a workflow JSON file").option("--name <name>", "override workflow name from the file").action((file, opts) => {
|
|
2262
|
+
const store = new Store;
|
|
2263
|
+
try {
|
|
2264
|
+
const body = workflowBodyFromJson(JSON.parse(readFileSync2(file, "utf8")), opts.name);
|
|
2265
|
+
const workflow = store.createWorkflow(body);
|
|
2266
|
+
print(publicWorkflow(workflow), `created workflow ${workflow.id} (${workflow.name}) steps=${workflow.steps.length}`);
|
|
2267
|
+
} finally {
|
|
2268
|
+
store.close();
|
|
2269
|
+
}
|
|
2270
|
+
});
|
|
2271
|
+
workflows.command("list").alias("ls").option("--status <status>", "active or archived", "active").action((opts) => {
|
|
2272
|
+
const store = new Store;
|
|
2273
|
+
try {
|
|
2274
|
+
const workflowsList = store.listWorkflows({ status: opts.status });
|
|
2275
|
+
if (isJson())
|
|
2276
|
+
print(workflowsList.map(publicWorkflow));
|
|
2277
|
+
else {
|
|
2278
|
+
for (const workflow of workflowsList) {
|
|
2279
|
+
console.log(`${workflow.id} ${workflow.status.padEnd(8)} steps=${workflow.steps.length} ${workflow.name}`);
|
|
2280
|
+
}
|
|
2281
|
+
}
|
|
2282
|
+
} finally {
|
|
2283
|
+
store.close();
|
|
2284
|
+
}
|
|
2285
|
+
});
|
|
2286
|
+
workflows.command("show <idOrName>").action((idOrName) => {
|
|
2287
|
+
const store = new Store;
|
|
2288
|
+
try {
|
|
2289
|
+
print(publicWorkflow(store.requireWorkflow(idOrName)));
|
|
2290
|
+
} finally {
|
|
2291
|
+
store.close();
|
|
2292
|
+
}
|
|
2293
|
+
});
|
|
2294
|
+
workflows.command("run <idOrName>").option("--show-output", "show step stdout/stderr").action(async (idOrName, opts) => {
|
|
2295
|
+
const store = new Store;
|
|
2296
|
+
try {
|
|
2297
|
+
const workflow = store.requireWorkflow(idOrName);
|
|
2298
|
+
const result = await executeWorkflow(store, workflow);
|
|
2299
|
+
const run = store.listWorkflowRuns({ workflowId: workflow.id, limit: 1 })[0];
|
|
2300
|
+
const steps = run ? store.listWorkflowStepRuns(run.id) : [];
|
|
2301
|
+
const value = {
|
|
2302
|
+
result,
|
|
2303
|
+
workflowRun: run ? publicWorkflowRun(run) : undefined,
|
|
2304
|
+
steps: steps.map((step) => publicWorkflowStepRun(step, opts.showOutput))
|
|
2305
|
+
};
|
|
2306
|
+
print(value, `${run?.id ?? workflow.id} ${result.status}`);
|
|
2307
|
+
} finally {
|
|
2308
|
+
store.close();
|
|
2309
|
+
}
|
|
2310
|
+
});
|
|
2311
|
+
workflows.command("runs [idOrName]").option("--limit <n>", "limit", "50").action((idOrName, opts) => {
|
|
2312
|
+
const store = new Store;
|
|
2313
|
+
try {
|
|
2314
|
+
const workflow = idOrName ? store.requireWorkflow(idOrName) : undefined;
|
|
2315
|
+
const runs = store.listWorkflowRuns({ workflowId: workflow?.id, limit: Number(opts.limit) });
|
|
2316
|
+
if (isJson())
|
|
2317
|
+
print(runs.map(publicWorkflowRun));
|
|
2318
|
+
else {
|
|
2319
|
+
for (const run of runs) {
|
|
2320
|
+
console.log(`${run.id} ${run.status.padEnd(10)} ${run.workflowName} started=${run.startedAt ?? "-"}`);
|
|
2321
|
+
}
|
|
2322
|
+
}
|
|
2323
|
+
} finally {
|
|
2324
|
+
store.close();
|
|
2325
|
+
}
|
|
2326
|
+
});
|
|
2327
|
+
workflows.command("events <runId>").option("--limit <n>", "limit", "200").action((runId, opts) => {
|
|
2328
|
+
const store = new Store;
|
|
2329
|
+
try {
|
|
2330
|
+
const events = store.listWorkflowEvents(runId, Number(opts.limit));
|
|
2331
|
+
if (isJson())
|
|
2332
|
+
print(events.map(publicWorkflowEvent));
|
|
2333
|
+
else {
|
|
2334
|
+
for (const event of events) {
|
|
2335
|
+
console.log(`${String(event.sequence).padStart(3, "0")} ${event.eventType.padEnd(14)} ${event.stepId ?? "-"} ${event.createdAt}`);
|
|
2336
|
+
}
|
|
2337
|
+
}
|
|
2338
|
+
} finally {
|
|
2339
|
+
store.close();
|
|
2340
|
+
}
|
|
2341
|
+
});
|
|
2342
|
+
workflows.command("archive <idOrName>").action((idOrName) => {
|
|
2343
|
+
const store = new Store;
|
|
2344
|
+
try {
|
|
2345
|
+
const workflow = store.archiveWorkflow(idOrName);
|
|
2346
|
+
print(publicWorkflow(workflow), `${workflow.id} ${workflow.status}`);
|
|
2347
|
+
} finally {
|
|
2348
|
+
store.close();
|
|
2349
|
+
}
|
|
2350
|
+
});
|
|
1458
2351
|
program.command("list").alias("ls").option("--status <status>", "filter by status").action((opts) => {
|
|
1459
2352
|
const store = new Store;
|
|
1460
2353
|
try {
|
|
@@ -1523,7 +2416,7 @@ program.command("run-now <idOrName>").option("--show-output", "show stdout/stder
|
|
|
1523
2416
|
const claim = store.claimRun(loop, new Date().toISOString(), `manual:${process.pid}`);
|
|
1524
2417
|
if (!claim)
|
|
1525
2418
|
throw new Error("could not claim manual run");
|
|
1526
|
-
const result = await
|
|
2419
|
+
const result = await executeLoopTarget(store, loop, claim.run);
|
|
1527
2420
|
const run = store.finalizeRun(claim.run.id, {
|
|
1528
2421
|
status: result.status,
|
|
1529
2422
|
finishedAt: result.finishedAt,
|
|
@@ -1533,6 +2426,8 @@ program.command("run-now <idOrName>").option("--show-output", "show stdout/stder
|
|
|
1533
2426
|
exitCode: result.exitCode,
|
|
1534
2427
|
error: result.error,
|
|
1535
2428
|
pid: result.pid
|
|
2429
|
+
}, {
|
|
2430
|
+
claimedBy: claim.run.claimedBy
|
|
1536
2431
|
});
|
|
1537
2432
|
print(publicRun(run, opts.showOutput), `${run.id} ${run.status}`);
|
|
1538
2433
|
} finally {
|
|
@@ -1574,7 +2469,7 @@ ${result.instructions.join(`
|
|
|
1574
2469
|
});
|
|
1575
2470
|
daemon.command("logs").option("-n, --lines <n>", "lines", "80").action((opts) => {
|
|
1576
2471
|
const path = daemonLogPath();
|
|
1577
|
-
if (!
|
|
2472
|
+
if (!existsSync3(path)) {
|
|
1578
2473
|
console.log("");
|
|
1579
2474
|
return;
|
|
1580
2475
|
}
|