@gh-symphony/cli 0.0.18 → 0.0.20

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.
@@ -351,6 +351,269 @@ var PROJECT_ITEMS_PAGE_QUERY = `
351
351
  }
352
352
  `;
353
353
 
354
+ // src/github/gh-auth.ts
355
+ import { execFileSync, spawnSync } from "child_process";
356
+ var REQUIRED_GH_SCOPES = ["repo", "read:org", "project"];
357
+ var GhAuthError = class extends Error {
358
+ constructor(code, message) {
359
+ super(message);
360
+ this.code = code;
361
+ this.name = "GhAuthError";
362
+ }
363
+ };
364
+ function ghTokenReadErrorMessage() {
365
+ return "Failed to read a GitHub token from gh CLI. Run 'gh auth status' and try again.";
366
+ }
367
+ function missingGhScopesMessage(missing) {
368
+ return `Run 'gh auth refresh --scopes repo,read:org,project'. Missing scopes: ${missing.join(", ")}`;
369
+ }
370
+ function classifyTokenValidationError(error, source) {
371
+ if (error instanceof GhAuthError) {
372
+ return error;
373
+ }
374
+ if (error instanceof GitHubApiError) {
375
+ if (error.status === 401) {
376
+ return new GhAuthError(
377
+ source === "env" ? "invalid_token" : "token_failed",
378
+ source === "env" ? "GITHUB_GRAPHQL_TOKEN is invalid or expired." : ghTokenReadErrorMessage()
379
+ );
380
+ }
381
+ const prefix = source === "env" ? "GITHUB_GRAPHQL_TOKEN could not be validated" : "gh CLI token could not be validated";
382
+ return new GhAuthError("token_failed", `${prefix}: ${error.message}`);
383
+ }
384
+ if (error instanceof Error) {
385
+ const prefix = source === "env" ? "GITHUB_GRAPHQL_TOKEN could not be validated" : "gh CLI token could not be validated";
386
+ return new GhAuthError("token_failed", `${prefix}: ${error.message}`);
387
+ }
388
+ return new GhAuthError(
389
+ "token_failed",
390
+ source === "env" ? "GITHUB_GRAPHQL_TOKEN could not be validated." : "gh CLI token could not be validated."
391
+ );
392
+ }
393
+ function getEnvGitHubToken() {
394
+ const token = process.env.GITHUB_GRAPHQL_TOKEN?.trim();
395
+ return token ? token : null;
396
+ }
397
+ function checkGhInstalled(opts) {
398
+ const execImpl = opts?.execImpl ?? execFileSync;
399
+ try {
400
+ execImpl("gh", ["--version"], { stdio: "pipe" });
401
+ return true;
402
+ } catch (error) {
403
+ const execError = error;
404
+ if (execError.code === "ENOENT") {
405
+ return false;
406
+ }
407
+ throw error;
408
+ }
409
+ }
410
+ function checkGhAuthenticated(opts) {
411
+ const spawnImpl = opts?.spawnImpl ?? spawnSync;
412
+ const result = spawnImpl("gh", ["auth", "status"], {
413
+ encoding: "utf8",
414
+ stdio: ["pipe", "pipe", "pipe"]
415
+ });
416
+ if ((result.status ?? 1) !== 0) {
417
+ return { authenticated: false };
418
+ }
419
+ const login = parseLogin((result.stdout ?? "").toString());
420
+ return { authenticated: true, login };
421
+ }
422
+ function checkGhScopes(opts) {
423
+ const spawnImpl = opts?.spawnImpl ?? spawnSync;
424
+ const result = spawnImpl("gh", ["auth", "status"], {
425
+ encoding: "utf8",
426
+ stdio: ["pipe", "pipe", "pipe"]
427
+ });
428
+ const output = (result.stdout ?? "").toString();
429
+ const scopes = parseScopes(output);
430
+ if (scopes.length === 0) {
431
+ return { valid: true, missing: [], scopes: [] };
432
+ }
433
+ const normalized = scopes.map((scope) => scope.toLowerCase());
434
+ const missing = REQUIRED_GH_SCOPES.filter(
435
+ (scope) => !normalized.includes(scope)
436
+ );
437
+ return {
438
+ valid: missing.length === 0,
439
+ missing: [...missing],
440
+ scopes
441
+ };
442
+ }
443
+ function getGhToken(opts) {
444
+ const envToken = opts?.allowEnv === false ? null : getEnvGitHubToken();
445
+ if (envToken) {
446
+ return envToken;
447
+ }
448
+ return getGhTokenWithSource({
449
+ execImpl: opts?.execImpl,
450
+ envToken: void 0
451
+ }).token;
452
+ }
453
+ function getGhTokenWithSource(opts) {
454
+ const hasExplicitEnvToken = opts !== void 0 && Object.prototype.hasOwnProperty.call(opts, "envToken");
455
+ const envToken = hasExplicitEnvToken ? opts.envToken?.trim() ?? null : getEnvGitHubToken();
456
+ if (envToken) {
457
+ return { token: envToken, source: "env" };
458
+ }
459
+ const execImpl = opts?.execImpl ?? execFileSync;
460
+ try {
461
+ const token = execImpl("gh", ["auth", "token"], {
462
+ encoding: "utf8",
463
+ stdio: ["pipe", "pipe", "pipe"]
464
+ }).toString().trim();
465
+ if (!token) {
466
+ throw new GhAuthError("token_failed", ghTokenReadErrorMessage());
467
+ }
468
+ return { token, source: "gh" };
469
+ } catch (error) {
470
+ if (error instanceof GhAuthError) {
471
+ throw error;
472
+ }
473
+ throw new GhAuthError("token_failed", ghTokenReadErrorMessage());
474
+ }
475
+ }
476
+ async function validateGitHubToken(token, source, opts) {
477
+ const createClientImpl = opts?.createClientImpl ?? createClient;
478
+ const validateTokenImpl = opts?.validateTokenImpl ?? validateToken;
479
+ const checkRequiredScopesImpl = opts?.checkRequiredScopesImpl ?? checkRequiredScopes;
480
+ let viewer;
481
+ try {
482
+ const client = createClientImpl(token);
483
+ viewer = await validateTokenImpl(client);
484
+ } catch (error) {
485
+ throw classifyTokenValidationError(error, source);
486
+ }
487
+ const scopeCheck = checkRequiredScopesImpl(viewer.scopes);
488
+ if (!scopeCheck.valid) {
489
+ if (source === "env") {
490
+ throw new GhAuthError(
491
+ "missing_scopes",
492
+ `GITHUB_GRAPHQL_TOKEN is missing required scopes: ${scopeCheck.missing.join(", ")}`
493
+ );
494
+ }
495
+ throw new GhAuthError(
496
+ "missing_scopes",
497
+ missingGhScopesMessage(scopeCheck.missing)
498
+ );
499
+ }
500
+ return {
501
+ source,
502
+ token,
503
+ login: viewer.login,
504
+ scopes: viewer.scopes
505
+ };
506
+ }
507
+ async function resolveGitHubAuth(opts) {
508
+ const envToken = getEnvGitHubToken();
509
+ let envError = null;
510
+ if (envToken) {
511
+ try {
512
+ return await validateGitHubToken(envToken, "env", opts);
513
+ } catch (error) {
514
+ if (error instanceof GhAuthError) {
515
+ envError = error;
516
+ } else {
517
+ throw error;
518
+ }
519
+ }
520
+ }
521
+ try {
522
+ const auth = ensureGhAuth(opts);
523
+ return await validateGitHubToken(auth.token, "gh", opts);
524
+ } catch (error) {
525
+ if (envError && error instanceof GhAuthError) {
526
+ throw envError;
527
+ }
528
+ throw error;
529
+ }
530
+ }
531
+ function ensureGhAuth(opts) {
532
+ const execImpl = opts?.execImpl ?? execFileSync;
533
+ const spawnImpl = opts?.spawnImpl ?? spawnSync;
534
+ if (!checkGhInstalled({ execImpl })) {
535
+ throw new GhAuthError(
536
+ "not_installed",
537
+ "gh CLI is not installed. Install it from https://cli.github.com or set GITHUB_GRAPHQL_TOKEN."
538
+ );
539
+ }
540
+ const auth = checkGhAuthenticated({ spawnImpl });
541
+ if (!auth.authenticated) {
542
+ throw new GhAuthError(
543
+ "not_authenticated",
544
+ "Run 'gh auth login --scopes repo,read:org,project' or set GITHUB_GRAPHQL_TOKEN."
545
+ );
546
+ }
547
+ const scopeCheck = checkGhScopes({ spawnImpl });
548
+ if (!scopeCheck.valid) {
549
+ throw new GhAuthError(
550
+ "missing_scopes",
551
+ missingGhScopesMessage(scopeCheck.missing)
552
+ );
553
+ }
554
+ const { token } = getGhTokenWithSource({
555
+ execImpl,
556
+ envToken: void 0
557
+ });
558
+ return { login: auth.login ?? "unknown", token, source: "gh" };
559
+ }
560
+ function isInteractiveTerminal() {
561
+ return process.stdin.isTTY === true && process.stdout.isTTY === true;
562
+ }
563
+ function runGhAuthCommand(mode, opts) {
564
+ const spawnImpl = opts?.spawnImpl ?? spawnSync;
565
+ const command = `gh auth ${mode} --scopes ${REQUIRED_GH_SCOPES.join(",")}`;
566
+ const interactive = opts?.interactive ?? isInteractiveTerminal();
567
+ if (!interactive) {
568
+ return {
569
+ mode,
570
+ status: "manual",
571
+ command,
572
+ summary: `Interactive terminal not available. Run '${command}' manually.`
573
+ };
574
+ }
575
+ const result = spawnImpl(
576
+ "gh",
577
+ ["auth", mode, "--scopes", REQUIRED_GH_SCOPES.join(",")],
578
+ {
579
+ stdio: "inherit"
580
+ }
581
+ );
582
+ if ((result.status ?? 1) === 0) {
583
+ return {
584
+ mode,
585
+ status: "applied",
586
+ command,
587
+ summary: `Executed '${command}'.`
588
+ };
589
+ }
590
+ return {
591
+ mode,
592
+ status: "manual",
593
+ command,
594
+ summary: `Failed to complete '${command}' automatically. Re-run it manually.`
595
+ };
596
+ }
597
+ function runGhAuthLogin(opts) {
598
+ return runGhAuthCommand("login", opts);
599
+ }
600
+ function runGhAuthRefresh(opts) {
601
+ return runGhAuthCommand("refresh", opts);
602
+ }
603
+ function parseLogin(output) {
604
+ const matched = output.match(
605
+ /Logged in to github\.com account\s+\*?\*?([A-Za-z0-9_-]+)\*?\*?/i
606
+ );
607
+ return matched?.[1];
608
+ }
609
+ function parseScopes(output) {
610
+ const matched = output.match(/Token scopes:\s*(.+)/i);
611
+ if (!matched) {
612
+ return [];
613
+ }
614
+ return matched[1].split(",").map((scope) => scope.trim().replace(/^'+|'+$/g, "")).filter((scope) => scope.length > 0);
615
+ }
616
+
354
617
  export {
355
618
  GitHubApiError,
356
619
  GitHubScopeError,
@@ -358,5 +621,18 @@ export {
358
621
  validateToken,
359
622
  checkRequiredScopes,
360
623
  listUserProjects,
361
- getProjectDetail
624
+ getProjectDetail,
625
+ REQUIRED_GH_SCOPES,
626
+ GhAuthError,
627
+ getEnvGitHubToken,
628
+ checkGhInstalled,
629
+ checkGhAuthenticated,
630
+ checkGhScopes,
631
+ getGhToken,
632
+ getGhTokenWithSource,
633
+ validateGitHubToken,
634
+ resolveGitHubAuth,
635
+ ensureGhAuth,
636
+ runGhAuthLogin,
637
+ runGhAuthRefresh
362
638
  };
@@ -28,7 +28,9 @@ var config_cmd_default = handler;
28
28
  async function configShow(options) {
29
29
  const config = await loadGlobalConfig(options.configDir);
30
30
  if (!config) {
31
- process.stderr.write("No configuration found. Run 'gh-symphony init'.\n");
31
+ process.stderr.write(
32
+ "No configuration found. Run 'gh-symphony workflow init'.\n"
33
+ );
32
34
  process.exitCode = 1;
33
35
  return;
34
36
  }