@matthugh1/conductor-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/dist/agent.js +717 -0
- package/dist/branch-overview-XVHTGFCJ.js +18 -0
- package/dist/chunk-4YEHSYVN.js +17 -0
- package/dist/chunk-FAZ7FCZQ.js +534 -0
- package/dist/chunk-IHARLSA6.js +605 -0
- package/dist/chunk-JZT526HU.js +536 -0
- package/dist/chunk-MJKFQIYA.js +137 -0
- package/dist/chunk-PANC6BTV.js +151 -0
- package/dist/chunk-VYINBHPQ.js +22 -0
- package/dist/cli-config-TDSTAXIA.js +17 -0
- package/dist/cli-tasks-NW3BONXC.js +179 -0
- package/dist/db-U6Y3QJDD.js +16 -0
- package/dist/git-hooks-UZJ6AER4.js +213 -0
- package/dist/git-snapshots-N3FBS7T3.js +90 -0
- package/dist/git-wrapper-DVJ46TMA.js +28 -0
- package/dist/health-CTND2ANA.js +147 -0
- package/dist/health-snapshots-6MUVHE3G.js +39 -0
- package/dist/runner-prompt-2B6EXGN6.js +139 -0
- package/dist/work-queue-YE5P4S7R.js +764 -0
- package/dist/worktree-manager-QKRBTPVC.js +30 -0
- package/package.json +32 -0
|
@@ -0,0 +1,536 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
runGit
|
|
4
|
+
} from "./chunk-FAZ7FCZQ.js";
|
|
5
|
+
import {
|
|
6
|
+
query
|
|
7
|
+
} from "./chunk-PANC6BTV.js";
|
|
8
|
+
|
|
9
|
+
// ../../src/core/worktree-manager.ts
|
|
10
|
+
import { existsSync } from "fs";
|
|
11
|
+
import path from "path";
|
|
12
|
+
|
|
13
|
+
// ../../src/core/git-activity.ts
|
|
14
|
+
function rowToEvent(row) {
|
|
15
|
+
return {
|
|
16
|
+
id: row.id,
|
|
17
|
+
projectId: row.project_id,
|
|
18
|
+
initiativeId: row.initiative_id,
|
|
19
|
+
worktreeId: row.worktree_id,
|
|
20
|
+
eventType: row.event_type,
|
|
21
|
+
summary: row.summary,
|
|
22
|
+
plainEnglish: row.plain_english,
|
|
23
|
+
branch: row.branch,
|
|
24
|
+
detail: row.detail,
|
|
25
|
+
source: row.source,
|
|
26
|
+
createdAt: row.created_at.toISOString()
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
async function logGitActivity(input) {
|
|
30
|
+
const rows = await query(
|
|
31
|
+
`
|
|
32
|
+
INSERT INTO git_activity (
|
|
33
|
+
project_id, initiative_id, worktree_id, event_type,
|
|
34
|
+
summary, plain_english, branch, detail, source
|
|
35
|
+
)
|
|
36
|
+
VALUES ($1, $2, $3, $4, $5, $6, $7, $8::jsonb, $9)
|
|
37
|
+
RETURNING *
|
|
38
|
+
`,
|
|
39
|
+
[
|
|
40
|
+
input.projectId,
|
|
41
|
+
input.initiativeId ?? null,
|
|
42
|
+
input.worktreeId ?? null,
|
|
43
|
+
input.eventType,
|
|
44
|
+
input.summary,
|
|
45
|
+
input.plainEnglish,
|
|
46
|
+
input.branch ?? null,
|
|
47
|
+
JSON.stringify(input.detail ?? {}),
|
|
48
|
+
input.source ?? "mcp"
|
|
49
|
+
]
|
|
50
|
+
);
|
|
51
|
+
if (rows.length === 0) {
|
|
52
|
+
throw new Error("Could not save the git activity event.");
|
|
53
|
+
}
|
|
54
|
+
return rowToEvent(rows[0]);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// ../../src/core/notifications.ts
|
|
58
|
+
var VALID_EVENT_TYPES = /* @__PURE__ */ new Set([
|
|
59
|
+
"gate_rejection",
|
|
60
|
+
"gate_bypassed",
|
|
61
|
+
"review_needed",
|
|
62
|
+
"handoff_needed",
|
|
63
|
+
"decision_pending",
|
|
64
|
+
"stale_session",
|
|
65
|
+
"abandoned_session",
|
|
66
|
+
"missing_checkin",
|
|
67
|
+
"orphan_work",
|
|
68
|
+
"stage_transition",
|
|
69
|
+
"watchdog_flag",
|
|
70
|
+
"stale_worktree",
|
|
71
|
+
"autonomous_flag_changed"
|
|
72
|
+
]);
|
|
73
|
+
var VALID_PRIORITIES = /* @__PURE__ */ new Set([
|
|
74
|
+
"info",
|
|
75
|
+
"warning",
|
|
76
|
+
"action_needed"
|
|
77
|
+
]);
|
|
78
|
+
var VALID_LINK_TYPES = /* @__PURE__ */ new Set([
|
|
79
|
+
"deliverable",
|
|
80
|
+
"decision",
|
|
81
|
+
"initiative",
|
|
82
|
+
"session"
|
|
83
|
+
]);
|
|
84
|
+
async function createNotification(params) {
|
|
85
|
+
if (!params.projectId || params.projectId.trim().length === 0) {
|
|
86
|
+
throw new Error("A project id is required to create a notification.");
|
|
87
|
+
}
|
|
88
|
+
if (!VALID_EVENT_TYPES.has(params.eventType)) {
|
|
89
|
+
throw new Error(
|
|
90
|
+
`Unknown event type "${params.eventType}". Expected one of: ${[...VALID_EVENT_TYPES].join(", ")}.`
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
if (!params.message || params.message.trim().length === 0) {
|
|
94
|
+
throw new Error("A notification needs a message.");
|
|
95
|
+
}
|
|
96
|
+
if (!VALID_PRIORITIES.has(params.priority)) {
|
|
97
|
+
throw new Error(
|
|
98
|
+
`Unknown priority "${params.priority}". Expected info, warning, or action_needed.`
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
if (params.linkType !== void 0 && !VALID_LINK_TYPES.has(params.linkType)) {
|
|
102
|
+
throw new Error(
|
|
103
|
+
`Unknown link type "${params.linkType}". Expected deliverable, decision, initiative, or session.`
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
const rows = await query(
|
|
107
|
+
`INSERT INTO notifications (project_id, event_type, message, priority, link_type, link_id, agent_type, agent_name)
|
|
108
|
+
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
|
|
109
|
+
RETURNING id`,
|
|
110
|
+
[
|
|
111
|
+
params.projectId,
|
|
112
|
+
params.eventType,
|
|
113
|
+
params.message.trim(),
|
|
114
|
+
params.priority,
|
|
115
|
+
params.linkType ?? null,
|
|
116
|
+
params.linkId ?? null,
|
|
117
|
+
params.agentType ?? null,
|
|
118
|
+
params.agentName ?? null
|
|
119
|
+
]
|
|
120
|
+
);
|
|
121
|
+
return rows[0].id;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// ../../src/core/worktree-manager.ts
|
|
125
|
+
var STALE_DAYS = 7;
|
|
126
|
+
var STALE_MS = STALE_DAYS * 24 * 60 * 60 * 1e3;
|
|
127
|
+
function rowToRecord(row) {
|
|
128
|
+
return {
|
|
129
|
+
id: row.id,
|
|
130
|
+
projectId: row.project_id,
|
|
131
|
+
initiativeId: row.initiative_id,
|
|
132
|
+
branchName: row.branch_name,
|
|
133
|
+
worktreePath: row.worktree_path,
|
|
134
|
+
status: row.status,
|
|
135
|
+
source: row.source,
|
|
136
|
+
createdAt: row.created_at.toISOString(),
|
|
137
|
+
mergedAt: row.merged_at?.toISOString() ?? null,
|
|
138
|
+
removedAt: row.removed_at?.toISOString() ?? null
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
function rowToWithInitiative(row) {
|
|
142
|
+
return {
|
|
143
|
+
...rowToRecord(row),
|
|
144
|
+
initiativeTitle: row.initiative_title,
|
|
145
|
+
lastCommitAt: null,
|
|
146
|
+
// Enriched later by enrichWithLastCommit
|
|
147
|
+
deliverableTitle: row.deliverable_title ?? null
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
async function enrichWithLastCommit(projectRoot, worktrees) {
|
|
151
|
+
return Promise.all(
|
|
152
|
+
worktrees.map(async (wt) => {
|
|
153
|
+
try {
|
|
154
|
+
const output = await runGit(projectRoot, [
|
|
155
|
+
"log",
|
|
156
|
+
"-1",
|
|
157
|
+
"--format=%cI",
|
|
158
|
+
wt.branchName,
|
|
159
|
+
"--"
|
|
160
|
+
]);
|
|
161
|
+
const trimmed = output.trim();
|
|
162
|
+
return { ...wt, lastCommitAt: trimmed || null };
|
|
163
|
+
} catch {
|
|
164
|
+
return wt;
|
|
165
|
+
}
|
|
166
|
+
})
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
function slugifyForBranch(title) {
|
|
170
|
+
return title.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 60);
|
|
171
|
+
}
|
|
172
|
+
async function createWorktree(projectId, projectRoot, initiativeId, initiativeTitle) {
|
|
173
|
+
const existing = await getWorktreeForInitiative(projectId, initiativeId);
|
|
174
|
+
if (existing !== null) {
|
|
175
|
+
throw new Error(
|
|
176
|
+
`A worktree already exists for "${initiativeTitle}" at ${existing.worktreePath}`
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
const slug = slugifyForBranch(initiativeTitle);
|
|
180
|
+
const branchName = slug;
|
|
181
|
+
const projectDir = path.basename(projectRoot);
|
|
182
|
+
const parentDir = path.dirname(projectRoot);
|
|
183
|
+
const worktreeDir = path.join(parentDir, `${projectDir}--${slug}`);
|
|
184
|
+
try {
|
|
185
|
+
await runGit(projectRoot, ["rev-parse", "--verify", branchName]);
|
|
186
|
+
} catch {
|
|
187
|
+
await runGit(projectRoot, ["branch", branchName]);
|
|
188
|
+
}
|
|
189
|
+
await runGit(projectRoot, ["worktree", "add", worktreeDir, branchName]);
|
|
190
|
+
const rows = await query(
|
|
191
|
+
`
|
|
192
|
+
WITH inserted AS (
|
|
193
|
+
INSERT INTO worktrees (project_id, initiative_id, branch_name, worktree_path)
|
|
194
|
+
VALUES ($1, $2, $3, $4)
|
|
195
|
+
RETURNING *
|
|
196
|
+
)
|
|
197
|
+
SELECT inserted.*, i.title AS initiative_title
|
|
198
|
+
FROM inserted
|
|
199
|
+
JOIN initiatives i ON i.id = inserted.initiative_id
|
|
200
|
+
`,
|
|
201
|
+
[projectId, initiativeId, branchName, worktreeDir]
|
|
202
|
+
);
|
|
203
|
+
if (rows.length === 0) {
|
|
204
|
+
throw new Error("Could not record the new worktree.");
|
|
205
|
+
}
|
|
206
|
+
const record = rowToWithInitiative(rows[0]);
|
|
207
|
+
await logGitActivity({
|
|
208
|
+
projectId,
|
|
209
|
+
initiativeId,
|
|
210
|
+
worktreeId: record.id,
|
|
211
|
+
eventType: "worktree_created",
|
|
212
|
+
summary: `Created worktree for "${initiativeTitle}"`,
|
|
213
|
+
plainEnglish: `Set up a separate working area for the "${initiativeTitle}" initiative at ${worktreeDir}. The agent can work here without affecting the main codebase.`,
|
|
214
|
+
branch: branchName,
|
|
215
|
+
source: "system"
|
|
216
|
+
});
|
|
217
|
+
return record;
|
|
218
|
+
}
|
|
219
|
+
async function listWorktrees(projectId, opts) {
|
|
220
|
+
const statusFilter = opts?.status;
|
|
221
|
+
const baseQuery = `
|
|
222
|
+
SELECT w.*, i.title AS initiative_title,
|
|
223
|
+
(
|
|
224
|
+
SELECT d.title FROM deliverables d
|
|
225
|
+
JOIN outcomes o ON o.id = d.outcome_id
|
|
226
|
+
WHERE o.initiative_id = w.initiative_id
|
|
227
|
+
AND d.status = 'in_progress'
|
|
228
|
+
LIMIT 1
|
|
229
|
+
) AS deliverable_title
|
|
230
|
+
FROM worktrees w
|
|
231
|
+
LEFT JOIN initiatives i ON i.id = w.initiative_id
|
|
232
|
+
WHERE w.project_id = $1
|
|
233
|
+
`;
|
|
234
|
+
if (statusFilter) {
|
|
235
|
+
const rows2 = await query(
|
|
236
|
+
`${baseQuery} AND w.status = $2 ORDER BY w.created_at DESC`,
|
|
237
|
+
[projectId, statusFilter]
|
|
238
|
+
);
|
|
239
|
+
return rows2.map(rowToWithInitiative);
|
|
240
|
+
}
|
|
241
|
+
const rows = await query(
|
|
242
|
+
`${baseQuery} ORDER BY w.created_at DESC`,
|
|
243
|
+
[projectId]
|
|
244
|
+
);
|
|
245
|
+
return rows.map(rowToWithInitiative);
|
|
246
|
+
}
|
|
247
|
+
async function getWorktreeForInitiative(projectId, initiativeId) {
|
|
248
|
+
const rows = await query(
|
|
249
|
+
`
|
|
250
|
+
SELECT * FROM worktrees
|
|
251
|
+
WHERE project_id = $1 AND initiative_id = $2 AND status = 'active'
|
|
252
|
+
LIMIT 1
|
|
253
|
+
`,
|
|
254
|
+
[projectId, initiativeId]
|
|
255
|
+
);
|
|
256
|
+
if (rows.length === 0) return null;
|
|
257
|
+
return rowToRecord(rows[0]);
|
|
258
|
+
}
|
|
259
|
+
async function getWorktreeByBranch(projectId, branchName) {
|
|
260
|
+
const rows = await query(
|
|
261
|
+
`
|
|
262
|
+
SELECT w.*, i.title AS initiative_title
|
|
263
|
+
FROM worktrees w
|
|
264
|
+
LEFT JOIN initiatives i ON i.id = w.initiative_id
|
|
265
|
+
WHERE w.project_id = $1 AND w.branch_name = $2 AND w.status = 'active'
|
|
266
|
+
LIMIT 1
|
|
267
|
+
`,
|
|
268
|
+
[projectId, branchName]
|
|
269
|
+
);
|
|
270
|
+
if (rows.length === 0) return null;
|
|
271
|
+
return rowToWithInitiative(rows[0]);
|
|
272
|
+
}
|
|
273
|
+
async function mergeWorktree(projectId, projectRoot, worktreeId, defaultBranch) {
|
|
274
|
+
const rows = await query(
|
|
275
|
+
`
|
|
276
|
+
SELECT w.*, i.title AS initiative_title
|
|
277
|
+
FROM worktrees w
|
|
278
|
+
LEFT JOIN initiatives i ON i.id = w.initiative_id
|
|
279
|
+
WHERE w.id = $1 AND w.project_id = $2 AND w.status = 'active'
|
|
280
|
+
LIMIT 1
|
|
281
|
+
`,
|
|
282
|
+
[worktreeId, projectId]
|
|
283
|
+
);
|
|
284
|
+
if (rows.length === 0) {
|
|
285
|
+
throw new Error("That worktree is not active or could not be found.");
|
|
286
|
+
}
|
|
287
|
+
const wt = rowToWithInitiative(rows[0]);
|
|
288
|
+
await runGit(projectRoot, ["switch", defaultBranch]);
|
|
289
|
+
const output = await runGit(projectRoot, ["merge", "--no-ff", wt.branchName]);
|
|
290
|
+
await query(
|
|
291
|
+
`UPDATE worktrees SET status = 'merged', merged_at = now() WHERE id = $1`,
|
|
292
|
+
[worktreeId]
|
|
293
|
+
);
|
|
294
|
+
await logGitActivity({
|
|
295
|
+
projectId,
|
|
296
|
+
initiativeId: wt.initiativeId,
|
|
297
|
+
worktreeId: wt.id,
|
|
298
|
+
eventType: "merge",
|
|
299
|
+
summary: `Merged "${wt.initiativeTitle}" into ${defaultBranch}`,
|
|
300
|
+
plainEnglish: `The "${wt.initiativeTitle}" initiative has been merged into ${defaultBranch}. All changes from that initiative are now part of the main codebase.`,
|
|
301
|
+
branch: defaultBranch,
|
|
302
|
+
source: "system"
|
|
303
|
+
});
|
|
304
|
+
return { output };
|
|
305
|
+
}
|
|
306
|
+
async function removeWorktree(projectId, projectRoot, worktreeId) {
|
|
307
|
+
const rows = await query(
|
|
308
|
+
`
|
|
309
|
+
SELECT w.*, i.title AS initiative_title
|
|
310
|
+
FROM worktrees w
|
|
311
|
+
LEFT JOIN initiatives i ON i.id = w.initiative_id
|
|
312
|
+
WHERE w.id = $1 AND w.project_id = $2
|
|
313
|
+
LIMIT 1
|
|
314
|
+
`,
|
|
315
|
+
[worktreeId, projectId]
|
|
316
|
+
);
|
|
317
|
+
if (rows.length === 0) {
|
|
318
|
+
throw new Error("That worktree could not be found.");
|
|
319
|
+
}
|
|
320
|
+
const wt = rowToWithInitiative(rows[0]);
|
|
321
|
+
try {
|
|
322
|
+
await runGit(projectRoot, ["worktree", "remove", wt.worktreePath, "--force"]);
|
|
323
|
+
} catch {
|
|
324
|
+
}
|
|
325
|
+
await query(
|
|
326
|
+
`UPDATE worktrees SET status = 'removed', removed_at = now() WHERE id = $1`,
|
|
327
|
+
[worktreeId]
|
|
328
|
+
);
|
|
329
|
+
await logGitActivity({
|
|
330
|
+
projectId,
|
|
331
|
+
initiativeId: wt.initiativeId,
|
|
332
|
+
worktreeId: wt.id,
|
|
333
|
+
eventType: "worktree_removed",
|
|
334
|
+
summary: `Removed worktree for "${wt.initiativeTitle}"`,
|
|
335
|
+
plainEnglish: `Cleaned up the working area for "${wt.initiativeTitle}". The branch still exists in git if you need it.`,
|
|
336
|
+
branch: wt.branchName,
|
|
337
|
+
source: "system"
|
|
338
|
+
});
|
|
339
|
+
}
|
|
340
|
+
function parseWorktreeListPorcelain(output) {
|
|
341
|
+
const entries = [];
|
|
342
|
+
const blocks = output.trim().split("\n\n");
|
|
343
|
+
for (const block of blocks) {
|
|
344
|
+
const lines = block.trim().split("\n");
|
|
345
|
+
let wtPath = "";
|
|
346
|
+
let head = "";
|
|
347
|
+
let branch = null;
|
|
348
|
+
let bare = false;
|
|
349
|
+
for (const line of lines) {
|
|
350
|
+
if (line.startsWith("worktree ")) {
|
|
351
|
+
wtPath = line.slice("worktree ".length);
|
|
352
|
+
} else if (line.startsWith("HEAD ")) {
|
|
353
|
+
head = line.slice("HEAD ".length);
|
|
354
|
+
} else if (line.startsWith("branch ")) {
|
|
355
|
+
const ref = line.slice("branch ".length);
|
|
356
|
+
branch = ref.replace("refs/heads/", "");
|
|
357
|
+
} else if (line === "bare") {
|
|
358
|
+
bare = true;
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
if (wtPath) {
|
|
362
|
+
entries.push({ path: wtPath, head, branch, bare });
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
return entries;
|
|
366
|
+
}
|
|
367
|
+
async function scanWorktrees(projectId, projectRoot) {
|
|
368
|
+
let mainRepoRoot = projectRoot;
|
|
369
|
+
try {
|
|
370
|
+
const commonDir = (await runGit(projectRoot, ["rev-parse", "--git-common-dir"])).trim();
|
|
371
|
+
if (path.isAbsolute(commonDir) && commonDir.endsWith(".git")) {
|
|
372
|
+
mainRepoRoot = path.dirname(commonDir);
|
|
373
|
+
}
|
|
374
|
+
} catch {
|
|
375
|
+
}
|
|
376
|
+
const output = await runGit(mainRepoRoot, ["worktree", "list", "--porcelain"]);
|
|
377
|
+
const diskWorktrees = parseWorktreeListPorcelain(output);
|
|
378
|
+
const additionalWorktrees = diskWorktrees.filter(
|
|
379
|
+
(wt) => !wt.bare && wt.path !== mainRepoRoot
|
|
380
|
+
);
|
|
381
|
+
const diskPaths = new Set(additionalWorktrees.map((wt) => wt.path));
|
|
382
|
+
const dbRows = await query(
|
|
383
|
+
`SELECT w.id, w.branch_name, w.worktree_path, i.title AS initiative_title,
|
|
384
|
+
w.status, w.source
|
|
385
|
+
FROM worktrees w
|
|
386
|
+
LEFT JOIN initiatives i ON i.id = w.initiative_id
|
|
387
|
+
WHERE w.project_id = $1 AND w.status = 'active'`,
|
|
388
|
+
[projectId]
|
|
389
|
+
);
|
|
390
|
+
const registeredPaths = new Set(dbRows.map((r) => r.worktree_path));
|
|
391
|
+
const entries = [];
|
|
392
|
+
let newlyDetected = 0;
|
|
393
|
+
let removedFromDisk = 0;
|
|
394
|
+
for (const wt of additionalWorktrees) {
|
|
395
|
+
const isNew = !registeredPaths.has(wt.path);
|
|
396
|
+
if (isNew) {
|
|
397
|
+
await query(
|
|
398
|
+
`INSERT INTO worktrees (project_id, initiative_id, branch_name, worktree_path, source)
|
|
399
|
+
VALUES ($1, NULL, $2, $3, 'auto')
|
|
400
|
+
ON CONFLICT (project_id, branch_name) DO NOTHING`,
|
|
401
|
+
[projectId, wt.branch ?? `detached-${wt.head.slice(0, 8)}`, wt.path]
|
|
402
|
+
);
|
|
403
|
+
newlyDetected++;
|
|
404
|
+
}
|
|
405
|
+
let lastCommitAt = null;
|
|
406
|
+
try {
|
|
407
|
+
const logOutput = await runGit(wt.path, ["log", "-1", "--format=%cI"]);
|
|
408
|
+
lastCommitAt = logOutput.trim() || null;
|
|
409
|
+
} catch {
|
|
410
|
+
}
|
|
411
|
+
entries.push({
|
|
412
|
+
branchName: wt.branch ?? `detached-${wt.head.slice(0, 8)}`,
|
|
413
|
+
worktreePath: wt.path,
|
|
414
|
+
initiativeTitle: isNew ? null : dbRows.find((r) => r.worktree_path === wt.path)?.initiative_title ?? null,
|
|
415
|
+
status: "active",
|
|
416
|
+
source: isNew ? "auto" : dbRows.find((r) => r.worktree_path === wt.path)?.source ?? "auto",
|
|
417
|
+
lastCommitAt,
|
|
418
|
+
removedFromDisk: false,
|
|
419
|
+
newlyRegistered: isNew
|
|
420
|
+
});
|
|
421
|
+
}
|
|
422
|
+
for (const row of dbRows) {
|
|
423
|
+
if (!diskPaths.has(row.worktree_path) && !existsSync(row.worktree_path)) {
|
|
424
|
+
await query(
|
|
425
|
+
`UPDATE worktrees SET status = 'removed', removed_at = now() WHERE id = $1`,
|
|
426
|
+
[row.id]
|
|
427
|
+
);
|
|
428
|
+
removedFromDisk++;
|
|
429
|
+
entries.push({
|
|
430
|
+
branchName: row.branch_name,
|
|
431
|
+
worktreePath: row.worktree_path,
|
|
432
|
+
initiativeTitle: row.initiative_title,
|
|
433
|
+
status: "removed",
|
|
434
|
+
source: row.source,
|
|
435
|
+
lastCommitAt: null,
|
|
436
|
+
removedFromDisk: true,
|
|
437
|
+
newlyRegistered: false
|
|
438
|
+
});
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
return {
|
|
442
|
+
registered: registeredPaths.size,
|
|
443
|
+
newlyDetected,
|
|
444
|
+
removedFromDisk,
|
|
445
|
+
total: registeredPaths.size + newlyDetected,
|
|
446
|
+
worktrees: entries
|
|
447
|
+
};
|
|
448
|
+
}
|
|
449
|
+
async function checkStaleness(projectId) {
|
|
450
|
+
const worktrees = await listWorktrees(projectId, { status: "active" });
|
|
451
|
+
const stale = [];
|
|
452
|
+
const now = Date.now();
|
|
453
|
+
for (const wt of worktrees) {
|
|
454
|
+
let lastCommitAt = null;
|
|
455
|
+
let directoryMissing = false;
|
|
456
|
+
if (!existsSync(wt.worktreePath)) {
|
|
457
|
+
directoryMissing = true;
|
|
458
|
+
} else {
|
|
459
|
+
try {
|
|
460
|
+
const output = await runGit(wt.worktreePath, ["log", "-1", "--format=%cI"]);
|
|
461
|
+
lastCommitAt = output.trim() || null;
|
|
462
|
+
} catch {
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
const commitTime = lastCommitAt ? new Date(lastCommitAt).getTime() : 0;
|
|
466
|
+
const msSinceCommit = lastCommitAt ? now - commitTime : Infinity;
|
|
467
|
+
const daysSinceCommit = Math.floor(msSinceCommit / (24 * 60 * 60 * 1e3));
|
|
468
|
+
if (directoryMissing || msSinceCommit >= STALE_MS) {
|
|
469
|
+
stale.push({
|
|
470
|
+
id: wt.id,
|
|
471
|
+
path: wt.worktreePath,
|
|
472
|
+
branch: wt.branchName,
|
|
473
|
+
lastCommitAt,
|
|
474
|
+
daysSinceCommit,
|
|
475
|
+
directoryMissing
|
|
476
|
+
});
|
|
477
|
+
const message = directoryMissing ? `Worktree "${wt.branchName}" directory is missing \u2014 it may have been deleted outside Conductor.` : `Worktree "${wt.branchName}" has had no commits for ${daysSinceCommit} days.`;
|
|
478
|
+
try {
|
|
479
|
+
await createNotification({
|
|
480
|
+
projectId,
|
|
481
|
+
eventType: "stale_worktree",
|
|
482
|
+
message,
|
|
483
|
+
priority: directoryMissing ? "action_needed" : "warning",
|
|
484
|
+
linkType: wt.initiativeId ? "initiative" : void 0,
|
|
485
|
+
linkId: wt.initiativeId ?? void 0
|
|
486
|
+
});
|
|
487
|
+
} catch {
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
return stale;
|
|
492
|
+
}
|
|
493
|
+
async function listWorktreesWithStaleness(projectId) {
|
|
494
|
+
const worktrees = await listWorktrees(projectId, { status: "active" });
|
|
495
|
+
const now = Date.now();
|
|
496
|
+
const results = [];
|
|
497
|
+
for (const wt of worktrees) {
|
|
498
|
+
let lastCommitAt = null;
|
|
499
|
+
let daysSinceCommit = null;
|
|
500
|
+
if (existsSync(wt.worktreePath)) {
|
|
501
|
+
try {
|
|
502
|
+
const output = await runGit(wt.worktreePath, ["log", "-1", "--format=%cI"]);
|
|
503
|
+
lastCommitAt = output.trim() || null;
|
|
504
|
+
if (lastCommitAt) {
|
|
505
|
+
const commitTime = new Date(lastCommitAt).getTime();
|
|
506
|
+
daysSinceCommit = Math.floor((now - commitTime) / (24 * 60 * 60 * 1e3));
|
|
507
|
+
}
|
|
508
|
+
} catch {
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
const stale = daysSinceCommit !== null ? daysSinceCommit >= STALE_DAYS : true;
|
|
512
|
+
results.push({
|
|
513
|
+
...wt,
|
|
514
|
+
stale,
|
|
515
|
+
lastCommitAt,
|
|
516
|
+
daysSinceCommit,
|
|
517
|
+
source: wt.source ?? "manual"
|
|
518
|
+
});
|
|
519
|
+
}
|
|
520
|
+
return results;
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
export {
|
|
524
|
+
logGitActivity,
|
|
525
|
+
enrichWithLastCommit,
|
|
526
|
+
slugifyForBranch,
|
|
527
|
+
createWorktree,
|
|
528
|
+
listWorktrees,
|
|
529
|
+
getWorktreeForInitiative,
|
|
530
|
+
getWorktreeByBranch,
|
|
531
|
+
mergeWorktree,
|
|
532
|
+
removeWorktree,
|
|
533
|
+
scanWorktrees,
|
|
534
|
+
checkStaleness,
|
|
535
|
+
listWorktreesWithStaleness
|
|
536
|
+
};
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// ../../src/core/cli-config.ts
|
|
4
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
|
|
5
|
+
import { dirname, join } from "path";
|
|
6
|
+
import { fileURLToPath } from "url";
|
|
7
|
+
import { homedir } from "os";
|
|
8
|
+
var __filename = fileURLToPath(import.meta.url);
|
|
9
|
+
var __dirname = dirname(__filename);
|
|
10
|
+
var CONFIG_DIR = join(homedir(), ".conductor");
|
|
11
|
+
var CONFIG_FILE = join(CONFIG_DIR, "config.json");
|
|
12
|
+
var KEY_FILE = join(CONFIG_DIR, "api-key");
|
|
13
|
+
function readConfig() {
|
|
14
|
+
if (!existsSync(CONFIG_FILE)) {
|
|
15
|
+
return {};
|
|
16
|
+
}
|
|
17
|
+
try {
|
|
18
|
+
const raw = readFileSync(CONFIG_FILE, "utf8");
|
|
19
|
+
const parsed = JSON.parse(raw);
|
|
20
|
+
if (parsed !== null && typeof parsed === "object") {
|
|
21
|
+
return parsed;
|
|
22
|
+
}
|
|
23
|
+
return {};
|
|
24
|
+
} catch {
|
|
25
|
+
return {};
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
function writeConfig(config) {
|
|
29
|
+
mkdirSync(CONFIG_DIR, { recursive: true });
|
|
30
|
+
writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2) + "\n", {
|
|
31
|
+
mode: 384
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
function readApiKey() {
|
|
35
|
+
if (!existsSync(KEY_FILE)) {
|
|
36
|
+
return void 0;
|
|
37
|
+
}
|
|
38
|
+
try {
|
|
39
|
+
const key = readFileSync(KEY_FILE, "utf8").trim();
|
|
40
|
+
return key.length > 0 ? key : void 0;
|
|
41
|
+
} catch {
|
|
42
|
+
return void 0;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
function getConfigEndpoint() {
|
|
46
|
+
const candidates = [
|
|
47
|
+
join(process.cwd(), ".mcp.json"),
|
|
48
|
+
join(__dirname, "../../.mcp.json")
|
|
49
|
+
];
|
|
50
|
+
for (const filePath of candidates) {
|
|
51
|
+
if (!existsSync(filePath)) continue;
|
|
52
|
+
try {
|
|
53
|
+
const raw = readFileSync(filePath, "utf8");
|
|
54
|
+
const parsed = JSON.parse(raw);
|
|
55
|
+
const mcpUrl = parsed.mcpServers?.conductor?.url;
|
|
56
|
+
if (mcpUrl) {
|
|
57
|
+
return mcpUrl.replace(/\/mcp\/?$/, "/config");
|
|
58
|
+
}
|
|
59
|
+
} catch {
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return "http://localhost:3001/config";
|
|
64
|
+
}
|
|
65
|
+
function getServerBaseUrl() {
|
|
66
|
+
const configUrl = getConfigEndpoint();
|
|
67
|
+
return configUrl.replace(/\/config\/?$/, "");
|
|
68
|
+
}
|
|
69
|
+
async function fetchConfigFromServer() {
|
|
70
|
+
const apiKey = readApiKey();
|
|
71
|
+
if (!apiKey) {
|
|
72
|
+
return void 0;
|
|
73
|
+
}
|
|
74
|
+
const configUrl = getConfigEndpoint();
|
|
75
|
+
try {
|
|
76
|
+
const resp = await fetch(configUrl, {
|
|
77
|
+
headers: { Authorization: `Bearer ${apiKey}` },
|
|
78
|
+
signal: AbortSignal.timeout(5e3)
|
|
79
|
+
});
|
|
80
|
+
if (!resp.ok) {
|
|
81
|
+
return void 0;
|
|
82
|
+
}
|
|
83
|
+
const body = await resp.json();
|
|
84
|
+
return body.databaseUrl ?? void 0;
|
|
85
|
+
} catch {
|
|
86
|
+
return void 0;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
async function postToServer(path, body, projectRoot) {
|
|
90
|
+
const apiKey = readApiKey();
|
|
91
|
+
if (!apiKey) return false;
|
|
92
|
+
const baseUrl = getServerBaseUrl();
|
|
93
|
+
const url = new URL(path, baseUrl);
|
|
94
|
+
if (projectRoot) {
|
|
95
|
+
url.searchParams.set("projectRoot", projectRoot);
|
|
96
|
+
}
|
|
97
|
+
try {
|
|
98
|
+
const resp = await fetch(url.toString(), {
|
|
99
|
+
method: "POST",
|
|
100
|
+
headers: {
|
|
101
|
+
"Content-Type": "application/json",
|
|
102
|
+
Authorization: `Bearer ${apiKey}`
|
|
103
|
+
},
|
|
104
|
+
body: JSON.stringify(body),
|
|
105
|
+
signal: AbortSignal.timeout(5e3)
|
|
106
|
+
});
|
|
107
|
+
return resp.ok;
|
|
108
|
+
} catch {
|
|
109
|
+
return false;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
async function ensureDatabaseUrl() {
|
|
113
|
+
if (process.env.DATABASE_URL) {
|
|
114
|
+
return true;
|
|
115
|
+
}
|
|
116
|
+
const config = readConfig();
|
|
117
|
+
if (config.databaseUrl) {
|
|
118
|
+
process.env.DATABASE_URL = config.databaseUrl;
|
|
119
|
+
return true;
|
|
120
|
+
}
|
|
121
|
+
const url = await fetchConfigFromServer();
|
|
122
|
+
if (url) {
|
|
123
|
+
writeConfig({ ...config, databaseUrl: url });
|
|
124
|
+
process.env.DATABASE_URL = url;
|
|
125
|
+
return true;
|
|
126
|
+
}
|
|
127
|
+
return false;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export {
|
|
131
|
+
readConfig,
|
|
132
|
+
writeConfig,
|
|
133
|
+
getServerBaseUrl,
|
|
134
|
+
fetchConfigFromServer,
|
|
135
|
+
postToServer,
|
|
136
|
+
ensureDatabaseUrl
|
|
137
|
+
};
|