@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.
- package/README.md +274 -0
- package/dist/cli/help.js +308 -0
- package/dist/cli/run-cli.js +945 -0
- package/dist/cli.js +4 -0
- package/dist/domain/contracts.js +57 -0
- package/dist/domain/entity-ids.js +47 -0
- package/dist/domain/flow-contract.js +233 -0
- package/dist/domain/validation.js +91 -0
- package/dist/protocol/local-files.js +141 -0
- package/dist/runtime/context.js +11 -0
- package/dist/schemas/code-stage-report.schema.json +181 -0
- package/dist/schemas/flow-run-index.schema.json +129 -0
- package/dist/schemas/mb-upgrade-review-data.schema.json +813 -0
- package/dist/schemas/memorybank-permissions-preflight.schema.json +154 -0
- package/dist/schemas/merge-stage-report.schema.json +135 -0
- package/dist/services/audit.js +19 -0
- package/dist/services/cleanup.js +310 -0
- package/dist/services/config.js +143 -0
- package/dist/services/dashboard.js +436 -0
- package/dist/services/hooks.js +929 -0
- package/dist/services/lanes.js +327 -0
- package/dist/services/memory-permissions.js +344 -0
- package/dist/services/merge-queue.js +333 -0
- package/dist/services/plans.js +149 -0
- package/dist/services/projects.js +286 -0
- package/dist/services/protocols.js +606 -0
- package/dist/services/runs.js +359 -0
- package/dist/services/schema-validation.js +185 -0
- package/dist/services/sessions.js +365 -0
- package/dist/services/worktrees.js +204 -0
- package/dist/shared/errors.js +14 -0
- package/dist/shared/json.js +17 -0
- package/dist/storage/database.js +325 -0
- package/dist/storage/paths.js +56 -0
- 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
|
+
}
|