bosun 0.41.8 → 0.41.10
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/.env.example +1 -1
- package/README.md +23 -1
- package/agent/agent-event-bus.mjs +31 -2
- package/agent/agent-pool.mjs +275 -40
- package/agent/agent-prompts.mjs +9 -1
- package/agent/agent-supervisor.mjs +22 -0
- package/agent/autofix.mjs +1 -1
- package/agent/primary-agent.mjs +115 -5
- package/cli.mjs +3 -2
- package/config/config.mjs +47 -33
- package/config/context-shredding-config.mjs +1 -1
- package/config/repo-root.mjs +41 -33
- package/desktop/main.mjs +350 -25
- package/desktop/preload.cjs +8 -0
- package/desktop/preload.mjs +19 -0
- package/entrypoint.mjs +332 -0
- package/git/sdk-conflict-resolver.mjs +1 -1
- package/infra/health-status.mjs +72 -0
- package/infra/library-manager.mjs +58 -1
- package/infra/maintenance.mjs +1 -2
- package/infra/monitor.mjs +26 -8
- package/infra/session-tracker.mjs +30 -3
- package/package.json +12 -4
- package/server/bosun-mcp-server.mjs +1004 -0
- package/server/setup-web-server.mjs +288 -259
- package/server/ui-server.mjs +1323 -26
- package/shell/claude-shell.mjs +14 -1
- package/shell/codex-config.mjs +1 -1
- package/shell/codex-model-profiles.mjs +170 -30
- package/shell/codex-shell.mjs +63 -18
- package/shell/opencode-providers.mjs +20 -8
- package/task/task-executor.mjs +28 -0
- package/task/task-store.mjs +13 -4
- package/telegram/telegram-sentinel.mjs +54 -3
- package/tools/list-todos.mjs +7 -1
- package/ui/app.js +3 -2
- package/ui/components/agent-selector.js +127 -0
- package/ui/components/session-list.js +15 -10
- package/ui/demo-defaults.js +334 -336
- package/ui/modules/router.js +2 -0
- package/ui/modules/state.js +13 -5
- package/ui/tabs/chat.js +3 -0
- package/ui/tabs/library.js +284 -52
- package/ui/tabs/tasks.js +5 -13
- package/ui/tabs/workflows.js +766 -3
- package/workflow/workflow-engine.mjs +246 -5
- package/workflow/workflow-nodes/definitions.mjs +37 -0
- package/workflow/workflow-nodes.mjs +1014 -184
- package/workflow/workflow-templates.mjs +0 -5
- package/workflow-templates/_helpers.mjs +253 -0
- package/workflow-templates/agents.mjs +199 -226
- package/workflow-templates/github.mjs +106 -16
- package/workflow-templates/sub-workflows.mjs +233 -0
- package/workflow-templates/task-execution.mjs +125 -471
- package/workflow-templates/task-lifecycle.mjs +11 -48
- package/workspace/command-diagnostics.mjs +460 -0
- package/workspace/context-cache.mjs +396 -28
- package/workspace/worktree-manager.mjs +1 -1
package/agent/primary-agent.mjs
CHANGED
|
@@ -11,6 +11,7 @@ import { ensureRepoConfigs, printRepoConfigSummary } from "../config/repo-config
|
|
|
11
11
|
import { resolveRepoRoot } from "../config/repo-root.mjs";
|
|
12
12
|
import { getAgentToolConfig, getEffectiveTools } from "./agent-tool-config.mjs";
|
|
13
13
|
import { getSessionTracker } from "../infra/session-tracker.mjs";
|
|
14
|
+
import { getEntry, getEntryContent, resolveAgentProfileLibraryMetadata } from "../infra/library-manager.mjs";
|
|
14
15
|
import { execPooledPrompt } from "./agent-pool.mjs";
|
|
15
16
|
import {
|
|
16
17
|
execCodexPrompt,
|
|
@@ -290,6 +291,108 @@ function buildPrimaryToolCapabilityContract(options = {}) {
|
|
|
290
291
|
].join("\n");
|
|
291
292
|
}
|
|
292
293
|
|
|
294
|
+
function toStringArray(value) {
|
|
295
|
+
if (!Array.isArray(value)) return [];
|
|
296
|
+
return value.map((item) => String(item || "").trim()).filter(Boolean);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
function resolveSelectedAgentProfileContext(rootDir, agentProfileId) {
|
|
300
|
+
const id = String(agentProfileId || "").trim();
|
|
301
|
+
if (!id) return null;
|
|
302
|
+
const entry = getEntry(rootDir, id);
|
|
303
|
+
if (!entry || entry.type !== "agent") return null;
|
|
304
|
+
const profile = getEntryContent(rootDir, entry);
|
|
305
|
+
if (!profile || typeof profile !== "object") return null;
|
|
306
|
+
|
|
307
|
+
const metadata = resolveAgentProfileLibraryMetadata(entry, profile);
|
|
308
|
+
const promptEntry = profile?.promptOverride ? getEntry(rootDir, profile.promptOverride) : null;
|
|
309
|
+
const promptContent = promptEntry ? getEntryContent(rootDir, promptEntry) : null;
|
|
310
|
+
const skills = toStringArray(profile?.skills)
|
|
311
|
+
.map((skillId) => {
|
|
312
|
+
const skillEntry = getEntry(rootDir, skillId);
|
|
313
|
+
if (!skillEntry || skillEntry.type !== "skill") return null;
|
|
314
|
+
return {
|
|
315
|
+
id: skillEntry.id,
|
|
316
|
+
name: skillEntry.name || skillEntry.id,
|
|
317
|
+
content: String(getEntryContent(rootDir, skillEntry) || "").trim(),
|
|
318
|
+
};
|
|
319
|
+
})
|
|
320
|
+
.filter(Boolean);
|
|
321
|
+
|
|
322
|
+
return {
|
|
323
|
+
id: entry.id,
|
|
324
|
+
name: entry.name || entry.id,
|
|
325
|
+
description: entry.description || "",
|
|
326
|
+
profile,
|
|
327
|
+
metadata,
|
|
328
|
+
promptOverride: promptEntry
|
|
329
|
+
? {
|
|
330
|
+
id: promptEntry.id,
|
|
331
|
+
name: promptEntry.name || promptEntry.id,
|
|
332
|
+
content: typeof promptContent === "string"
|
|
333
|
+
? promptContent.trim()
|
|
334
|
+
: String(promptContent || "").trim(),
|
|
335
|
+
}
|
|
336
|
+
: null,
|
|
337
|
+
skills,
|
|
338
|
+
};
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
function buildPrimaryAgentProfileContract(options = {}) {
|
|
342
|
+
let rootDir = "";
|
|
343
|
+
try {
|
|
344
|
+
rootDir = String(options.cwd || resolveRepoRoot() || process.cwd()).trim();
|
|
345
|
+
} catch {
|
|
346
|
+
rootDir = String(options.cwd || process.cwd()).trim();
|
|
347
|
+
}
|
|
348
|
+
const selected = resolveSelectedAgentProfileContext(rootDir, options.agentProfileId);
|
|
349
|
+
if (!selected) return { block: "", preferredMode: "", preferredModel: "" };
|
|
350
|
+
|
|
351
|
+
const profileInstructions = String(
|
|
352
|
+
selected.profile?.instructions
|
|
353
|
+
|| selected.profile?.manualInstructions
|
|
354
|
+
|| selected.profile?.voiceInstructions
|
|
355
|
+
|| "",
|
|
356
|
+
).trim();
|
|
357
|
+
const summary = {
|
|
358
|
+
id: selected.id,
|
|
359
|
+
name: selected.name,
|
|
360
|
+
description: selected.description,
|
|
361
|
+
agentCategory: selected.metadata.agentCategory,
|
|
362
|
+
interactiveMode: selected.metadata.interactiveMode,
|
|
363
|
+
interactiveLabel: selected.metadata.interactiveLabel,
|
|
364
|
+
sdk: String(selected.profile?.sdk || "").trim() || null,
|
|
365
|
+
model: String(selected.profile?.model || "").trim() || null,
|
|
366
|
+
showInChatDropdown: selected.metadata.showInChatDropdown,
|
|
367
|
+
skillIds: selected.skills.map((skill) => skill.id),
|
|
368
|
+
};
|
|
369
|
+
const lines = [
|
|
370
|
+
"## Selected Agent Profile",
|
|
371
|
+
"Apply this profile consistently unless the user explicitly overrides it.",
|
|
372
|
+
"```json",
|
|
373
|
+
JSON.stringify(summary, null, 2),
|
|
374
|
+
"```",
|
|
375
|
+
];
|
|
376
|
+
if (profileInstructions) {
|
|
377
|
+
lines.push("## Profile Instructions", profileInstructions);
|
|
378
|
+
}
|
|
379
|
+
if (selected.promptOverride?.content) {
|
|
380
|
+
lines.push(`## Prompt Override: ${selected.promptOverride.name}`, selected.promptOverride.content);
|
|
381
|
+
}
|
|
382
|
+
if (selected.skills.length > 0) {
|
|
383
|
+
lines.push("## Profile Skills");
|
|
384
|
+
for (const skill of selected.skills) {
|
|
385
|
+
if (!skill.content) continue;
|
|
386
|
+
lines.push(`### ${skill.name}`, skill.content);
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
return {
|
|
390
|
+
block: lines.join("\n\n"),
|
|
391
|
+
preferredMode: String(selected.metadata.interactiveMode || "").trim(),
|
|
392
|
+
preferredModel: String(selected.profile?.model || "").trim(),
|
|
393
|
+
};
|
|
394
|
+
}
|
|
395
|
+
|
|
293
396
|
const ADAPTERS = {
|
|
294
397
|
"codex-sdk": {
|
|
295
398
|
name: "codex-sdk",
|
|
@@ -933,13 +1036,18 @@ export async function execPrimaryPrompt(userMessage, options = {}) {
|
|
|
933
1036
|
if (!initialized) {
|
|
934
1037
|
await initPrimaryAgent();
|
|
935
1038
|
}
|
|
1039
|
+
const selectedProfile = buildPrimaryAgentProfileContract(options);
|
|
936
1040
|
const sessionId =
|
|
937
1041
|
(options && options.sessionId ? String(options.sessionId) : "") ||
|
|
938
1042
|
`primary-${activeAdapter.name}`;
|
|
939
1043
|
const sessionType =
|
|
940
1044
|
(options && options.sessionType ? String(options.sessionType) : "") ||
|
|
941
1045
|
"primary";
|
|
942
|
-
const effectiveMode = normalizeAgentMode(
|
|
1046
|
+
const effectiveMode = normalizeAgentMode(
|
|
1047
|
+
options.mode || selectedProfile.preferredMode || agentMode,
|
|
1048
|
+
agentMode,
|
|
1049
|
+
);
|
|
1050
|
+
const effectiveModel = options.model || selectedProfile.preferredModel || undefined;
|
|
943
1051
|
const modePolicy = getModeExecPolicy(effectiveMode);
|
|
944
1052
|
const timeoutMs = options.timeoutMs || modePolicy?.timeoutMs || PRIMARY_EXEC_TIMEOUT_MS;
|
|
945
1053
|
const maxFailoverAttempts = Number.isInteger(options.maxFailoverAttempts)
|
|
@@ -955,7 +1063,9 @@ export async function execPrimaryPrompt(userMessage, options = {}) {
|
|
|
955
1063
|
? appendAttachmentsToPrompt(userMessage, attachments).message
|
|
956
1064
|
: userMessage;
|
|
957
1065
|
const toolContract = buildPrimaryToolCapabilityContract(options);
|
|
958
|
-
const messageWithToolContract =
|
|
1066
|
+
const messageWithToolContract = [selectedProfile.block, toolContract, messageWithAttachments]
|
|
1067
|
+
.filter(Boolean)
|
|
1068
|
+
.join("\n\n");
|
|
959
1069
|
const framedMessage = modePrefix ? modePrefix + messageWithToolContract : messageWithToolContract;
|
|
960
1070
|
|
|
961
1071
|
// Record user message (original, without mode prefix)
|
|
@@ -974,7 +1084,7 @@ export async function execPrimaryPrompt(userMessage, options = {}) {
|
|
|
974
1084
|
onEvent: options.onEvent,
|
|
975
1085
|
abortController: options.abortController,
|
|
976
1086
|
cwd: options.cwd,
|
|
977
|
-
model:
|
|
1087
|
+
model: effectiveModel,
|
|
978
1088
|
sdk: mapAdapterToPoolSdk(activeAdapter.name),
|
|
979
1089
|
sessionType,
|
|
980
1090
|
});
|
|
@@ -1064,7 +1174,7 @@ export async function execPrimaryPrompt(userMessage, options = {}) {
|
|
|
1064
1174
|
}
|
|
1065
1175
|
}
|
|
1066
1176
|
const result = await withTimeout(
|
|
1067
|
-
adapter.exec(framedMessage, { ...options, sessionId, abortController: timeoutAbort }),
|
|
1177
|
+
adapter.exec(framedMessage, { ...options, sessionId, model: effectiveModel, abortController: timeoutAbort }),
|
|
1068
1178
|
timeoutMs,
|
|
1069
1179
|
`${adapterName}.exec`,
|
|
1070
1180
|
timeoutAbort,
|
|
@@ -1133,7 +1243,7 @@ export async function execPrimaryPrompt(userMessage, options = {}) {
|
|
|
1133
1243
|
}
|
|
1134
1244
|
}
|
|
1135
1245
|
const retryResult = await withTimeout(
|
|
1136
|
-
adapter.exec(framedMessage, { ...options, sessionId, abortController: timeoutAbort }),
|
|
1246
|
+
adapter.exec(framedMessage, { ...options, sessionId, model: effectiveModel, abortController: timeoutAbort }),
|
|
1137
1247
|
timeoutMs,
|
|
1138
1248
|
`${adapterName}.exec.retry`,
|
|
1139
1249
|
timeoutAbort,
|
package/cli.mjs
CHANGED
|
@@ -79,6 +79,7 @@ function showHelp() {
|
|
|
79
79
|
COMMANDS
|
|
80
80
|
workflow list List declarative pipeline workflows
|
|
81
81
|
workflow run <name> Run a declarative pipeline workflow
|
|
82
|
+
audit <command> Run codebase annotation audit tools (scan|generate|warn|manifest|index|trim|conformity|migrate)
|
|
82
83
|
--setup Launch the web-based setup wizard (default)
|
|
83
84
|
--setup-terminal Run the legacy terminal setup wizard
|
|
84
85
|
--where Show the resolved bosun config directory
|
|
@@ -1368,8 +1369,8 @@ async function main() {
|
|
|
1368
1369
|
const { runAuditCli } = await import("./lib/codebase-audit.mjs");
|
|
1369
1370
|
const commandStartIndex = auditCommandIndex >= 0 ? auditCommandIndex : auditFlagIndex;
|
|
1370
1371
|
const auditArgs = args.slice(commandStartIndex + 1);
|
|
1371
|
-
await runAuditCli(auditArgs);
|
|
1372
|
-
process.exit(
|
|
1372
|
+
const { exitCode } = await runAuditCli(auditArgs);
|
|
1373
|
+
process.exit(exitCode);
|
|
1373
1374
|
}
|
|
1374
1375
|
|
|
1375
1376
|
if (args[0] === "node:create" || (args[0] === "node" && args[1] === "create")) {
|
package/config/config.mjs
CHANGED
|
@@ -711,9 +711,15 @@ function detectRepoSlug(repoRoot = "") {
|
|
|
711
711
|
const direct = tryResolve(process.cwd());
|
|
712
712
|
if (direct) return direct;
|
|
713
713
|
|
|
714
|
-
// Fall back to detected repo root if provided
|
|
715
|
-
|
|
716
|
-
|
|
714
|
+
// Fall back to detected repo root if provided
|
|
715
|
+
if (repoRoot) {
|
|
716
|
+
const viaRoot = tryResolve(repoRoot);
|
|
717
|
+
if (viaRoot) return viaRoot;
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
// Last resort — use cached detectRepoRoot (avoids redundant subprocess calls)
|
|
721
|
+
const root = detectRepoRoot();
|
|
722
|
+
if (root && root !== process.cwd()) {
|
|
717
723
|
const viaRoot = tryResolve(root);
|
|
718
724
|
if (viaRoot) return viaRoot;
|
|
719
725
|
}
|
|
@@ -721,7 +727,16 @@ function detectRepoSlug(repoRoot = "") {
|
|
|
721
727
|
return null;
|
|
722
728
|
}
|
|
723
729
|
|
|
730
|
+
let _detectRepoRootCache = null;
|
|
731
|
+
|
|
724
732
|
function detectRepoRoot() {
|
|
733
|
+
if (_detectRepoRootCache) return _detectRepoRootCache;
|
|
734
|
+
const result = _detectRepoRootUncached();
|
|
735
|
+
_detectRepoRootCache = result;
|
|
736
|
+
return result;
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
function _detectRepoRootUncached() {
|
|
725
740
|
const gitExecOptions = {
|
|
726
741
|
encoding: "utf8",
|
|
727
742
|
stdio: ["pipe", "pipe", "ignore"],
|
|
@@ -734,7 +749,30 @@ function detectRepoRoot() {
|
|
|
734
749
|
if (existsSync(envRoot)) return envRoot;
|
|
735
750
|
}
|
|
736
751
|
|
|
737
|
-
// 2.
|
|
752
|
+
// 2. Check bosun config for workspace repos FIRST — this is the primary
|
|
753
|
+
// source of truth and works regardless of whether cwd is inside a git repo.
|
|
754
|
+
const configDirs = getConfigSearchDirs();
|
|
755
|
+
let fallbackRepo = null;
|
|
756
|
+
for (const cfgName of CONFIG_FILES) {
|
|
757
|
+
for (const configDir of configDirs) {
|
|
758
|
+
const cfgPath = resolve(configDir, cfgName);
|
|
759
|
+
if (!existsSync(cfgPath)) continue;
|
|
760
|
+
try {
|
|
761
|
+
const cfg = JSON.parse(readFileSync(cfgPath, "utf8"));
|
|
762
|
+
const repoPaths = collectRepoPathsFromConfig(cfg, configDir);
|
|
763
|
+
for (const repoPath of repoPaths) {
|
|
764
|
+
if (!repoPath || !existsSync(repoPath)) continue;
|
|
765
|
+
if (existsSync(resolve(repoPath, ".git"))) return repoPath;
|
|
766
|
+
fallbackRepo ??= repoPath;
|
|
767
|
+
}
|
|
768
|
+
} catch {
|
|
769
|
+
/* invalid config */
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
if (fallbackRepo) return fallbackRepo;
|
|
774
|
+
|
|
775
|
+
// 3. Try git from cwd
|
|
738
776
|
try {
|
|
739
777
|
const gitRoot = execSync("git rev-parse --show-toplevel", {
|
|
740
778
|
...gitExecOptions,
|
|
@@ -744,7 +782,7 @@ function detectRepoRoot() {
|
|
|
744
782
|
// not in a git repo from cwd
|
|
745
783
|
}
|
|
746
784
|
|
|
747
|
-
//
|
|
785
|
+
// 4. Bosun package directory may be inside a repo (common: scripts/bosun/ within a project)
|
|
748
786
|
try {
|
|
749
787
|
const gitRoot = execSync("git rev-parse --show-toplevel", {
|
|
750
788
|
cwd: __dirname,
|
|
@@ -755,7 +793,7 @@ function detectRepoRoot() {
|
|
|
755
793
|
// bosun installed standalone, not in a repo
|
|
756
794
|
}
|
|
757
795
|
|
|
758
|
-
//
|
|
796
|
+
// 5. Module root detection — when bosun is installed as a standalone npm package,
|
|
759
797
|
// use the module root directory as a stable base for config resolution.
|
|
760
798
|
const moduleRoot = detectBosunModuleRoot();
|
|
761
799
|
if (moduleRoot && moduleRoot !== process.cwd()) {
|
|
@@ -770,33 +808,9 @@ function detectRepoRoot() {
|
|
|
770
808
|
}
|
|
771
809
|
}
|
|
772
810
|
|
|
773
|
-
//
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
for (const cfgName of CONFIG_FILES) {
|
|
777
|
-
for (const configDir of configDirs) {
|
|
778
|
-
const cfgPath = resolve(configDir, cfgName);
|
|
779
|
-
if (!existsSync(cfgPath)) continue;
|
|
780
|
-
try {
|
|
781
|
-
const cfg = JSON.parse(readFileSync(cfgPath, "utf8"));
|
|
782
|
-
const repoPaths = collectRepoPathsFromConfig(cfg, configDir);
|
|
783
|
-
for (const repoPath of repoPaths) {
|
|
784
|
-
if (!repoPath || !existsSync(repoPath)) continue;
|
|
785
|
-
if (existsSync(resolve(repoPath, ".git"))) return repoPath;
|
|
786
|
-
fallbackRepo ??= repoPath;
|
|
787
|
-
}
|
|
788
|
-
} catch {
|
|
789
|
-
/* invalid config */
|
|
790
|
-
}
|
|
791
|
-
}
|
|
792
|
-
}
|
|
793
|
-
if (fallbackRepo) return fallbackRepo;
|
|
794
|
-
|
|
795
|
-
// 6. Final fallback — warn and return cwd.
|
|
796
|
-
// git repo (e.g. when the daemon spawns with cwd=homedir), but returning
|
|
797
|
-
// null would crash downstream callers like resolve(repoRoot). The warning
|
|
798
|
-
// helps diagnose "not a git repository" errors from child processes.
|
|
799
|
-
console.warn("[config] detectRepoRoot: no git repository found — falling back to cwd:", process.cwd());
|
|
811
|
+
// 6. Final fallback — return cwd silently. The config system resolves
|
|
812
|
+
// repos from BOSUN_HOME workspaces, so a missing git repo in cwd is
|
|
813
|
+
// expected for globally-installed usage and daemon spawns.
|
|
800
814
|
return process.cwd();
|
|
801
815
|
}
|
|
802
816
|
|
|
@@ -147,7 +147,7 @@ export const DEFAULT_SHREDDING_CONFIG = Object.freeze({
|
|
|
147
147
|
userMsgFullTurns: 1, // only the current turn keeps the full user prompt
|
|
148
148
|
|
|
149
149
|
// ── Live command/tool compaction ─────────────────────────────
|
|
150
|
-
liveToolCompactionEnabled:
|
|
150
|
+
liveToolCompactionEnabled: true,
|
|
151
151
|
liveToolCompactionMode: "auto",
|
|
152
152
|
liveToolCompactionMinChars: 4000,
|
|
153
153
|
liveToolCompactionTargetChars: 1800,
|
package/config/repo-root.mjs
CHANGED
|
@@ -98,40 +98,9 @@ export function resolveRepoRoot(options = {}) {
|
|
|
98
98
|
|
|
99
99
|
const cwd = options.cwd || process.cwd();
|
|
100
100
|
|
|
101
|
-
//
|
|
102
|
-
try {
|
|
103
|
-
const gitRoot = execSync("git rev-parse --show-toplevel", {
|
|
104
|
-
encoding: "utf8",
|
|
105
|
-
cwd,
|
|
106
|
-
stdio: ["ignore", "pipe", "ignore"],
|
|
107
|
-
}).trim();
|
|
108
|
-
if (gitRoot) return gitRoot;
|
|
109
|
-
} catch {
|
|
110
|
-
// ignore - fall back
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
// Try git from the bosun package directory (may be inside a repo)
|
|
114
|
-
try {
|
|
115
|
-
const gitRoot = execSync("git rev-parse --show-toplevel", {
|
|
116
|
-
encoding: "utf8",
|
|
117
|
-
cwd: __dirname,
|
|
118
|
-
stdio: ["ignore", "pipe", "ignore"],
|
|
119
|
-
}).trim();
|
|
120
|
-
if (gitRoot) return gitRoot;
|
|
121
|
-
} catch {
|
|
122
|
-
// bosun installed standalone
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
// Check if __dirname itself is the bosun module root (installed as npm package)
|
|
126
|
-
const moduleRoot = detectBosunModuleRoot();
|
|
127
|
-
if (moduleRoot && moduleRoot !== cwd && existsSync(resolve(moduleRoot, "package.json"))) {
|
|
128
|
-
// When bosun is installed globally/locally outside a git repo, use its directory
|
|
129
|
-
// as a reference point if no git root was found above.
|
|
130
|
-
// Only use it if it's a valid directory on disk (don't return the fallback as a repo root).
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
// Check bosun config for workspace repos
|
|
101
|
+
// Check bosun config for workspace repos FIRST — works regardless of cwd
|
|
134
102
|
const configDirs = [...getConfigSearchDirs(), __dirname];
|
|
103
|
+
if (process.env.BOSUN_HOME) configDirs.unshift(resolve(process.env.BOSUN_HOME));
|
|
135
104
|
let fallbackRepo = null;
|
|
136
105
|
for (const cfgName of CONFIG_FILES) {
|
|
137
106
|
for (const dir of configDirs) {
|
|
@@ -139,6 +108,21 @@ export function resolveRepoRoot(options = {}) {
|
|
|
139
108
|
if (!existsSync(cfgPath)) continue;
|
|
140
109
|
try {
|
|
141
110
|
const cfg = JSON.parse(readFileSync(cfgPath, "utf8"));
|
|
111
|
+
// Check workspace repos (higher priority — explicit workspace configuration)
|
|
112
|
+
const workspaces = Array.isArray(cfg.workspaces) ? cfg.workspaces : [];
|
|
113
|
+
for (const ws of workspaces) {
|
|
114
|
+
const wsRepos = ws?.repos || ws?.repositories || [];
|
|
115
|
+
if (!Array.isArray(wsRepos)) continue;
|
|
116
|
+
const wsId = ws?.id || "default";
|
|
117
|
+
for (const repo of wsRepos) {
|
|
118
|
+
const name = typeof repo === "string" ? repo.split("/").pop() : (repo?.name || repo?.id || "");
|
|
119
|
+
if (!name) continue;
|
|
120
|
+
const repoPath = resolve(dir, "workspaces", wsId, name);
|
|
121
|
+
if (existsSync(resolve(repoPath, ".git"))) return repoPath;
|
|
122
|
+
if (existsSync(repoPath)) fallbackRepo ??= repoPath;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
// Check flat repositories
|
|
142
126
|
const repos = cfg.repositories || cfg.repos || [];
|
|
143
127
|
if (Array.isArray(repos) && repos.length > 0) {
|
|
144
128
|
const primary = repos.find((r) => r.primary) || repos[0];
|
|
@@ -155,6 +139,30 @@ export function resolveRepoRoot(options = {}) {
|
|
|
155
139
|
}
|
|
156
140
|
if (fallbackRepo) return fallbackRepo;
|
|
157
141
|
|
|
142
|
+
// Try git from cwd
|
|
143
|
+
try {
|
|
144
|
+
const gitRoot = execSync("git rev-parse --show-toplevel", {
|
|
145
|
+
encoding: "utf8",
|
|
146
|
+
cwd,
|
|
147
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
148
|
+
}).trim();
|
|
149
|
+
if (gitRoot) return gitRoot;
|
|
150
|
+
} catch {
|
|
151
|
+
// ignore - fall back
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Try git from the bosun package directory (may be inside a repo)
|
|
155
|
+
try {
|
|
156
|
+
const gitRoot = execSync("git rev-parse --show-toplevel", {
|
|
157
|
+
encoding: "utf8",
|
|
158
|
+
cwd: __dirname,
|
|
159
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
160
|
+
}).trim();
|
|
161
|
+
if (gitRoot) return gitRoot;
|
|
162
|
+
} catch {
|
|
163
|
+
// bosun installed standalone
|
|
164
|
+
}
|
|
165
|
+
|
|
158
166
|
return resolve(cwd);
|
|
159
167
|
}
|
|
160
168
|
|