@deksden-com/dd-flow-cli 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.
Files changed (35) hide show
  1. package/README.md +274 -0
  2. package/dist/cli/help.js +308 -0
  3. package/dist/cli/run-cli.js +945 -0
  4. package/dist/cli.js +4 -0
  5. package/dist/domain/contracts.js +57 -0
  6. package/dist/domain/entity-ids.js +47 -0
  7. package/dist/domain/flow-contract.js +233 -0
  8. package/dist/domain/validation.js +91 -0
  9. package/dist/protocol/local-files.js +141 -0
  10. package/dist/runtime/context.js +11 -0
  11. package/dist/schemas/code-stage-report.schema.json +181 -0
  12. package/dist/schemas/flow-run-index.schema.json +129 -0
  13. package/dist/schemas/mb-upgrade-review-data.schema.json +813 -0
  14. package/dist/schemas/memorybank-permissions-preflight.schema.json +154 -0
  15. package/dist/schemas/merge-stage-report.schema.json +135 -0
  16. package/dist/services/audit.js +19 -0
  17. package/dist/services/cleanup.js +310 -0
  18. package/dist/services/config.js +143 -0
  19. package/dist/services/dashboard.js +436 -0
  20. package/dist/services/hooks.js +929 -0
  21. package/dist/services/lanes.js +327 -0
  22. package/dist/services/memory-permissions.js +344 -0
  23. package/dist/services/merge-queue.js +333 -0
  24. package/dist/services/plans.js +149 -0
  25. package/dist/services/projects.js +286 -0
  26. package/dist/services/protocols.js +606 -0
  27. package/dist/services/runs.js +359 -0
  28. package/dist/services/schema-validation.js +185 -0
  29. package/dist/services/sessions.js +365 -0
  30. package/dist/services/worktrees.js +204 -0
  31. package/dist/shared/errors.js +14 -0
  32. package/dist/shared/json.js +17 -0
  33. package/dist/storage/database.js +325 -0
  34. package/dist/storage/paths.js +56 -0
  35. package/package.json +44 -0
@@ -0,0 +1,286 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { formatFullId, isFullEntityId, isShortEntityId, parseFullEntityId, slugFromRoot } from "../domain/entity-ids.js";
4
+ import { AppError } from "../shared/errors.js";
5
+ import { projectRuntimeRoot, resolveProjectRoot } from "../storage/paths.js";
6
+ import { appendAudit } from "./audit.js";
7
+ import { activeCodexSessionBindingsForProject, activeFlowSessionBindingsForProject, codexHomeProfilesForProject, codexHookEventsForProject, hookStatusForProject } from "./hooks.js";
8
+ import { dashboardMarkdownPath, globalDashboardMarkdownPath, readProjectConfig } from "./config.js";
9
+ export function registerProject(context, input) {
10
+ const root = resolveProjectRoot(input.root);
11
+ const existing = findProjectByRoot(context, root);
12
+ const now = context.now();
13
+ const project = existing ?? newProjectRecord(context, root, now);
14
+ if (existing) {
15
+ context.db.run("UPDATE projects SET status = 'active', updated_at = ? WHERE id = ?", [now, existing.id]);
16
+ }
17
+ else {
18
+ context.db.run("INSERT INTO projects (id, short_id, slug, root, status, state_root, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?)", [
19
+ project.id,
20
+ project.short_id,
21
+ project.slug,
22
+ project.root,
23
+ project.status,
24
+ project.state_root,
25
+ project.created_at,
26
+ project.updated_at
27
+ ]);
28
+ appendAudit(context, {
29
+ projectId: project.id,
30
+ eventType: "project.registered",
31
+ payload: { project_id: project.id, root }
32
+ });
33
+ }
34
+ return { ok: true, project: { ...project, status: "active", updated_at: now }, db_path: context.db.path };
35
+ }
36
+ export function archiveProject(context, input) {
37
+ const reason = input.reason.trim();
38
+ if (!reason) {
39
+ throw new AppError("validation", "project archive requires --reason", 2);
40
+ }
41
+ const project = input.root
42
+ ? findProjectByRoot(context, normalizeStoredRoot(input.root))
43
+ : input.idOrAlias
44
+ ? resolveSingleProject(context, input.idOrAlias)
45
+ : undefined;
46
+ if (!project) {
47
+ throw new AppError("not_found", "Project is not registered.", 1, { root: input.root, input: input.idOrAlias });
48
+ }
49
+ const now = context.now();
50
+ const result = context.db.run("UPDATE projects SET status = 'archived', updated_at = ? WHERE id = ? AND status <> 'archived'", [now, project.id]);
51
+ if (result.changes === 1) {
52
+ appendAudit(context, {
53
+ projectId: project.id,
54
+ eventType: "project.archived",
55
+ reason,
56
+ payload: { project_id: project.id, root: project.root }
57
+ });
58
+ }
59
+ return {
60
+ ok: true,
61
+ changed: result.changes === 1,
62
+ project: context.db.get("SELECT * FROM projects WHERE id = ?", [project.id]),
63
+ ...(result.changes === 1 ? {} : { skipped: true, reason: "already_archived" })
64
+ };
65
+ }
66
+ function normalizeStoredRoot(root) {
67
+ const absolute = path.resolve(root);
68
+ if (fs.existsSync(absolute)) {
69
+ return fs.realpathSync(absolute);
70
+ }
71
+ const parent = path.dirname(absolute);
72
+ if (fs.existsSync(parent)) {
73
+ return path.join(fs.realpathSync(parent), path.basename(absolute));
74
+ }
75
+ return absolute;
76
+ }
77
+ export function resolveProject(context, input) {
78
+ const candidates = resolveProjectCandidates(context, input.idOrAlias);
79
+ if (candidates.length === 1) {
80
+ return { ok: true, project: candidates[0], input: input.idOrAlias };
81
+ }
82
+ if (candidates.length > 1) {
83
+ throw new AppError("ambiguous_alias", `Project alias is ambiguous: ${input.idOrAlias}`, 1, {
84
+ input: input.idOrAlias,
85
+ candidates: candidates.map(projectSummary)
86
+ });
87
+ }
88
+ const details = { input: input.idOrAlias };
89
+ if (!isFullEntityId(input.idOrAlias) && !isShortEntityId(input.idOrAlias)) {
90
+ details.expected = "Use full id PRJ-NNN-slug, short alias PRJ-NNN, or --root for root-based commands.";
91
+ }
92
+ throw new AppError("not_found", `Project is not registered: ${input.idOrAlias}`, 1, details);
93
+ }
94
+ export function migrateProjectIds(context, input) {
95
+ const root = resolveProjectRoot(input.root);
96
+ const existing = findProjectByRoot(context, root);
97
+ if (!existing) {
98
+ throw new AppError("not_found", `Project is not registered: ${root}`, 1);
99
+ }
100
+ const typed = typedProjectMetadata(existing);
101
+ if (typed && existing.state_root) {
102
+ return { ok: true, action: "noop", apply: input.apply, project: existing, message: "Project already uses typed ids." };
103
+ }
104
+ const now = context.now();
105
+ const target = typed ? backfilledTypedProjectRecord(context, existing, typed, now) : newProjectRecord(context, root, now);
106
+ const oldId = existing.id;
107
+ const plan = {
108
+ old_project_id: oldId,
109
+ new_project_id: target.id,
110
+ short_id: target.short_id,
111
+ slug: target.slug,
112
+ state_root: target.state_root,
113
+ tables: projectReferenceTables()
114
+ };
115
+ if (!input.apply) {
116
+ return { ok: true, action: "dry_run", apply: false, plan };
117
+ }
118
+ const conflictingProject = context.db.get("SELECT * FROM projects WHERE id = ? AND root <> ?", [target.id, root]);
119
+ if (conflictingProject) {
120
+ throw new AppError("conflict", `Target project id already exists: ${target.id}`, 1, plan);
121
+ }
122
+ context.db.exec("PRAGMA foreign_keys = OFF");
123
+ context.db.exec("BEGIN IMMEDIATE");
124
+ try {
125
+ context.db.run("UPDATE projects SET id = ?, short_id = ?, slug = ?, state_root = ?, updated_at = ? WHERE id = ?", [target.id, target.short_id, target.slug, target.state_root, now, oldId]);
126
+ for (const table of projectReferenceTables()) {
127
+ context.db.run(`UPDATE ${table} SET project_id = ? WHERE project_id = ?`, [target.id, oldId]);
128
+ }
129
+ const foreignKeyErrors = context.db.all("PRAGMA foreign_key_check");
130
+ if (foreignKeyErrors.length > 0) {
131
+ throw new AppError("integrity_error", "Project id migration would leave invalid foreign keys.", 1, {
132
+ plan,
133
+ foreign_key_errors: foreignKeyErrors
134
+ });
135
+ }
136
+ context.db.exec("COMMIT");
137
+ context.db.exec("PRAGMA foreign_keys = ON");
138
+ }
139
+ catch (error) {
140
+ context.db.exec("ROLLBACK");
141
+ context.db.exec("PRAGMA foreign_keys = ON");
142
+ throw error;
143
+ }
144
+ appendAudit(context, {
145
+ projectId: target.id,
146
+ eventType: "project.id_migrated",
147
+ payload: plan
148
+ });
149
+ return { ok: true, action: "applied", apply: true, plan, project: findProjectByRoot(context, root) };
150
+ }
151
+ export function getProjectStatus(context, input) {
152
+ const root = resolveProjectRoot(input.root);
153
+ const project = findProjectByRoot(context, root);
154
+ if (!project) {
155
+ throw new AppError("not_found", `Project is not registered: ${root}`, 1);
156
+ }
157
+ const protocols = context.db.all(`SELECT id, status, stage, next_action, updated_at
158
+ FROM protocols
159
+ WHERE project_id = ?
160
+ ORDER BY updated_at DESC`, [project.id]);
161
+ const mergeQueue = context.db.all(`SELECT protocol_id, status, claimed_by_session_id, claimed_at, attempts_count, last_reason, completed_at,
162
+ created_at, updated_at
163
+ FROM merge_queue
164
+ WHERE project_id = ?
165
+ ORDER BY created_at ASC`, [project.id]);
166
+ const config = readProjectConfig(context, project.id);
167
+ const worktrees = context.db.all(`SELECT protocol_id, feature_branch, worktree_path, bootstrap_status, status, updated_at
168
+ FROM worktree_records
169
+ WHERE project_id = ?
170
+ ORDER BY updated_at DESC`, [project.id]);
171
+ return {
172
+ ok: true,
173
+ project,
174
+ config,
175
+ dashboard: {
176
+ project_markdown_path: dashboardMarkdownPath(project.root, config),
177
+ global_markdown_path: globalDashboardMarkdownPath(context, config),
178
+ auto_refresh: config.dashboard.auto_refresh,
179
+ project_enabled: config.dashboard.project,
180
+ global_enabled: config.dashboard.global,
181
+ cmux_mode: config.integrations.cmux.mode
182
+ },
183
+ protocols,
184
+ merge_queue: mergeQueue,
185
+ hook_status: hookStatusForProject(context, project.id),
186
+ codex_home_profiles: codexHomeProfilesForProject(context, project.id),
187
+ flow_sessions: activeFlowSessionBindingsForProject(context, project.id),
188
+ codex_session_bindings: activeCodexSessionBindingsForProject(context, project.id),
189
+ codex_hook_events: codexHookEventsForProject(context, project.id),
190
+ worktrees,
191
+ lane_status: {
192
+ lanes: context.db.all("SELECT * FROM lanes WHERE project_id = ? ORDER BY name ASC", [project.id]),
193
+ locks: context.db.all(`SELECT * FROM lane_locks WHERE project_id = ? ORDER BY updated_at DESC, id DESC LIMIT 50`, [project.id])
194
+ }
195
+ };
196
+ }
197
+ export function findProjectByRoot(context, root) {
198
+ return context.db.get("SELECT * FROM projects WHERE root = ?", [root]);
199
+ }
200
+ export function requireProjectByRoot(context, root) {
201
+ const project = findProjectByRoot(context, root);
202
+ if (!project) {
203
+ throw new AppError("not_found", `Project is not registered: ${root}`, 1);
204
+ }
205
+ return project;
206
+ }
207
+ function newProjectRecord(context, root, now) {
208
+ const slug = slugFromRoot(root);
209
+ const id = nextFullProjectId(context, slug);
210
+ const { shortId } = parseFullEntityId(id);
211
+ return {
212
+ id,
213
+ short_id: shortId,
214
+ slug,
215
+ root,
216
+ status: "active",
217
+ state_root: projectRuntimeRoot(context.ddFlowHome, id),
218
+ created_at: now,
219
+ updated_at: now
220
+ };
221
+ }
222
+ function backfilledTypedProjectRecord(context, project, typed, now) {
223
+ return {
224
+ ...project,
225
+ short_id: typed.shortId,
226
+ slug: typed.slug,
227
+ state_root: projectRuntimeRoot(context.ddFlowHome, project.id),
228
+ updated_at: now
229
+ };
230
+ }
231
+ function nextFullProjectId(context, slug) {
232
+ const rows = context.db.all("SELECT id FROM projects WHERE id LIKE 'PRJ-%'");
233
+ const used = rows
234
+ .map((row) => /^PRJ-(\d{3})-/.exec(row.id)?.[1])
235
+ .filter((value) => Boolean(value))
236
+ .map((value) => Number(value));
237
+ const next = Math.max(0, ...used) + 1;
238
+ return formatFullId("PRJ", next, slug);
239
+ }
240
+ function typedProjectMetadata(project) {
241
+ if (!isFullEntityId(project.id)) {
242
+ return null;
243
+ }
244
+ const parsed = parseFullEntityId(project.id);
245
+ return { shortId: parsed.shortId, slug: parsed.slug };
246
+ }
247
+ function resolveProjectCandidates(context, idOrAlias) {
248
+ if (isShortEntityId(idOrAlias)) {
249
+ return context.db.all("SELECT * FROM projects WHERE short_id = ? ORDER BY id ASC", [idOrAlias]);
250
+ }
251
+ if (isFullEntityId(idOrAlias)) {
252
+ return context.db.all("SELECT * FROM projects WHERE id = ? ORDER BY id ASC", [idOrAlias]);
253
+ }
254
+ return [];
255
+ }
256
+ function resolveSingleProject(context, idOrAlias) {
257
+ return resolveProject(context, { idOrAlias }).project;
258
+ }
259
+ function projectSummary(project) {
260
+ return {
261
+ id: project.id,
262
+ short_id: project.short_id,
263
+ slug: project.slug,
264
+ root: project.root,
265
+ status: project.status,
266
+ state_root: project.state_root
267
+ };
268
+ }
269
+ function projectReferenceTables() {
270
+ return [
271
+ "protocols",
272
+ "audit_events",
273
+ "merge_queue",
274
+ "merge_sessions",
275
+ "lanes",
276
+ "lane_locks",
277
+ "hook_installations",
278
+ "codex_home_profiles",
279
+ "codex_session_bindings",
280
+ "project_config",
281
+ "flow_sessions",
282
+ "pending_flow_session_bindings",
283
+ "codex_hook_events",
284
+ "worktree_records"
285
+ ];
286
+ }