@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.
@@ -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
+ };