@ekanos/cli 0.14.0 → 0.16.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.
Files changed (115) hide show
  1. package/README.md +41 -4
  2. package/dist/auth/ekanos-api.d.ts +23 -0
  3. package/dist/auth/ekanos-api.js +33 -6
  4. package/dist/auth/ekanos-api.js.map +1 -1
  5. package/dist/commands/agents.d.ts +2 -2
  6. package/dist/commands/agents.js +14 -20
  7. package/dist/commands/agents.js.map +1 -1
  8. package/dist/commands/apps.d.ts +11 -3
  9. package/dist/commands/apps.js +86 -28
  10. package/dist/commands/apps.js.map +1 -1
  11. package/dist/commands/catalog.d.ts +4 -2
  12. package/dist/commands/catalog.js +166 -35
  13. package/dist/commands/catalog.js.map +1 -1
  14. package/dist/commands/login.d.ts +2 -0
  15. package/dist/commands/login.js +17 -0
  16. package/dist/commands/login.js.map +1 -1
  17. package/dist/commands/runs.d.ts +7 -3
  18. package/dist/commands/runs.js +24 -6
  19. package/dist/commands/runs.js.map +1 -1
  20. package/dist/commands/scope-notice.d.ts +7 -0
  21. package/dist/commands/scope-notice.js +4 -11
  22. package/dist/commands/scope-notice.js.map +1 -1
  23. package/dist/commands/sources-roles.d.ts +5 -0
  24. package/dist/commands/sources-roles.js +235 -0
  25. package/dist/commands/sources-roles.js.map +1 -0
  26. package/dist/commands/sources.d.ts +5 -0
  27. package/dist/commands/sources.js +6 -1
  28. package/dist/commands/sources.js.map +1 -1
  29. package/dist/commands/team-agent-readiness.d.ts +23 -17
  30. package/dist/commands/team-agent-readiness.js +57 -90
  31. package/dist/commands/team-agent-readiness.js.map +1 -1
  32. package/dist/commands/team-agents-agents.d.ts +8 -5
  33. package/dist/commands/team-agents-agents.js +173 -58
  34. package/dist/commands/team-agents-agents.js.map +1 -1
  35. package/dist/commands/team-agents-define.d.ts +31 -0
  36. package/dist/commands/team-agents-define.js +176 -0
  37. package/dist/commands/team-agents-define.js.map +1 -0
  38. package/dist/commands/team-agents-mcp.js +112 -10
  39. package/dist/commands/team-agents-mcp.js.map +1 -1
  40. package/dist/commands/team-agents-triggers.d.ts +1 -1
  41. package/dist/commands/team-agents-triggers.js +137 -5
  42. package/dist/commands/team-agents-triggers.js.map +1 -1
  43. package/dist/commands/team-agents.d.ts +4 -1
  44. package/dist/commands/team-agents.js +17 -4
  45. package/dist/commands/team-agents.js.map +1 -1
  46. package/dist/commands/team-llm-key.d.ts +14 -0
  47. package/dist/commands/team-llm-key.js +122 -0
  48. package/dist/commands/team-llm-key.js.map +1 -0
  49. package/dist/commands/teams-connect.js +123 -31
  50. package/dist/commands/teams-connect.js.map +1 -1
  51. package/dist/commands/teams-lifecycle.d.ts +90 -0
  52. package/dist/commands/teams-lifecycle.js +457 -0
  53. package/dist/commands/teams-lifecycle.js.map +1 -0
  54. package/dist/commands/teams-onboard.d.ts +14 -0
  55. package/dist/commands/teams-onboard.js +62 -13
  56. package/dist/commands/teams-onboard.js.map +1 -1
  57. package/dist/commands/teams-overview.js +7 -3
  58. package/dist/commands/teams-overview.js.map +1 -1
  59. package/dist/commands/teams.d.ts +25 -5
  60. package/dist/commands/teams.js +101 -39
  61. package/dist/commands/teams.js.map +1 -1
  62. package/dist/commands/workflows.d.ts +37 -0
  63. package/dist/commands/workflows.js +589 -0
  64. package/dist/commands/workflows.js.map +1 -0
  65. package/dist/connect-fields.d.ts +22 -0
  66. package/dist/connect-fields.js +115 -0
  67. package/dist/connect-fields.js.map +1 -0
  68. package/dist/context.d.ts +12 -0
  69. package/dist/context.js +32 -9
  70. package/dist/context.js.map +1 -1
  71. package/dist/index.d.ts +13 -0
  72. package/dist/index.js +367 -133
  73. package/dist/index.js.map +1 -1
  74. package/dist/invocation.d.ts +31 -0
  75. package/dist/invocation.js +58 -0
  76. package/dist/invocation.js.map +1 -0
  77. package/dist/runs-api.js +8 -3
  78. package/dist/runs-api.js.map +1 -1
  79. package/dist/safe-fs.d.ts +18 -0
  80. package/dist/safe-fs.js +57 -0
  81. package/dist/safe-fs.js.map +1 -1
  82. package/dist/secret-form.d.ts +48 -0
  83. package/dist/secret-form.js +212 -0
  84. package/dist/secret-form.js.map +1 -0
  85. package/dist/secret-input.d.ts +96 -0
  86. package/dist/secret-input.js +299 -0
  87. package/dist/secret-input.js.map +1 -0
  88. package/dist/team-connect-api.d.ts +78 -3
  89. package/dist/team-connect-api.js +17 -2
  90. package/dist/team-connect-api.js.map +1 -1
  91. package/dist/team-lifecycle-api.d.ts +435 -0
  92. package/dist/team-lifecycle-api.js +301 -0
  93. package/dist/team-lifecycle-api.js.map +1 -0
  94. package/dist/teams-api.d.ts +13 -1
  95. package/dist/teams-api.js +90 -31
  96. package/dist/teams-api.js.map +1 -1
  97. package/dist/unknown-command.d.ts +18 -0
  98. package/dist/unknown-command.js +50 -0
  99. package/dist/unknown-command.js.map +1 -0
  100. package/dist/workflows-api.d.ts +69 -0
  101. package/dist/workflows-api.js +182 -0
  102. package/dist/workflows-api.js.map +1 -0
  103. package/package.json +3 -3
  104. package/templates/AGENTS.md.tmpl +6 -4
  105. package/templates/claude-skill.md.tmpl +4 -1
  106. package/templates/package.json.tmpl +4 -4
  107. package/templates/skills/build-agent/SKILL.md +56 -22
  108. package/templates/skills/build-agent/references/agent-json.md +4 -1
  109. package/templates/skills/build-agent/references/app-setup.md +14 -7
  110. package/templates/skills/build-agent/references/rollout.md +40 -20
  111. package/templates/skills/build-agent/references/set-up-an-integration.md +46 -29
  112. package/templates/skills/build-agent/references/triggers.md +4 -3
  113. package/templates/skills/build-integration/references/widget-design.md +1 -1
  114. package/templates/skills/ekanos-platform/SKILL.md +77 -23
  115. package/templates/skills/polish-widgets/references/patterns.md +3 -3
@@ -0,0 +1,589 @@
1
+ import { TeamWorkflowRunStatusSchema, } from '@ekanos/integration-schema';
2
+ import * as fs from 'node:fs';
3
+ import * as path from 'node:path';
4
+ import { loginHint } from '../auth/host-trust.js';
5
+ import { refreshStoredSession, requireSession, resolveAuthEnvironment, } from '../auth/session.js';
6
+ import { authRequiredError, usageError, validationError } from '../errors.js';
7
+ import { writePulledFiles } from '../pull-write.js';
8
+ import { resolveSourceTarget } from '../source-target.js';
9
+ import { applyWorkflow, cancelWorkflowRun, decideWorkflowApproval, listWorkflowApprovals, listWorkflowRuns, listWorkflows, setWorkflowStatus, showWorkflow, showWorkflowRun, startWorkflowRun, } from '../workflows-api.js';
10
+ import { promptYesNo } from './publish.js';
11
+ export const WORKFLOWS_SUBCOMMANDS = [
12
+ 'list',
13
+ 'show',
14
+ 'pull',
15
+ 'apply',
16
+ 'enable',
17
+ 'disable',
18
+ 'run',
19
+ 'runs',
20
+ 'approvals',
21
+ ];
22
+ const RUNS_VERBS = ['list', 'show', 'cancel'];
23
+ const APPROVALS_VERBS = ['list', 'approve', 'reject'];
24
+ /** The file `pull` writes and a directory given to `apply` is read from. */
25
+ export const WORKFLOW_FILE = 'workflow.json';
26
+ const FOLLOW_INTERVAL_MS = 2000;
27
+ const FOLLOW_TIMEOUT_MS = 15 * 60 * 1000;
28
+ /**
29
+ * `workflows` — a team's workflows, for its Source admin: the same
30
+ * definitions the visual editor saves, saved through the same checks
31
+ * (shape, effect pinning, Custom Code review, write-behind-gate), activated
32
+ * through the same gate, run and approved through the same paths. A
33
+ * member's personal workflow is theirs alone and never reachable here.
34
+ */
35
+ export async function runWorkflows(ctx, args) {
36
+ const [rawSubcommand, operand, third, ...extra] = args.positionals;
37
+ const subcommand = WORKFLOWS_SUBCOMMANDS.find((name) => name === rawSubcommand);
38
+ if (!subcommand) {
39
+ throw usageError(rawSubcommand
40
+ ? `Unknown workflows subcommand "${rawSubcommand}".`
41
+ : 'workflows needs a subcommand.', `Run one of: ${WORKFLOWS_SUBCOMMANDS.map((name) => `ekanos workflows ${name}`).join(', ')} — each with --team <slug>.`);
42
+ }
43
+ rejectExtra(extra);
44
+ const team = requireTeam(args.team);
45
+ switch (subcommand) {
46
+ case 'list':
47
+ rejectExtra([operand, third]);
48
+ return list(ctx, args, team);
49
+ case 'show':
50
+ case 'pull':
51
+ case 'enable':
52
+ case 'disable':
53
+ case 'run': {
54
+ rejectExtra([third]);
55
+ const workflow = requireOperand(operand, 'workflow (its id or name)', subcommand);
56
+ if (subcommand === 'show')
57
+ return show(ctx, args, team, workflow);
58
+ if (subcommand === 'pull')
59
+ return pull(ctx, args, team, workflow);
60
+ if (subcommand === 'run')
61
+ return run(ctx, args, team, workflow);
62
+ return setStatus(ctx, args, team, workflow, subcommand === 'enable' ? 'active' : 'paused');
63
+ }
64
+ case 'apply': {
65
+ rejectExtra([third]);
66
+ const file = requireOperand(operand, 'file or folder', 'apply');
67
+ return apply(ctx, args, team, file);
68
+ }
69
+ case 'runs':
70
+ return runs(ctx, args, team, operand, third);
71
+ case 'approvals':
72
+ return approvals(ctx, args, team, operand, third);
73
+ }
74
+ }
75
+ async function list(ctx, args, team) {
76
+ const { env, client, source } = await connect(ctx, args);
77
+ const listed = await client.call((token) => listWorkflows(env.host, token, { source, team, all: args.all }));
78
+ return ctx.succeed(Object.assign({ host: env.host }, listed), listed.workflows.length === 0
79
+ ? ctx.ui.result({
80
+ title: `No workflows on ${listed.team.slug}`,
81
+ tone: 'warn',
82
+ notes: [
83
+ 'Write one as JSON (the editor\'s format) and "ekanos workflows apply <file> --team <slug>".',
84
+ ],
85
+ })
86
+ : ctx.ui.table(['ID', 'NAME', 'STATUS', 'TRIGGER', 'LAST RUN'], listed.workflows.map((workflow) => { var _a; return [
87
+ workflow.id,
88
+ workflow.name,
89
+ workflow.status,
90
+ (_a = workflow.trigger) !== null && _a !== void 0 ? _a : '?',
91
+ lastRunLabel(workflow),
92
+ ]; })));
93
+ }
94
+ async function show(ctx, args, team, workflow) {
95
+ var _a;
96
+ const { env, client, source } = await connect(ctx, args);
97
+ const detail = await client.call((token) => showWorkflow(env.host, token, { source, team, workflow }));
98
+ const { workflow: shown } = detail;
99
+ const rows = [
100
+ ['Id', shown.id],
101
+ ['Status', shown.status],
102
+ ['Trigger', (_a = shown.trigger) !== null && _a !== void 0 ? _a : 'unreadable definition'],
103
+ ['Updated', shown.updatedAt],
104
+ ...(detail.codeSteps.length > 0
105
+ ? [
106
+ [
107
+ 'Custom Code',
108
+ detail.codeSteps.map((step) => [
109
+ `${step.stepId}: ${CODE_STATUS_WORDS[step.reviewStatus]}`,
110
+ step.reviewSummary ? ` — ${step.reviewSummary}` : '',
111
+ ].join('')),
112
+ ],
113
+ ]
114
+ : []),
115
+ [
116
+ 'Recent runs',
117
+ detail.recentRuns.length === 0
118
+ ? 'none'
119
+ : detail.recentRuns.map((run) => runLine(run)),
120
+ ],
121
+ ];
122
+ return ctx.succeed(Object.assign({ host: env.host }, detail), ctx.ui.result({
123
+ title: shown.name,
124
+ tone: detail.codeSteps.length > 0 ? 'warn' : 'ok',
125
+ rows,
126
+ notes: [
127
+ `The definition: --json, or "ekanos workflows pull ${shown.id} --team ${team}".`,
128
+ ],
129
+ }));
130
+ }
131
+ const CODE_STATUS_WORDS = {
132
+ pending: 'security review not finished',
133
+ rejected: 'rejected by security review',
134
+ missing: 'not reviewed yet (save it again)',
135
+ needs_approval: 'reviewed; needs your approval of what it reaches',
136
+ };
137
+ async function pull(ctx, args, team, workflow) {
138
+ var _a;
139
+ const { env, client, source } = await connect(ctx, args);
140
+ const detail = await client.call((token) => showWorkflow(env.host, token, { source, team, workflow }));
141
+ const outDir = path.resolve(args.dir, (_a = args.out) !== null && _a !== void 0 ? _a : path.join('workflows', folderName(detail.workflow.name)));
142
+ const written = writePulledFiles({
143
+ projectDir: args.dir,
144
+ outDir,
145
+ files: [
146
+ {
147
+ path: WORKFLOW_FILE,
148
+ contents: `${JSON.stringify(detail.workflow.dsl, null, 2)}\n`,
149
+ },
150
+ ],
151
+ force: args.force,
152
+ rerun: `ekanos workflows pull ${workflow} --team ${team}`,
153
+ });
154
+ const relative = path.relative(args.dir, outDir) || '.';
155
+ return ctx.succeed({
156
+ host: env.host,
157
+ team: detail.team,
158
+ workflow: { id: detail.workflow.id, name: detail.workflow.name },
159
+ files: written,
160
+ }, ctx.ui.result({
161
+ title: `Pulled ${detail.workflow.name}`,
162
+ rows: written.map((file) => ['Wrote', file]),
163
+ notes: [
164
+ `Edit, then "ekanos workflows apply ${relative} --team ${team} --update ${detail.workflow.id}".`,
165
+ ],
166
+ }));
167
+ }
168
+ async function apply(ctx, args, team, target) {
169
+ const dsl = readDefinition(args.dir, target);
170
+ const { env, client, source } = await connect(ctx, args);
171
+ const applied = await client.call((token) => applyWorkflow(env.host, token, { source, team }, Object.assign({ dsl }, (args.update ? { update: args.update } : {}))));
172
+ const { workflow } = applied;
173
+ const blocking = applied.codeReviews.filter((review) => review.reviewStatus !== 'approved');
174
+ return ctx.succeed(Object.assign({ host: env.host }, applied), ctx.ui.result({
175
+ title: `${applied.created ? 'Created' : 'Updated'} ${workflow.name} on ${applied.team.slug}`,
176
+ tone: applied.warnings.length > 0 || blocking.length > 0 ? 'warn' : 'ok',
177
+ rows: [
178
+ ['Id', workflow.id],
179
+ ['Status', workflow.status],
180
+ ...(applied.codeReviews.length > 0
181
+ ? [
182
+ [
183
+ 'Custom Code',
184
+ applied.codeReviews.map((review) => `${review.stepId}: review ${review.reviewStatus}${review.reviewSummary ? ` — ${review.reviewSummary}` : ''}`),
185
+ ],
186
+ ]
187
+ : []),
188
+ ...(applied.warnings.length > 0
189
+ ? [['Warnings', applied.warnings]]
190
+ : []),
191
+ ],
192
+ notes: [
193
+ applied.warnings.length > 0
194
+ ? 'Saved, but activation refuses until the warnings are fixed.'
195
+ : workflow.status === 'draft'
196
+ ? `A draft until you turn it on: "ekanos workflows enable ${workflow.id} --team ${team}" ("workflows run ${workflow.id} --dry-run" rehearses it first).`
197
+ : `It stays ${workflow.status}.`,
198
+ ],
199
+ }));
200
+ }
201
+ async function setStatus(ctx, args, team, workflow, status) {
202
+ var _a, _b, _c;
203
+ const { env, client, source } = await connect(ctx, args);
204
+ const send = (confirmAccess) => client.call((token) => setWorkflowStatus(env.host, token, { source, team, workflow }, { status, confirmAccess }), { allowNeedsAccess: true });
205
+ let changed = await send(args.confirmAccess);
206
+ if ('needsAccess' in changed) {
207
+ const lines = changed.needsAccess.map(accessLine);
208
+ if (ctx.jsonMode || !((_a = args.stdinIsTTY) !== null && _a !== void 0 ? _a : process.stdin.isTTY === true)) {
209
+ throw validationError([
210
+ `Turning on ${workflow} needs your approval of what its Custom Code reaches:`,
211
+ ...lines.map((line) => ` ${line}`),
212
+ ].join('\n'), `Nothing changed. If you approve it, as yourself: ekanos workflows enable ${workflow} --team ${team} --confirm-access`);
213
+ }
214
+ for (const line of lines)
215
+ ctx.log(` ${line}`);
216
+ const confirmed = await ((_b = args.confirm) !== null && _b !== void 0 ? _b : promptYesNo)('Approve this access, as yourself, and turn the workflow on? [y/N]');
217
+ if (!confirmed) {
218
+ throw validationError('The Custom Code access was not approved.', 'Nothing changed.');
219
+ }
220
+ changed = await send(true);
221
+ if ('needsAccess' in changed) {
222
+ throw validationError('The host still asks for approval of the Custom Code access.', 'Nothing changed. Run "ekanos workflows show" for what it reaches.');
223
+ }
224
+ }
225
+ return ctx.succeed(Object.assign({ host: env.host }, changed), ctx.ui.result({
226
+ title: `${status === 'active' ? 'Turned on' : 'Paused'} ${changed.workflow.name} on ${changed.team.slug}`,
227
+ tone: status === 'active' ? 'ok' : 'warn',
228
+ rows: [
229
+ ['Status', `${changed.from} → ${changed.workflow.status}`],
230
+ ['Trigger', (_c = changed.workflow.trigger) !== null && _c !== void 0 ? _c : '?'],
231
+ ],
232
+ notes: status === 'active'
233
+ ? [
234
+ 'Its trigger now starts it; every write still waits on its approval gate.',
235
+ ]
236
+ : ['Its trigger no longer starts it. Runs already going continue.'],
237
+ }));
238
+ }
239
+ function accessLine(access) {
240
+ const parts = [
241
+ access.effect === 'write' ? 'writes' : 'reads',
242
+ ...(access.egressHosts.length > 0
243
+ ? [`reaches ${access.egressHosts.join(', ')}`]
244
+ : []),
245
+ ...(access.tools.length > 0 ? [`calls ${access.tools.join(', ')}`] : []),
246
+ ];
247
+ return `${access.stepId}: ${parts.join('; ')}${access.reviewSummary ? ` — review: ${access.reviewSummary}` : ''}`;
248
+ }
249
+ async function run(ctx, args, team, workflow) {
250
+ if (args.input && !args.dryRun) {
251
+ throw usageError('--input is a sample trigger payload for a dry run.', `A live run takes what its trigger gives it. Rehearse with it: ekanos workflows run ${workflow} --team ${team} --dry-run --input ${args.input}`);
252
+ }
253
+ const input = args.input ? readSample(args.dir, args.input) : undefined;
254
+ const { env, client, source } = await connect(ctx, args);
255
+ const started = await client.call((token) => startWorkflowRun(env.host, token, { source, team, workflow }, Object.assign({ dryRun: args.dryRun }, (input ? { input } : {}))));
256
+ const note = `${started.dryRun ? 'Rehearsing' : 'Started'} ${started.workflow.name} on ${started.team.slug}: run ${started.runId}`;
257
+ if (!args.follow) {
258
+ return ctx.succeed(Object.assign({ host: env.host }, started), ctx.ui.result({
259
+ title: note,
260
+ notes: [
261
+ ...(started.dryRun
262
+ ? ['A dry run: writes are recorded, not sent, and gates pass.']
263
+ : []),
264
+ `Watch it: "ekanos workflows runs show ${started.runId} --team ${team}".`,
265
+ ],
266
+ }));
267
+ }
268
+ ctx.log(note);
269
+ const detail = await follow(ctx, args, () => client.call((token) => showWorkflowRun(env.host, token, {
270
+ source,
271
+ team,
272
+ runId: started.runId,
273
+ })));
274
+ return ctx.succeed(Object.assign({ host: env.host, started }, detail), runDetail(ctx.ui, detail));
275
+ }
276
+ /** Poll until the run ends or parks on a gate, logging each step it finishes. */
277
+ async function follow(ctx, args, read) {
278
+ var _a;
279
+ const sleep = (_a = args.sleep) !== null && _a !== void 0 ? _a : ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
280
+ const seen = new Set();
281
+ let lastStatus = '';
282
+ const deadline = Date.now() + FOLLOW_TIMEOUT_MS;
283
+ for (;;) {
284
+ const detail = await read();
285
+ for (const step of detail.run.stepsExecuted) {
286
+ if (seen.has(step))
287
+ continue;
288
+ seen.add(step);
289
+ ctx.log(` ✓ ${step}`);
290
+ }
291
+ if (detail.run.status !== lastStatus) {
292
+ lastStatus = detail.run.status;
293
+ ctx.log(` status: ${lastStatus}`);
294
+ }
295
+ if (detail.run.status !== 'running' || Date.now() > deadline) {
296
+ return detail;
297
+ }
298
+ await sleep(FOLLOW_INTERVAL_MS);
299
+ }
300
+ }
301
+ async function runs(ctx, args, team, rawVerb, operand) {
302
+ const verb = RUNS_VERBS.find((name) => name === rawVerb);
303
+ if (!verb) {
304
+ throw usageError(rawVerb
305
+ ? `Unknown workflows runs verb "${rawVerb}".`
306
+ : 'workflows runs needs a verb.', 'Run one of: ekanos workflows runs list --team <slug> [--workflow <id>] [--status <status>], ekanos workflows runs show <run-id> --team <slug>, ekanos workflows runs cancel <run-id> --team <slug> [--yes].');
307
+ }
308
+ if (verb === 'list') {
309
+ rejectExtra([operand]);
310
+ const status = parseStatus(args.status);
311
+ const { env, client, source } = await connect(ctx, args);
312
+ const listed = await client.call((token) => listWorkflowRuns(env.host, token, {
313
+ source,
314
+ team,
315
+ workflow: args.workflow,
316
+ status,
317
+ }));
318
+ return ctx.succeed(Object.assign({ host: env.host }, listed), listed.runs.length === 0
319
+ ? ctx.ui.result({
320
+ title: `No workflow runs on ${listed.team.slug}`,
321
+ tone: 'warn',
322
+ })
323
+ : ctx.ui.table(['RUN', 'WORKFLOW', 'STATUS', 'STARTED'], listed.runs.map((run) => [
324
+ run.id,
325
+ run.workflow.name,
326
+ `${run.status}${run.dryRun ? ' (dry run)' : ''}`,
327
+ run.startedAt.slice(0, 19).replace('T', ' '),
328
+ ])));
329
+ }
330
+ const runId = requireOperand(operand, 'run id', `runs ${verb}`);
331
+ if (verb === 'cancel')
332
+ await confirmCancel(ctx, args, runId, team);
333
+ const { env, client, source } = await connect(ctx, args);
334
+ if (verb === 'show') {
335
+ const detail = await client.call((token) => showWorkflowRun(env.host, token, { source, team, runId }));
336
+ return ctx.succeed(Object.assign({ host: env.host }, detail), runDetail(ctx.ui, detail));
337
+ }
338
+ const cancelled = await client.call((token) => cancelWorkflowRun(env.host, token, { source, team, runId }));
339
+ return ctx.succeed(Object.assign({ host: env.host }, cancelled), ctx.ui.result({
340
+ title: `Cancelled run ${cancelled.run.id} on ${cancelled.team.slug}`,
341
+ tone: 'warn',
342
+ rows: [
343
+ ['Workflow', cancelled.run.workflow.name],
344
+ ['Status', cancelled.run.status],
345
+ ],
346
+ notes: ['Its pending approvals expired.'],
347
+ }));
348
+ }
349
+ async function confirmCancel(ctx, args, runId, team) {
350
+ var _a, _b;
351
+ if (args.yes)
352
+ return;
353
+ if (ctx.jsonMode || !((_a = args.stdinIsTTY) !== null && _a !== void 0 ? _a : process.stdin.isTTY === true)) {
354
+ throw validationError(`Cancelling run ${runId} on ${team} needs confirmation.`, `It stops the run for good and expires its pending approvals. Pass --yes to confirm: ekanos workflows runs cancel ${runId} --team ${team} --yes`);
355
+ }
356
+ const confirmed = await ((_b = args.confirm) !== null && _b !== void 0 ? _b : promptYesNo)(`Stop run ${runId} on ${team} for good? [y/N]`);
357
+ if (!confirmed) {
358
+ throw validationError(`Cancelling run ${runId} was not confirmed.`, 'Nothing was changed.');
359
+ }
360
+ }
361
+ async function approvals(ctx, args, team, rawVerb, operand) {
362
+ const verb = APPROVALS_VERBS.find((name) => name === rawVerb);
363
+ if (!verb) {
364
+ throw usageError(rawVerb
365
+ ? `Unknown workflows approvals verb "${rawVerb}".`
366
+ : 'workflows approvals needs a verb.', 'Run one of: ekanos workflows approvals list --team <slug>, ekanos workflows approvals approve|reject <id> --team <slug> [--reason "…"].');
367
+ }
368
+ if (verb === 'list') {
369
+ rejectExtra([operand]);
370
+ const { env, client, source } = await connect(ctx, args);
371
+ const listed = await client.call((token) => listWorkflowApprovals(env.host, token, { source, team }));
372
+ return ctx.succeed(Object.assign({ host: env.host }, listed), listed.approvals.length === 0
373
+ ? ctx.ui.result({ title: `Nothing waiting on ${listed.team.slug}` })
374
+ : ctx.ui.result({
375
+ title: `${listed.approvals.length} waiting on ${listed.team.slug}`,
376
+ tone: 'warn',
377
+ rows: listed.approvals.map((approval) => [
378
+ approval.id,
379
+ approvalLines(approval),
380
+ ]),
381
+ notes: [
382
+ '"ekanos workflows approvals approve|reject <id> --team <slug>" decides one.',
383
+ ],
384
+ }));
385
+ }
386
+ const approvalId = requireOperand(operand, 'approval id', `approvals ${verb}`);
387
+ const { env, client, source } = await connect(ctx, args);
388
+ const decided = await client.call((token) => decideWorkflowApproval(env.host, token, { source, team, approvalId }, Object.assign({ decision: verb === 'reject' ? 'rejected' : 'approved' }, (args.reason ? { reason: args.reason } : {}))));
389
+ const { approval } = decided;
390
+ return ctx.succeed(Object.assign({ host: env.host }, decided), ctx.ui.result({
391
+ title: `${approval.status === 'rejected' ? 'Rejected' : 'Approved'} ${approval.stepId} in ${approval.workflow.name}`,
392
+ tone: approval.status === 'rejected' ? 'warn' : 'ok',
393
+ rows: [['Run', approval.runId]],
394
+ notes: [
395
+ `The run picks it up now: "ekanos workflows runs show ${approval.runId} --team ${team}".`,
396
+ ],
397
+ }));
398
+ }
399
+ function runDetail(ui, detail) {
400
+ const { run } = detail;
401
+ const pending = detail.approvals.filter((approval) => approval.status === 'pending');
402
+ const decisions = Object.entries(run.gateDecisions);
403
+ return ui.result({
404
+ title: `Run ${run.id}`,
405
+ tone: run.status === 'failed' || run.status === 'rejected'
406
+ ? 'err'
407
+ : run.status === 'completed'
408
+ ? 'ok'
409
+ : 'warn',
410
+ rows: [
411
+ ['Workflow', run.workflow.name],
412
+ ['Status', `${run.status}${run.dryRun ? ' (dry run)' : ''}`],
413
+ ['Started', run.startedAt],
414
+ ...(run.finishedAt ? [['Finished', run.finishedAt]] : []),
415
+ ...(run.error ? [['Error', run.error]] : []),
416
+ [
417
+ 'Steps',
418
+ run.stepsExecuted.length === 0 ? 'none yet' : run.stepsExecuted,
419
+ ],
420
+ ...(decisions.length > 0
421
+ ? [
422
+ [
423
+ 'Gates',
424
+ decisions.map(([step, decision]) => `${step}: ${decision}`),
425
+ ],
426
+ ]
427
+ : []),
428
+ ...(pending.length > 0
429
+ ? [
430
+ [
431
+ 'Waiting on',
432
+ pending.map((approval) => `${approval.id} ${approval.stepId}`),
433
+ ],
434
+ ]
435
+ : []),
436
+ ],
437
+ notes: pending.length > 0
438
+ ? [
439
+ `Decide: "ekanos workflows approvals approve|reject <id> --team ${detail.team.slug}".`,
440
+ ]
441
+ : ['The full record: --json.'],
442
+ });
443
+ }
444
+ function approvalLines(approval) {
445
+ return [
446
+ `${approval.stepId} · ${approval.workflow.name}`,
447
+ `run ${approval.runId}${approval.expiresAt ? ` · expires ${approval.expiresAt}` : ''}`,
448
+ clip(approval.summary),
449
+ ];
450
+ }
451
+ function runLine(run) {
452
+ return `${run.id} ${run.status}${run.dryRun ? ' (dry run)' : ''} ${run.startedAt.slice(0, 19).replace('T', ' ')}`;
453
+ }
454
+ function lastRunLabel(workflow) {
455
+ const run = workflow.lastRun;
456
+ if (!run)
457
+ return 'never';
458
+ return `${run.status}${run.dryRun ? ' (dry)' : ''} ${run.startedAt.slice(0, 16).replace('T', ' ')}`;
459
+ }
460
+ function clip(text, max = 200) {
461
+ const flat = text.replace(/\s+/g, ' ').trim();
462
+ return flat.length > max ? `${flat.slice(0, max - 1)}…` : flat;
463
+ }
464
+ /** A folder name for `pull`'s default `workflows/<name>`. */
465
+ function folderName(name) {
466
+ const slug = name
467
+ .toLowerCase()
468
+ .replace(/[^a-z0-9]+/g, '-')
469
+ .replace(/^-+|-+$/g, '')
470
+ .slice(0, 60);
471
+ return slug || 'workflow';
472
+ }
473
+ /** `apply <file|dir>`: the JSON the editor saves, from a file or `<dir>/workflow.json`. */
474
+ function readDefinition(dir, target) {
475
+ const resolved = path.resolve(dir, target);
476
+ let file = resolved;
477
+ try {
478
+ if (fs.statSync(resolved).isDirectory()) {
479
+ file = path.join(resolved, WORKFLOW_FILE);
480
+ }
481
+ }
482
+ catch (_a) {
483
+ throw usageError(`Cannot read ${resolved}.`, `Pass a workflow JSON file, or a folder holding ${WORKFLOW_FILE}.`);
484
+ }
485
+ let raw;
486
+ try {
487
+ raw = fs.readFileSync(file, 'utf8');
488
+ }
489
+ catch (_b) {
490
+ throw usageError(`Cannot read ${file}.`, `Pass a workflow JSON file, or a folder holding ${WORKFLOW_FILE}.`);
491
+ }
492
+ let parsed;
493
+ try {
494
+ parsed = JSON.parse(raw);
495
+ }
496
+ catch (_c) {
497
+ throw validationError(`${file} is not valid JSON.`, 'A workflow is the JSON the editor saves: { version, name, scope, trigger, steps, edges }.');
498
+ }
499
+ if (typeof parsed !== 'object' ||
500
+ parsed === null ||
501
+ Array.isArray(parsed) ||
502
+ typeof parsed.name !== 'string') {
503
+ throw validationError(`${file} is not a workflow definition.`, 'A workflow is the JSON the editor saves: { version, name, scope, trigger, steps, edges }. "ekanos workflows pull <id>" writes one.');
504
+ }
505
+ return parsed;
506
+ }
507
+ /** `--input`: a JSON object, the dry run's sample `trigger.*`. */
508
+ function readSample(dir, file) {
509
+ const resolved = path.resolve(dir, file);
510
+ let raw;
511
+ try {
512
+ raw = fs.readFileSync(resolved, 'utf8');
513
+ }
514
+ catch (_a) {
515
+ throw usageError(`Cannot read ${resolved}.`, 'Pass --input <file.json>.');
516
+ }
517
+ let parsed;
518
+ try {
519
+ parsed = JSON.parse(raw);
520
+ }
521
+ catch (_b) {
522
+ throw validationError(`${resolved} is not valid JSON.`, 'Fix the file.');
523
+ }
524
+ if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
525
+ throw validationError(`${resolved} is not a JSON object.`, 'The sample payload is read as trigger.*, so it is an object of fields.');
526
+ }
527
+ return parsed;
528
+ }
529
+ function parseStatus(raw) {
530
+ if (raw === undefined)
531
+ return undefined;
532
+ const parsed = TeamWorkflowRunStatusSchema.safeParse(raw.trim());
533
+ if (parsed.success)
534
+ return parsed.data;
535
+ throw usageError(`--status "${raw}" is not a workflow run status.`, `Use one of: ${TeamWorkflowRunStatusSchema.options.join(', ')}.`);
536
+ }
537
+ function requireTeam(value) {
538
+ const slug = value === null || value === void 0 ? void 0 : value.trim().toLowerCase();
539
+ if (slug && /^[a-z0-9][a-z0-9-]*$/.test(slug))
540
+ return slug;
541
+ throw usageError(value
542
+ ? `"${value}" is not a team slug.`
543
+ : 'Name the team with --team <slug>.', '"ekanos teams list" lists the teams on this Source.');
544
+ }
545
+ function requireOperand(value, what, command) {
546
+ const trimmed = value === null || value === void 0 ? void 0 : value.trim();
547
+ if (trimmed)
548
+ return trimmed;
549
+ throw usageError(`Name the ${what}.`, `e.g. "ekanos workflows ${command} <…> --team <slug>". See "ekanos workflows --help".`);
550
+ }
551
+ function rejectExtra(extra) {
552
+ const first = extra.find((value) => value !== undefined);
553
+ if (first !== undefined) {
554
+ throw usageError(`Unexpected argument "${first}".`, 'See "ekanos workflows --help".');
555
+ }
556
+ }
557
+ async function connect(ctx, args) {
558
+ const env = resolveAuthEnvironment(ctx, args.host, args.env, {
559
+ projectDir: args.dir,
560
+ });
561
+ const resolved = await resolveSourceTarget(env, requireSession(env), args);
562
+ return {
563
+ env,
564
+ client: new WorkflowsClient(env, resolved.session),
565
+ source: resolved.source,
566
+ };
567
+ }
568
+ class WorkflowsClient {
569
+ constructor(env, session) {
570
+ this.env = env;
571
+ this.session = session;
572
+ }
573
+ async call(send, options) {
574
+ let result = await send(this.session.accessToken);
575
+ if (result.status === 'unauthorized') {
576
+ this.session = await refreshStoredSession(this.env, this.session);
577
+ result = await send(this.session.accessToken);
578
+ }
579
+ if (result.status === 'ok')
580
+ return result.data;
581
+ if (result.status === 'needs-access') {
582
+ if (options === null || options === void 0 ? void 0 : options.allowNeedsAccess)
583
+ return { needsAccess: result.codeAccess };
584
+ throw validationError(result.error, 'Run "ekanos workflows enable <id> --team <slug>" to review and approve it.');
585
+ }
586
+ throw authRequiredError(`The session for ${this.env.host} could not authorize the call.`, loginHint(this.env));
587
+ }
588
+ }
589
+ //# sourceMappingURL=workflows.js.map