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 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 (set this in App settings Webhook, and keep in sync):
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 "Device Flow" in GitHub App settings (Settings Optional features)
358
- # 3. Go to Settings → GitHub in the Bosun UI and click "Sign in with GitHub"
359
- # 4. That's it — no callback URL, no tunnel URL, no client secret needed
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. Thats it — no webhook URL, no tunnel, no public server needed
360
371
  #
361
- # ALTERNATIVE: OAuth Callback (requires a stable public URL)
372
+ # ALTERNATIVE: OAuth Callback
362
373
  # Set BOSUN_GITHUB_CLIENT_ID + BOSUN_GITHUB_CLIENT_SECRET
363
- # Register callback URL in App settings:
364
- # https://<your-bosun-public-url>/api/github/callback
374
+ # Register callback URL: http://127.0.0.1:54317/github/callback
365
375
  #
366
- # WEBHOOKS (optional — for real-time PR/issue sync):
367
- # Register webhook URL in App settings:
368
- # https://<your-bosun-public-url>/api/webhooks/github/app
369
- # Webhooks require a stable public URL. Without them, Bosun polls instead.
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
- # BOSUN_GITHUB_APP_ID=
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 CLI > undefined (SDK will use default auth).
322
+ * Priority: Copilot-specific ENV > github-auth-manager (OAuth/App/gh-CLI/env) > undefined
322
323
  */
323
- function detectGitHubToken() {
324
- // 1. Direct token env vars (highest priority)
325
- const envToken =
326
- process.env.COPILOT_CLI_TOKEN ||
327
- process.env.GITHUB_TOKEN ||
328
- process.env.GH_TOKEN ||
329
- process.env.GITHUB_PAT;
330
- if (envToken) {
331
- console.log("[copilot-shell] using token from environment");
332
- return envToken;
333
- }
334
-
335
- // 2. Try to read from gh CLI auth
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] detected gh CLI authentication");
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.