gitnexushub 0.4.5 → 0.7.0

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/dist/index.js CHANGED
@@ -18,6 +18,7 @@ import { removeProjectContext } from './context.js';
18
18
  import { runSync } from './sync-command.js';
19
19
  import { ok, info, warn, fail, resolveAuth, DEFAULT_HUB_URL, EDITORS } from './cli-helpers.js';
20
20
  import { runConnect } from './connect-command.js';
21
+ import { runInstallCi } from './install-ci-command.js';
21
22
  import { registerWikiCommand } from './wiki/index.js';
22
23
  const BANNER = [
23
24
  ' ██████╗ ██╗████████╗███╗ ██╗███████╗██╗ ██╗██╗ ██╗███████╗',
@@ -56,7 +57,7 @@ program
56
57
  .command('connect', { isDefault: true })
57
58
  .description('Register Hub MCP, install skills, and write project files')
58
59
  .argument('[token]', 'gnx_ API token (optional if already saved)')
59
- .option('--editor <name>', 'Editor to configure: claude-code | cursor | windsurf | opencode')
60
+ .option('--editor <name>', 'Editor to configure: claude-code | cursor | windsurf | opencode | kiro')
60
61
  .option('--hub <url>', `Hub URL (default: saved config or ${DEFAULT_HUB_URL})`)
61
62
  .option('--skip-project', 'Only configure MCP, skip project files')
62
63
  .action(connectAction);
@@ -64,7 +65,7 @@ program
64
65
  program
65
66
  .command('disconnect')
66
67
  .description('Remove GitNexus MCP config, skills, and project files')
67
- .option('--editor <name>', 'Editor to unconfigure: claude-code | cursor | windsurf | opencode')
68
+ .option('--editor <name>', 'Editor to unconfigure: claude-code | cursor | windsurf | opencode | kiro')
68
69
  .action(async (opts) => {
69
70
  try {
70
71
  printBanner();
@@ -111,6 +112,32 @@ program
111
112
  process.exit(1);
112
113
  }
113
114
  });
115
+ // ─── install-ci command ───────────────────────────────────────────
116
+ //
117
+ // Opens a PR on the user's GitHub repo that adds the GitNexus PR
118
+ // review workflow. Mints a long-lived CI token via the Hub, sets it
119
+ // (and CLAUDE_CODE_OAUTH_TOKEN, when a local Claude Code login is
120
+ // detected) as repo Actions secrets, and base64-PUTs the rendered
121
+ // workflow YAML through the user's `gh` CLI. v2 ships a single
122
+ // workflow file; Claude is the primary review mechanism, no opt-out.
123
+ program
124
+ .command('install-ci')
125
+ .description('Open a PR adding the GitNexus PR review workflow + set Actions secrets')
126
+ .option('--hub <url>', `Hub URL (default: saved config or ${DEFAULT_HUB_URL})`)
127
+ .option('--claude-token <token>', 'Claude OAuth token from `claude setup-token` (leaks to argv — prefer the CLAUDE_CODE_OAUTH_TOKEN env var)')
128
+ .option('--claude-token-file <path>', 'Path to a file containing the Claude OAuth token (alternative to --claude-token / env var)')
129
+ .action(async (opts) => {
130
+ try {
131
+ printBanner();
132
+ await runInstallCi(opts);
133
+ }
134
+ catch (err) {
135
+ console.error('');
136
+ fail(err.message);
137
+ console.error('');
138
+ process.exit(1);
139
+ }
140
+ });
114
141
  // ─── wiki commands ────────────────────────────────────────────────
115
142
  registerWikiCommand(program);
116
143
  program.parse();
@@ -0,0 +1,176 @@
1
+ /**
2
+ * `gnx install-ci` (v2) — opens a PR on the user's GitHub repo that
3
+ * adds the single Claude-led GitNexus CI workflow, using the user's
4
+ * local `gh` CLI rather than a Hub-side OAuth-write grant.
5
+ *
6
+ * v1 shipped three workflow files (gitnexus.yml + claude.yml +
7
+ * claude-code-review.yml) and a `--no-claude` opt-out. v2 collapses
8
+ * to one file and makes Claude mandatory: the workflow's only review
9
+ * mechanism is the `anthropics/claude-code-action@v1` step, primed
10
+ * with a pre-computed Context Pack from `Akon-Labs/gitnexus-check@v2`.
11
+ *
12
+ * Pivot rationale (unchanged from v1): we don't want the Hub to keep
13
+ * a write-capable GitHub token per user. Doing the GitHub write from
14
+ * the user's machine via `gh` keeps the Hub free of `workflow` /
15
+ * `repo` OAuth scopes.
16
+ *
17
+ * Flow:
18
+ * 1. Detect git repo + GitHub remote.
19
+ * 2. Resolve fullName → repoId via `GET /api/repos`.
20
+ * 3. POST install-ci-bundle (mints CI token, renders workflow YAML).
21
+ * 4. `gh` preflight (installed, authed, has `repo` scope).
22
+ * 5. Push branch + PUT workflow file + open PR.
23
+ * 6. Set GITNEXUS_TOKEN + CLAUDE_CODE_OAUTH_TOKEN secrets via stdin.
24
+ *
25
+ * The exported `runInstallCi` is the commander entry point; the
26
+ * GitHub-write helpers (`pushWorkflowAndOpenPr`, `setRepoSecret`)
27
+ * accept an injected `runGh` for unit-testability.
28
+ */
29
+ /**
30
+ * Adapter for `gh` invocations. Real impl shells out via execFileSync;
31
+ * tests replace it with a recording fake. The Promise return shape
32
+ * mirrors what we actually need from `gh` — stdout, stderr, exit code.
33
+ *
34
+ * `input`, when set, is piped to stdin. We use this for `gh secret
35
+ * set NAME --repo OWNER/REPO`, which reads the secret value from stdin
36
+ * when no `--body` / `--body-file` flag is set. That keeps the secret
37
+ * out of argv (`ps` leak) and out of any shell history.
38
+ */
39
+ export type GhRunner = (args: string[], opts?: {
40
+ input?: string;
41
+ }) => Promise<{
42
+ stdout: string;
43
+ stderr: string;
44
+ code: number;
45
+ }>;
46
+ interface InstallCiOpts {
47
+ hub?: string;
48
+ /**
49
+ * Raw Claude Code OAuth token (output of `claude setup-token`). Lowest
50
+ * priority — leaks to argv / `ps` / shell history. Prefer the env var
51
+ * `CLAUDE_CODE_OAUTH_TOKEN` for routine use; this flag is the escape
52
+ * hatch for one-off / scripted runs.
53
+ */
54
+ claudeToken?: string;
55
+ /**
56
+ * Path to a file whose contents are the Claude OAuth token. Mid
57
+ * priority — keeps the token off argv. Useful for ops that pre-stage
58
+ * the token in a file rather than the environment.
59
+ */
60
+ claudeTokenFile?: string;
61
+ }
62
+ /**
63
+ * Entry point for `gnx install-ci`. Imported by index.ts so commander
64
+ * has a stable handle.
65
+ *
66
+ * Output is structured as 6 numbered steps with a final boxed summary.
67
+ * The styling matches the v1 install-ci output that the user explicitly
68
+ * called out as "polished" — we keep the visual contract.
69
+ */
70
+ export declare function runInstallCi(opts: InstallCiOpts): Promise<void>;
71
+ /**
72
+ * Returns the missing scope name (e.g. "repo") if the active gh auth
73
+ * session lacks one we need, else null. We check `repo` only — that
74
+ * scope covers everything install-ci does:
75
+ * - push branches & file content (Contents API)
76
+ * - open PRs (Pulls API)
77
+ * - read+write Actions secrets (`repo` is sufficient for repo-level
78
+ * secrets; only org-level secrets need `admin:org`)
79
+ *
80
+ * Falls back to "no missing scope" if we can't parse `gh auth status`
81
+ * — better to let the real call surface the real error than block on
82
+ * a parser misfire across gh versions.
83
+ */
84
+ export declare function ghMissingScope(): string | null;
85
+ /**
86
+ * Read the user's Claude Code OAuth token from local credentials so
87
+ * we can auto-set CLAUDE_CODE_OAUTH_TOKEN as a GitHub Actions secret
88
+ * — the same source Claude Code's own `/install-github-app` reads.
89
+ *
90
+ * Lookup order (descending preference — first hit wins):
91
+ *
92
+ * 1. `opts.claudeToken` from `--claude-token=<value>` (lowest priority,
93
+ * flagged for completeness — leaks to argv / `ps` / shell history).
94
+ * 2. `opts.claudeTokenFile` from `--claude-token-file=<path>` — reads
95
+ * the file's trimmed contents. Keeps the token off argv.
96
+ * 3. `process.env.CLAUDE_CODE_OAUTH_TOKEN` — the documented happy path.
97
+ * User runs `claude setup-token` once, exports the result via shell
98
+ * rc, then `gnx install-ci` picks it up automatically on every run.
99
+ *
100
+ * Why we no longer auto-detect from `~/.claude/.credentials.json`: that
101
+ * file's `claudeAiOauth.accessToken` is the user's *interactive session*
102
+ * token (~1h expiry, refreshed in-band). It is NOT a valid CI OAuth
103
+ * token — those are minted explicitly by `claude setup-token` and have
104
+ * the longer-lived headless scope set the GHA expects. Live testing on
105
+ * deer-flow PR #11 confirmed: shipping the session token to the GHA
106
+ * causes "Could not resolve auth credentials" inside the bundled
107
+ * Anthropic SDK. Better to fail loud with a clear instruction than to
108
+ * silently set the wrong token.
109
+ *
110
+ * Returns the trimmed token if any source yields one; null otherwise.
111
+ * The caller prints a clear "run `claude setup-token` first" instruction
112
+ * on null and exits non-zero rather than shipping a half-installed CI.
113
+ */
114
+ export declare function readClaudeOAuthToken(opts: {
115
+ claudeToken?: string;
116
+ claudeTokenFile?: string;
117
+ }): string | null;
118
+ /**
119
+ * Default `gh` runner — shells out via execFileSync. Captures stdout
120
+ * + stderr separately so callers can inspect each. When `input` is set,
121
+ * pipes it to stdin (used for `gh secret set` so secret material never
122
+ * lands in argv or process listings). Translates non-zero exit into a
123
+ * thrown Error whose message is the stderr text, matching the contract
124
+ * tests expect.
125
+ */
126
+ export declare const defaultGhRunner: GhRunner;
127
+ /**
128
+ * Set a repository-level Actions secret without ever putting the value
129
+ * in argv. `gh secret set NAME --repo OWNER/REPO` reads the secret
130
+ * value from stdin when neither `--body` nor `--body-file` is set —
131
+ * documented behavior across all supported gh versions.
132
+ *
133
+ * (We deliberately avoid `--body-file -`. That flag was added later
134
+ * than our minimum supported gh version; older installs reject it as
135
+ * "unknown flag" and crash the install-ci flow even though the
136
+ * underlying API supports stdin natively.)
137
+ */
138
+ export declare function setRepoSecret(runGh: GhRunner, fullName: string, name: string, value: string): Promise<void>;
139
+ export interface PushResult {
140
+ prUrl: string;
141
+ /**
142
+ * True when we found the workflow file already present on the branch
143
+ * with byte-identical content AND a PR already exists. We still
144
+ * surface the PR URL so the user can check on it; the summary box
145
+ * formats this case as "already installed" rather than "completed".
146
+ */
147
+ alreadyInstalled: boolean;
148
+ }
149
+ /**
150
+ * Push the workflow file to `branchName` and open a PR.
151
+ *
152
+ * Idempotency:
153
+ * - If `branchName` already exists we keep using it (gh's create-branch
154
+ * fails with "Reference already exists" — caught and ignored).
155
+ * - If the workflow file is already present with byte-identical
156
+ * content AND a matching PR is already open from this head, we
157
+ * mark `alreadyInstalled = true` and skip the PUT + create-PR
158
+ * calls. The branch name embeds a timestamp so practical retries
159
+ * never collide, but tests inject a fixed branch name to drive
160
+ * this code path.
161
+ * - If the file exists with different content, we PUT with the
162
+ * existing sha so it's an update.
163
+ * - If a PR creation returns 422 (duplicate), we fall back to the
164
+ * `pulls?head=...&state=all` lookup and surface that URL.
165
+ *
166
+ * `runGh` is injected so tests can drive the full idempotency matrix
167
+ * without spawning a real `gh`.
168
+ */
169
+ export declare function pushWorkflowAndOpenPr(args: {
170
+ owner: string;
171
+ repo: string;
172
+ branchName: string;
173
+ workflowYaml: string;
174
+ runGh: GhRunner;
175
+ }): Promise<PushResult>;
176
+ export {};