@stoneforge/quarry 0.1.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/LICENSE +13 -0
- package/README.md +160 -0
- package/dist/api/index.d.ts +8 -0
- package/dist/api/index.d.ts.map +1 -0
- package/dist/api/index.js +8 -0
- package/dist/api/index.js.map +1 -0
- package/dist/api/quarry-api.d.ts +268 -0
- package/dist/api/quarry-api.d.ts.map +1 -0
- package/dist/api/quarry-api.js +3905 -0
- package/dist/api/quarry-api.js.map +1 -0
- package/dist/api/types.d.ts +1359 -0
- package/dist/api/types.d.ts.map +1 -0
- package/dist/api/types.js +204 -0
- package/dist/api/types.js.map +1 -0
- package/dist/bin/sf.d.ts +3 -0
- package/dist/bin/sf.d.ts.map +1 -0
- package/dist/bin/sf.js +9 -0
- package/dist/bin/sf.js.map +1 -0
- package/dist/cli/commands/admin.d.ts +11 -0
- package/dist/cli/commands/admin.d.ts.map +1 -0
- package/dist/cli/commands/admin.js +465 -0
- package/dist/cli/commands/admin.js.map +1 -0
- package/dist/cli/commands/alias.d.ts +8 -0
- package/dist/cli/commands/alias.d.ts.map +1 -0
- package/dist/cli/commands/alias.js +70 -0
- package/dist/cli/commands/alias.js.map +1 -0
- package/dist/cli/commands/channel.d.ts +13 -0
- package/dist/cli/commands/channel.d.ts.map +1 -0
- package/dist/cli/commands/channel.js +680 -0
- package/dist/cli/commands/channel.js.map +1 -0
- package/dist/cli/commands/completion.d.ts +8 -0
- package/dist/cli/commands/completion.d.ts.map +1 -0
- package/dist/cli/commands/completion.js +87 -0
- package/dist/cli/commands/completion.js.map +1 -0
- package/dist/cli/commands/config.d.ts +12 -0
- package/dist/cli/commands/config.d.ts.map +1 -0
- package/dist/cli/commands/config.js +242 -0
- package/dist/cli/commands/config.js.map +1 -0
- package/dist/cli/commands/crud.d.ts +64 -0
- package/dist/cli/commands/crud.d.ts.map +1 -0
- package/dist/cli/commands/crud.js +805 -0
- package/dist/cli/commands/crud.js.map +1 -0
- package/dist/cli/commands/dep.d.ts +16 -0
- package/dist/cli/commands/dep.d.ts.map +1 -0
- package/dist/cli/commands/dep.js +499 -0
- package/dist/cli/commands/dep.js.map +1 -0
- package/dist/cli/commands/document.d.ts +12 -0
- package/dist/cli/commands/document.d.ts.map +1 -0
- package/dist/cli/commands/document.js +1039 -0
- package/dist/cli/commands/document.js.map +1 -0
- package/dist/cli/commands/embeddings.d.ts +12 -0
- package/dist/cli/commands/embeddings.d.ts.map +1 -0
- package/dist/cli/commands/embeddings.js +273 -0
- package/dist/cli/commands/embeddings.js.map +1 -0
- package/dist/cli/commands/entity.d.ts +16 -0
- package/dist/cli/commands/entity.d.ts.map +1 -0
- package/dist/cli/commands/entity.js +522 -0
- package/dist/cli/commands/entity.js.map +1 -0
- package/dist/cli/commands/gc.d.ts +10 -0
- package/dist/cli/commands/gc.d.ts.map +1 -0
- package/dist/cli/commands/gc.js +257 -0
- package/dist/cli/commands/gc.js.map +1 -0
- package/dist/cli/commands/help.d.ts +11 -0
- package/dist/cli/commands/help.d.ts.map +1 -0
- package/dist/cli/commands/help.js +169 -0
- package/dist/cli/commands/help.js.map +1 -0
- package/dist/cli/commands/history.d.ts +9 -0
- package/dist/cli/commands/history.d.ts.map +1 -0
- package/dist/cli/commands/history.js +160 -0
- package/dist/cli/commands/history.js.map +1 -0
- package/dist/cli/commands/identity.d.ts +18 -0
- package/dist/cli/commands/identity.d.ts.map +1 -0
- package/dist/cli/commands/identity.js +698 -0
- package/dist/cli/commands/identity.js.map +1 -0
- package/dist/cli/commands/inbox.d.ts +20 -0
- package/dist/cli/commands/inbox.d.ts.map +1 -0
- package/dist/cli/commands/inbox.js +493 -0
- package/dist/cli/commands/inbox.js.map +1 -0
- package/dist/cli/commands/init.d.ts +20 -0
- package/dist/cli/commands/init.d.ts.map +1 -0
- package/dist/cli/commands/init.js +144 -0
- package/dist/cli/commands/init.js.map +1 -0
- package/dist/cli/commands/install.d.ts +9 -0
- package/dist/cli/commands/install.d.ts.map +1 -0
- package/dist/cli/commands/install.js +200 -0
- package/dist/cli/commands/install.js.map +1 -0
- package/dist/cli/commands/library.d.ts +12 -0
- package/dist/cli/commands/library.d.ts.map +1 -0
- package/dist/cli/commands/library.js +665 -0
- package/dist/cli/commands/library.js.map +1 -0
- package/dist/cli/commands/message.d.ts +11 -0
- package/dist/cli/commands/message.d.ts.map +1 -0
- package/dist/cli/commands/message.js +608 -0
- package/dist/cli/commands/message.js.map +1 -0
- package/dist/cli/commands/plan.d.ts +17 -0
- package/dist/cli/commands/plan.d.ts.map +1 -0
- package/dist/cli/commands/plan.js +698 -0
- package/dist/cli/commands/plan.js.map +1 -0
- package/dist/cli/commands/playbook.d.ts +12 -0
- package/dist/cli/commands/playbook.d.ts.map +1 -0
- package/dist/cli/commands/playbook.js +730 -0
- package/dist/cli/commands/playbook.js.map +1 -0
- package/dist/cli/commands/reset.d.ts +12 -0
- package/dist/cli/commands/reset.d.ts.map +1 -0
- package/dist/cli/commands/reset.js +306 -0
- package/dist/cli/commands/reset.js.map +1 -0
- package/dist/cli/commands/serve.d.ts +11 -0
- package/dist/cli/commands/serve.d.ts.map +1 -0
- package/dist/cli/commands/serve.js +106 -0
- package/dist/cli/commands/serve.js.map +1 -0
- package/dist/cli/commands/stats.d.ts +8 -0
- package/dist/cli/commands/stats.d.ts.map +1 -0
- package/dist/cli/commands/stats.js +82 -0
- package/dist/cli/commands/stats.js.map +1 -0
- package/dist/cli/commands/sync.d.ts +14 -0
- package/dist/cli/commands/sync.d.ts.map +1 -0
- package/dist/cli/commands/sync.js +370 -0
- package/dist/cli/commands/sync.js.map +1 -0
- package/dist/cli/commands/task.d.ts +25 -0
- package/dist/cli/commands/task.d.ts.map +1 -0
- package/dist/cli/commands/task.js +1153 -0
- package/dist/cli/commands/task.js.map +1 -0
- package/dist/cli/commands/team.d.ts +13 -0
- package/dist/cli/commands/team.d.ts.map +1 -0
- package/dist/cli/commands/team.js +471 -0
- package/dist/cli/commands/team.js.map +1 -0
- package/dist/cli/commands/workflow.d.ts +16 -0
- package/dist/cli/commands/workflow.d.ts.map +1 -0
- package/dist/cli/commands/workflow.js +753 -0
- package/dist/cli/commands/workflow.js.map +1 -0
- package/dist/cli/completion.d.ts +28 -0
- package/dist/cli/completion.d.ts.map +1 -0
- package/dist/cli/completion.js +295 -0
- package/dist/cli/completion.js.map +1 -0
- package/dist/cli/db.d.ts +38 -0
- package/dist/cli/db.d.ts.map +1 -0
- package/dist/cli/db.js +90 -0
- package/dist/cli/db.js.map +1 -0
- package/dist/cli/formatter.d.ts +87 -0
- package/dist/cli/formatter.d.ts.map +1 -0
- package/dist/cli/formatter.js +464 -0
- package/dist/cli/formatter.js.map +1 -0
- package/dist/cli/index.d.ts +33 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +38 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/cli/parser.d.ts +45 -0
- package/dist/cli/parser.d.ts.map +1 -0
- package/dist/cli/parser.js +256 -0
- package/dist/cli/parser.js.map +1 -0
- package/dist/cli/plugin-loader.d.ts +39 -0
- package/dist/cli/plugin-loader.d.ts.map +1 -0
- package/dist/cli/plugin-loader.js +165 -0
- package/dist/cli/plugin-loader.js.map +1 -0
- package/dist/cli/plugin-registry.d.ts +50 -0
- package/dist/cli/plugin-registry.d.ts.map +1 -0
- package/dist/cli/plugin-registry.js +206 -0
- package/dist/cli/plugin-registry.js.map +1 -0
- package/dist/cli/plugin-types.d.ts +106 -0
- package/dist/cli/plugin-types.d.ts.map +1 -0
- package/dist/cli/plugin-types.js +103 -0
- package/dist/cli/plugin-types.js.map +1 -0
- package/dist/cli/runner.d.ts +35 -0
- package/dist/cli/runner.d.ts.map +1 -0
- package/dist/cli/runner.js +340 -0
- package/dist/cli/runner.js.map +1 -0
- package/dist/cli/suggest.d.ts +15 -0
- package/dist/cli/suggest.d.ts.map +1 -0
- package/dist/cli/suggest.js +49 -0
- package/dist/cli/suggest.js.map +1 -0
- package/dist/cli/types.d.ts +138 -0
- package/dist/cli/types.d.ts.map +1 -0
- package/dist/cli/types.js +63 -0
- package/dist/cli/types.js.map +1 -0
- package/dist/config/config.d.ts +86 -0
- package/dist/config/config.d.ts.map +1 -0
- package/dist/config/config.js +348 -0
- package/dist/config/config.js.map +1 -0
- package/dist/config/defaults.d.ts +66 -0
- package/dist/config/defaults.d.ts.map +1 -0
- package/dist/config/defaults.js +114 -0
- package/dist/config/defaults.js.map +1 -0
- package/dist/config/duration.d.ts +75 -0
- package/dist/config/duration.d.ts.map +1 -0
- package/dist/config/duration.js +190 -0
- package/dist/config/duration.js.map +1 -0
- package/dist/config/env.d.ts +67 -0
- package/dist/config/env.d.ts.map +1 -0
- package/dist/config/env.js +207 -0
- package/dist/config/env.js.map +1 -0
- package/dist/config/file.d.ts +97 -0
- package/dist/config/file.d.ts.map +1 -0
- package/dist/config/file.js +365 -0
- package/dist/config/file.js.map +1 -0
- package/dist/config/index.d.ts +35 -0
- package/dist/config/index.d.ts.map +1 -0
- package/dist/config/index.js +41 -0
- package/dist/config/index.js.map +1 -0
- package/dist/config/merge.d.ts +53 -0
- package/dist/config/merge.d.ts.map +1 -0
- package/dist/config/merge.js +226 -0
- package/dist/config/merge.js.map +1 -0
- package/dist/config/types.d.ts +257 -0
- package/dist/config/types.d.ts.map +1 -0
- package/dist/config/types.js +72 -0
- package/dist/config/types.js.map +1 -0
- package/dist/config/validation.d.ts +55 -0
- package/dist/config/validation.d.ts.map +1 -0
- package/dist/config/validation.js +251 -0
- package/dist/config/validation.js.map +1 -0
- package/dist/http/index.d.ts +8 -0
- package/dist/http/index.d.ts.map +1 -0
- package/dist/http/index.js +12 -0
- package/dist/http/index.js.map +1 -0
- package/dist/http/sync-handlers.d.ts +162 -0
- package/dist/http/sync-handlers.d.ts.map +1 -0
- package/dist/http/sync-handlers.js +271 -0
- package/dist/http/sync-handlers.js.map +1 -0
- package/dist/index.d.ts +25 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +69 -0
- package/dist/index.js.map +1 -0
- package/dist/server/index.d.ts +34 -0
- package/dist/server/index.d.ts.map +1 -0
- package/dist/server/index.js +3329 -0
- package/dist/server/index.js.map +1 -0
- package/dist/server/static.d.ts +18 -0
- package/dist/server/static.d.ts.map +1 -0
- package/dist/server/static.js +71 -0
- package/dist/server/static.js.map +1 -0
- package/dist/server/ws/broadcaster.d.ts +8 -0
- package/dist/server/ws/broadcaster.d.ts.map +1 -0
- package/dist/server/ws/broadcaster.js +7 -0
- package/dist/server/ws/broadcaster.js.map +1 -0
- package/dist/server/ws/handler.d.ts +55 -0
- package/dist/server/ws/handler.d.ts.map +1 -0
- package/dist/server/ws/handler.js +160 -0
- package/dist/server/ws/handler.js.map +1 -0
- package/dist/services/blocked-cache.d.ts +297 -0
- package/dist/services/blocked-cache.d.ts.map +1 -0
- package/dist/services/blocked-cache.js +755 -0
- package/dist/services/blocked-cache.js.map +1 -0
- package/dist/services/dependency.d.ts +205 -0
- package/dist/services/dependency.d.ts.map +1 -0
- package/dist/services/dependency.js +566 -0
- package/dist/services/dependency.js.map +1 -0
- package/dist/services/embeddings/fusion.d.ts +33 -0
- package/dist/services/embeddings/fusion.d.ts.map +1 -0
- package/dist/services/embeddings/fusion.js +34 -0
- package/dist/services/embeddings/fusion.js.map +1 -0
- package/dist/services/embeddings/index.d.ts +12 -0
- package/dist/services/embeddings/index.d.ts.map +1 -0
- package/dist/services/embeddings/index.js +10 -0
- package/dist/services/embeddings/index.js.map +1 -0
- package/dist/services/embeddings/local-provider.d.ts +31 -0
- package/dist/services/embeddings/local-provider.d.ts.map +1 -0
- package/dist/services/embeddings/local-provider.js +80 -0
- package/dist/services/embeddings/local-provider.js.map +1 -0
- package/dist/services/embeddings/service.d.ts +76 -0
- package/dist/services/embeddings/service.d.ts.map +1 -0
- package/dist/services/embeddings/service.js +153 -0
- package/dist/services/embeddings/service.js.map +1 -0
- package/dist/services/embeddings/types.d.ts +70 -0
- package/dist/services/embeddings/types.d.ts.map +1 -0
- package/dist/services/embeddings/types.js +8 -0
- package/dist/services/embeddings/types.js.map +1 -0
- package/dist/services/id-length-cache.d.ts +156 -0
- package/dist/services/id-length-cache.d.ts.map +1 -0
- package/dist/services/id-length-cache.js +197 -0
- package/dist/services/id-length-cache.js.map +1 -0
- package/dist/services/inbox.d.ts +147 -0
- package/dist/services/inbox.d.ts.map +1 -0
- package/dist/services/inbox.js +428 -0
- package/dist/services/inbox.js.map +1 -0
- package/dist/services/index.d.ts +10 -0
- package/dist/services/index.d.ts.map +1 -0
- package/dist/services/index.js +10 -0
- package/dist/services/index.js.map +1 -0
- package/dist/services/priority-service.d.ts +145 -0
- package/dist/services/priority-service.d.ts.map +1 -0
- package/dist/services/priority-service.js +272 -0
- package/dist/services/priority-service.js.map +1 -0
- package/dist/services/search-utils.d.ts +47 -0
- package/dist/services/search-utils.d.ts.map +1 -0
- package/dist/services/search-utils.js +83 -0
- package/dist/services/search-utils.js.map +1 -0
- package/dist/sync/hash.d.ts +48 -0
- package/dist/sync/hash.d.ts.map +1 -0
- package/dist/sync/hash.js +136 -0
- package/dist/sync/hash.js.map +1 -0
- package/dist/sync/index.d.ts +11 -0
- package/dist/sync/index.d.ts.map +1 -0
- package/dist/sync/index.js +16 -0
- package/dist/sync/index.js.map +1 -0
- package/dist/sync/merge.d.ts +80 -0
- package/dist/sync/merge.d.ts.map +1 -0
- package/dist/sync/merge.js +310 -0
- package/dist/sync/merge.js.map +1 -0
- package/dist/sync/serialization.d.ts +132 -0
- package/dist/sync/serialization.d.ts.map +1 -0
- package/dist/sync/serialization.js +306 -0
- package/dist/sync/serialization.js.map +1 -0
- package/dist/sync/service.d.ts +102 -0
- package/dist/sync/service.d.ts.map +1 -0
- package/dist/sync/service.js +493 -0
- package/dist/sync/service.js.map +1 -0
- package/dist/sync/types.d.ts +275 -0
- package/dist/sync/types.d.ts.map +1 -0
- package/dist/sync/types.js +76 -0
- package/dist/sync/types.js.map +1 -0
- package/dist/systems/identity.d.ts +479 -0
- package/dist/systems/identity.d.ts.map +1 -0
- package/dist/systems/identity.js +817 -0
- package/dist/systems/identity.js.map +1 -0
- package/dist/systems/index.d.ts +8 -0
- package/dist/systems/index.d.ts.map +1 -0
- package/dist/systems/index.js +29 -0
- package/dist/systems/index.js.map +1 -0
- package/package.json +121 -0
- package/web/assets/charts-vendor-D1YcbGux.js +55 -0
- package/web/assets/dnd-vendor-DmxE-_ZH.js +5 -0
- package/web/assets/editor-vendor-BxraAWts.js +279 -0
- package/web/assets/index-B77vv208.js +341 -0
- package/web/assets/index-CF_XnVLh.css +1 -0
- package/web/assets/router-vendor-BCKpRBrB.js +41 -0
- package/web/assets/ui-vendor-DUahGnbT.js +45 -0
- package/web/assets/utils-vendor-CfYKiENT.js +813 -0
- package/web/favicon.ico +0 -0
- package/web/index.html +23 -0
- package/web/logo.png +0 -0
|
@@ -0,0 +1,753 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Workflow Commands - Collection command interface for workflows
|
|
3
|
+
*
|
|
4
|
+
* Provides CLI commands for workflow operations:
|
|
5
|
+
* - workflow create: Instantiate a playbook into a workflow
|
|
6
|
+
* - workflow list: List workflows with filtering
|
|
7
|
+
* - workflow show: Show workflow details
|
|
8
|
+
* - workflow tasks: List tasks in a workflow
|
|
9
|
+
* - workflow progress: Show workflow progress metrics
|
|
10
|
+
* - workflow delete: Delete ephemeral workflow and tasks
|
|
11
|
+
* - workflow promote: Promote ephemeral to durable
|
|
12
|
+
* - workflow gc: Garbage collect old ephemeral workflows
|
|
13
|
+
*/
|
|
14
|
+
import { success, failure, ExitCode } from '../types.js';
|
|
15
|
+
import { getFormatter, getOutputMode, getStatusIcon } from '../formatter.js';
|
|
16
|
+
import { createWorkflow, WorkflowStatus, promoteWorkflow, filterGarbageCollectionByAge, } from '@stoneforge/core';
|
|
17
|
+
import { suggestCommands } from '../suggest.js';
|
|
18
|
+
import { resolveActor, createAPI } from '../db.js';
|
|
19
|
+
// ============================================================================
|
|
20
|
+
// Constants
|
|
21
|
+
// ============================================================================
|
|
22
|
+
// Default GC age: 7 days in milliseconds
|
|
23
|
+
const DEFAULT_GC_AGE_DAYS = 7;
|
|
24
|
+
const MS_PER_DAY = 24 * 60 * 60 * 1000;
|
|
25
|
+
const workflowCreateOptions = [
|
|
26
|
+
{
|
|
27
|
+
name: 'var',
|
|
28
|
+
description: 'Set variable (name=value, can be repeated)',
|
|
29
|
+
hasValue: true,
|
|
30
|
+
array: true,
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
name: 'ephemeral',
|
|
34
|
+
short: 'e',
|
|
35
|
+
description: 'Create as ephemeral (not synced)',
|
|
36
|
+
hasValue: false,
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
name: 'title',
|
|
40
|
+
short: 't',
|
|
41
|
+
description: 'Override workflow title',
|
|
42
|
+
hasValue: true,
|
|
43
|
+
},
|
|
44
|
+
];
|
|
45
|
+
async function workflowCreateHandler(args, options) {
|
|
46
|
+
const [playbookNameOrId] = args;
|
|
47
|
+
if (!playbookNameOrId) {
|
|
48
|
+
return failure('Usage: sf workflow create <playbook> [options]\nExample: sf workflow create deploy --var env=prod', ExitCode.INVALID_ARGUMENTS);
|
|
49
|
+
}
|
|
50
|
+
const { api, error } = createAPI(options, true);
|
|
51
|
+
if (error) {
|
|
52
|
+
return failure(error, ExitCode.GENERAL_ERROR);
|
|
53
|
+
}
|
|
54
|
+
try {
|
|
55
|
+
const actor = resolveActor(options);
|
|
56
|
+
// Parse variables
|
|
57
|
+
const variables = {};
|
|
58
|
+
if (options.var) {
|
|
59
|
+
const varArgs = Array.isArray(options.var) ? options.var : [options.var];
|
|
60
|
+
for (const varArg of varArgs) {
|
|
61
|
+
const eqIndex = varArg.indexOf('=');
|
|
62
|
+
if (eqIndex === -1) {
|
|
63
|
+
return failure(`Invalid variable format: ${varArg}. Use name=value`, ExitCode.VALIDATION);
|
|
64
|
+
}
|
|
65
|
+
const name = varArg.slice(0, eqIndex);
|
|
66
|
+
const value = varArg.slice(eqIndex + 1);
|
|
67
|
+
variables[name] = value;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
// For now, create a workflow directly
|
|
71
|
+
// TODO: When playbook instantiation is implemented, look up playbook and create workflow
|
|
72
|
+
const title = options.title || `Workflow from ${playbookNameOrId}`;
|
|
73
|
+
const input = {
|
|
74
|
+
title,
|
|
75
|
+
createdBy: actor,
|
|
76
|
+
ephemeral: options.ephemeral ?? false,
|
|
77
|
+
variables,
|
|
78
|
+
// playbookId would be set here when playbook lookup is implemented
|
|
79
|
+
};
|
|
80
|
+
const workflow = await createWorkflow(input);
|
|
81
|
+
const created = await api.create(workflow);
|
|
82
|
+
const mode = getOutputMode(options);
|
|
83
|
+
if (mode === 'quiet') {
|
|
84
|
+
return success(created.id);
|
|
85
|
+
}
|
|
86
|
+
return success(created, `Created workflow ${created.id}${options.ephemeral ? ' (ephemeral)' : ''}`);
|
|
87
|
+
}
|
|
88
|
+
catch (err) {
|
|
89
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
90
|
+
return failure(`Failed to create workflow: ${message}`, ExitCode.GENERAL_ERROR);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
const workflowCreateCommand = {
|
|
94
|
+
name: 'create',
|
|
95
|
+
description: 'Instantiate a playbook into a workflow',
|
|
96
|
+
usage: 'sf workflow create <playbook> [options]',
|
|
97
|
+
help: `Create a workflow by instantiating a playbook template.
|
|
98
|
+
|
|
99
|
+
Arguments:
|
|
100
|
+
playbook Playbook name or ID to instantiate
|
|
101
|
+
|
|
102
|
+
Options:
|
|
103
|
+
--var <name=value> Set variable (can be repeated)
|
|
104
|
+
-e, --ephemeral Create as ephemeral (not synced to JSONL)
|
|
105
|
+
-t, --title <text> Override workflow title
|
|
106
|
+
|
|
107
|
+
Examples:
|
|
108
|
+
sf workflow create deploy --var env=prod --var version=1.2
|
|
109
|
+
sf workflow create sprint-setup --ephemeral
|
|
110
|
+
sf workflow create deploy --title "Production Deploy v1.2"`,
|
|
111
|
+
options: workflowCreateOptions,
|
|
112
|
+
handler: workflowCreateHandler,
|
|
113
|
+
};
|
|
114
|
+
const workflowListOptions = [
|
|
115
|
+
{
|
|
116
|
+
name: 'status',
|
|
117
|
+
short: 's',
|
|
118
|
+
description: 'Filter by status (pending, running, completed, failed, cancelled)',
|
|
119
|
+
hasValue: true,
|
|
120
|
+
},
|
|
121
|
+
{
|
|
122
|
+
name: 'ephemeral',
|
|
123
|
+
short: 'e',
|
|
124
|
+
description: 'Show only ephemeral workflows',
|
|
125
|
+
hasValue: false,
|
|
126
|
+
},
|
|
127
|
+
{
|
|
128
|
+
name: 'durable',
|
|
129
|
+
short: 'd',
|
|
130
|
+
description: 'Show only durable workflows',
|
|
131
|
+
hasValue: false,
|
|
132
|
+
},
|
|
133
|
+
{
|
|
134
|
+
name: 'limit',
|
|
135
|
+
short: 'l',
|
|
136
|
+
description: 'Maximum number of results',
|
|
137
|
+
hasValue: true,
|
|
138
|
+
},
|
|
139
|
+
];
|
|
140
|
+
async function workflowListHandler(_args, options) {
|
|
141
|
+
const { api, error } = createAPI(options);
|
|
142
|
+
if (error) {
|
|
143
|
+
return failure(error, ExitCode.GENERAL_ERROR);
|
|
144
|
+
}
|
|
145
|
+
try {
|
|
146
|
+
// Build filter
|
|
147
|
+
const filter = {
|
|
148
|
+
type: 'workflow',
|
|
149
|
+
};
|
|
150
|
+
// Status filter
|
|
151
|
+
if (options.status) {
|
|
152
|
+
const validStatuses = Object.values(WorkflowStatus);
|
|
153
|
+
if (!validStatuses.includes(options.status)) {
|
|
154
|
+
return failure(`Invalid status: ${options.status}. Must be one of: ${validStatuses.join(', ')}`, ExitCode.VALIDATION);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
// Limit
|
|
158
|
+
if (options.limit) {
|
|
159
|
+
const limit = parseInt(options.limit, 10);
|
|
160
|
+
if (isNaN(limit) || limit < 1) {
|
|
161
|
+
return failure('Limit must be a positive number', ExitCode.VALIDATION);
|
|
162
|
+
}
|
|
163
|
+
filter.limit = limit;
|
|
164
|
+
}
|
|
165
|
+
const result = await api.listPaginated(filter);
|
|
166
|
+
// Post-filter
|
|
167
|
+
let items = result.items;
|
|
168
|
+
// Status filter
|
|
169
|
+
if (options.status) {
|
|
170
|
+
items = items.filter((w) => w.status === options.status);
|
|
171
|
+
}
|
|
172
|
+
// Ephemeral/durable filter
|
|
173
|
+
if (options.ephemeral && !options.durable) {
|
|
174
|
+
items = items.filter((w) => w.ephemeral);
|
|
175
|
+
}
|
|
176
|
+
else if (options.durable && !options.ephemeral) {
|
|
177
|
+
items = items.filter((w) => !w.ephemeral);
|
|
178
|
+
}
|
|
179
|
+
const mode = getOutputMode(options);
|
|
180
|
+
const formatter = getFormatter(mode);
|
|
181
|
+
if (mode === 'json') {
|
|
182
|
+
return success(items);
|
|
183
|
+
}
|
|
184
|
+
if (mode === 'quiet') {
|
|
185
|
+
return success(items.map((w) => w.id).join('\n'));
|
|
186
|
+
}
|
|
187
|
+
if (items.length === 0) {
|
|
188
|
+
return success(null, 'No workflows found');
|
|
189
|
+
}
|
|
190
|
+
// Build table
|
|
191
|
+
const headers = ['ID', 'TITLE', 'STATUS', 'MODE', 'CREATED'];
|
|
192
|
+
const rows = items.map((w) => [
|
|
193
|
+
w.id,
|
|
194
|
+
w.title.length > 40 ? w.title.substring(0, 37) + '...' : w.title,
|
|
195
|
+
`${getStatusIcon(w.status)} ${w.status}`,
|
|
196
|
+
w.ephemeral ? 'ephemeral' : 'durable',
|
|
197
|
+
w.createdAt.split('T')[0],
|
|
198
|
+
]);
|
|
199
|
+
const table = formatter.table(headers, rows);
|
|
200
|
+
const summary = `\nShowing ${items.length} of ${result.total} workflows`;
|
|
201
|
+
return success(items, table + summary);
|
|
202
|
+
}
|
|
203
|
+
catch (err) {
|
|
204
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
205
|
+
return failure(`Failed to list workflows: ${message}`, ExitCode.GENERAL_ERROR);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
const workflowListCommand = {
|
|
209
|
+
name: 'list',
|
|
210
|
+
description: 'List workflows',
|
|
211
|
+
usage: 'sf workflow list [options]',
|
|
212
|
+
help: `List workflows with optional filtering.
|
|
213
|
+
|
|
214
|
+
Options:
|
|
215
|
+
-s, --status <status> Filter by status: pending, running, completed, failed, cancelled
|
|
216
|
+
-e, --ephemeral Show only ephemeral workflows
|
|
217
|
+
-d, --durable Show only durable workflows
|
|
218
|
+
-l, --limit <n> Maximum results
|
|
219
|
+
|
|
220
|
+
Examples:
|
|
221
|
+
sf workflow list
|
|
222
|
+
sf workflow list --status running
|
|
223
|
+
sf workflow list --ephemeral`,
|
|
224
|
+
options: workflowListOptions,
|
|
225
|
+
handler: workflowListHandler,
|
|
226
|
+
};
|
|
227
|
+
// ============================================================================
|
|
228
|
+
// Workflow Show Command
|
|
229
|
+
// ============================================================================
|
|
230
|
+
async function workflowShowHandler(args, options) {
|
|
231
|
+
const [id] = args;
|
|
232
|
+
if (!id) {
|
|
233
|
+
return failure('Usage: sf workflow show <id>\nExample: sf workflow show el-abc123', ExitCode.INVALID_ARGUMENTS);
|
|
234
|
+
}
|
|
235
|
+
const { api, error } = createAPI(options);
|
|
236
|
+
if (error) {
|
|
237
|
+
return failure(error, ExitCode.GENERAL_ERROR);
|
|
238
|
+
}
|
|
239
|
+
try {
|
|
240
|
+
const workflow = await api.get(id);
|
|
241
|
+
if (!workflow) {
|
|
242
|
+
return failure(`Workflow not found: ${id}`, ExitCode.NOT_FOUND);
|
|
243
|
+
}
|
|
244
|
+
if (workflow.type !== 'workflow') {
|
|
245
|
+
return failure(`Element ${id} is not a workflow (type: ${workflow.type})`, ExitCode.VALIDATION);
|
|
246
|
+
}
|
|
247
|
+
// Check if workflow is deleted (tombstone)
|
|
248
|
+
const data = workflow;
|
|
249
|
+
if (data.status === 'tombstone' || data.deletedAt) {
|
|
250
|
+
return failure(`Workflow not found: ${id}`, ExitCode.NOT_FOUND);
|
|
251
|
+
}
|
|
252
|
+
const mode = getOutputMode(options);
|
|
253
|
+
const formatter = getFormatter(mode);
|
|
254
|
+
if (mode === 'json') {
|
|
255
|
+
return success(workflow);
|
|
256
|
+
}
|
|
257
|
+
if (mode === 'quiet') {
|
|
258
|
+
return success(workflow.id);
|
|
259
|
+
}
|
|
260
|
+
// Human-readable output
|
|
261
|
+
let output = formatter.element(workflow);
|
|
262
|
+
// Add workflow-specific info
|
|
263
|
+
output += '\n\n--- Workflow Info ---\n';
|
|
264
|
+
output += `Mode: ${workflow.ephemeral ? 'ephemeral' : 'durable'}\n`;
|
|
265
|
+
if (workflow.playbookId) {
|
|
266
|
+
output += `Playbook: ${workflow.playbookId}\n`;
|
|
267
|
+
}
|
|
268
|
+
if (workflow.startedAt) {
|
|
269
|
+
output += `Started: ${workflow.startedAt}\n`;
|
|
270
|
+
}
|
|
271
|
+
if (workflow.finishedAt) {
|
|
272
|
+
output += `Finished: ${workflow.finishedAt}\n`;
|
|
273
|
+
}
|
|
274
|
+
if (workflow.failureReason) {
|
|
275
|
+
output += `Failure: ${workflow.failureReason}\n`;
|
|
276
|
+
}
|
|
277
|
+
if (workflow.cancelReason) {
|
|
278
|
+
output += `Cancelled: ${workflow.cancelReason}\n`;
|
|
279
|
+
}
|
|
280
|
+
// Show variables if any
|
|
281
|
+
const varKeys = Object.keys(workflow.variables);
|
|
282
|
+
if (varKeys.length > 0) {
|
|
283
|
+
output += '\n--- Variables ---\n';
|
|
284
|
+
for (const key of varKeys) {
|
|
285
|
+
output += `${key}: ${JSON.stringify(workflow.variables[key])}\n`;
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
return success(workflow, output);
|
|
289
|
+
}
|
|
290
|
+
catch (err) {
|
|
291
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
292
|
+
return failure(`Failed to show workflow: ${message}`, ExitCode.GENERAL_ERROR);
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
const workflowShowCommand = {
|
|
296
|
+
name: 'show',
|
|
297
|
+
description: 'Show workflow details',
|
|
298
|
+
usage: 'sf workflow show <id>',
|
|
299
|
+
help: `Display detailed information about a workflow.
|
|
300
|
+
|
|
301
|
+
Arguments:
|
|
302
|
+
id Workflow identifier (e.g., el-abc123)
|
|
303
|
+
|
|
304
|
+
Examples:
|
|
305
|
+
sf workflow show el-abc123
|
|
306
|
+
sf workflow show el-abc123 --json`,
|
|
307
|
+
handler: workflowShowHandler,
|
|
308
|
+
};
|
|
309
|
+
const workflowTasksOptions = [
|
|
310
|
+
{
|
|
311
|
+
name: 'ready',
|
|
312
|
+
short: 'r',
|
|
313
|
+
description: 'Show only ready tasks (not blocked, not scheduled for future)',
|
|
314
|
+
hasValue: false,
|
|
315
|
+
},
|
|
316
|
+
{
|
|
317
|
+
name: 'status',
|
|
318
|
+
short: 's',
|
|
319
|
+
description: 'Filter by status (open, in_progress, blocked, closed, deferred)',
|
|
320
|
+
hasValue: true,
|
|
321
|
+
},
|
|
322
|
+
{
|
|
323
|
+
name: 'limit',
|
|
324
|
+
short: 'l',
|
|
325
|
+
description: 'Maximum number of results',
|
|
326
|
+
hasValue: true,
|
|
327
|
+
},
|
|
328
|
+
];
|
|
329
|
+
async function workflowTasksHandler(args, options) {
|
|
330
|
+
const [id] = args;
|
|
331
|
+
if (!id) {
|
|
332
|
+
return failure('Usage: sf workflow tasks <id>\nExample: sf workflow tasks el-abc123', ExitCode.INVALID_ARGUMENTS);
|
|
333
|
+
}
|
|
334
|
+
const { api, error } = createAPI(options);
|
|
335
|
+
if (error) {
|
|
336
|
+
return failure(error, ExitCode.GENERAL_ERROR);
|
|
337
|
+
}
|
|
338
|
+
try {
|
|
339
|
+
// Build filter
|
|
340
|
+
const filter = {};
|
|
341
|
+
// Status filter
|
|
342
|
+
if (options.status) {
|
|
343
|
+
const validStatuses = ['open', 'in_progress', 'blocked', 'closed', 'deferred', 'tombstone'];
|
|
344
|
+
if (!validStatuses.includes(options.status)) {
|
|
345
|
+
return failure(`Invalid status: ${options.status}. Must be one of: ${validStatuses.join(', ')}`, ExitCode.VALIDATION);
|
|
346
|
+
}
|
|
347
|
+
filter.status = options.status;
|
|
348
|
+
}
|
|
349
|
+
// Limit
|
|
350
|
+
if (options.limit) {
|
|
351
|
+
const limit = parseInt(options.limit, 10);
|
|
352
|
+
if (isNaN(limit) || limit < 1) {
|
|
353
|
+
return failure('Limit must be a positive number', ExitCode.VALIDATION);
|
|
354
|
+
}
|
|
355
|
+
filter.limit = limit;
|
|
356
|
+
}
|
|
357
|
+
// Get tasks based on --ready flag
|
|
358
|
+
const tasks = options.ready
|
|
359
|
+
? await api.getReadyTasksInWorkflow(id, filter)
|
|
360
|
+
: await api.getTasksInWorkflow(id, filter);
|
|
361
|
+
const mode = getOutputMode(options);
|
|
362
|
+
const formatter = getFormatter(mode);
|
|
363
|
+
if (mode === 'json') {
|
|
364
|
+
return success(tasks);
|
|
365
|
+
}
|
|
366
|
+
if (mode === 'quiet') {
|
|
367
|
+
return success(tasks.map((t) => t.id).join('\n'));
|
|
368
|
+
}
|
|
369
|
+
if (tasks.length === 0) {
|
|
370
|
+
return success(null, options.ready ? 'No ready tasks in workflow' : 'No tasks in workflow');
|
|
371
|
+
}
|
|
372
|
+
// Build table
|
|
373
|
+
const headers = ['ID', 'TITLE', 'STATUS', 'PRIORITY', 'ASSIGNEE'];
|
|
374
|
+
const rows = tasks.map((t) => [
|
|
375
|
+
t.id,
|
|
376
|
+
t.title.length > 40 ? t.title.substring(0, 37) + '...' : t.title,
|
|
377
|
+
`${getStatusIcon(t.status)} ${t.status}`,
|
|
378
|
+
`P${t.priority}`,
|
|
379
|
+
t.assignee ?? '-',
|
|
380
|
+
]);
|
|
381
|
+
const table = formatter.table(headers, rows);
|
|
382
|
+
const summary = `\n${tasks.length} task(s)`;
|
|
383
|
+
return success(tasks, table + summary);
|
|
384
|
+
}
|
|
385
|
+
catch (err) {
|
|
386
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
387
|
+
return failure(`Failed to list workflow tasks: ${message}`, ExitCode.GENERAL_ERROR);
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
const workflowTasksCommand = {
|
|
391
|
+
name: 'tasks',
|
|
392
|
+
description: 'List tasks in a workflow',
|
|
393
|
+
usage: 'sf workflow tasks <id> [options]',
|
|
394
|
+
help: `List all tasks that belong to a workflow.
|
|
395
|
+
|
|
396
|
+
Arguments:
|
|
397
|
+
id Workflow identifier (e.g., el-abc123)
|
|
398
|
+
|
|
399
|
+
Options:
|
|
400
|
+
-r, --ready Show only ready tasks (not blocked, not scheduled for future)
|
|
401
|
+
-s, --status <s> Filter by status: open, in_progress, blocked, closed, deferred
|
|
402
|
+
-l, --limit <n> Maximum results
|
|
403
|
+
|
|
404
|
+
Examples:
|
|
405
|
+
sf workflow tasks el-abc123
|
|
406
|
+
sf workflow tasks el-abc123 --ready
|
|
407
|
+
sf workflow tasks el-abc123 --status open
|
|
408
|
+
sf workflow tasks el-abc123 --json`,
|
|
409
|
+
options: workflowTasksOptions,
|
|
410
|
+
handler: workflowTasksHandler,
|
|
411
|
+
};
|
|
412
|
+
// ============================================================================
|
|
413
|
+
// Workflow Progress Command
|
|
414
|
+
// ============================================================================
|
|
415
|
+
async function workflowProgressHandler(args, options) {
|
|
416
|
+
const [id] = args;
|
|
417
|
+
if (!id) {
|
|
418
|
+
return failure('Usage: sf workflow progress <id>\nExample: sf workflow progress el-abc123', ExitCode.INVALID_ARGUMENTS);
|
|
419
|
+
}
|
|
420
|
+
const { api, error } = createAPI(options);
|
|
421
|
+
if (error) {
|
|
422
|
+
return failure(error, ExitCode.GENERAL_ERROR);
|
|
423
|
+
}
|
|
424
|
+
try {
|
|
425
|
+
const progress = await api.getWorkflowProgress(id);
|
|
426
|
+
const mode = getOutputMode(options);
|
|
427
|
+
if (mode === 'json') {
|
|
428
|
+
return success(progress);
|
|
429
|
+
}
|
|
430
|
+
if (mode === 'quiet') {
|
|
431
|
+
return success(`${progress.completionPercentage}%`);
|
|
432
|
+
}
|
|
433
|
+
// Human-readable output
|
|
434
|
+
let output = `Workflow Progress: ${id}\n\n`;
|
|
435
|
+
output += `Total Tasks: ${progress.totalTasks}\n`;
|
|
436
|
+
output += `Completion: ${progress.completionPercentage}%\n`;
|
|
437
|
+
output += `Ready Tasks: ${progress.readyTasks}\n`;
|
|
438
|
+
output += `Blocked Tasks: ${progress.blockedTasks}\n\n`;
|
|
439
|
+
output += '--- Status Breakdown ---\n';
|
|
440
|
+
const statusOrder = ['open', 'in_progress', 'blocked', 'closed', 'deferred'];
|
|
441
|
+
for (const status of statusOrder) {
|
|
442
|
+
const count = progress.statusCounts[status] ?? 0;
|
|
443
|
+
if (count > 0) {
|
|
444
|
+
output += `${getStatusIcon(status)} ${status}: ${count}\n`;
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
// Visual progress bar
|
|
448
|
+
const barWidth = 30;
|
|
449
|
+
const filled = Math.round((progress.completionPercentage / 100) * barWidth);
|
|
450
|
+
const empty = barWidth - filled;
|
|
451
|
+
const bar = '█'.repeat(filled) + '░'.repeat(empty);
|
|
452
|
+
output += `\n[${bar}] ${progress.completionPercentage}%`;
|
|
453
|
+
return success(progress, output);
|
|
454
|
+
}
|
|
455
|
+
catch (err) {
|
|
456
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
457
|
+
return failure(`Failed to get workflow progress: ${message}`, ExitCode.GENERAL_ERROR);
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
const workflowProgressCommand = {
|
|
461
|
+
name: 'progress',
|
|
462
|
+
description: 'Show workflow progress metrics',
|
|
463
|
+
usage: 'sf workflow progress <id>',
|
|
464
|
+
help: `Display progress metrics for a workflow.
|
|
465
|
+
|
|
466
|
+
Shows task status counts, completion percentage, and ready/blocked task counts.
|
|
467
|
+
|
|
468
|
+
Arguments:
|
|
469
|
+
id Workflow identifier (e.g., el-abc123)
|
|
470
|
+
|
|
471
|
+
Examples:
|
|
472
|
+
sf workflow progress el-abc123
|
|
473
|
+
sf workflow progress el-abc123 --json`,
|
|
474
|
+
handler: workflowProgressHandler,
|
|
475
|
+
};
|
|
476
|
+
const workflowDeleteOptions = [
|
|
477
|
+
{
|
|
478
|
+
name: 'force',
|
|
479
|
+
short: 'f',
|
|
480
|
+
description: 'Force delete even for durable workflows',
|
|
481
|
+
hasValue: false,
|
|
482
|
+
},
|
|
483
|
+
];
|
|
484
|
+
async function workflowDeleteHandler(args, options) {
|
|
485
|
+
const [id] = args;
|
|
486
|
+
if (!id) {
|
|
487
|
+
return failure('Usage: sf workflow delete <id>\nExample: sf workflow delete el-abc123', ExitCode.INVALID_ARGUMENTS);
|
|
488
|
+
}
|
|
489
|
+
const { api, error } = createAPI(options);
|
|
490
|
+
if (error) {
|
|
491
|
+
return failure(error, ExitCode.GENERAL_ERROR);
|
|
492
|
+
}
|
|
493
|
+
try {
|
|
494
|
+
const workflow = await api.get(id);
|
|
495
|
+
if (!workflow) {
|
|
496
|
+
return failure(`Workflow not found: ${id}`, ExitCode.NOT_FOUND);
|
|
497
|
+
}
|
|
498
|
+
if (workflow.type !== 'workflow') {
|
|
499
|
+
return failure(`Element ${id} is not a workflow (type: ${workflow.type})`, ExitCode.VALIDATION);
|
|
500
|
+
}
|
|
501
|
+
// Check if workflow is deleted (tombstone)
|
|
502
|
+
const data = workflow;
|
|
503
|
+
if (data.status === 'tombstone' || data.deletedAt) {
|
|
504
|
+
return failure(`Workflow not found: ${id}`, ExitCode.NOT_FOUND);
|
|
505
|
+
}
|
|
506
|
+
if (!workflow.ephemeral && !options.force) {
|
|
507
|
+
return failure(`Workflow ${id} is durable. Use --force to delete anyway, or 'sf delete ${id}' for soft delete.`, ExitCode.VALIDATION);
|
|
508
|
+
}
|
|
509
|
+
const actor = resolveActor(options);
|
|
510
|
+
// Use deleteWorkflow API to delete workflow and all its tasks
|
|
511
|
+
const result = await api.deleteWorkflow(id, { actor });
|
|
512
|
+
return success(result, `Deleted workflow ${id}: ${result.tasksDeleted} task(s), ${result.dependenciesDeleted} dependency(ies) deleted`);
|
|
513
|
+
}
|
|
514
|
+
catch (err) {
|
|
515
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
516
|
+
return failure(`Failed to delete workflow: ${message}`, ExitCode.GENERAL_ERROR);
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
const workflowDeleteCommand = {
|
|
520
|
+
name: 'delete',
|
|
521
|
+
description: 'Delete workflow and all its tasks',
|
|
522
|
+
usage: 'sf workflow delete <id>',
|
|
523
|
+
help: `Delete a workflow and all its tasks immediately (hard delete).
|
|
524
|
+
|
|
525
|
+
By default, only ephemeral workflows can be deleted. Use --force to delete
|
|
526
|
+
durable workflows as well.
|
|
527
|
+
|
|
528
|
+
Arguments:
|
|
529
|
+
id Workflow identifier
|
|
530
|
+
|
|
531
|
+
Options:
|
|
532
|
+
-f, --force Force delete even for durable workflows
|
|
533
|
+
|
|
534
|
+
Examples:
|
|
535
|
+
sf workflow delete el-abc123
|
|
536
|
+
sf workflow delete el-abc123 --force`,
|
|
537
|
+
options: workflowDeleteOptions,
|
|
538
|
+
handler: workflowDeleteHandler,
|
|
539
|
+
};
|
|
540
|
+
// ============================================================================
|
|
541
|
+
// Workflow Promote Command
|
|
542
|
+
// ============================================================================
|
|
543
|
+
async function workflowPromoteHandler(args, options) {
|
|
544
|
+
const [id] = args;
|
|
545
|
+
if (!id) {
|
|
546
|
+
return failure('Usage: sf workflow promote <id>\nExample: sf workflow promote el-abc123', ExitCode.INVALID_ARGUMENTS);
|
|
547
|
+
}
|
|
548
|
+
const { api, error } = createAPI(options);
|
|
549
|
+
if (error) {
|
|
550
|
+
return failure(error, ExitCode.GENERAL_ERROR);
|
|
551
|
+
}
|
|
552
|
+
try {
|
|
553
|
+
const workflow = await api.get(id);
|
|
554
|
+
if (!workflow) {
|
|
555
|
+
return failure(`Workflow not found: ${id}`, ExitCode.NOT_FOUND);
|
|
556
|
+
}
|
|
557
|
+
if (workflow.type !== 'workflow') {
|
|
558
|
+
return failure(`Element ${id} is not a workflow (type: ${workflow.type})`, ExitCode.VALIDATION);
|
|
559
|
+
}
|
|
560
|
+
// Check if workflow is deleted (tombstone)
|
|
561
|
+
const data = workflow;
|
|
562
|
+
if (data.status === 'tombstone' || data.deletedAt) {
|
|
563
|
+
return failure(`Workflow not found: ${id}`, ExitCode.NOT_FOUND);
|
|
564
|
+
}
|
|
565
|
+
if (!workflow.ephemeral) {
|
|
566
|
+
return success(workflow, `Workflow ${id} is already durable`);
|
|
567
|
+
}
|
|
568
|
+
const actor = resolveActor(options);
|
|
569
|
+
// Use the promoteWorkflow function to get updated values
|
|
570
|
+
const promoted = promoteWorkflow(workflow);
|
|
571
|
+
// Update in database
|
|
572
|
+
const updated = await api.update(id, { ephemeral: promoted.ephemeral }, { actor });
|
|
573
|
+
return success(updated, `Promoted workflow ${id} to durable`);
|
|
574
|
+
}
|
|
575
|
+
catch (err) {
|
|
576
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
577
|
+
return failure(`Failed to promote workflow: ${message}`, ExitCode.GENERAL_ERROR);
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
const workflowPromoteCommand = {
|
|
581
|
+
name: 'promote',
|
|
582
|
+
description: 'Promote ephemeral workflow to durable',
|
|
583
|
+
usage: 'sf workflow promote <id>',
|
|
584
|
+
help: `Promote an ephemeral workflow to durable so it gets synced to JSONL.
|
|
585
|
+
|
|
586
|
+
After promoting, the workflow and its tasks will be included in exports
|
|
587
|
+
and git sync.
|
|
588
|
+
|
|
589
|
+
Arguments:
|
|
590
|
+
id Workflow identifier
|
|
591
|
+
|
|
592
|
+
Examples:
|
|
593
|
+
sf workflow promote el-abc123`,
|
|
594
|
+
handler: workflowPromoteHandler,
|
|
595
|
+
};
|
|
596
|
+
const workflowGcOptions = [
|
|
597
|
+
{
|
|
598
|
+
name: 'age',
|
|
599
|
+
short: 'a',
|
|
600
|
+
description: `Maximum age in days (default: ${DEFAULT_GC_AGE_DAYS})`,
|
|
601
|
+
hasValue: true,
|
|
602
|
+
},
|
|
603
|
+
{
|
|
604
|
+
name: 'dry-run',
|
|
605
|
+
description: 'Show what would be deleted without deleting',
|
|
606
|
+
hasValue: false,
|
|
607
|
+
},
|
|
608
|
+
];
|
|
609
|
+
async function workflowGcHandler(_args, options) {
|
|
610
|
+
const { api, error } = createAPI(options);
|
|
611
|
+
if (error) {
|
|
612
|
+
return failure(error, ExitCode.GENERAL_ERROR);
|
|
613
|
+
}
|
|
614
|
+
try {
|
|
615
|
+
// Parse age
|
|
616
|
+
let ageDays = DEFAULT_GC_AGE_DAYS;
|
|
617
|
+
if (options.age) {
|
|
618
|
+
ageDays = parseInt(options.age, 10);
|
|
619
|
+
if (isNaN(ageDays) || ageDays < 0) {
|
|
620
|
+
return failure('Age must be a non-negative number', ExitCode.VALIDATION);
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
const maxAgeMs = ageDays * MS_PER_DAY;
|
|
624
|
+
// Check if dry run by getting eligible workflows first
|
|
625
|
+
if (options.dryRun) {
|
|
626
|
+
// Get all workflows for preview
|
|
627
|
+
const allWorkflows = await api.list({ type: 'workflow' });
|
|
628
|
+
// Filter to those eligible for GC
|
|
629
|
+
const eligible = filterGarbageCollectionByAge(allWorkflows, maxAgeMs);
|
|
630
|
+
if (eligible.length === 0) {
|
|
631
|
+
return success({ deleted: 0 }, 'No workflows eligible for garbage collection');
|
|
632
|
+
}
|
|
633
|
+
const mode = getOutputMode(options);
|
|
634
|
+
const formatter = getFormatter(mode);
|
|
635
|
+
if (mode === 'json') {
|
|
636
|
+
return success({ wouldDelete: eligible.map((w) => w.id), count: eligible.length });
|
|
637
|
+
}
|
|
638
|
+
if (mode === 'quiet') {
|
|
639
|
+
return success(eligible.map((w) => w.id).join('\n'));
|
|
640
|
+
}
|
|
641
|
+
const headers = ['ID', 'TITLE', 'STATUS', 'FINISHED'];
|
|
642
|
+
const rows = eligible.map((w) => [
|
|
643
|
+
w.id,
|
|
644
|
+
w.title.length > 40 ? w.title.substring(0, 37) + '...' : w.title,
|
|
645
|
+
w.status,
|
|
646
|
+
w.finishedAt ? w.finishedAt.split('T')[0] : '-',
|
|
647
|
+
]);
|
|
648
|
+
const table = formatter.table(headers, rows);
|
|
649
|
+
return success({ wouldDelete: eligible.map((w) => w.id), count: eligible.length }, `Would delete ${eligible.length} workflow(s):\n${table}`);
|
|
650
|
+
}
|
|
651
|
+
// Use garbageCollectWorkflows API
|
|
652
|
+
const gcResult = await api.garbageCollectWorkflows({
|
|
653
|
+
maxAgeMs,
|
|
654
|
+
dryRun: false,
|
|
655
|
+
});
|
|
656
|
+
if (gcResult.workflowsDeleted === 0) {
|
|
657
|
+
return success({ deleted: 0 }, 'No workflows eligible for garbage collection');
|
|
658
|
+
}
|
|
659
|
+
return success(gcResult, `Garbage collected ${gcResult.workflowsDeleted} workflow(s), ${gcResult.tasksDeleted} task(s), ${gcResult.dependenciesDeleted} dependency(ies)`);
|
|
660
|
+
}
|
|
661
|
+
catch (err) {
|
|
662
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
663
|
+
return failure(`Failed to garbage collect: ${message}`, ExitCode.GENERAL_ERROR);
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
const workflowGcCommand = {
|
|
667
|
+
name: 'gc',
|
|
668
|
+
description: 'Garbage collect old ephemeral workflows',
|
|
669
|
+
usage: 'sf workflow gc [options]',
|
|
670
|
+
help: `Delete old ephemeral workflows that have reached a terminal state.
|
|
671
|
+
|
|
672
|
+
Workflows are eligible for garbage collection if they are:
|
|
673
|
+
- Ephemeral (not durable)
|
|
674
|
+
- In a terminal state (completed, failed, or cancelled)
|
|
675
|
+
- Older than the specified age
|
|
676
|
+
|
|
677
|
+
Options:
|
|
678
|
+
-a, --age <days> Maximum age in days (default: ${DEFAULT_GC_AGE_DAYS})
|
|
679
|
+
--dry-run Show what would be deleted without deleting
|
|
680
|
+
|
|
681
|
+
Examples:
|
|
682
|
+
sf workflow gc
|
|
683
|
+
sf workflow gc --age 30
|
|
684
|
+
sf workflow gc --dry-run`,
|
|
685
|
+
options: workflowGcOptions,
|
|
686
|
+
handler: workflowGcHandler,
|
|
687
|
+
};
|
|
688
|
+
// ============================================================================
|
|
689
|
+
// Workflow Root Command
|
|
690
|
+
// ============================================================================
|
|
691
|
+
export const workflowCommand = {
|
|
692
|
+
name: 'workflow',
|
|
693
|
+
description: 'Manage workflows (executable task sequences)',
|
|
694
|
+
usage: 'sf workflow <subcommand> [options]',
|
|
695
|
+
help: `Manage workflows - executable sequences of tasks.
|
|
696
|
+
|
|
697
|
+
Workflows can be instantiated from playbook templates or created ad-hoc.
|
|
698
|
+
They support both durable (synced) and ephemeral (temporary) modes.
|
|
699
|
+
|
|
700
|
+
Subcommands:
|
|
701
|
+
create Instantiate a playbook into a workflow
|
|
702
|
+
list List workflows
|
|
703
|
+
show Show workflow details
|
|
704
|
+
tasks List tasks in a workflow
|
|
705
|
+
progress Show workflow progress metrics
|
|
706
|
+
delete Delete ephemeral workflow and tasks
|
|
707
|
+
promote Promote ephemeral to durable
|
|
708
|
+
gc Garbage collect old ephemeral workflows
|
|
709
|
+
|
|
710
|
+
Examples:
|
|
711
|
+
sf workflow create deploy --var env=prod
|
|
712
|
+
sf workflow list --status running
|
|
713
|
+
sf workflow show el-abc123
|
|
714
|
+
sf workflow tasks el-abc123
|
|
715
|
+
sf workflow tasks el-abc123 --ready
|
|
716
|
+
sf workflow progress el-abc123
|
|
717
|
+
sf workflow delete el-abc123
|
|
718
|
+
sf workflow promote el-abc123
|
|
719
|
+
sf workflow gc --age 30`,
|
|
720
|
+
subcommands: {
|
|
721
|
+
create: workflowCreateCommand,
|
|
722
|
+
list: workflowListCommand,
|
|
723
|
+
show: workflowShowCommand,
|
|
724
|
+
tasks: workflowTasksCommand,
|
|
725
|
+
progress: workflowProgressCommand,
|
|
726
|
+
delete: workflowDeleteCommand,
|
|
727
|
+
promote: workflowPromoteCommand,
|
|
728
|
+
gc: workflowGcCommand,
|
|
729
|
+
// Aliases (hidden from --help via dedup in getCommandHelp)
|
|
730
|
+
new: workflowCreateCommand,
|
|
731
|
+
add: workflowCreateCommand,
|
|
732
|
+
ls: workflowListCommand,
|
|
733
|
+
rm: workflowDeleteCommand,
|
|
734
|
+
get: workflowShowCommand,
|
|
735
|
+
view: workflowShowCommand,
|
|
736
|
+
},
|
|
737
|
+
handler: async (args, options) => {
|
|
738
|
+
// Default to list if no subcommand
|
|
739
|
+
if (args.length === 0) {
|
|
740
|
+
return workflowListHandler(args, options);
|
|
741
|
+
}
|
|
742
|
+
// Show "did you mean?" for unknown subcommands
|
|
743
|
+
const subNames = Object.keys(workflowCommand.subcommands);
|
|
744
|
+
const suggestions = suggestCommands(args[0], subNames);
|
|
745
|
+
let msg = `Unknown subcommand: ${args[0]}`;
|
|
746
|
+
if (suggestions.length > 0) {
|
|
747
|
+
msg += `\n\nDid you mean?\n${suggestions.map(s => ` ${s}`).join('\n')}`;
|
|
748
|
+
}
|
|
749
|
+
msg += '\n\nRun "sf workflow --help" to see available subcommands.';
|
|
750
|
+
return failure(msg, ExitCode.INVALID_ARGUMENTS);
|
|
751
|
+
},
|
|
752
|
+
};
|
|
753
|
+
//# sourceMappingURL=workflow.js.map
|