@matthugh1/conductor-cli 0.2.2 → 0.2.3
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 +166 -90
- package/dist/{chunk-7S5HKGS5.js → branch-overview-RRHX3XGY.js} +114 -9
- package/dist/{chunk-FAZ7FCZQ.js → chunk-N2KKNG4C.js} +15 -3
- package/dist/{chunk-B2WDTKD7.js → cli-config-LEERSU5N.js} +6 -7
- package/dist/{daemon-GGOJDZDB.js → daemon-ZJDZIP3R.js} +24 -15
- package/dist/{daemon-client-BE64H437.js → daemon-client-CTYOJMJP.js} +124 -1
- package/dist/{git-hooks-UZJ6AER4.js → git-hooks-RQ6WJQS4.js} +1 -2
- package/dist/{git-wrapper-DVJ46TMA.js → git-wrapper-QRZYTYCZ.js} +1 -2
- package/package.json +2 -2
- package/dist/branch-overview-DSSCUE5F.js +0 -18
- package/dist/chunk-3MJBQK2F.js +0 -75
- package/dist/chunk-4YEHSYVN.js +0 -17
- package/dist/chunk-6VMREHG4.js +0 -22
- package/dist/chunk-KB2DTST2.js +0 -482
- package/dist/chunk-PANC6BTV.js +0 -151
- package/dist/cli-config-2ZDXUUQN.js +0 -21
- package/dist/cli-tasks-NM5D5PIZ.js +0 -180
- package/dist/db-U6Y3QJDD.js +0 -16
- package/dist/git-snapshots-N3FBS7T3.js +0 -90
- package/dist/health-UFK7YCKQ.js +0 -147
- package/dist/health-snapshots-6MUVHE3G.js +0 -39
- package/dist/work-queue-U3JYHLX2.js +0 -758
- package/dist/worktree-manager-2ZUJEL3L.js +0 -31
package/dist/chunk-KB2DTST2.js
DELETED
|
@@ -1,482 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
3
|
-
createNotification
|
|
4
|
-
} from "./chunk-3MJBQK2F.js";
|
|
5
|
-
import {
|
|
6
|
-
runGit
|
|
7
|
-
} from "./chunk-FAZ7FCZQ.js";
|
|
8
|
-
import {
|
|
9
|
-
query
|
|
10
|
-
} from "./chunk-PANC6BTV.js";
|
|
11
|
-
|
|
12
|
-
// ../../src/core/worktree-manager.ts
|
|
13
|
-
import { existsSync } from "fs";
|
|
14
|
-
import path from "path";
|
|
15
|
-
|
|
16
|
-
// ../../src/core/git-activity.ts
|
|
17
|
-
function rowToEvent(row) {
|
|
18
|
-
return {
|
|
19
|
-
id: row.id,
|
|
20
|
-
projectId: row.project_id,
|
|
21
|
-
initiativeId: row.initiative_id,
|
|
22
|
-
worktreeId: row.worktree_id,
|
|
23
|
-
eventType: row.event_type,
|
|
24
|
-
summary: row.summary,
|
|
25
|
-
plainEnglish: row.plain_english,
|
|
26
|
-
branch: row.branch,
|
|
27
|
-
detail: row.detail,
|
|
28
|
-
source: row.source,
|
|
29
|
-
createdAt: row.created_at.toISOString()
|
|
30
|
-
};
|
|
31
|
-
}
|
|
32
|
-
async function logGitActivity(input) {
|
|
33
|
-
const rows = await query(
|
|
34
|
-
`
|
|
35
|
-
INSERT INTO git_activity (
|
|
36
|
-
project_id, initiative_id, worktree_id, event_type,
|
|
37
|
-
summary, plain_english, branch, detail, source
|
|
38
|
-
)
|
|
39
|
-
VALUES ($1, $2, $3, $4, $5, $6, $7, $8::jsonb, $9)
|
|
40
|
-
RETURNING *
|
|
41
|
-
`,
|
|
42
|
-
[
|
|
43
|
-
input.projectId,
|
|
44
|
-
input.initiativeId ?? null,
|
|
45
|
-
input.worktreeId ?? null,
|
|
46
|
-
input.eventType,
|
|
47
|
-
input.summary,
|
|
48
|
-
input.plainEnglish,
|
|
49
|
-
input.branch ?? null,
|
|
50
|
-
JSON.stringify(input.detail ?? {}),
|
|
51
|
-
input.source ?? "mcp"
|
|
52
|
-
]
|
|
53
|
-
);
|
|
54
|
-
if (rows.length === 0) {
|
|
55
|
-
throw new Error("Could not save the git activity event.");
|
|
56
|
-
}
|
|
57
|
-
return rowToEvent(rows[0]);
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
// ../../src/core/worktree-manager.ts
|
|
61
|
-
var STALE_DAYS = 7;
|
|
62
|
-
var STALE_MS = STALE_DAYS * 24 * 60 * 60 * 1e3;
|
|
63
|
-
function rowToRecord(row) {
|
|
64
|
-
return {
|
|
65
|
-
id: row.id,
|
|
66
|
-
projectId: row.project_id,
|
|
67
|
-
initiativeId: row.initiative_id,
|
|
68
|
-
branchName: row.branch_name,
|
|
69
|
-
worktreePath: row.worktree_path,
|
|
70
|
-
status: row.status,
|
|
71
|
-
source: row.source,
|
|
72
|
-
createdAt: row.created_at.toISOString(),
|
|
73
|
-
mergedAt: row.merged_at?.toISOString() ?? null,
|
|
74
|
-
removedAt: row.removed_at?.toISOString() ?? null
|
|
75
|
-
};
|
|
76
|
-
}
|
|
77
|
-
function rowToWithInitiative(row) {
|
|
78
|
-
return {
|
|
79
|
-
...rowToRecord(row),
|
|
80
|
-
initiativeTitle: row.initiative_title,
|
|
81
|
-
lastCommitAt: null,
|
|
82
|
-
// Enriched later by enrichWithLastCommit
|
|
83
|
-
deliverableTitle: row.deliverable_title ?? null
|
|
84
|
-
};
|
|
85
|
-
}
|
|
86
|
-
async function enrichWithLastCommit(projectRoot, worktrees) {
|
|
87
|
-
return Promise.all(
|
|
88
|
-
worktrees.map(async (wt) => {
|
|
89
|
-
try {
|
|
90
|
-
const output = await runGit(projectRoot, [
|
|
91
|
-
"log",
|
|
92
|
-
"-1",
|
|
93
|
-
"--format=%cI",
|
|
94
|
-
wt.branchName,
|
|
95
|
-
"--"
|
|
96
|
-
]);
|
|
97
|
-
const trimmed = output.trim();
|
|
98
|
-
return { ...wt, lastCommitAt: trimmed || null };
|
|
99
|
-
} catch {
|
|
100
|
-
return wt;
|
|
101
|
-
}
|
|
102
|
-
})
|
|
103
|
-
);
|
|
104
|
-
}
|
|
105
|
-
function slugifyForBranch(title) {
|
|
106
|
-
return title.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 60);
|
|
107
|
-
}
|
|
108
|
-
async function createWorktree(projectId, projectRoot, initiativeId, initiativeTitle) {
|
|
109
|
-
const existing = await getWorktreeForInitiative(projectId, initiativeId);
|
|
110
|
-
if (existing !== null) {
|
|
111
|
-
throw new Error(
|
|
112
|
-
`A worktree already exists for "${initiativeTitle}" at ${existing.worktreePath}`
|
|
113
|
-
);
|
|
114
|
-
}
|
|
115
|
-
await query(
|
|
116
|
-
`DELETE FROM worktrees
|
|
117
|
-
WHERE project_id = $1 AND initiative_id = $2 AND status IN ('removed', 'merged')`,
|
|
118
|
-
[projectId, initiativeId]
|
|
119
|
-
);
|
|
120
|
-
const slug = slugifyForBranch(initiativeTitle);
|
|
121
|
-
const branchName = slug;
|
|
122
|
-
const projectDir = path.basename(projectRoot);
|
|
123
|
-
const parentDir = path.dirname(projectRoot);
|
|
124
|
-
const worktreeDir = path.join(parentDir, `${projectDir}--${slug}`);
|
|
125
|
-
try {
|
|
126
|
-
await runGit(projectRoot, ["rev-parse", "--verify", branchName]);
|
|
127
|
-
} catch {
|
|
128
|
-
await runGit(projectRoot, ["branch", branchName]);
|
|
129
|
-
}
|
|
130
|
-
await runGit(projectRoot, ["worktree", "add", worktreeDir, branchName]);
|
|
131
|
-
const rows = await query(
|
|
132
|
-
`
|
|
133
|
-
WITH inserted AS (
|
|
134
|
-
INSERT INTO worktrees (project_id, initiative_id, branch_name, worktree_path)
|
|
135
|
-
VALUES ($1, $2, $3, $4)
|
|
136
|
-
RETURNING *
|
|
137
|
-
)
|
|
138
|
-
SELECT inserted.*, i.title AS initiative_title
|
|
139
|
-
FROM inserted
|
|
140
|
-
JOIN initiatives i ON i.id = inserted.initiative_id
|
|
141
|
-
`,
|
|
142
|
-
[projectId, initiativeId, branchName, worktreeDir]
|
|
143
|
-
);
|
|
144
|
-
if (rows.length === 0) {
|
|
145
|
-
throw new Error("Could not record the new worktree.");
|
|
146
|
-
}
|
|
147
|
-
const record = rowToWithInitiative(rows[0]);
|
|
148
|
-
await logGitActivity({
|
|
149
|
-
projectId,
|
|
150
|
-
initiativeId,
|
|
151
|
-
worktreeId: record.id,
|
|
152
|
-
eventType: "worktree_created",
|
|
153
|
-
summary: `Created worktree for "${initiativeTitle}"`,
|
|
154
|
-
plainEnglish: `Set up a separate working area for the "${initiativeTitle}" initiative at ${worktreeDir}. The agent can work here without affecting the main codebase.`,
|
|
155
|
-
branch: branchName,
|
|
156
|
-
source: "system"
|
|
157
|
-
});
|
|
158
|
-
return record;
|
|
159
|
-
}
|
|
160
|
-
async function listWorktrees(projectId, opts) {
|
|
161
|
-
const conditions = ["w.project_id = $1"];
|
|
162
|
-
const params = [projectId];
|
|
163
|
-
let paramIndex = 2;
|
|
164
|
-
if (opts?.status) {
|
|
165
|
-
conditions.push(`w.status = $${paramIndex}`);
|
|
166
|
-
params.push(opts.status);
|
|
167
|
-
paramIndex++;
|
|
168
|
-
}
|
|
169
|
-
if (opts?.initiativeId) {
|
|
170
|
-
conditions.push(`w.initiative_id = $${paramIndex}`);
|
|
171
|
-
params.push(opts.initiativeId);
|
|
172
|
-
paramIndex++;
|
|
173
|
-
}
|
|
174
|
-
const rows = await query(
|
|
175
|
-
`
|
|
176
|
-
SELECT w.*, i.title AS initiative_title,
|
|
177
|
-
(
|
|
178
|
-
SELECT d.title FROM deliverables d
|
|
179
|
-
JOIN outcomes o ON o.id = d.outcome_id
|
|
180
|
-
WHERE o.initiative_id = w.initiative_id
|
|
181
|
-
AND d.status = 'in_progress'
|
|
182
|
-
LIMIT 1
|
|
183
|
-
) AS deliverable_title
|
|
184
|
-
FROM worktrees w
|
|
185
|
-
LEFT JOIN initiatives i ON i.id = w.initiative_id
|
|
186
|
-
WHERE ${conditions.join(" AND ")}
|
|
187
|
-
ORDER BY w.created_at DESC
|
|
188
|
-
`,
|
|
189
|
-
params
|
|
190
|
-
);
|
|
191
|
-
return rows.map(rowToWithInitiative);
|
|
192
|
-
}
|
|
193
|
-
async function getWorktreeForInitiative(projectId, initiativeId) {
|
|
194
|
-
const rows = await query(
|
|
195
|
-
`
|
|
196
|
-
SELECT * FROM worktrees
|
|
197
|
-
WHERE project_id = $1 AND initiative_id = $2 AND status = 'active'
|
|
198
|
-
LIMIT 1
|
|
199
|
-
`,
|
|
200
|
-
[projectId, initiativeId]
|
|
201
|
-
);
|
|
202
|
-
if (rows.length === 0) return null;
|
|
203
|
-
return rowToRecord(rows[0]);
|
|
204
|
-
}
|
|
205
|
-
async function getWorktreeByBranch(projectId, branchName) {
|
|
206
|
-
const rows = await query(
|
|
207
|
-
`
|
|
208
|
-
SELECT w.*, i.title AS initiative_title
|
|
209
|
-
FROM worktrees w
|
|
210
|
-
LEFT JOIN initiatives i ON i.id = w.initiative_id
|
|
211
|
-
WHERE w.project_id = $1 AND w.branch_name = $2 AND w.status = 'active'
|
|
212
|
-
LIMIT 1
|
|
213
|
-
`,
|
|
214
|
-
[projectId, branchName]
|
|
215
|
-
);
|
|
216
|
-
if (rows.length === 0) return null;
|
|
217
|
-
return rowToWithInitiative(rows[0]);
|
|
218
|
-
}
|
|
219
|
-
async function mergeWorktree(projectId, projectRoot, worktreeId, defaultBranch) {
|
|
220
|
-
const rows = await query(
|
|
221
|
-
`
|
|
222
|
-
SELECT w.*, i.title AS initiative_title
|
|
223
|
-
FROM worktrees w
|
|
224
|
-
LEFT JOIN initiatives i ON i.id = w.initiative_id
|
|
225
|
-
WHERE w.id = $1 AND w.project_id = $2 AND w.status = 'active'
|
|
226
|
-
LIMIT 1
|
|
227
|
-
`,
|
|
228
|
-
[worktreeId, projectId]
|
|
229
|
-
);
|
|
230
|
-
if (rows.length === 0) {
|
|
231
|
-
throw new Error("That worktree is not active or could not be found.");
|
|
232
|
-
}
|
|
233
|
-
const wt = rowToWithInitiative(rows[0]);
|
|
234
|
-
await runGit(projectRoot, ["switch", defaultBranch]);
|
|
235
|
-
const output = await runGit(projectRoot, ["merge", "--no-ff", wt.branchName]);
|
|
236
|
-
await query(
|
|
237
|
-
`UPDATE worktrees SET status = 'merged', merged_at = now() WHERE id = $1`,
|
|
238
|
-
[worktreeId]
|
|
239
|
-
);
|
|
240
|
-
await logGitActivity({
|
|
241
|
-
projectId,
|
|
242
|
-
initiativeId: wt.initiativeId,
|
|
243
|
-
worktreeId: wt.id,
|
|
244
|
-
eventType: "merge",
|
|
245
|
-
summary: `Merged "${wt.initiativeTitle}" into ${defaultBranch}`,
|
|
246
|
-
plainEnglish: `The "${wt.initiativeTitle}" initiative has been merged into ${defaultBranch}. All changes from that initiative are now part of the main codebase.`,
|
|
247
|
-
branch: defaultBranch,
|
|
248
|
-
source: "system"
|
|
249
|
-
});
|
|
250
|
-
return { output };
|
|
251
|
-
}
|
|
252
|
-
async function removeWorktree(projectId, projectRoot, worktreeId) {
|
|
253
|
-
const rows = await query(
|
|
254
|
-
`
|
|
255
|
-
SELECT w.*, i.title AS initiative_title
|
|
256
|
-
FROM worktrees w
|
|
257
|
-
LEFT JOIN initiatives i ON i.id = w.initiative_id
|
|
258
|
-
WHERE w.id = $1 AND w.project_id = $2
|
|
259
|
-
LIMIT 1
|
|
260
|
-
`,
|
|
261
|
-
[worktreeId, projectId]
|
|
262
|
-
);
|
|
263
|
-
if (rows.length === 0) {
|
|
264
|
-
throw new Error("That worktree could not be found.");
|
|
265
|
-
}
|
|
266
|
-
const wt = rowToWithInitiative(rows[0]);
|
|
267
|
-
try {
|
|
268
|
-
await runGit(projectRoot, ["worktree", "remove", wt.worktreePath, "--force"]);
|
|
269
|
-
} catch {
|
|
270
|
-
}
|
|
271
|
-
await query(
|
|
272
|
-
`UPDATE worktrees SET status = 'removed', removed_at = now() WHERE id = $1`,
|
|
273
|
-
[worktreeId]
|
|
274
|
-
);
|
|
275
|
-
await logGitActivity({
|
|
276
|
-
projectId,
|
|
277
|
-
initiativeId: wt.initiativeId,
|
|
278
|
-
worktreeId: wt.id,
|
|
279
|
-
eventType: "worktree_removed",
|
|
280
|
-
summary: `Removed worktree for "${wt.initiativeTitle}"`,
|
|
281
|
-
plainEnglish: `Cleaned up the working area for "${wt.initiativeTitle}". The branch still exists in git if you need it.`,
|
|
282
|
-
branch: wt.branchName,
|
|
283
|
-
source: "system"
|
|
284
|
-
});
|
|
285
|
-
}
|
|
286
|
-
function parseWorktreeListPorcelain(output) {
|
|
287
|
-
const entries = [];
|
|
288
|
-
const blocks = output.trim().split("\n\n");
|
|
289
|
-
for (const block of blocks) {
|
|
290
|
-
const lines = block.trim().split("\n");
|
|
291
|
-
let wtPath = "";
|
|
292
|
-
let head = "";
|
|
293
|
-
let branch = null;
|
|
294
|
-
let bare = false;
|
|
295
|
-
for (const line of lines) {
|
|
296
|
-
if (line.startsWith("worktree ")) {
|
|
297
|
-
wtPath = line.slice("worktree ".length);
|
|
298
|
-
} else if (line.startsWith("HEAD ")) {
|
|
299
|
-
head = line.slice("HEAD ".length);
|
|
300
|
-
} else if (line.startsWith("branch ")) {
|
|
301
|
-
const ref = line.slice("branch ".length);
|
|
302
|
-
branch = ref.replace("refs/heads/", "");
|
|
303
|
-
} else if (line === "bare") {
|
|
304
|
-
bare = true;
|
|
305
|
-
}
|
|
306
|
-
}
|
|
307
|
-
if (wtPath) {
|
|
308
|
-
entries.push({ path: wtPath, head, branch, bare });
|
|
309
|
-
}
|
|
310
|
-
}
|
|
311
|
-
return entries;
|
|
312
|
-
}
|
|
313
|
-
async function scanWorktrees(projectId, projectRoot) {
|
|
314
|
-
let mainRepoRoot = projectRoot;
|
|
315
|
-
try {
|
|
316
|
-
const commonDir = (await runGit(projectRoot, ["rev-parse", "--git-common-dir"])).trim();
|
|
317
|
-
if (path.isAbsolute(commonDir) && commonDir.endsWith(".git")) {
|
|
318
|
-
mainRepoRoot = path.dirname(commonDir);
|
|
319
|
-
}
|
|
320
|
-
} catch {
|
|
321
|
-
}
|
|
322
|
-
const output = await runGit(mainRepoRoot, ["worktree", "list", "--porcelain"]);
|
|
323
|
-
const diskWorktrees = parseWorktreeListPorcelain(output);
|
|
324
|
-
const additionalWorktrees = diskWorktrees.filter(
|
|
325
|
-
(wt) => !wt.bare && wt.path !== mainRepoRoot
|
|
326
|
-
);
|
|
327
|
-
const diskPaths = new Set(additionalWorktrees.map((wt) => wt.path));
|
|
328
|
-
const dbRows = await query(
|
|
329
|
-
`SELECT w.id, w.branch_name, w.worktree_path, i.title AS initiative_title,
|
|
330
|
-
w.status, w.source
|
|
331
|
-
FROM worktrees w
|
|
332
|
-
LEFT JOIN initiatives i ON i.id = w.initiative_id
|
|
333
|
-
WHERE w.project_id = $1 AND w.status = 'active'`,
|
|
334
|
-
[projectId]
|
|
335
|
-
);
|
|
336
|
-
const registeredPaths = new Set(dbRows.map((r) => r.worktree_path));
|
|
337
|
-
const entries = [];
|
|
338
|
-
let newlyDetected = 0;
|
|
339
|
-
let removedFromDisk = 0;
|
|
340
|
-
for (const wt of additionalWorktrees) {
|
|
341
|
-
const isNew = !registeredPaths.has(wt.path);
|
|
342
|
-
if (isNew) {
|
|
343
|
-
await query(
|
|
344
|
-
`INSERT INTO worktrees (project_id, initiative_id, branch_name, worktree_path, source)
|
|
345
|
-
VALUES ($1, NULL, $2, $3, 'auto')
|
|
346
|
-
ON CONFLICT (project_id, branch_name) DO NOTHING`,
|
|
347
|
-
[projectId, wt.branch ?? `detached-${wt.head.slice(0, 8)}`, wt.path]
|
|
348
|
-
);
|
|
349
|
-
newlyDetected++;
|
|
350
|
-
}
|
|
351
|
-
let lastCommitAt = null;
|
|
352
|
-
try {
|
|
353
|
-
const logOutput = await runGit(wt.path, ["log", "-1", "--format=%cI"]);
|
|
354
|
-
lastCommitAt = logOutput.trim() || null;
|
|
355
|
-
} catch {
|
|
356
|
-
}
|
|
357
|
-
entries.push({
|
|
358
|
-
branchName: wt.branch ?? `detached-${wt.head.slice(0, 8)}`,
|
|
359
|
-
worktreePath: wt.path,
|
|
360
|
-
initiativeTitle: isNew ? null : dbRows.find((r) => r.worktree_path === wt.path)?.initiative_title ?? null,
|
|
361
|
-
status: "active",
|
|
362
|
-
source: isNew ? "auto" : dbRows.find((r) => r.worktree_path === wt.path)?.source ?? "auto",
|
|
363
|
-
lastCommitAt,
|
|
364
|
-
removedFromDisk: false,
|
|
365
|
-
newlyRegistered: isNew
|
|
366
|
-
});
|
|
367
|
-
}
|
|
368
|
-
for (const row of dbRows) {
|
|
369
|
-
if (!diskPaths.has(row.worktree_path) && !existsSync(row.worktree_path)) {
|
|
370
|
-
await query(
|
|
371
|
-
`UPDATE worktrees SET status = 'removed', removed_at = now() WHERE id = $1`,
|
|
372
|
-
[row.id]
|
|
373
|
-
);
|
|
374
|
-
removedFromDisk++;
|
|
375
|
-
entries.push({
|
|
376
|
-
branchName: row.branch_name,
|
|
377
|
-
worktreePath: row.worktree_path,
|
|
378
|
-
initiativeTitle: row.initiative_title,
|
|
379
|
-
status: "removed",
|
|
380
|
-
source: row.source,
|
|
381
|
-
lastCommitAt: null,
|
|
382
|
-
removedFromDisk: true,
|
|
383
|
-
newlyRegistered: false
|
|
384
|
-
});
|
|
385
|
-
}
|
|
386
|
-
}
|
|
387
|
-
return {
|
|
388
|
-
registered: registeredPaths.size,
|
|
389
|
-
newlyDetected,
|
|
390
|
-
removedFromDisk,
|
|
391
|
-
total: registeredPaths.size + newlyDetected,
|
|
392
|
-
worktrees: entries
|
|
393
|
-
};
|
|
394
|
-
}
|
|
395
|
-
async function checkStaleness(projectId) {
|
|
396
|
-
const worktrees = await listWorktrees(projectId, { status: "active" });
|
|
397
|
-
const stale = [];
|
|
398
|
-
const now = Date.now();
|
|
399
|
-
for (const wt of worktrees) {
|
|
400
|
-
let lastCommitAt = null;
|
|
401
|
-
let directoryMissing = false;
|
|
402
|
-
if (!existsSync(wt.worktreePath)) {
|
|
403
|
-
directoryMissing = true;
|
|
404
|
-
} else {
|
|
405
|
-
try {
|
|
406
|
-
const output = await runGit(wt.worktreePath, ["log", "-1", "--format=%cI"]);
|
|
407
|
-
lastCommitAt = output.trim() || null;
|
|
408
|
-
} catch {
|
|
409
|
-
}
|
|
410
|
-
}
|
|
411
|
-
const commitTime = lastCommitAt ? new Date(lastCommitAt).getTime() : 0;
|
|
412
|
-
const msSinceCommit = lastCommitAt ? now - commitTime : Infinity;
|
|
413
|
-
const daysSinceCommit = Math.floor(msSinceCommit / (24 * 60 * 60 * 1e3));
|
|
414
|
-
if (directoryMissing || msSinceCommit >= STALE_MS) {
|
|
415
|
-
stale.push({
|
|
416
|
-
id: wt.id,
|
|
417
|
-
path: wt.worktreePath,
|
|
418
|
-
branch: wt.branchName,
|
|
419
|
-
lastCommitAt,
|
|
420
|
-
daysSinceCommit,
|
|
421
|
-
directoryMissing
|
|
422
|
-
});
|
|
423
|
-
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.`;
|
|
424
|
-
try {
|
|
425
|
-
await createNotification({
|
|
426
|
-
projectId,
|
|
427
|
-
eventType: "stale_worktree",
|
|
428
|
-
message,
|
|
429
|
-
priority: directoryMissing ? "action_needed" : "warning",
|
|
430
|
-
linkType: wt.initiativeId ? "initiative" : void 0,
|
|
431
|
-
linkId: wt.initiativeId ?? void 0
|
|
432
|
-
});
|
|
433
|
-
} catch {
|
|
434
|
-
}
|
|
435
|
-
}
|
|
436
|
-
}
|
|
437
|
-
return stale;
|
|
438
|
-
}
|
|
439
|
-
async function listWorktreesWithStaleness(projectId) {
|
|
440
|
-
const worktrees = await listWorktrees(projectId, { status: "active" });
|
|
441
|
-
const now = Date.now();
|
|
442
|
-
const results = [];
|
|
443
|
-
for (const wt of worktrees) {
|
|
444
|
-
let lastCommitAt = null;
|
|
445
|
-
let daysSinceCommit = null;
|
|
446
|
-
if (existsSync(wt.worktreePath)) {
|
|
447
|
-
try {
|
|
448
|
-
const output = await runGit(wt.worktreePath, ["log", "-1", "--format=%cI"]);
|
|
449
|
-
lastCommitAt = output.trim() || null;
|
|
450
|
-
if (lastCommitAt) {
|
|
451
|
-
const commitTime = new Date(lastCommitAt).getTime();
|
|
452
|
-
daysSinceCommit = Math.floor((now - commitTime) / (24 * 60 * 60 * 1e3));
|
|
453
|
-
}
|
|
454
|
-
} catch {
|
|
455
|
-
}
|
|
456
|
-
}
|
|
457
|
-
const stale = daysSinceCommit !== null ? daysSinceCommit >= STALE_DAYS : true;
|
|
458
|
-
results.push({
|
|
459
|
-
...wt,
|
|
460
|
-
stale,
|
|
461
|
-
lastCommitAt,
|
|
462
|
-
daysSinceCommit,
|
|
463
|
-
source: wt.source ?? "manual"
|
|
464
|
-
});
|
|
465
|
-
}
|
|
466
|
-
return results;
|
|
467
|
-
}
|
|
468
|
-
|
|
469
|
-
export {
|
|
470
|
-
logGitActivity,
|
|
471
|
-
enrichWithLastCommit,
|
|
472
|
-
slugifyForBranch,
|
|
473
|
-
createWorktree,
|
|
474
|
-
listWorktrees,
|
|
475
|
-
getWorktreeForInitiative,
|
|
476
|
-
getWorktreeByBranch,
|
|
477
|
-
mergeWorktree,
|
|
478
|
-
removeWorktree,
|
|
479
|
-
scanWorktrees,
|
|
480
|
-
checkStaleness,
|
|
481
|
-
listWorktreesWithStaleness
|
|
482
|
-
};
|
package/dist/chunk-PANC6BTV.js
DELETED
|
@@ -1,151 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
3
|
-
ConductorError
|
|
4
|
-
} from "./chunk-4YEHSYVN.js";
|
|
5
|
-
|
|
6
|
-
// ../../src/core/db.ts
|
|
7
|
-
import { Pool } from "pg";
|
|
8
|
-
var pool = null;
|
|
9
|
-
function getDatabaseUrl() {
|
|
10
|
-
const url = process.env.DATABASE_URL;
|
|
11
|
-
if (!url) {
|
|
12
|
-
throw new ConductorError(
|
|
13
|
-
"DB_UNREACHABLE" /* DB_UNREACHABLE */,
|
|
14
|
-
"DATABASE_URL is not set. Set it to your Supabase (or other Postgres) connection string."
|
|
15
|
-
);
|
|
16
|
-
}
|
|
17
|
-
return url;
|
|
18
|
-
}
|
|
19
|
-
function getPool() {
|
|
20
|
-
if (pool) {
|
|
21
|
-
return pool;
|
|
22
|
-
}
|
|
23
|
-
const connectionString = getDatabaseUrl();
|
|
24
|
-
const isRemote = connectionString.includes("supabase.co");
|
|
25
|
-
pool = new Pool({
|
|
26
|
-
connectionString,
|
|
27
|
-
max: 10,
|
|
28
|
-
connectionTimeoutMillis: 5e3,
|
|
29
|
-
idleTimeoutMillis: 3e4,
|
|
30
|
-
...isRemote ? { ssl: { rejectUnauthorized: false } } : {}
|
|
31
|
-
});
|
|
32
|
-
pool.on("error", () => {
|
|
33
|
-
console.error(
|
|
34
|
-
"Unexpected database error on an idle connection. Check that Postgres is running."
|
|
35
|
-
);
|
|
36
|
-
});
|
|
37
|
-
return pool;
|
|
38
|
-
}
|
|
39
|
-
var TRANSIENT_CODES = /* @__PURE__ */ new Set(["ECONNRESET", "ECONNREFUSED", "ETIMEDOUT"]);
|
|
40
|
-
function isTransientConnectionError(err) {
|
|
41
|
-
const code = getErrorCode(err);
|
|
42
|
-
if (code && TRANSIENT_CODES.has(code)) return true;
|
|
43
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
44
|
-
return msg.includes("ECONNRESET") || msg.includes("ECONNREFUSED") || msg.includes("ETIMEDOUT") || msg.includes("connection terminated unexpectedly");
|
|
45
|
-
}
|
|
46
|
-
async function query(sql, params) {
|
|
47
|
-
const p = getPool();
|
|
48
|
-
try {
|
|
49
|
-
const result = await p.query(sql, params);
|
|
50
|
-
return result.rows;
|
|
51
|
-
} catch (err) {
|
|
52
|
-
if (isTransientConnectionError(err)) {
|
|
53
|
-
await new Promise((r) => setTimeout(r, 1e3));
|
|
54
|
-
try {
|
|
55
|
-
const result = await p.query(sql, params);
|
|
56
|
-
return result.rows;
|
|
57
|
-
} catch (retryErr) {
|
|
58
|
-
throw mapDbError(retryErr);
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
throw mapDbError(err);
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
async function getClient() {
|
|
65
|
-
const p = getPool();
|
|
66
|
-
try {
|
|
67
|
-
return await p.connect();
|
|
68
|
-
} catch (err) {
|
|
69
|
-
if (isTransientConnectionError(err)) {
|
|
70
|
-
await new Promise((r) => setTimeout(r, 1e3));
|
|
71
|
-
try {
|
|
72
|
-
return await p.connect();
|
|
73
|
-
} catch (retryErr) {
|
|
74
|
-
throw mapDbError(retryErr);
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
throw mapDbError(err);
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
async function isDbReachable() {
|
|
81
|
-
try {
|
|
82
|
-
const p = getPool();
|
|
83
|
-
const result = await p.query("SELECT 1");
|
|
84
|
-
return result.rows.length > 0;
|
|
85
|
-
} catch {
|
|
86
|
-
return false;
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
async function closePool() {
|
|
90
|
-
if (pool) {
|
|
91
|
-
await pool.end();
|
|
92
|
-
pool = null;
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
function getErrorCode(err) {
|
|
96
|
-
if (err !== null && typeof err === "object" && "code" in err) {
|
|
97
|
-
const code = err.code;
|
|
98
|
-
return typeof code === "string" ? code : void 0;
|
|
99
|
-
}
|
|
100
|
-
return void 0;
|
|
101
|
-
}
|
|
102
|
-
function getBestErrorMessage(err) {
|
|
103
|
-
if (err instanceof AggregateError && err.errors.length > 0) {
|
|
104
|
-
const first = err.errors[0];
|
|
105
|
-
if (first instanceof Error && first.message.trim().length > 0) {
|
|
106
|
-
return first.message;
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
if (err instanceof Error && err.message.trim().length > 0) {
|
|
110
|
-
return err.message;
|
|
111
|
-
}
|
|
112
|
-
return String(err);
|
|
113
|
-
}
|
|
114
|
-
function mapDbError(err) {
|
|
115
|
-
const code = getErrorCode(err);
|
|
116
|
-
const message = getBestErrorMessage(err);
|
|
117
|
-
if (code === "ECONNREFUSED" || code === "ECONNRESET" || code === "ETIMEDOUT" || message.includes("ECONNREFUSED") || message.includes("ECONNRESET") || message.includes("ETIMEDOUT") || message.includes("connect ECONNREFUSED") || message.includes("connection terminated unexpectedly")) {
|
|
118
|
-
return new ConductorError(
|
|
119
|
-
"DB_UNREACHABLE" /* DB_UNREACHABLE */,
|
|
120
|
-
"Could not reach the database. Postgres may not be running on this machine.",
|
|
121
|
-
true
|
|
122
|
-
);
|
|
123
|
-
}
|
|
124
|
-
if (message.includes("password") || message.includes("authentication") || message.includes("28P01")) {
|
|
125
|
-
return new ConductorError(
|
|
126
|
-
"DB_QUERY_FAILED" /* DB_QUERY_FAILED */,
|
|
127
|
-
"Could not sign in to the database. Check your username and password."
|
|
128
|
-
);
|
|
129
|
-
}
|
|
130
|
-
if (message.includes("does not exist") && message.includes("database")) {
|
|
131
|
-
return new ConductorError(
|
|
132
|
-
"DB_QUERY_FAILED" /* DB_QUERY_FAILED */,
|
|
133
|
-
"That database does not exist yet. Create it first, then try again."
|
|
134
|
-
);
|
|
135
|
-
}
|
|
136
|
-
if (message.trim().length > 0) {
|
|
137
|
-
return new ConductorError("DB_QUERY_FAILED" /* DB_QUERY_FAILED */, message);
|
|
138
|
-
}
|
|
139
|
-
return new ConductorError(
|
|
140
|
-
"DB_QUERY_FAILED" /* DB_QUERY_FAILED */,
|
|
141
|
-
"Something went wrong while talking to the database."
|
|
142
|
-
);
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
export {
|
|
146
|
-
getPool,
|
|
147
|
-
query,
|
|
148
|
-
getClient,
|
|
149
|
-
isDbReachable,
|
|
150
|
-
closePool
|
|
151
|
-
};
|
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
3
|
-
ensureDatabaseUrl,
|
|
4
|
-
fetchConfigFromServer,
|
|
5
|
-
getServerBaseUrl,
|
|
6
|
-
postToServer,
|
|
7
|
-
readApiKey,
|
|
8
|
-
readConfig,
|
|
9
|
-
readProjectId,
|
|
10
|
-
writeConfig
|
|
11
|
-
} from "./chunk-B2WDTKD7.js";
|
|
12
|
-
export {
|
|
13
|
-
ensureDatabaseUrl,
|
|
14
|
-
fetchConfigFromServer,
|
|
15
|
-
getServerBaseUrl,
|
|
16
|
-
postToServer,
|
|
17
|
-
readApiKey,
|
|
18
|
-
readConfig,
|
|
19
|
-
readProjectId,
|
|
20
|
-
writeConfig
|
|
21
|
-
};
|