akm-cli 0.4.1 → 0.5.0-rc2

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.
@@ -0,0 +1,364 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import fs from "node:fs";
3
+ import { loadConfig } from "./config";
4
+ import { closeDatabase, openDatabase } from "./db";
5
+ import { NotFoundError, UsageError } from "./errors";
6
+ import { resolveSourcesForOrigin } from "./origin-resolve";
7
+ import { getDbPath } from "./paths";
8
+ import { resolveStashSources } from "./search-source";
9
+ import { parseAssetRef } from "./stash-ref";
10
+ import { resolveAssetPath } from "./stash-resolve";
11
+ import { closeWorkflowDatabase, openWorkflowDatabase } from "./workflow-db";
12
+ import { parseWorkflowMarkdown, WorkflowValidationError } from "./workflow-markdown";
13
+ export async function startWorkflowRun(ref, params = {}) {
14
+ const asset = await loadWorkflowAsset(ref);
15
+ const workflowDb = openWorkflowDatabase();
16
+ try {
17
+ const now = new Date().toISOString();
18
+ const runId = randomUUID();
19
+ const currentStepId = asset.steps[0]?.id ?? null;
20
+ const workflowEntryId = resolveWorkflowEntryId(asset.sourcePath, asset.ref);
21
+ workflowDb.transaction(() => {
22
+ workflowDb
23
+ .prepare(`INSERT INTO workflow_runs (
24
+ id, workflow_ref, workflow_entry_id, workflow_title, status, params_json, current_step_id, created_at, updated_at
25
+ ) VALUES (?, ?, ?, ?, 'active', ?, ?, ?, ?)`)
26
+ .run(runId, asset.ref, workflowEntryId, asset.title, JSON.stringify(params), currentStepId, now, now);
27
+ const insertStep = workflowDb.prepare(`INSERT INTO workflow_run_steps (
28
+ run_id, step_id, step_title, instructions, completion_json, sequence_index, status
29
+ ) VALUES (?, ?, ?, ?, ?, ?, 'pending')`);
30
+ for (const step of asset.steps) {
31
+ insertStep.run(runId, step.id, step.title, step.instructions, step.completionCriteria ? JSON.stringify(step.completionCriteria) : null, step.sequenceIndex ?? 0);
32
+ }
33
+ })();
34
+ return getWorkflowStatus(runId);
35
+ }
36
+ finally {
37
+ closeWorkflowDatabase(workflowDb);
38
+ }
39
+ }
40
+ export function getWorkflowStatus(runId) {
41
+ const workflowDb = openWorkflowDatabase();
42
+ try {
43
+ const run = readWorkflowRun(workflowDb, runId);
44
+ const steps = readWorkflowRunSteps(workflowDb, run.id);
45
+ return buildWorkflowRunDetail(run, steps);
46
+ }
47
+ finally {
48
+ closeWorkflowDatabase(workflowDb);
49
+ }
50
+ }
51
+ export function listWorkflowRuns(input) {
52
+ const workflowDb = openWorkflowDatabase();
53
+ try {
54
+ const filters = [];
55
+ const params = [];
56
+ if (input?.workflowRef) {
57
+ const parsed = parseAssetRef(input.workflowRef);
58
+ if (parsed.type !== "workflow") {
59
+ throw new UsageError(`Expected a workflow ref (workflow:<name>), got "${input.workflowRef}".`);
60
+ }
61
+ filters.push("workflow_ref = ?");
62
+ params.push(`${parsed.origin ? `${parsed.origin}//` : ""}workflow:${parsed.name}`);
63
+ }
64
+ if (input?.activeOnly) {
65
+ filters.push("status IN ('active', 'blocked')");
66
+ }
67
+ const where = filters.length > 0 ? `WHERE ${filters.join(" AND ")}` : "";
68
+ const rows = workflowDb
69
+ .prepare(`SELECT * FROM workflow_runs ${where} ORDER BY updated_at DESC, created_at DESC`)
70
+ .all(...params);
71
+ return { runs: rows.map(toWorkflowRunSummary) };
72
+ }
73
+ finally {
74
+ closeWorkflowDatabase(workflowDb);
75
+ }
76
+ }
77
+ export async function getNextWorkflowStep(specifier, params) {
78
+ const workflowDb = openWorkflowDatabase();
79
+ try {
80
+ const { run, autoStarted } = await resolveRunSpecifier(workflowDb, specifier, params);
81
+ const steps = readWorkflowRunSteps(workflowDb, run.id);
82
+ const currentStep = resolveCurrentStep(run, steps);
83
+ const done = run.status === "completed" ? true : undefined;
84
+ return {
85
+ run: toWorkflowRunSummary(run),
86
+ workflow: {
87
+ ref: run.workflow_ref,
88
+ title: run.workflow_title,
89
+ steps: steps.map(toWorkflowRunStepState),
90
+ },
91
+ step: currentStep ? toWorkflowRunStepState(currentStep) : null,
92
+ ...(done ? { done } : {}),
93
+ ...(autoStarted ? { autoStarted } : {}),
94
+ };
95
+ }
96
+ finally {
97
+ closeWorkflowDatabase(workflowDb);
98
+ }
99
+ }
100
+ export function resumeWorkflowRun(runId) {
101
+ const workflowDb = openWorkflowDatabase();
102
+ try {
103
+ const run = readWorkflowRun(workflowDb, runId);
104
+ if (run.status === "completed") {
105
+ throw new UsageError(`Workflow run ${run.id} is already completed and cannot be resumed.`);
106
+ }
107
+ if (run.status === "active") {
108
+ const steps = readWorkflowRunSteps(workflowDb, run.id);
109
+ return buildWorkflowRunDetail(run, steps);
110
+ }
111
+ // blocked or failed → flip back to active
112
+ const now = new Date().toISOString();
113
+ workflowDb.prepare("UPDATE workflow_runs SET status = 'active', updated_at = ? WHERE id = ?").run(now, run.id);
114
+ const updated = { ...run, status: "active", updated_at: now };
115
+ const steps = readWorkflowRunSteps(workflowDb, run.id);
116
+ return buildWorkflowRunDetail(updated, steps);
117
+ }
118
+ finally {
119
+ closeWorkflowDatabase(workflowDb);
120
+ }
121
+ }
122
+ export function completeWorkflowStep(input) {
123
+ const workflowDb = openWorkflowDatabase();
124
+ try {
125
+ let updatedRun;
126
+ let refreshedSteps = [];
127
+ workflowDb.transaction(() => {
128
+ const run = readWorkflowRun(workflowDb, input.runId);
129
+ if (run.status !== "active") {
130
+ throw new UsageError(`Workflow run ${run.id} is ${run.status} and cannot be updated.`);
131
+ }
132
+ const existing = workflowDb
133
+ .prepare("SELECT * FROM workflow_run_steps WHERE run_id = ? AND step_id = ?")
134
+ .get(run.id, input.stepId);
135
+ if (!existing) {
136
+ throw new NotFoundError(`Step "${input.stepId}" was not found in workflow run ${run.id}.`);
137
+ }
138
+ if (existing.status !== "pending") {
139
+ throw new UsageError(`Step "${input.stepId}" is already ${existing.status} in workflow run ${run.id}.`);
140
+ }
141
+ if (run.current_step_id !== existing.step_id) {
142
+ throw new UsageError(`Step "${input.stepId}" is not the current step for workflow run ${run.id}. Complete "${run.current_step_id}" first.`);
143
+ }
144
+ const completedAt = new Date().toISOString();
145
+ workflowDb
146
+ .prepare(`UPDATE workflow_run_steps
147
+ SET status = ?, notes = ?, evidence_json = ?, completed_at = ?
148
+ WHERE run_id = ? AND step_id = ?`)
149
+ .run(input.status, input.notes?.trim() || null, input.evidence ? JSON.stringify(input.evidence) : null, completedAt, run.id, input.stepId);
150
+ refreshedSteps = readWorkflowRunSteps(workflowDb, run.id);
151
+ const state = deriveRunState(refreshedSteps);
152
+ workflowDb
153
+ .prepare(`UPDATE workflow_runs
154
+ SET status = ?, current_step_id = ?, updated_at = ?, completed_at = ?
155
+ WHERE id = ?`)
156
+ .run(state.status, state.currentStepId, completedAt, state.completedAt, run.id);
157
+ updatedRun = {
158
+ ...run,
159
+ status: state.status,
160
+ current_step_id: state.currentStepId,
161
+ updated_at: completedAt,
162
+ completed_at: state.completedAt,
163
+ };
164
+ })();
165
+ return buildWorkflowRunDetail(updatedRun, refreshedSteps);
166
+ }
167
+ finally {
168
+ closeWorkflowDatabase(workflowDb);
169
+ }
170
+ }
171
+ async function resolveRunSpecifier(db, specifier, params) {
172
+ const explicitRun = db.prepare("SELECT * FROM workflow_runs WHERE id = ?").get(specifier);
173
+ if (explicitRun) {
174
+ if (params && Object.keys(params).length > 0) {
175
+ throw new UsageError(`--params can only be used when starting a new run from a workflow ref, not with an existing run id ("${specifier}")`);
176
+ }
177
+ return { run: explicitRun, autoStarted: false };
178
+ }
179
+ const parsed = parseAssetRef(specifier);
180
+ if (parsed.type !== "workflow") {
181
+ throw new UsageError(`Expected a workflow ref or workflow run id, got "${specifier}".`);
182
+ }
183
+ const ref = `${parsed.origin ? `${parsed.origin}//` : ""}workflow:${parsed.name}`;
184
+ const active = db
185
+ .prepare("SELECT * FROM workflow_runs WHERE workflow_ref = ? AND status = 'active' ORDER BY updated_at DESC LIMIT 1")
186
+ .get(ref);
187
+ if (active) {
188
+ if (params && Object.keys(params).length > 0) {
189
+ throw new UsageError(`--params can only be set on a new run; ${ref} already has an active run`);
190
+ }
191
+ return { run: active, autoStarted: false };
192
+ }
193
+ const started = await startWorkflowRun(ref, params ?? {});
194
+ return { run: readWorkflowRun(db, started.run.id), autoStarted: true };
195
+ }
196
+ async function loadWorkflowAsset(ref) {
197
+ const parsed = parseAssetRef(ref);
198
+ if (parsed.type !== "workflow") {
199
+ throw new UsageError(`Expected a workflow ref (workflow:<name>), got "${ref}".`);
200
+ }
201
+ const config = loadConfig();
202
+ const allSources = resolveStashSources(undefined, config);
203
+ const searchSources = resolveSourcesForOrigin(parsed.origin, allSources);
204
+ let assetPath;
205
+ let sourcePath;
206
+ for (const source of searchSources) {
207
+ try {
208
+ assetPath = await resolveAssetPath(source.path, "workflow", parsed.name);
209
+ sourcePath = source.path;
210
+ break;
211
+ }
212
+ catch {
213
+ /* continue */
214
+ }
215
+ }
216
+ if (!assetPath) {
217
+ throw new NotFoundError(`Workflow not found for ref: workflow:${parsed.name}`);
218
+ }
219
+ const content = fs.readFileSync(assetPath, "utf8");
220
+ const workflow = parseWorkflowDocument(content);
221
+ return {
222
+ ref: `${parsed.origin ? `${parsed.origin}//` : ""}workflow:${parsed.name}`,
223
+ path: assetPath,
224
+ sourcePath: sourcePath ?? loadConfig().stashDir ?? assetPath,
225
+ title: workflow.title,
226
+ ...(workflow.parameters ? { parameters: workflow.parameters } : {}),
227
+ steps: workflow.steps,
228
+ };
229
+ }
230
+ function resolveWorkflowEntryId(sourcePath, ref) {
231
+ const dbPath = getDbPath();
232
+ if (!fs.existsSync(dbPath))
233
+ return null;
234
+ const db = openDatabase(dbPath);
235
+ try {
236
+ const parsed = parseAssetRef(ref);
237
+ const entryKey = `${sourcePath}:${parsed.type}:${parsed.name}`;
238
+ const row = db
239
+ .prepare(`SELECT id
240
+ FROM entries
241
+ WHERE entry_type = 'workflow'
242
+ AND entry_key = ?
243
+ LIMIT 1`)
244
+ .get(entryKey);
245
+ return row?.id ?? null;
246
+ }
247
+ finally {
248
+ closeDatabase(db);
249
+ }
250
+ }
251
+ function readWorkflowRun(db, runId) {
252
+ const run = db.prepare("SELECT * FROM workflow_runs WHERE id = ?").get(runId);
253
+ if (!run) {
254
+ throw new NotFoundError(`Workflow run not found: ${runId}`);
255
+ }
256
+ return run;
257
+ }
258
+ function readWorkflowRunSteps(db, runId) {
259
+ return db
260
+ .prepare("SELECT * FROM workflow_run_steps WHERE run_id = ? ORDER BY sequence_index ASC")
261
+ .all(runId);
262
+ }
263
+ function buildWorkflowRunDetail(run, steps) {
264
+ return {
265
+ run: toWorkflowRunSummary(run),
266
+ workflow: {
267
+ ref: run.workflow_ref,
268
+ title: run.workflow_title,
269
+ steps: steps.map(toWorkflowRunStepState),
270
+ },
271
+ };
272
+ }
273
+ function toWorkflowRunSummary(run) {
274
+ return {
275
+ id: run.id,
276
+ workflowRef: run.workflow_ref,
277
+ workflowEntryId: run.workflow_entry_id,
278
+ workflowTitle: run.workflow_title,
279
+ status: run.status,
280
+ currentStepId: run.current_step_id,
281
+ createdAt: run.created_at,
282
+ updatedAt: run.updated_at,
283
+ completedAt: run.completed_at,
284
+ params: parseJsonObject(run.params_json),
285
+ };
286
+ }
287
+ function toWorkflowRunStepState(step) {
288
+ return {
289
+ id: step.step_id,
290
+ title: step.step_title,
291
+ instructions: step.instructions,
292
+ completionCriteria: parseJsonArray(step.completion_json),
293
+ sequenceIndex: step.sequence_index,
294
+ status: step.status,
295
+ notes: step.notes ?? undefined,
296
+ evidence: parseJsonObject(step.evidence_json),
297
+ completedAt: step.completed_at,
298
+ };
299
+ }
300
+ function resolveCurrentStep(run, steps) {
301
+ if (run.current_step_id) {
302
+ return steps.find((step) => step.step_id === run.current_step_id);
303
+ }
304
+ return steps.find((step) => step.status === "pending");
305
+ }
306
+ function deriveRunState(steps) {
307
+ const unresolved = steps.find((step) => step.status === "failed" || step.status === "blocked");
308
+ if (unresolved) {
309
+ return {
310
+ status: unresolved.status === "failed" ? "failed" : "blocked",
311
+ currentStepId: unresolved.step_id,
312
+ completedAt: null,
313
+ };
314
+ }
315
+ const pending = steps.find((step) => step.status === "pending");
316
+ if (pending) {
317
+ return { status: "active", currentStepId: pending.step_id, completedAt: null };
318
+ }
319
+ const completedAt = steps
320
+ .map((step) => step.completed_at)
321
+ .filter((value) => typeof value === "string")
322
+ .sort()
323
+ .at(-1);
324
+ return { status: "completed", currentStepId: null, completedAt: completedAt ?? null };
325
+ }
326
+ function parseJsonObject(value) {
327
+ if (!value)
328
+ return undefined;
329
+ try {
330
+ const parsed = JSON.parse(value);
331
+ if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) {
332
+ return parsed;
333
+ }
334
+ }
335
+ catch {
336
+ /* ignore corrupt data */
337
+ }
338
+ return undefined;
339
+ }
340
+ function parseJsonArray(value) {
341
+ if (!value)
342
+ return undefined;
343
+ try {
344
+ const parsed = JSON.parse(value);
345
+ if (Array.isArray(parsed)) {
346
+ return parsed.filter((item) => typeof item === "string");
347
+ }
348
+ }
349
+ catch {
350
+ /* ignore corrupt data */
351
+ }
352
+ return undefined;
353
+ }
354
+ function parseWorkflowDocument(content) {
355
+ try {
356
+ return parseWorkflowMarkdown(content);
357
+ }
358
+ catch (error) {
359
+ if (error instanceof WorkflowValidationError) {
360
+ throw new UsageError(error.message);
361
+ }
362
+ throw error;
363
+ }
364
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "akm-cli",
3
- "version": "0.4.1",
3
+ "version": "0.5.0-rc2",
4
4
  "type": "module",
5
5
  "description": "akm (Agent Kit Manager) — A package manager for AI agent skills, commands, tools, and knowledge. Works with Claude Code, OpenCode, Cursor, and any AI coding assistant.",
6
6
  "keywords": [
@@ -34,6 +34,7 @@
34
34
  "files": [
35
35
  "dist",
36
36
  "README.md",
37
+ "CHANGELOG.md",
37
38
  "LICENSE"
38
39
  ],
39
40
  "bin": {
@@ -70,6 +71,7 @@
70
71
  "dependencies": {
71
72
  "@clack/prompts": "^1.1.0",
72
73
  "citty": "^0.2.1",
74
+ "dotenv": "^17.4.2",
73
75
  "yaml": "^2.8.2"
74
76
  }
75
77
  }