bosun 0.27.0 → 0.27.2
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/autofix.mjs +1 -1
- package/bosun.schema.json +47 -0
- package/cli.mjs +152 -4
- package/compat.mjs +54 -8
- package/config.mjs +12 -6
- package/package.json +3 -1
- package/publish.mjs +2 -4
- package/setup.mjs +120 -42
- package/task-store.mjs +3 -0
- package/ui/app.js +17 -12
- package/ui/components/kanban-board.js +48 -5
- package/ui/components/workspace-switcher.js +121 -0
- package/ui/demo.html +635 -463
- package/ui/index.html +1 -0
- package/ui/styles/base.css +3 -3
- package/ui/styles/components.css +1428 -399
- package/ui/styles/layout.css +143 -79
- package/ui/styles/variables.css +81 -74
- package/ui/styles/workspace-switcher.css +142 -0
- package/ui/tabs/agents.js +326 -181
- package/ui/tabs/control.js +432 -385
- package/ui/tabs/dashboard.js +356 -156
- package/ui/tabs/tasks.js +206 -55
- package/ui-server.mjs +152 -0
- package/update-check.mjs +6 -0
- package/workspace-manager.mjs +527 -0
package/autofix.mjs
CHANGED
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
*/
|
|
29
29
|
|
|
30
30
|
import { spawn, execSync } from "node:child_process";
|
|
31
|
-
import { existsSync, mkdirSync, createWriteStream } from "node:fs";
|
|
31
|
+
import { existsSync, mkdirSync, createWriteStream, readFileSync } from "node:fs";
|
|
32
32
|
import { readFile, writeFile } from "node:fs/promises";
|
|
33
33
|
import { resolve, dirname } from "node:path";
|
|
34
34
|
import { fileURLToPath } from "node:url";
|
package/bosun.schema.json
CHANGED
|
@@ -221,6 +221,53 @@
|
|
|
221
221
|
"type": "string",
|
|
222
222
|
"enum": ["codex-sdk", "kanban", "disabled"]
|
|
223
223
|
},
|
|
224
|
+
"activeWorkspace": {
|
|
225
|
+
"type": "string",
|
|
226
|
+
"description": "ID of the currently active workspace"
|
|
227
|
+
},
|
|
228
|
+
"workspaces": {
|
|
229
|
+
"type": "array",
|
|
230
|
+
"description": "Multi-repo workspace definitions",
|
|
231
|
+
"items": {
|
|
232
|
+
"type": "object",
|
|
233
|
+
"additionalProperties": true,
|
|
234
|
+
"required": ["id", "name"],
|
|
235
|
+
"properties": {
|
|
236
|
+
"id": {
|
|
237
|
+
"type": "string",
|
|
238
|
+
"description": "Unique workspace identifier (lowercase, alphanumeric, dashes)"
|
|
239
|
+
},
|
|
240
|
+
"name": {
|
|
241
|
+
"type": "string",
|
|
242
|
+
"description": "Human-readable workspace name"
|
|
243
|
+
},
|
|
244
|
+
"repos": {
|
|
245
|
+
"type": "array",
|
|
246
|
+
"description": "Repositories in this workspace",
|
|
247
|
+
"items": {
|
|
248
|
+
"type": "object",
|
|
249
|
+
"additionalProperties": true,
|
|
250
|
+
"required": ["name"],
|
|
251
|
+
"properties": {
|
|
252
|
+
"name": { "type": "string", "description": "Repository directory name" },
|
|
253
|
+
"url": { "type": "string", "description": "Git clone URL" },
|
|
254
|
+
"slug": { "type": "string", "description": "GitHub slug (org/repo)" },
|
|
255
|
+
"primary": { "type": "boolean", "description": "Whether this is the primary repo" },
|
|
256
|
+
"branch": { "type": "string", "description": "Default branch to track" }
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
},
|
|
260
|
+
"activeRepo": {
|
|
261
|
+
"type": ["string", "null"],
|
|
262
|
+
"description": "Currently active repository name within the workspace"
|
|
263
|
+
},
|
|
264
|
+
"createdAt": {
|
|
265
|
+
"type": "string",
|
|
266
|
+
"description": "ISO 8601 creation timestamp"
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
},
|
|
224
271
|
"defaultRepository": { "type": "string" },
|
|
225
272
|
"repositoryDefaults": { "$ref": "#/$defs/repositoryDefaults" },
|
|
226
273
|
"repositories": {
|
package/cli.mjs
CHANGED
|
@@ -24,7 +24,7 @@ import {
|
|
|
24
24
|
mkdirSync,
|
|
25
25
|
} from "node:fs";
|
|
26
26
|
import { fileURLToPath } from "node:url";
|
|
27
|
-
import { fork, spawn } from "node:child_process";
|
|
27
|
+
import { execFileSync, fork, spawn } from "node:child_process";
|
|
28
28
|
import os from "node:os";
|
|
29
29
|
import { createDaemonCrashTracker } from "./daemon-restart-policy.mjs";
|
|
30
30
|
import {
|
|
@@ -109,6 +109,12 @@ function showHelp() {
|
|
|
109
109
|
CONTAINER_ENABLED=1 Enable container isolation for agent execution
|
|
110
110
|
CONTAINER_RUNTIME=docker Runtime to use (docker|podman|container)
|
|
111
111
|
|
|
112
|
+
WORKSPACES
|
|
113
|
+
--workspace-list List configured workspaces
|
|
114
|
+
--workspace-add <name> Create a new workspace
|
|
115
|
+
--workspace-switch <id> Switch active workspace
|
|
116
|
+
--workspace-add-repo Add repo to workspace (interactive)
|
|
117
|
+
|
|
112
118
|
VIBE-KANBAN
|
|
113
119
|
--no-vk-spawn Don't auto-spawn Vibe-Kanban
|
|
114
120
|
--vk-ensure-interval <ms> VK health check interval (default: 60000)
|
|
@@ -329,7 +335,6 @@ function getDaemonPid() {
|
|
|
329
335
|
if (!existsSync(PID_FILE)) return null;
|
|
330
336
|
const pid = parseInt(readFileSync(PID_FILE, "utf8").trim(), 10);
|
|
331
337
|
if (isNaN(pid)) return null;
|
|
332
|
-
// Check if process is alive
|
|
333
338
|
try {
|
|
334
339
|
process.kill(pid, 0);
|
|
335
340
|
return pid;
|
|
@@ -341,6 +346,28 @@ function getDaemonPid() {
|
|
|
341
346
|
}
|
|
342
347
|
}
|
|
343
348
|
|
|
349
|
+
/**
|
|
350
|
+
* Scan for ghost bosun daemon-child processes that are alive but have no PID
|
|
351
|
+
* file (e.g. PID file was removed by compat migration). Uses pgrep on Linux/Mac.
|
|
352
|
+
* Returns an array of PIDs (may be empty).
|
|
353
|
+
*/
|
|
354
|
+
function findGhostDaemonPids() {
|
|
355
|
+
if (process.platform === "win32") return [];
|
|
356
|
+
try {
|
|
357
|
+
const out = execFileSync(
|
|
358
|
+
"pgrep",
|
|
359
|
+
["-f", "bosun.*--daemon-child|cli\.mjs.*--daemon-child"],
|
|
360
|
+
{ encoding: "utf8", stdio: ["pipe", "pipe", "pipe"], timeout: 3000 },
|
|
361
|
+
).trim();
|
|
362
|
+
return out
|
|
363
|
+
.split("\n")
|
|
364
|
+
.map((s) => parseInt(s.trim(), 10))
|
|
365
|
+
.filter((n) => Number.isFinite(n) && n > 0 && n !== process.pid);
|
|
366
|
+
} catch {
|
|
367
|
+
return [];
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
|
|
344
371
|
function writePidFile(pid) {
|
|
345
372
|
try {
|
|
346
373
|
mkdirSync(dirname(PID_FILE), { recursive: true });
|
|
@@ -366,6 +393,28 @@ function startDaemon() {
|
|
|
366
393
|
process.exit(1);
|
|
367
394
|
}
|
|
368
395
|
|
|
396
|
+
// Check for ghost processes that have no PID file (e.g. after compat migration
|
|
397
|
+
// deleted the old codex-monitor directory and its PID file with it).
|
|
398
|
+
const ghosts = findGhostDaemonPids();
|
|
399
|
+
if (ghosts.length > 0) {
|
|
400
|
+
console.log(` ⚠️ Found ${ghosts.length} ghost bosun daemon process(es) with no PID file: ${ghosts.join(", ")}`);
|
|
401
|
+
console.log(` Stopping ghost process(es) before starting fresh...`);
|
|
402
|
+
for (const gpid of ghosts) {
|
|
403
|
+
try { process.kill(gpid, "SIGTERM"); } catch { /* already dead */ }
|
|
404
|
+
}
|
|
405
|
+
// Give them a moment to exit
|
|
406
|
+
const deadline = Date.now() + 3000;
|
|
407
|
+
let alive = ghosts.filter((p) => isProcessAlive(p));
|
|
408
|
+
while (alive.length > 0 && Date.now() < deadline) {
|
|
409
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 200);
|
|
410
|
+
alive = alive.filter((p) => isProcessAlive(p));
|
|
411
|
+
}
|
|
412
|
+
for (const gpid of alive) {
|
|
413
|
+
try { process.kill(gpid, "SIGKILL"); } catch { /* ok */ }
|
|
414
|
+
}
|
|
415
|
+
console.log(` ✅ Ghost process(es) stopped.`);
|
|
416
|
+
}
|
|
417
|
+
|
|
369
418
|
// Ensure log directory exists
|
|
370
419
|
try {
|
|
371
420
|
mkdirSync(dirname(DAEMON_LOG), { recursive: true });
|
|
@@ -456,8 +505,16 @@ function daemonStatus() {
|
|
|
456
505
|
if (pid) {
|
|
457
506
|
console.log(` bosun daemon is running (PID ${pid})`);
|
|
458
507
|
} else {
|
|
459
|
-
|
|
460
|
-
|
|
508
|
+
// Check for ghost processes (alive but no PID file)
|
|
509
|
+
const ghosts = findGhostDaemonPids();
|
|
510
|
+
if (ghosts.length > 0) {
|
|
511
|
+
console.log(` ⚠️ bosun daemon is NOT tracked (no PID file), but ${ghosts.length} ghost process(es) found: ${ghosts.join(", ")}`);
|
|
512
|
+
console.log(` The daemon is likely running but its PID file was lost.`);
|
|
513
|
+
console.log(` Run --stop-daemon to clean up, then --daemon to restart.`);
|
|
514
|
+
} else {
|
|
515
|
+
console.log(" bosun daemon is not running.");
|
|
516
|
+
removePidFile();
|
|
517
|
+
}
|
|
461
518
|
}
|
|
462
519
|
process.exit(0);
|
|
463
520
|
}
|
|
@@ -667,6 +724,97 @@ async function main() {
|
|
|
667
724
|
// agent sessions that happen to have hook config files in their tree.
|
|
668
725
|
process.env.VE_MANAGED = "1";
|
|
669
726
|
|
|
727
|
+
// Handle workspace commands
|
|
728
|
+
if (args.includes("--workspace-list") || args.includes("workspace-list")) {
|
|
729
|
+
const { listWorkspaces, getActiveWorkspace } = await import("./workspace-manager.mjs");
|
|
730
|
+
const configDirArg = getArgValue("--config-dir");
|
|
731
|
+
const configDir = configDirArg || process.env.BOSUN_DIR || resolve(os.homedir(), "bosun");
|
|
732
|
+
const workspaces = listWorkspaces(configDir);
|
|
733
|
+
const active = getActiveWorkspace(configDir);
|
|
734
|
+
if (workspaces.length === 0) {
|
|
735
|
+
console.log("\n No workspaces configured. Run 'bosun --setup' to create one.\n");
|
|
736
|
+
} else {
|
|
737
|
+
console.log("\n Workspaces:");
|
|
738
|
+
for (const ws of workspaces) {
|
|
739
|
+
const marker = ws.id === active?.id ? " ← active" : "";
|
|
740
|
+
console.log(` ${ws.name} (${ws.id})${marker}`);
|
|
741
|
+
for (const repo of ws.repos || []) {
|
|
742
|
+
const primary = repo.primary ? " [primary]" : "";
|
|
743
|
+
const exists = repo.exists ? "✓" : "✗";
|
|
744
|
+
console.log(` ${exists} ${repo.name} — ${repo.slug || repo.url || "local"}${primary}`);
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
console.log("");
|
|
748
|
+
}
|
|
749
|
+
process.exit(0);
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
if (args.includes("--workspace-add")) {
|
|
753
|
+
const { createWorkspace } = await import("./workspace-manager.mjs");
|
|
754
|
+
const configDirArg = getArgValue("--config-dir");
|
|
755
|
+
const configDir = configDirArg || process.env.BOSUN_DIR || resolve(os.homedir(), "bosun");
|
|
756
|
+
const name = getArgValue("--workspace-add");
|
|
757
|
+
if (!name) {
|
|
758
|
+
console.error(" Error: workspace name is required. Usage: bosun --workspace-add <name>");
|
|
759
|
+
process.exit(1);
|
|
760
|
+
}
|
|
761
|
+
try {
|
|
762
|
+
const ws = createWorkspace(configDir, { name });
|
|
763
|
+
console.log(`\n ✓ Workspace "${ws.name}" created at ${ws.path}\n`);
|
|
764
|
+
} catch (err) {
|
|
765
|
+
console.error(` Error: ${err.message}`);
|
|
766
|
+
process.exit(1);
|
|
767
|
+
}
|
|
768
|
+
process.exit(0);
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
if (args.includes("--workspace-switch")) {
|
|
772
|
+
const { setActiveWorkspace, getWorkspace } = await import("./workspace-manager.mjs");
|
|
773
|
+
const configDirArg = getArgValue("--config-dir");
|
|
774
|
+
const configDir = configDirArg || process.env.BOSUN_DIR || resolve(os.homedir(), "bosun");
|
|
775
|
+
const wsId = getArgValue("--workspace-switch");
|
|
776
|
+
if (!wsId) {
|
|
777
|
+
console.error(" Error: workspace ID required. Usage: bosun --workspace-switch <id>");
|
|
778
|
+
process.exit(1);
|
|
779
|
+
}
|
|
780
|
+
try {
|
|
781
|
+
setActiveWorkspace(configDir, wsId);
|
|
782
|
+
const ws = getWorkspace(configDir, wsId);
|
|
783
|
+
console.log(`\n ✓ Switched to workspace "${ws?.name || wsId}"\n`);
|
|
784
|
+
} catch (err) {
|
|
785
|
+
console.error(` Error: ${err.message}`);
|
|
786
|
+
process.exit(1);
|
|
787
|
+
}
|
|
788
|
+
process.exit(0);
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
if (args.includes("--workspace-add-repo")) {
|
|
792
|
+
const { addRepoToWorkspace, getActiveWorkspace, listWorkspaces } = await import("./workspace-manager.mjs");
|
|
793
|
+
const configDirArg = getArgValue("--config-dir");
|
|
794
|
+
const configDir = configDirArg || process.env.BOSUN_DIR || resolve(os.homedir(), "bosun");
|
|
795
|
+
const active = getActiveWorkspace(configDir);
|
|
796
|
+
if (!active) {
|
|
797
|
+
console.error(" No active workspace. Create one first: bosun --workspace-add <name>");
|
|
798
|
+
process.exit(1);
|
|
799
|
+
}
|
|
800
|
+
const url = getArgValue("--workspace-add-repo");
|
|
801
|
+
if (!url) {
|
|
802
|
+
console.error(" Error: repo URL required. Usage: bosun --workspace-add-repo <git-url>");
|
|
803
|
+
process.exit(1);
|
|
804
|
+
}
|
|
805
|
+
try {
|
|
806
|
+
console.log(` Cloning into workspace "${active.name}"...`);
|
|
807
|
+
const repo = addRepoToWorkspace(configDir, active.id, { url });
|
|
808
|
+
console.log(`\n ✓ Added repo "${repo.name}" to workspace "${active.name}"`);
|
|
809
|
+
if (repo.cloned) console.log(` Cloned to: ${repo.path}`);
|
|
810
|
+
console.log("");
|
|
811
|
+
} catch (err) {
|
|
812
|
+
console.error(` Error: ${err.message}`);
|
|
813
|
+
process.exit(1);
|
|
814
|
+
}
|
|
815
|
+
process.exit(0);
|
|
816
|
+
}
|
|
817
|
+
|
|
670
818
|
// Handle --setup
|
|
671
819
|
if (args.includes("--setup") || args.includes("setup")) {
|
|
672
820
|
const configDirArg = getArgValue("--config-dir");
|
package/compat.mjs
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
* should be imported as early as possible in the startup path.
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
|
-
import { existsSync, readdirSync, readFileSync, writeFileSync, mkdirSync, copyFileSync } from "node:fs";
|
|
14
|
+
import { existsSync, readdirSync, readFileSync, writeFileSync, mkdirSync, copyFileSync, rmSync } from "node:fs";
|
|
15
15
|
import { resolve, join } from "node:path";
|
|
16
16
|
|
|
17
17
|
// ── Legacy config file names accepted from old codex-monitor installations ───
|
|
@@ -255,12 +255,29 @@ function rewriteEnvContent(content) {
|
|
|
255
255
|
}
|
|
256
256
|
|
|
257
257
|
/**
|
|
258
|
-
*
|
|
259
|
-
*
|
|
260
|
-
|
|
258
|
+
* Remove the legacy codex-monitor config directory after successful migration.
|
|
259
|
+
* Runs in a deferred setTimeout so it doesn't block startup.
|
|
260
|
+
*/
|
|
261
|
+
function scheduleLegacyCleanup(legacyDir) {
|
|
262
|
+
if (!legacyDir || !existsSync(legacyDir)) return;
|
|
263
|
+
setTimeout(() => {
|
|
264
|
+
try {
|
|
265
|
+
rmSync(legacyDir, { recursive: true, force: true });
|
|
266
|
+
console.log(`[compat] Cleaned up legacy config directory: ${legacyDir}`);
|
|
267
|
+
} catch (err) {
|
|
268
|
+
console.warn(
|
|
269
|
+
`[compat] Could not remove legacy directory ${legacyDir}: ${err.message}`,
|
|
270
|
+
);
|
|
271
|
+
}
|
|
272
|
+
}, 5000);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* If BOSUN_DIR is not set but a legacy codex-monitor dir exists,
|
|
277
|
+
* automatically migrate config to ~/bosun and set BOSUN_DIR to the new location.
|
|
278
|
+
* After successful migration, schedule legacy directory removal.
|
|
261
279
|
*
|
|
262
|
-
*
|
|
263
|
-
* it works. Migration is optional (improves going forward).
|
|
280
|
+
* Returns true if legacy dir was detected and migration was performed.
|
|
264
281
|
*/
|
|
265
282
|
export function autoApplyLegacyDir() {
|
|
266
283
|
// Already set — nothing to do
|
|
@@ -269,10 +286,39 @@ export function autoApplyLegacyDir() {
|
|
|
269
286
|
const legacyDir = getLegacyConfigDir();
|
|
270
287
|
if (!legacyDir) return false;
|
|
271
288
|
|
|
272
|
-
|
|
289
|
+
const newDir = getNewConfigDir();
|
|
290
|
+
|
|
291
|
+
// If new dir already has config, just use it (already migrated)
|
|
292
|
+
if (hasLegacyMarkers(newDir)) {
|
|
293
|
+
// Legacy dir still exists but new dir is set up — clean up legacy
|
|
294
|
+
scheduleLegacyCleanup(legacyDir);
|
|
295
|
+
return false;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// Perform migration
|
|
273
299
|
console.log(
|
|
274
|
-
`[compat] Legacy codex-monitor config detected at ${legacyDir} —
|
|
300
|
+
`[compat] Legacy codex-monitor config detected at ${legacyDir} — migrating to ${newDir}...`,
|
|
275
301
|
);
|
|
302
|
+
const result = migrateFromLegacy(legacyDir, newDir);
|
|
303
|
+
|
|
304
|
+
if (result.errors.length > 0) {
|
|
305
|
+
console.warn(
|
|
306
|
+
`[compat] Migration had errors: ${result.errors.join(", ")}`,
|
|
307
|
+
);
|
|
308
|
+
// Fall back to legacy dir if migration failed
|
|
309
|
+
process.env.BOSUN_DIR = legacyDir;
|
|
310
|
+
return true;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
if (result.migrated.length > 0) {
|
|
314
|
+
console.log(
|
|
315
|
+
`[compat] Migrated ${result.migrated.length} files to ${newDir}: ${result.migrated.join(", ")}`,
|
|
316
|
+
);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// Schedule cleanup of legacy directory
|
|
320
|
+
scheduleLegacyCleanup(legacyDir);
|
|
321
|
+
|
|
276
322
|
return true;
|
|
277
323
|
}
|
|
278
324
|
|
package/config.mjs
CHANGED
|
@@ -59,11 +59,6 @@ function isWslInteropRuntime() {
|
|
|
59
59
|
}
|
|
60
60
|
|
|
61
61
|
function resolveConfigDir(repoRoot) {
|
|
62
|
-
const repoPath = resolve(repoRoot || process.cwd());
|
|
63
|
-
const packageDir = resolve(__dirname);
|
|
64
|
-
if (isPathInside(repoPath, packageDir) || hasConfigFiles(packageDir)) {
|
|
65
|
-
return packageDir;
|
|
66
|
-
}
|
|
67
62
|
const preferWindowsDirs =
|
|
68
63
|
process.platform === "win32" && !isWslInteropRuntime();
|
|
69
64
|
const baseDir = preferWindowsDirs
|
|
@@ -737,6 +732,15 @@ export function loadConfig(argv = process.argv, options = {}) {
|
|
|
737
732
|
let configData = configFile.data || {};
|
|
738
733
|
|
|
739
734
|
const repoRootOverride = cli["repo-root"] || process.env.REPO_ROOT || "";
|
|
735
|
+
|
|
736
|
+
// Load workspace configuration
|
|
737
|
+
const workspacesDir = resolve(configDir, "workspaces");
|
|
738
|
+
const activeWorkspace = cli["workspace"] ||
|
|
739
|
+
process.env.BOSUN_WORKSPACE ||
|
|
740
|
+
configData.activeWorkspace ||
|
|
741
|
+
configData.defaultWorkspace ||
|
|
742
|
+
"";
|
|
743
|
+
|
|
740
744
|
let repositories = loadRepoConfig(configDir, configData, {
|
|
741
745
|
repoRootOverride,
|
|
742
746
|
});
|
|
@@ -1626,9 +1630,11 @@ export function loadConfig(argv = process.argv, options = {}) {
|
|
|
1626
1630
|
executorConfig,
|
|
1627
1631
|
scheduler,
|
|
1628
1632
|
|
|
1629
|
-
// Multi-repo
|
|
1633
|
+
// Multi-repo / Workspaces
|
|
1630
1634
|
repositories,
|
|
1631
1635
|
selectedRepository,
|
|
1636
|
+
workspacesDir,
|
|
1637
|
+
activeWorkspace,
|
|
1632
1638
|
|
|
1633
1639
|
// Agent prompts
|
|
1634
1640
|
agentPrompts,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "bosun",
|
|
3
|
-
"version": "0.27.
|
|
3
|
+
"version": "0.27.2",
|
|
4
4
|
"description": "AI-powered orchestrator supervisor — manages AI agent executors with failover, auto-restarts on failure, analyzes crashes with Codex SDK, creates PRs via Vibe-Kanban API, and sends Telegram notifications. Supports N executors with weighted distribution, multi-repo projects, and auto-setup.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "Apache 2.0",
|
|
@@ -43,6 +43,7 @@
|
|
|
43
43
|
"./maintenance": "./maintenance.mjs",
|
|
44
44
|
"./telegram-bot": "./telegram-bot.mjs",
|
|
45
45
|
"./ui-server": "./ui-server.mjs",
|
|
46
|
+
"./workspace-manager": "./workspace-manager.mjs",
|
|
46
47
|
"./workspace-registry": "./workspace-registry.mjs",
|
|
47
48
|
"./shared-workspace-registry": "./shared-workspace-registry.mjs",
|
|
48
49
|
"./workspace-reaper": "./workspace-reaper.mjs",
|
|
@@ -173,6 +174,7 @@
|
|
|
173
174
|
"vibe-kanban-wrapper.mjs",
|
|
174
175
|
"vk-error-resolver.mjs",
|
|
175
176
|
"vk-log-stream.mjs",
|
|
177
|
+
"workspace-manager.mjs",
|
|
176
178
|
"workspace-monitor.mjs",
|
|
177
179
|
"workspace-reaper.mjs",
|
|
178
180
|
"workspace-registry.mjs",
|
package/publish.mjs
CHANGED
|
@@ -224,10 +224,8 @@ function main() {
|
|
|
224
224
|
const status = run(NPM_BIN, publishArgs, env);
|
|
225
225
|
if (status === 0 && !dryRun) {
|
|
226
226
|
console.log(
|
|
227
|
-
"\n[publish]
|
|
228
|
-
" npm deprecate
|
|
229
|
-
" # If a scoped legacy package exists:\n" +
|
|
230
|
-
" npm deprecate bosun@'*' \"Renamed to bosun. Install: npm install -g bosun\"\n",
|
|
227
|
+
"\n[publish] :\n" +
|
|
228
|
+
" npm deprecate openfleet@'*' \"⚠️ openfleet has been renamed to bosun. Install the latest: npm install -g bosun\"\n",
|
|
231
229
|
);
|
|
232
230
|
}
|
|
233
231
|
process.exit(status);
|
package/setup.mjs
CHANGED
|
@@ -1815,52 +1815,130 @@ async function main() {
|
|
|
1815
1815
|
);
|
|
1816
1816
|
configJson.projectName = env.PROJECT_NAME;
|
|
1817
1817
|
|
|
1818
|
-
// ── Step 3: Repository
|
|
1819
|
-
heading("Step 3 of 9 — Repository Configuration");
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
info(
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
repoIdx === 0 ? repoRoot : "",
|
|
1839
|
-
);
|
|
1840
|
-
const repoSlug = await prompt.ask(
|
|
1841
|
-
` Repo ${repoIdx + 1} — GitHub slug`,
|
|
1842
|
-
repoIdx === 0 ? env.GITHUB_REPO : "",
|
|
1818
|
+
// ── Step 3: Workspace & Repository ─────────────────────
|
|
1819
|
+
heading("Step 3 of 9 — Workspace & Repository Configuration");
|
|
1820
|
+
|
|
1821
|
+
const useWorkspaces = await prompt.confirm(
|
|
1822
|
+
"Set up multi-repo workspaces? (organizes repos into ~/bosun/workspaces/)",
|
|
1823
|
+
isAdvancedSetup,
|
|
1824
|
+
);
|
|
1825
|
+
|
|
1826
|
+
if (useWorkspaces) {
|
|
1827
|
+
info("Workspaces group related repositories together.\n");
|
|
1828
|
+
info(`Repositories will be cloned into: ${resolve(configDir, "workspaces")}\n`);
|
|
1829
|
+
|
|
1830
|
+
configJson.workspaces = [];
|
|
1831
|
+
let addMoreWs = true;
|
|
1832
|
+
let wsIdx = 0;
|
|
1833
|
+
|
|
1834
|
+
while (addMoreWs) {
|
|
1835
|
+
const wsName = await prompt.ask(
|
|
1836
|
+
` Workspace ${wsIdx + 1} — name`,
|
|
1837
|
+
wsIdx === 0 ? projectName : "",
|
|
1843
1838
|
);
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
1839
|
+
const wsId = wsName.toLowerCase().replace(/[^a-z0-9_-]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
|
|
1840
|
+
|
|
1841
|
+
const wsRepos = [];
|
|
1842
|
+
let addMoreRepos = true;
|
|
1843
|
+
let repoIdx = 0;
|
|
1844
|
+
|
|
1845
|
+
while (addMoreRepos) {
|
|
1846
|
+
const repoUrl = await prompt.ask(
|
|
1847
|
+
` Repo ${repoIdx + 1} — git URL (SSH or HTTPS)`,
|
|
1848
|
+
repoIdx === 0 ? (env.GITHUB_REPO ? `git@github.com:${env.GITHUB_REPO}.git` : "") : "",
|
|
1849
|
+
);
|
|
1850
|
+
const defaultName = repoUrl
|
|
1851
|
+
? (repoUrl.match(/[/:]([^/]+?)(?:\.git)?$/) || [])[1] || ""
|
|
1852
|
+
: "";
|
|
1853
|
+
const repoName = await prompt.ask(
|
|
1854
|
+
` Repo ${repoIdx + 1} — directory name`,
|
|
1855
|
+
defaultName || (repoIdx === 0 ? basename(repoRoot) : ""),
|
|
1856
|
+
);
|
|
1857
|
+
const repoSlug = await prompt.ask(
|
|
1858
|
+
` Repo ${repoIdx + 1} — GitHub slug (org/repo)`,
|
|
1859
|
+
repoIdx === 0 ? env.GITHUB_REPO : "",
|
|
1860
|
+
);
|
|
1861
|
+
|
|
1862
|
+
wsRepos.push({
|
|
1863
|
+
name: repoName,
|
|
1864
|
+
url: repoUrl,
|
|
1865
|
+
slug: repoSlug,
|
|
1866
|
+
primary: repoIdx === 0,
|
|
1867
|
+
});
|
|
1868
|
+
repoIdx++;
|
|
1869
|
+
addMoreRepos = await prompt.confirm(" Add another repo to this workspace?", false);
|
|
1870
|
+
}
|
|
1871
|
+
|
|
1872
|
+
configJson.workspaces.push({
|
|
1873
|
+
id: wsId,
|
|
1874
|
+
name: wsName,
|
|
1875
|
+
repos: wsRepos,
|
|
1876
|
+
createdAt: new Date().toISOString(),
|
|
1877
|
+
activeRepo: wsRepos[0]?.name || null,
|
|
1849
1878
|
});
|
|
1850
|
-
|
|
1851
|
-
|
|
1879
|
+
|
|
1880
|
+
// Also populate legacy repositories array for backward compat
|
|
1881
|
+
for (const repo of wsRepos) {
|
|
1882
|
+
configJson.repositories.push({
|
|
1883
|
+
name: repo.name,
|
|
1884
|
+
slug: repo.slug,
|
|
1885
|
+
primary: repo.primary,
|
|
1886
|
+
});
|
|
1887
|
+
}
|
|
1888
|
+
|
|
1889
|
+
wsIdx++;
|
|
1890
|
+
addMoreWs = await prompt.confirm("Add another workspace?", false);
|
|
1891
|
+
}
|
|
1892
|
+
|
|
1893
|
+
if (configJson.workspaces.length > 0) {
|
|
1894
|
+
configJson.activeWorkspace = configJson.workspaces[0].id;
|
|
1852
1895
|
}
|
|
1853
1896
|
} else {
|
|
1854
|
-
// Single-repo
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
);
|
|
1897
|
+
// Single-repo mode (classic) — still works as before
|
|
1898
|
+
const multiRepo = isAdvancedSetup
|
|
1899
|
+
? await prompt.confirm(
|
|
1900
|
+
"Do you have multiple repositories (e.g. separate backend/frontend)?",
|
|
1901
|
+
false,
|
|
1902
|
+
)
|
|
1903
|
+
: false;
|
|
1904
|
+
|
|
1905
|
+
if (multiRepo) {
|
|
1906
|
+
info("Configure each repository. The first is the primary.\n");
|
|
1907
|
+
let addMore = true;
|
|
1908
|
+
let repoIdx = 0;
|
|
1909
|
+
while (addMore) {
|
|
1910
|
+
const repoName = await prompt.ask(
|
|
1911
|
+
` Repo ${repoIdx + 1} — name`,
|
|
1912
|
+
repoIdx === 0 ? basename(repoRoot) : "",
|
|
1913
|
+
);
|
|
1914
|
+
const repoPath = await prompt.ask(
|
|
1915
|
+
` Repo ${repoIdx + 1} — local path`,
|
|
1916
|
+
repoIdx === 0 ? repoRoot : "",
|
|
1917
|
+
);
|
|
1918
|
+
const repoSlug = await prompt.ask(
|
|
1919
|
+
` Repo ${repoIdx + 1} — GitHub slug`,
|
|
1920
|
+
repoIdx === 0 ? env.GITHUB_REPO : "",
|
|
1921
|
+
);
|
|
1922
|
+
configJson.repositories.push({
|
|
1923
|
+
name: repoName,
|
|
1924
|
+
path: repoPath,
|
|
1925
|
+
slug: repoSlug,
|
|
1926
|
+
primary: repoIdx === 0,
|
|
1927
|
+
});
|
|
1928
|
+
repoIdx++;
|
|
1929
|
+
addMore = await prompt.confirm("Add another repository?", false);
|
|
1930
|
+
}
|
|
1931
|
+
} else {
|
|
1932
|
+
configJson.repositories.push({
|
|
1933
|
+
name: basename(repoRoot),
|
|
1934
|
+
slug: env.GITHUB_REPO,
|
|
1935
|
+
primary: true,
|
|
1936
|
+
});
|
|
1937
|
+
if (!isAdvancedSetup) {
|
|
1938
|
+
info(
|
|
1939
|
+
"Using single-repo defaults (recommended mode). Re-run setup in Advanced mode for multi-repo config.",
|
|
1940
|
+
);
|
|
1941
|
+
}
|
|
1864
1942
|
}
|
|
1865
1943
|
}
|
|
1866
1944
|
|
package/task-store.mjs
CHANGED
package/ui/app.js
CHANGED
|
@@ -65,6 +65,7 @@ import {
|
|
|
65
65
|
selectedSessionId,
|
|
66
66
|
sessionsData,
|
|
67
67
|
} from "./components/session-list.js";
|
|
68
|
+
import { WorkspaceSwitcher, loadWorkspaces } from "./components/workspace-switcher.js";
|
|
68
69
|
import { DiffViewer } from "./components/diff-viewer.js";
|
|
69
70
|
import {
|
|
70
71
|
CommandPalette,
|
|
@@ -112,15 +113,16 @@ if (typeof document !== "undefined" && !document.getElementById("offline-banner-
|
|
|
112
113
|
gap: 12px;
|
|
113
114
|
padding: 12px 16px;
|
|
114
115
|
margin: 8px 16px;
|
|
115
|
-
background: rgba(239, 68, 68, 0.
|
|
116
|
-
border: 1px solid rgba(239, 68, 68, 0.
|
|
117
|
-
border-radius:
|
|
118
|
-
|
|
116
|
+
background: rgba(239, 68, 68, 0.08);
|
|
117
|
+
border: 1px solid rgba(239, 68, 68, 0.2);
|
|
118
|
+
border-radius: 14px;
|
|
119
|
+
box-shadow: var(--shadow-sm);
|
|
120
|
+
backdrop-filter: blur(6px);
|
|
119
121
|
animation: slideDown 0.3s ease-out;
|
|
120
122
|
}
|
|
121
|
-
.offline-banner-icon { font-size:
|
|
123
|
+
.offline-banner-icon { font-size: 20px; }
|
|
122
124
|
.offline-banner-content { flex: 1; }
|
|
123
|
-
.offline-banner-title { font-weight: 600; font-size:
|
|
125
|
+
.offline-banner-title { font-weight: 600; font-size: 13px; color: #ef4444; }
|
|
124
126
|
.offline-banner-meta { font-size: 12px; opacity: 0.7; margin-top: 2px; }
|
|
125
127
|
`;
|
|
126
128
|
document.head.appendChild(style);
|
|
@@ -257,15 +259,18 @@ function Header() {
|
|
|
257
259
|
? html`<div class="app-header-hint">${navHint}</div>`
|
|
258
260
|
: null}
|
|
259
261
|
</div>
|
|
262
|
+
<${WorkspaceSwitcher} />
|
|
260
263
|
</div>
|
|
261
264
|
<div class="header-actions">
|
|
262
|
-
<div class="
|
|
263
|
-
<
|
|
264
|
-
|
|
265
|
+
<div class="header-status">
|
|
266
|
+
<div class="connection-pill ${connClass}">
|
|
267
|
+
<span class="connection-dot"></span>
|
|
268
|
+
${connLabel}
|
|
269
|
+
</div>
|
|
270
|
+
${freshnessLabel
|
|
271
|
+
? html`<div class="header-freshness">${freshnessLabel}</div>`
|
|
272
|
+
: null}
|
|
265
273
|
</div>
|
|
266
|
-
${freshnessLabel
|
|
267
|
-
? html`<div class="header-freshness" style="font-size:11px;opacity:0.55;margin-top:2px">${freshnessLabel}</div>`
|
|
268
|
-
: null}
|
|
269
274
|
${user
|
|
270
275
|
? html`<div class="app-header-user">@${user.username || user.first_name}</div>`
|
|
271
276
|
: null}
|