bosun 0.31.7 → 0.31.8
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 +23 -12
- package/config-doctor.mjs +21 -0
- package/config.mjs +81 -0
- package/copilot-shell.mjs +27 -19
- package/github-app-auth.mjs +463 -0
- package/github-auth-manager.mjs +265 -0
- package/github-oauth-portal.mjs +949 -0
- package/monitor.mjs +58 -22
- package/package.json +10 -2
- package/primary-agent.mjs +62 -1
- package/sync-engine.mjs +14 -7
- package/task-executor.mjs +1 -1
- package/telegram-bot.mjs +8 -3
package/.env.example
CHANGED
|
@@ -345,31 +345,42 @@ TELEGRAM_MINIAPP_ENABLED=false
|
|
|
345
345
|
# OAuth Client Secret (only needed for callback-based OAuth, not for Device Flow):
|
|
346
346
|
# BOSUN_GITHUB_CLIENT_SECRET=
|
|
347
347
|
#
|
|
348
|
-
# Webhook secret (
|
|
348
|
+
# Webhook secret (VirtEngine relay signs forwarded events with this — leave blank
|
|
349
|
+
# until VirtEngine’s relay server is live; Bosun polls GitHub API in the meantime):
|
|
349
350
|
# BOSUN_GITHUB_WEBHOOK_SECRET=
|
|
350
351
|
#
|
|
351
352
|
# Path to the PEM private key downloaded from App settings → Generate a private key:
|
|
352
353
|
# BOSUN_GITHUB_PRIVATE_KEY_PATH=/path/to/bosun-botswain.pem
|
|
353
354
|
#
|
|
355
|
+
# ─── GitHub App Settings (enable all three in https://github.com/settings/apps/bosun-botswain) ────
|
|
356
|
+
# ✅ Callback URL → http://127.0.0.1:54317/github/callback (set this FIRST, then Save)
|
|
357
|
+
# ✅ "Request user authorization (OAuth) during installation" → ON
|
|
358
|
+
# GitHub does OAuth at install time, redirecting to the Callback URL with
|
|
359
|
+
# installation_id + setup_action=install. Setup URL is DISABLED — that's fine.
|
|
360
|
+
# ✅ "Enable Device Flow" → ON (only available AFTER Callback URL is saved)
|
|
361
|
+
# Allows CLI/terminal auth without a public URL (like VS Code / Roo Code)
|
|
362
|
+
# ✕ Setup URL → leave BLANK (GitHub disables this field when OAuth-at-install is ON)
|
|
363
|
+
# ✕ "Redirect on update" → leave OFF (disabled alongside Setup URL)
|
|
364
|
+
#
|
|
354
365
|
# ─── Authentication Method ───────────────────────────────────────────────
|
|
355
366
|
# RECOMMENDED: Device Flow (like VS Code / Roo Code — no public URL needed!)
|
|
356
367
|
# 1. Set BOSUN_GITHUB_CLIENT_ID above
|
|
357
|
-
# 2. Enable
|
|
358
|
-
# 3. Go to Settings → GitHub in the Bosun UI and click
|
|
359
|
-
# 4. That
|
|
368
|
+
# 2. Enable “Device Flow” in GitHub App settings (only clickable after Callback URL is saved)
|
|
369
|
+
# 3. Go to Settings → GitHub in the Bosun UI and click “Sign in with GitHub”
|
|
370
|
+
# 4. That’s it — no webhook URL, no tunnel, no public server needed
|
|
360
371
|
#
|
|
361
|
-
# ALTERNATIVE: OAuth Callback
|
|
372
|
+
# ALTERNATIVE: OAuth Callback
|
|
362
373
|
# Set BOSUN_GITHUB_CLIENT_ID + BOSUN_GITHUB_CLIENT_SECRET
|
|
363
|
-
# Register callback URL
|
|
364
|
-
# https://<your-bosun-public-url>/api/github/callback
|
|
374
|
+
# Register callback URL: http://127.0.0.1:54317/github/callback
|
|
365
375
|
#
|
|
366
|
-
#
|
|
367
|
-
#
|
|
368
|
-
#
|
|
369
|
-
#
|
|
376
|
+
# NOTE on webhooks:
|
|
377
|
+
# Real-time GitHub events (PR comments, issue mentions) are received via
|
|
378
|
+
# VirtEngine’s relay server and forwarded to your Bosun instance.
|
|
379
|
+
# Until the relay is live, Bosun polls the GitHub API every few minutes instead.
|
|
380
|
+
# Users do NOT need to configure a webhook URL or run any tunnel.
|
|
370
381
|
#
|
|
371
382
|
# Leave BOSUN_GITHUB_APP_ID unset to disable co-author trailer injection.
|
|
372
|
-
#
|
|
383
|
+
# (App ID and Client ID are already filled in above — no need to set them again.)
|
|
373
384
|
|
|
374
385
|
# ─── Kanban Backend ──────────────────────────────────────────────────────────
|
|
375
386
|
# Task-board backend:
|
package/config-doctor.mjs
CHANGED
|
@@ -340,6 +340,27 @@ export function runConfigDoctor(options = {}) {
|
|
|
340
340
|
}
|
|
341
341
|
}
|
|
342
342
|
|
|
343
|
+
if (backend === "jira") {
|
|
344
|
+
const missing = [];
|
|
345
|
+
if (!effective.JIRA_BASE_URL) missing.push("JIRA_BASE_URL");
|
|
346
|
+
if (!effective.JIRA_EMAIL) missing.push("JIRA_EMAIL");
|
|
347
|
+
if (!effective.JIRA_API_TOKEN) missing.push("JIRA_API_TOKEN");
|
|
348
|
+
const hasProjectKey = Boolean(
|
|
349
|
+
effective.JIRA_PROJECT_KEY || effective.KANBAN_PROJECT_ID,
|
|
350
|
+
);
|
|
351
|
+
if (!hasProjectKey) {
|
|
352
|
+
missing.push("JIRA_PROJECT_KEY (or KANBAN_PROJECT_ID)");
|
|
353
|
+
}
|
|
354
|
+
if (missing.length > 0) {
|
|
355
|
+
issues.errors.push({
|
|
356
|
+
code: "JIRA_BACKEND_REQUIRED",
|
|
357
|
+
message: `KANBAN_BACKEND=jira is missing required config: ${missing.join(", ")}`,
|
|
358
|
+
fix:
|
|
359
|
+
"Set required JIRA_* variables (and project key), or switch KANBAN_BACKEND=internal.",
|
|
360
|
+
});
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
343
364
|
const vkNeeded = backend === "vk" || mode === "vk" || mode === "hybrid";
|
|
344
365
|
if (vkNeeded) {
|
|
345
366
|
const vkBaseUrl = effective.VK_BASE_URL || "";
|
package/config.mjs
CHANGED
|
@@ -162,6 +162,80 @@ function loadDotEnvFile(envPath, options = {}) {
|
|
|
162
162
|
}
|
|
163
163
|
}
|
|
164
164
|
|
|
165
|
+
function readEnvValueFromFile(envPath, key) {
|
|
166
|
+
if (!envPath || !existsSync(envPath)) return undefined;
|
|
167
|
+
const lines = readFileSync(envPath, "utf8").split("\n");
|
|
168
|
+
let found;
|
|
169
|
+
for (const line of lines) {
|
|
170
|
+
const trimmed = line.trim();
|
|
171
|
+
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
172
|
+
const eqIdx = trimmed.indexOf("=");
|
|
173
|
+
if (eqIdx === -1) continue;
|
|
174
|
+
const parsedKey = trimmed.slice(0, eqIdx).trim();
|
|
175
|
+
if (parsedKey !== key) continue;
|
|
176
|
+
let value = trimmed.slice(eqIdx + 1).trim();
|
|
177
|
+
if (
|
|
178
|
+
(value.startsWith('"') && value.endsWith('"')) ||
|
|
179
|
+
(value.startsWith("'") && value.endsWith("'"))
|
|
180
|
+
) {
|
|
181
|
+
value = value.slice(1, -1);
|
|
182
|
+
}
|
|
183
|
+
found = value;
|
|
184
|
+
}
|
|
185
|
+
return found;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function resolveKanbanBackendSource({ envPaths = [], configFilePath, configData }) {
|
|
189
|
+
const key = "KANBAN_BACKEND";
|
|
190
|
+
let source = "default";
|
|
191
|
+
let sourcePath = null;
|
|
192
|
+
|
|
193
|
+
if (process.env[key] != null && String(process.env[key]).trim() !== "") {
|
|
194
|
+
let envFileMatch = null;
|
|
195
|
+
for (const envPath of envPaths) {
|
|
196
|
+
const value = readEnvValueFromFile(envPath, key);
|
|
197
|
+
if (value != null && String(value).trim() !== "") {
|
|
198
|
+
envFileMatch = envPath;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
if (envFileMatch) {
|
|
202
|
+
source = "env-file";
|
|
203
|
+
sourcePath = envFileMatch;
|
|
204
|
+
} else {
|
|
205
|
+
source = "process-env";
|
|
206
|
+
}
|
|
207
|
+
} else if (configData?.kanban?.backend != null) {
|
|
208
|
+
source = "config-file";
|
|
209
|
+
sourcePath = configFilePath || null;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
return Object.freeze({
|
|
213
|
+
key,
|
|
214
|
+
rawValue:
|
|
215
|
+
process.env[key] || configData?.kanban?.backend || "internal",
|
|
216
|
+
source,
|
|
217
|
+
sourcePath,
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function validateKanbanBackendConfig({ kanbanBackend, kanban, jira }) {
|
|
222
|
+
if (kanbanBackend !== "jira") return;
|
|
223
|
+
const missing = [];
|
|
224
|
+
if (!jira?.baseUrl) missing.push("JIRA_BASE_URL");
|
|
225
|
+
if (!jira?.email) missing.push("JIRA_EMAIL");
|
|
226
|
+
if (!jira?.apiToken) missing.push("JIRA_API_TOKEN");
|
|
227
|
+
const hasProjectKey = Boolean(jira?.projectKey || kanban?.projectId);
|
|
228
|
+
if (!hasProjectKey) {
|
|
229
|
+
missing.push("JIRA_PROJECT_KEY (or KANBAN_PROJECT_ID)");
|
|
230
|
+
}
|
|
231
|
+
if (missing.length > 0) {
|
|
232
|
+
throw new Error(
|
|
233
|
+
`[config] KANBAN_BACKEND=jira requires ${missing.join(", ")}. ` +
|
|
234
|
+
`Either configure Jira credentials/project key or switch KANBAN_BACKEND=internal.`,
|
|
235
|
+
);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
165
239
|
function loadConfigFile(configDir) {
|
|
166
240
|
for (const name of CONFIG_FILES) {
|
|
167
241
|
const p = resolve(configDir, name);
|
|
@@ -1147,6 +1221,11 @@ export function loadConfig(argv = process.argv, options = {}) {
|
|
|
1147
1221
|
resolve(configDir, ".env"),
|
|
1148
1222
|
resolve(repoRoot, ".env"),
|
|
1149
1223
|
].filter((p, i, arr) => arr.indexOf(p) === i);
|
|
1224
|
+
const kanbanSource = resolveKanbanBackendSource({
|
|
1225
|
+
envPaths,
|
|
1226
|
+
configFilePath: configFile.path,
|
|
1227
|
+
configData,
|
|
1228
|
+
});
|
|
1150
1229
|
|
|
1151
1230
|
// ── Project identity ─────────────────────────────────────
|
|
1152
1231
|
const projectName =
|
|
@@ -1461,6 +1540,7 @@ export function loadConfig(argv = process.argv, options = {}) {
|
|
|
1461
1540
|
"",
|
|
1462
1541
|
}),
|
|
1463
1542
|
});
|
|
1543
|
+
validateKanbanBackendConfig({ kanbanBackend, kanban, jira });
|
|
1464
1544
|
|
|
1465
1545
|
const internalExecutorConfig = configData.internalExecutor || {};
|
|
1466
1546
|
const projectRequirements = {
|
|
@@ -1900,6 +1980,7 @@ export function loadConfig(argv = process.argv, options = {}) {
|
|
|
1900
1980
|
internalExecutor,
|
|
1901
1981
|
executorMode: internalExecutor.mode,
|
|
1902
1982
|
kanban,
|
|
1983
|
+
kanbanSource,
|
|
1903
1984
|
githubProjectSync,
|
|
1904
1985
|
jira,
|
|
1905
1986
|
projectRequirements,
|
package/copilot-shell.mjs
CHANGED
|
@@ -13,6 +13,7 @@ import { resolve } from "node:path";
|
|
|
13
13
|
import { fileURLToPath } from "node:url";
|
|
14
14
|
import { execSync } from "node:child_process";
|
|
15
15
|
import { resolveRepoRoot } from "./repo-root.mjs";
|
|
16
|
+
import { getGitHubToken } from "./github-auth-manager.mjs";
|
|
16
17
|
|
|
17
18
|
const __dirname = resolve(fileURLToPath(new URL(".", import.meta.url)));
|
|
18
19
|
|
|
@@ -318,32 +319,39 @@ async function loadCopilotSdk() {
|
|
|
318
319
|
|
|
319
320
|
/**
|
|
320
321
|
* Detect GitHub token from multiple sources (auth passthrough).
|
|
321
|
-
* Priority: ENV > gh
|
|
322
|
+
* Priority: Copilot-specific ENV > github-auth-manager (OAuth/App/gh-CLI/env) > undefined
|
|
322
323
|
*/
|
|
323
|
-
function detectGitHubToken() {
|
|
324
|
-
// 1.
|
|
325
|
-
const
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
324
|
+
async function detectGitHubToken() {
|
|
325
|
+
// 1. Copilot-specific token env var (highest priority, no round-trips)
|
|
326
|
+
const copilotEnvToken = process.env.COPILOT_CLI_TOKEN || process.env.GITHUB_PAT;
|
|
327
|
+
if (copilotEnvToken) {
|
|
328
|
+
console.log("[copilot-shell] using Copilot token from environment");
|
|
329
|
+
return copilotEnvToken;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// 2. Use unified auth manager (OAuth > App installation > gh CLI > GITHUB_TOKEN/GH_TOKEN)
|
|
333
|
+
try {
|
|
334
|
+
const { token, type } = await getGitHubToken().catch(() => ({
|
|
335
|
+
token: process.env.GITHUB_TOKEN || process.env.GH_TOKEN || "",
|
|
336
|
+
type: "env",
|
|
337
|
+
}));
|
|
338
|
+
if (token) {
|
|
339
|
+
console.log(`[copilot-shell] using token from auth manager (${type})`);
|
|
340
|
+
return token;
|
|
341
|
+
}
|
|
342
|
+
} catch {
|
|
343
|
+
// fall through
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
// 3. gh CLI is authenticated — SDK will use it automatically
|
|
336
347
|
try {
|
|
337
348
|
execSync("gh auth status", { stdio: "pipe", encoding: "utf8" });
|
|
338
|
-
console.log("[copilot-shell]
|
|
339
|
-
// gh CLI is authenticated - SDK will use it automatically
|
|
349
|
+
console.log("[copilot-shell] gh CLI is authenticated — using SDK default auth");
|
|
340
350
|
return undefined;
|
|
341
351
|
} catch {
|
|
342
352
|
// gh not authenticated or not installed
|
|
343
353
|
}
|
|
344
354
|
|
|
345
|
-
// 3. VS Code auth detection could be added here
|
|
346
|
-
// For now, return undefined to let SDK use default auth flow
|
|
347
355
|
console.log("[copilot-shell] no pre-auth detected, using SDK default auth");
|
|
348
356
|
return undefined;
|
|
349
357
|
}
|
|
@@ -383,7 +391,7 @@ async function ensureClientStarted() {
|
|
|
383
391
|
process.env.GITHUB_COPILOT_CLI_PATH ||
|
|
384
392
|
undefined;
|
|
385
393
|
const cliUrl = process.env.COPILOT_CLI_URL || undefined;
|
|
386
|
-
const token = detectGitHubToken();
|
|
394
|
+
const token = await detectGitHubToken();
|
|
387
395
|
const transport = resolveCopilotTransport();
|
|
388
396
|
|
|
389
397
|
// Session mode: "local" (default) uses stdio for full model access + MCP + sub-agents.
|