@bragi-gmbh/codebus 1.2.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.
Files changed (133) hide show
  1. package/bin/codebus.js +2 -0
  2. package/dist/binding-guard.js +97 -0
  3. package/dist/binding-guard.js.map +1 -0
  4. package/dist/client.js +94 -0
  5. package/dist/client.js.map +1 -0
  6. package/dist/commands/ack.js +47 -0
  7. package/dist/commands/ack.js.map +1 -0
  8. package/dist/commands/archive.js +21 -0
  9. package/dist/commands/archive.js.map +1 -0
  10. package/dist/commands/as.js +299 -0
  11. package/dist/commands/as.js.map +1 -0
  12. package/dist/commands/ask.js +189 -0
  13. package/dist/commands/ask.js.map +1 -0
  14. package/dist/commands/auth.js +33 -0
  15. package/dist/commands/auth.js.map +1 -0
  16. package/dist/commands/checkin.js +235 -0
  17. package/dist/commands/checkin.js.map +1 -0
  18. package/dist/commands/decision.js +52 -0
  19. package/dist/commands/decision.js.map +1 -0
  20. package/dist/commands/digest.js +117 -0
  21. package/dist/commands/digest.js.map +1 -0
  22. package/dist/commands/doctor.js +106 -0
  23. package/dist/commands/doctor.js.map +1 -0
  24. package/dist/commands/factory.js +191 -0
  25. package/dist/commands/factory.js.map +1 -0
  26. package/dist/commands/github-login.js +205 -0
  27. package/dist/commands/github-login.js.map +1 -0
  28. package/dist/commands/graph.js +88 -0
  29. package/dist/commands/graph.js.map +1 -0
  30. package/dist/commands/health.js +34 -0
  31. package/dist/commands/health.js.map +1 -0
  32. package/dist/commands/inbox.js +83 -0
  33. package/dist/commands/inbox.js.map +1 -0
  34. package/dist/commands/ingest.js +553 -0
  35. package/dist/commands/ingest.js.map +1 -0
  36. package/dist/commands/init.js +97 -0
  37. package/dist/commands/init.js.map +1 -0
  38. package/dist/commands/install.js +520 -0
  39. package/dist/commands/install.js.map +1 -0
  40. package/dist/commands/invite.js +152 -0
  41. package/dist/commands/invite.js.map +1 -0
  42. package/dist/commands/jira.js +237 -0
  43. package/dist/commands/jira.js.map +1 -0
  44. package/dist/commands/join.js +96 -0
  45. package/dist/commands/join.js.map +1 -0
  46. package/dist/commands/links.js +66 -0
  47. package/dist/commands/links.js.map +1 -0
  48. package/dist/commands/login.js +84 -0
  49. package/dist/commands/login.js.map +1 -0
  50. package/dist/commands/morning.js +121 -0
  51. package/dist/commands/morning.js.map +1 -0
  52. package/dist/commands/next.js +66 -0
  53. package/dist/commands/next.js.map +1 -0
  54. package/dist/commands/onboarding.js +109 -0
  55. package/dist/commands/onboarding.js.map +1 -0
  56. package/dist/commands/pack.js +276 -0
  57. package/dist/commands/pack.js.map +1 -0
  58. package/dist/commands/plan-gh.js +425 -0
  59. package/dist/commands/plan-gh.js.map +1 -0
  60. package/dist/commands/plan.js +465 -0
  61. package/dist/commands/plan.js.map +1 -0
  62. package/dist/commands/pr.js +88 -0
  63. package/dist/commands/pr.js.map +1 -0
  64. package/dist/commands/question.js +142 -0
  65. package/dist/commands/question.js.map +1 -0
  66. package/dist/commands/read.js +84 -0
  67. package/dist/commands/read.js.map +1 -0
  68. package/dist/commands/refs.js +357 -0
  69. package/dist/commands/refs.js.map +1 -0
  70. package/dist/commands/reply.js +38 -0
  71. package/dist/commands/reply.js.map +1 -0
  72. package/dist/commands/repo.js +1198 -0
  73. package/dist/commands/repo.js.map +1 -0
  74. package/dist/commands/report.js +41 -0
  75. package/dist/commands/report.js.map +1 -0
  76. package/dist/commands/resolve-refs.js +21 -0
  77. package/dist/commands/resolve-refs.js.map +1 -0
  78. package/dist/commands/resolve.js +93 -0
  79. package/dist/commands/resolve.js.map +1 -0
  80. package/dist/commands/send.js +168 -0
  81. package/dist/commands/send.js.map +1 -0
  82. package/dist/commands/sent.js +22 -0
  83. package/dist/commands/sent.js.map +1 -0
  84. package/dist/commands/session.js +426 -0
  85. package/dist/commands/session.js.map +1 -0
  86. package/dist/commands/statusline.js +43 -0
  87. package/dist/commands/statusline.js.map +1 -0
  88. package/dist/commands/task.js +140 -0
  89. package/dist/commands/task.js.map +1 -0
  90. package/dist/commands/tick.js +233 -0
  91. package/dist/commands/tick.js.map +1 -0
  92. package/dist/commands/upgrade.js +76 -0
  93. package/dist/commands/upgrade.js.map +1 -0
  94. package/dist/commands/watch.js +169 -0
  95. package/dist/commands/watch.js.map +1 -0
  96. package/dist/commands/whoami.js +27 -0
  97. package/dist/commands/whoami.js.map +1 -0
  98. package/dist/config.js +103 -0
  99. package/dist/config.js.map +1 -0
  100. package/dist/directives.js +102 -0
  101. package/dist/directives.js.map +1 -0
  102. package/dist/errors.js +110 -0
  103. package/dist/errors.js.map +1 -0
  104. package/dist/git-hooks.js +116 -0
  105. package/dist/git-hooks.js.map +1 -0
  106. package/dist/graph-bootstrap.js +95 -0
  107. package/dist/graph-bootstrap.js.map +1 -0
  108. package/dist/graph-remote.js +889 -0
  109. package/dist/graph-remote.js.map +1 -0
  110. package/dist/graph.js +335 -0
  111. package/dist/graph.js.map +1 -0
  112. package/dist/index.js +154 -0
  113. package/dist/index.js.map +1 -0
  114. package/dist/kit/graph-publish.mjs +400 -0
  115. package/dist/mail-latency.js +120 -0
  116. package/dist/mail-latency.js.map +1 -0
  117. package/dist/mcp-registry.js +859 -0
  118. package/dist/mcp-registry.js.map +1 -0
  119. package/dist/mcp.js +193 -0
  120. package/dist/mcp.js.map +1 -0
  121. package/dist/output.js +12 -0
  122. package/dist/output.js.map +1 -0
  123. package/dist/positioning.js +21 -0
  124. package/dist/positioning.js.map +1 -0
  125. package/dist/rolestore.js +89 -0
  126. package/dist/rolestore.js.map +1 -0
  127. package/dist/session.js +49 -0
  128. package/dist/session.js.map +1 -0
  129. package/dist/sessionstore.js +736 -0
  130. package/dist/sessionstore.js.map +1 -0
  131. package/dist/version-check.js +97 -0
  132. package/dist/version-check.js.map +1 -0
  133. package/package.json +47 -0
@@ -0,0 +1,1198 @@
1
+ /**
2
+ * repo.ts — `codebus repo connect|status|list` (docs/plans/repo-intelligence.md
3
+ * Track A4).
4
+ *
5
+ * `repo connect <github-url> [--slug <s>] [--default-branch <b>] [--force]
6
+ * [--write-workflow] [--build] [--no-pr-sync]`
7
+ * (`--branch` is accepted as a deprecated alias for `--default-branch` —
8
+ * round-11 review finding F6: the plan's own file spec names the flag
9
+ * `--default-branch`, matching the RPC parameter it feeds.)
10
+ * Parses <github-url> (https, ssh, or bare owner/repo — always normalized
11
+ * to a canonical https://github.com/<owner>/<repo> URL before it reaches
12
+ * the RPC, since 0070's trg_repos_enforce_remote_url_normalized only
13
+ * accepts an https(s) scheme). The resolved default branch is validated
14
+ * against git's OWN ref-format rules (round-12 review finding 7,
15
+ * CONFIRMED) before anything else runs — see `isValidGitBranchName`.
16
+ * Calls `repo_connect` (0070). If the caller is not this repo's authority
17
+ * (workspace admin or designated curator — 0050 graph_repo_authority),
18
+ * prints remediation and stops WITHOUT attempting a token mint
19
+ * (re-runnable later once authority is granted). Otherwise mints a fresh
20
+ * `ingest`-scope token (graph_ingest_token_mint, 0059) — round-12 review
21
+ * finding 5 (CONFIRMED): minting now happens LAST among the fallible
22
+ * steps below (after the workflow write/validate and the ci_wired
23
+ * transition), immediately before the one remaining step that structurally
24
+ * needs the plaintext (`--build`), so a failure in an EARLIER step never
25
+ * wastes an unrecoverable token row; see repoConnectFull's own doc for the
26
+ * residual `--build`-failure case and how it is covered. `--json` mode
27
+ * prints the token ONLY as a field inside the single JSON object on
28
+ * stdout — nothing else follows it (round-12 review finding 2: this now
29
+ * also holds when `--build` is combined with `--json` — the child
30
+ * process's own stdout no longer shares this process's stdout, see
31
+ * repoConnectFull); human mode prints it exactly once, inline with the
32
+ * `gh secret set` instructions (round-11 review finding F3: plaintext
33
+ * token must never hit `--json` output mixed with free text; round-12
34
+ * review finding 6, CONFIRMED: it must also never print twice within
35
+ * human mode's own block — the `gh secret set` line now references the
36
+ * token printed just above it rather than repeating it inline).
37
+ * `--write-workflow` validates this cwd's own `origin` remote against the
38
+ * connected repo and emits `.github/workflows/codebus.yml`, with the
39
+ * resolved default branch single-quoted and escaped into the YAML (round-
40
+ * 12 review finding 7, CONFIRMED — a comma or `]` is legal in a git
41
+ * branch name but breaks an UNQUOTED YAML flow sequence). Round-12 review
42
+ * finding 1 (CONFIRMED, BLOCKER): `--write-workflow` no longer transitions
43
+ * the attachment to `ci_wired` by itself. Round-13 review finding 2
44
+ * (CONFIRMED, BLOCKER) fixed the underlying reusable workflow itself
45
+ * (`.github/workflows/graphify-publish.yml` now checks out CodeBus's own
46
+ * scripts/lockfiles into `.codebus-kit/` for a foreign caller, see that
47
+ * file's own comment) — the emitted workflow CAN now actually run for a
48
+ * foreign repo — but a file merely existing on disk still proves nothing
49
+ * about whether IT ACTUALLY DID (secrets not yet added to the repo's
50
+ * `graph-publish` environment, the environment itself not yet created, a
51
+ * network blip, ...), so this command still does not claim `ci_wired`
52
+ * just because the write succeeded. `ci_wired` must mean "CI actually
53
+ * ran," not "a file was written" — writing the file is necessary but
54
+ * never sufficient by itself. Two things now earn `ci_wired`: `--build`
55
+ * transitions pending -> ci_wired BEFORE running (plan's own state-machine
56
+ * trap: attachment must not sit `pending` while a build has already run),
57
+ * then actually runs `scripts/graph-publish.mjs` locally against the
58
+ * freshly minted token, surfacing a quarantine/25MB-reject verbatim — a
59
+ * real, local, end-to-end proof that ingest works; and, round-13 review
60
+ * finding 3 (CONFIRMED, MAJOR), a genuine CI-sourced ingest arriving while
61
+ * still `pending` (0063's `graph_ingest_artifact`) now itself advances
62
+ * pending -> ci_wired -> first_build_ok -> live in one call — equally
63
+ * strong proof, since it is a REAL external caller authenticating with
64
+ * this repo's own token and actually landing servable content. Nothing
65
+ * else auto-promotes OUT of `pending`, and `--write-workflow` deliberately
66
+ * still does not. Round-11 review finding F4 (CONFIRMED, still enforced):
67
+ * the transition used to run BEFORE the origin-match validation and file
68
+ * write, so a mismatched checkout or a filesystem failure could leave the
69
+ * database falsely claiming CI is wired despite no workflow file ever
70
+ * landing; validate-then-write still fully precedes the `--build`
71
+ * transition. Round-13 review finding 3 additionally moved the MINT
72
+ * ahead of that transition too (see `repoConnectFull`'s own doc) — minting
73
+ * is itself fallible, and a false `ci_wired` claim on a failed mint would
74
+ * be exactly the same class of bug this whole paragraph is about.
75
+ *
76
+ * `repo status <slug>` — graph_status (0050) + a PR status aggregate.
77
+ * `repo list` — every repo in this workspace + its attachment state.
78
+ */
79
+ import { execFileSync, spawnSync, execFile } from "node:child_process";
80
+ import { promisify } from "node:util";
81
+ import { mkdirSync, writeFileSync, existsSync } from "node:fs";
82
+ import { dirname, join } from "node:path";
83
+ import { fileURLToPath } from "node:url";
84
+ import { resolveConfig } from "../config.js";
85
+ import { makeCtx, workspaceId, repoId as resolveRepoIdBySlug } from "../client.js";
86
+ import { render } from "../output.js";
87
+ import { CodebusError, PostMintFailureError } from "../errors.js";
88
+ import { redactUrlUserinfo } from "./plan-gh.js";
89
+ const OWNER_REPO_TAIL = "([A-Za-z0-9._-]+)/([A-Za-z0-9._-]+?)(?:\\.git)?/?$";
90
+ /**
91
+ * Accepts https://github.com/<owner>/<repo>[.git], git@github.com:<owner>/
92
+ * <repo>[.git], ssh://git@github.com[:port]/<owner>/<repo>[.git], and a bare
93
+ * <owner>/<repo>. Returns null (never throws) on anything else. Always
94
+ * returns the canonical https form — repo_connect's trigger requires it.
95
+ */
96
+ export function parseGithubUrl(input) {
97
+ const s = input.trim();
98
+ const patterns = [
99
+ new RegExp(`^https?://(?:[^/@\\s]+@)?github\\.com(?::\\d+)?/${OWNER_REPO_TAIL}`, "i"),
100
+ new RegExp(`^git@github\\.com:${OWNER_REPO_TAIL}`, "i"),
101
+ new RegExp(`^ssh://git@github\\.com(?::\\d+)?/${OWNER_REPO_TAIL}`, "i"),
102
+ ];
103
+ for (const re of patterns) {
104
+ const m = s.match(re);
105
+ if (m)
106
+ return { owner: m[1], name: m[2], canonicalUrl: `https://github.com/${m[1]}/${m[2]}` };
107
+ }
108
+ // Bare "owner/repo" — only when the input doesn't already look like a URL
109
+ // or scp-style remote (no scheme separator, no '@').
110
+ if (!s.includes("://") && !s.includes("@")) {
111
+ const m = s.match(new RegExp(`^${OWNER_REPO_TAIL}`, "i"));
112
+ if (m)
113
+ return { owner: m[1], name: m[2], canonicalUrl: `https://github.com/${m[1]}/${m[2]}` };
114
+ }
115
+ return null;
116
+ }
117
+ /** `git remote get-url origin`, or null if there is no such remote / this
118
+ * isn't a git checkout. Never throws. */
119
+ export function getGitRemoteUrl(repoRoot) {
120
+ try {
121
+ const url = execFileSync("git", ["remote", "get-url", "origin"], { cwd: repoRoot, encoding: "utf-8" }).trim();
122
+ return url || null;
123
+ }
124
+ catch {
125
+ return null;
126
+ }
127
+ }
128
+ /**
129
+ * Round-16 review finding 1, part 2 (CONFIRMED, BLOCKER): this checkout's
130
+ * own git origin must match the connected repo before EITHER
131
+ * `--write-workflow` writes a file for it OR `--build` runs a local
132
+ * ingest against whatever code sits in it. The previous revision only ran
133
+ * this check for `--write-workflow` — `--build` alone (no
134
+ * `--write-workflow`) could run unchecked against ANY cwd, so an operator
135
+ * (or a script) that minted a token for repo A while sitting in an
136
+ * unrelated checkout B would locally build+ingest B's code into A's graph,
137
+ * using A's own credentials, with nothing catching the mismatch. Extracted
138
+ * so both flags run the IDENTICAL check rather than two copies that can
139
+ * drift; `flagName` only changes the error message's wording, never the
140
+ * logic.
141
+ */
142
+ export function validateOriginMatchesConnectedRepo(repoRoot, owner, name, flagName) {
143
+ const originUrl = getGitRemoteUrl(repoRoot);
144
+ const originParsed = originUrl ? parseGithubUrl(originUrl) : null;
145
+ if (!originParsed || originParsed.owner.toLowerCase() !== owner.toLowerCase() || originParsed.name.toLowerCase() !== name.toLowerCase()) {
146
+ // Round-19 review finding (CONFIRMED, credential-leakage theme): this
147
+ // checkout's own 'origin' is fully local-environment-controlled, and a
148
+ // git remote legally embeds HTTP Basic-style userinfo
149
+ // (`https://token@github.com/...`) — never echo it raw.
150
+ throw new CodebusError(`${flagName} refused: this checkout's origin remote (${originUrl ? redactUrlUserinfo(originUrl) : "none"}) does not match the ` +
151
+ `connected repo (${owner}/${name}). Run this from a checkout of the connected repo.`);
152
+ }
153
+ }
154
+ const execFileP = promisify(execFile);
155
+ const CODEBUS_ORG = "Bragi-GmbH";
156
+ export function realRepoVisibilityChecker() {
157
+ return {
158
+ /* c8 ignore start -- shells out to the real `gh` binary/GitHub API,
159
+ * same irreducible-external-I/O exemption as plan-gh.ts's
160
+ * realGhRunner() (DEC-CB-009 EX-001 class) — every test below injects
161
+ * its own mock RepoVisibilityChecker instead. */
162
+ async visibility(owner, name) {
163
+ const { stdout } = await execFileP("gh", ["repo", "view", `${owner}/${name}`, "--json", "visibility"]);
164
+ const parsed = JSON.parse(stdout);
165
+ if (!parsed.visibility)
166
+ throw new Error(`\`gh repo view ${owner}/${name} --json visibility\` returned no visibility field`);
167
+ return parsed.visibility.toLowerCase();
168
+ },
169
+ /* c8 ignore stop */
170
+ };
171
+ }
172
+ /**
173
+ * Round-12 review finding F1 (CONFIRMED, MAJOR): the reusable workflow this
174
+ * CLI's own generated `codebus.yml` calls (`uses: Bragi-GmbH/CodeBus/
175
+ * .github/workflows/graphify-publish.yml@main`) lives in Bragi-GmbH/CodeBus
176
+ * — a PRIVATE repository (verified via `gh repo view Bragi-GmbH/CodeBus
177
+ * --json visibility` -> PRIVATE, round-14 review finding 1). GitHub's own
178
+ * reusable-workflow access rules (docs/actions/reference/workflows-and-
179
+ * actions/reusing-workflow-configurations#access-to-reusable-workflows,
180
+ * verified against the docs before writing this predicate) resolve the
181
+ * `uses:` reference BEFORE the calling workflow run's secrets — including
182
+ * this CLI's own emitted `CODEBUS_KIT_TOKEN` — are ever read:
183
+ * - A PUBLIC caller repo can NEVER resolve a private called workflow, in
184
+ * any configuration; no secret or token can retroactively authorize a
185
+ * reference GitHub already refused to resolve.
186
+ * - A PRIVATE or INTERNAL caller repo can resolve it only if it is owned
187
+ * by the SAME organization as the called repo (here: Bragi-GmbH) AND
188
+ * Bragi-GmbH/CodeBus's own Actions "Access" setting (Settings ->
189
+ * Actions -> General -> "Access", on Bragi-GmbH/CodeBus itself) is set
190
+ * to "Accessible from repositories in the Bragi-GmbH organization" (or
191
+ * broader) rather than its more restrictive default ("Not
192
+ * accessible"). This CLI cannot verify that second, CodeBus-side
193
+ * setting remotely (it requires admin access to Bragi-GmbH/CodeBus,
194
+ * which an arbitrary connecting repo's operator will not have) — the
195
+ * error text below names it explicitly so an operator whose repo DOES
196
+ * pass this predicate but still sees a dead workflow at push time knows
197
+ * exactly which CodeBus-side setting to ask an admin about, rather than
198
+ * assuming the CLI itself is broken.
199
+ *
200
+ * Before this fix, `--write-workflow` (and `--build`, which always follows
201
+ * the identical write-workflow gate in repoConnectFull) accepted ANY
202
+ * connected repo and silently emitted a workflow file that could never
203
+ * actually run for one owned outside Bragi-GmbH, or one that is public —
204
+ * GitHub itself would refuse the `uses:` resolution on the very first push,
205
+ * with no signal back to this CLI or its operator at connect time.
206
+ *
207
+ * `checker` is injectable (default: `realRepoVisibilityChecker()`) so tests
208
+ * never shell out to the real `gh` — matches plan-gh.ts's GhRunner
209
+ * injection convention.
210
+ */
211
+ export async function assertEligibleForCodebusReusableWorkflow(owner, name, checker = realRepoVisibilityChecker()) {
212
+ // Round-13 review finding (CONFIRMED, MINOR): GitHub owner/org identities
213
+ // are case-INSENSITIVE (github.com/Bragi-GmbH and github.com/bragi-gmbh
214
+ // are the identical entity) — `validateOriginMatchesConnectedRepo` above
215
+ // already compares owner/name via `.toLowerCase()` for exactly this
216
+ // reason, but this check used a bare `!==`, so a perfectly valid,
217
+ // differently-cased reference to the SAME organization (e.g.
218
+ // `bragi-gmbh/repo`, or `BRAGI-GMBH/repo` — `parseGithubUrl` preserves
219
+ // whatever casing the caller typed, it does not itself normalize) was
220
+ // incorrectly rejected as "not owned by Bragi-GmbH". Fixed by comparing
221
+ // lowercase-normalized values, matching the origin check's own
222
+ // convention. `name` is never compared against anything in this
223
+ // function (only interpolated into messages and passed through to
224
+ // `checker.visibility`, which — like `gh repo view`/GitHub itself — is
225
+ // already case-insensitive), so no equivalent fix is needed for it.
226
+ if (owner.toLowerCase() !== CODEBUS_ORG.toLowerCase()) {
227
+ throw new CodebusError(`--write-workflow refused: '${owner}/${name}' is not owned by ${CODEBUS_ORG}. The generated workflow calls a ` +
228
+ `PRIVATE reusable workflow (${CODEBUS_ORG}/CodeBus/.github/workflows/graphify-publish.yml) — GitHub's own ` +
229
+ "reusable-workflow access rules mean a repo outside that organization can never resolve this reference at " +
230
+ "all, regardless of any secret this repo supplies (CODEBUS_KIT_TOKEN arrives only after GitHub would already " +
231
+ `have refused the resolution). Connect a repo owned by ${CODEBUS_ORG}, or omit --write-workflow/--build.`);
232
+ }
233
+ let vis;
234
+ try {
235
+ vis = await checker.visibility(owner, name);
236
+ }
237
+ catch (e) {
238
+ throw new CodebusError(`--write-workflow refused: could not determine '${owner}/${name}'s visibility via \`gh repo view\` (required ` +
239
+ `to confirm it is eligible to call ${CODEBUS_ORG}/CodeBus's private reusable workflow) — ` +
240
+ `${e instanceof Error ? e.message : String(e)}`);
241
+ }
242
+ if (vis !== "private" && vis !== "internal") {
243
+ throw new CodebusError(`--write-workflow refused: '${owner}/${name}' is ${vis}, but ${CODEBUS_ORG}/CodeBus's reusable workflow ` +
244
+ "(graphify-publish.yml) is PRIVATE. GitHub only allows a private or internal caller repository to resolve a " +
245
+ "private called workflow — a public caller can never resolve it, in any configuration. Make this repository " +
246
+ `private or internal within ${CODEBUS_ORG}, or omit --write-workflow/--build. Even once private/internal, ` +
247
+ `${CODEBUS_ORG}/CodeBus's own Settings -> Actions -> General -> "Access" setting must ALSO be configured to ` +
248
+ `"Accessible from repositories in the ${CODEBUS_ORG} organization" (or broader) — this CLI cannot verify that ` +
249
+ "setting remotely; ask a CodeBus admin to confirm it if the emitted workflow still cannot resolve the " +
250
+ "reusable workflow after this repo is made private/internal.");
251
+ }
252
+ }
253
+ /**
254
+ * Round-16 review finding 1, part 1 (CONFIRMED, BLOCKER): `--build` used to
255
+ * execute `<opts.repoRoot>/scripts/graph-publish.mjs` — a path INSIDE the
256
+ * repo the operator just connected. For any repo other than CodeBus itself
257
+ * that script normally does not exist at all (the build simply fails) —
258
+ * but far worse, it is a path a malicious or merely-compromised connected
259
+ * repo fully controls: planting its OWN file there would hand it the
260
+ * freshly-minted ingest token plus SUPABASE_URL/SUPABASE_ANON_KEY, in full,
261
+ * the instant `--build` ran. Fixed: `--build` now ALWAYS resolves and runs
262
+ * a TRUSTED copy of `graph-publish.mjs` shipped alongside the CLI's OWN
263
+ * installation — never anything inside the connected repo's checkout,
264
+ * regardless of what that checkout contains.
265
+ *
266
+ * Resolution walks up from the CURRENTLY RUNNING module's own file
267
+ * location (`moduleUrl`, always this file's own `import.meta.url` at the
268
+ * real call site — never `opts.repoRoot`) to this CLI package's own root:
269
+ * exactly three levels up from `<pkg>/src/commands/repo.ts` (dev, tsx) or
270
+ * `<pkg>/dist/commands/repo.js` (compiled) lands at `<pkg>/` either way,
271
+ * since `src/` and `dist/` are each exactly one level under the package
272
+ * root and `commands/` is exactly one level under EACH of those — the
273
+ * level count is identical for both the dev-worktree layout and the
274
+ * installed-npm-package layout (cli/package.json's own `files: ["bin",
275
+ * "dist"]`). From there, the ONLY path ever trusted is
276
+ * `<pkg>/dist/kit/graph-publish.mjs` — the copy `npm run build` places
277
+ * there from this monorepo's own `scripts/graph-publish.mjs` (see
278
+ * cli/package.json's `build` script), which `files: ["dist"]` therefore
279
+ * already ships inside the published npm package too (no `files` entry
280
+ * change needed — the whole `dist/` tree, `kit/` included, is already
281
+ * covered).
282
+ *
283
+ * Deliberately NO fallback to any monorepo-relative path (e.g.
284
+ * `<pkg>/../scripts/...`) — that would reintroduce exactly the kind of
285
+ * "resolve relative to something outside the CLI's own trusted
286
+ * installation" hazard this fix exists to close, for a dev-convenience
287
+ * savings that is not worth it. A dev worktree run without `npm run build`
288
+ * having populated `dist/kit/` first fails loudly here instead — the
289
+ * correct, safe behavior; never a silent wrong-file execution.
290
+ */
291
+ export function resolveTrustedGraphPublishScript(moduleUrl) {
292
+ const packageRoot = dirname(dirname(dirname(fileURLToPath(moduleUrl))));
293
+ const trustedPath = join(packageRoot, "dist", "kit", "graph-publish.mjs");
294
+ if (!existsSync(trustedPath)) {
295
+ throw new CodebusError(`--build cannot find its trusted graph-publish.mjs at ${trustedPath} — this CLI installation looks ` +
296
+ "incomplete (run `npm run build` in the CodeBus monorepo, or reinstall the @bragi-gmbh/codebus package). " +
297
+ "Refusing to fall back to any script inside the connected repo's own checkout.");
298
+ }
299
+ return trustedPath;
300
+ }
301
+ /**
302
+ * Round-12 review finding 7 (CONFIRMED): the previous revision interpolated
303
+ * `p_default_branch` straight into the emitted workflow's flow-style YAML
304
+ * (`branches: [${branch}]`) with no quoting and no validation. Two DISTINCT
305
+ * problems, fixed independently, neither one a substitute for the other:
306
+ *
307
+ * 1. Git's own ref-format rules are far more permissive than the set of
308
+ * characters that are safe to drop unquoted into a YAML flow sequence
309
+ * — a comma (`release,prod`) or a `]` are both perfectly legal git
310
+ * branch names, yet either one changes the meaning (or breaks the
311
+ * parse) of an unquoted `[...]` list. `renderCodebusWorkflow` fixes
312
+ * this half by always emitting the branch as a single-quoted YAML
313
+ * scalar with embedded `'` doubled — the one escaping rule single-
314
+ * quoted YAML scalars need — regardless of what this function decides.
315
+ * 2. Separately, a caller can still pass a string that is not a legal git
316
+ * branch name AT ALL (stray whitespace, a leading `-`, a control
317
+ * character, `..`, a leading `/`, etc.) — quoting alone would make that
318
+ * syntactically safe YAML but semantically useless (a push trigger
319
+ * naming a branch nothing can ever push to). This function rejects
320
+ * those outright, BEFORE `repo_connect` ever writes `p_default_branch`
321
+ * to the database or `renderCodebusWorkflow` ever emits it.
322
+ *
323
+ * Delegates the actual rule set to git's own `check-ref-format` rather than
324
+ * re-deriving it by regex (git's rules — no ASCII control chars/space/
325
+ * `~^:?*[`, no leading/trailing/doubled `/`, no leading `.` component, no
326
+ * trailing `.lock`, no `@{`, not bare `@`, no `\`, no trailing `.`, no `..`
327
+ * — are exactly the kind of detail a hand-rolled pattern drifts out of sync
328
+ * with over time). Deliberately validates `refs/heads/<name>` rather than
329
+ * using `check-ref-format --branch <name>` directly: `--branch` mode ALSO
330
+ * expands shorthand like a bare `@` or `@{-1}` (git's "previous branch"
331
+ * syntax) into a different, expanded ref — useful for a human typing at a
332
+ * shell, actively wrong here, where the input is expected to be an exact,
333
+ * literal branch name and any shorthand-expansion behavior would validate
334
+ * (and silently rewrite the meaning of) a string this function should
335
+ * simply accept-or-reject as-is.
336
+ */
337
+ export function isValidGitBranchName(name) {
338
+ if (!name)
339
+ return false;
340
+ try {
341
+ execFileSync("git", ["check-ref-format", `refs/heads/${name}`], { stdio: "pipe" });
342
+ return true;
343
+ }
344
+ catch {
345
+ return false;
346
+ }
347
+ }
348
+ /**
349
+ * Round-9 review finding (CONFIRMED, MINOR): `isValidGitBranchName` only
350
+ * enforces git's OWN ref-format rules, which are far more permissive than
351
+ * what GitHub Actions treats as safe once that same string is dropped into
352
+ * a `branches:` push filter or interpolated into a workflow file GitHub
353
+ * itself parses and evaluates. `yamlSingleQuoted` (below) protects the YAML
354
+ * *parse* — it does not, and cannot, protect against GitHub's OWN
355
+ * downstream interpretation of the parsed value. Two concrete failure
356
+ * modes, both using perfectly legal git branch names `isValidGitBranchName`
357
+ * happily accepts:
358
+ *
359
+ * 1. A branch named `!release` (or anything else starting with `!`) is
360
+ * valid to git, but GitHub's `on.push.branches` filter treats a
361
+ * leading `!` as a NEGATION glob, not a literal branch name — the
362
+ * emitted workflow would simply never trigger for that branch,
363
+ * silently, with no error anywhere.
364
+ * 2. A branch named `${{github.actor}}` is also valid to git
365
+ * (`check-ref-format` does not forbid `$`, `{`, or `}`), but GitHub
366
+ * evaluates `${{ ... }}` as an expression wherever it appears in a
367
+ * workflow file — regardless of YAML quoting, since quoting only
368
+ * determines the parsed STRING value, and GitHub's expression
369
+ * evaluator runs on that value afterward. This is an expression
370
+ * injection into the emitted CI file itself.
371
+ *
372
+ * Such default-branch names are vanishingly rare in practice (git's own
373
+ * rules already reject the far more common `*`, `?`, and `[` for unrelated
374
+ * reasons — see `isValidGitBranchName`'s doc above), so this rejects
375
+ * outright with a clear, specific error naming the offending character
376
+ * rather than attempting to escape GitHub's own branch-filter glob syntax,
377
+ * which GitHub documents inconsistently across `branches:`/
378
+ * `branches-ignore:`/`paths:` filters. Rejection is a strictly more honest
379
+ * contract than escaping semantics this codebase would then be on the hook
380
+ * to keep exactly right forever.
381
+ *
382
+ * Round-14 review finding (CONFIRMED, MINOR): `+` was missing from this
383
+ * list — `release+prod` is a perfectly legal git branch name
384
+ * (`check-ref-format` does not forbid `+`) but GitHub's branch/tag/path
385
+ * filter glob syntax treats `+` as a special pattern character exactly
386
+ * like `*`/`?`/`[]`/`!`, so the emitted workflow would silently never
387
+ * trigger for that branch — the identical failure mode as the `!release`
388
+ * case above, just via a different character. Cross-checked GitHub's own
389
+ * enumeration of branch-filter special characters again against this
390
+ * list (docs/actions/reference/workflows-and-actions/workflow-syntax's
391
+ * filter pattern cheat sheet, and the on.push.branches section, both
392
+ * verified via direct fetch and independent search before this fix): the
393
+ * complete documented set is `* ? + [ ] !` plus `\` for escaping any of
394
+ * them literally. Accounting for every one of those against this list:
395
+ * - `*`, `?`, `!`, `[`, `]` — already present above.
396
+ * - `+` — was missing; added by this fix.
397
+ * - `]` alone is git-legal (only `[` is git-banned) and was already
398
+ * covered before this fix.
399
+ * - `\` (backslash) needs NO entry here: git's OWN ref-format rules
400
+ * (`check-ref-format`, enforced by `isValidGitBranchName` BEFORE this
401
+ * function ever runs — see `repoConnectCore`) already reject any raw
402
+ * backslash outright (verified: `git check-ref-format
403
+ * refs/heads/back\slash` rejects), so a branch name reaching this
404
+ * function can never contain one in the first place — nothing here
405
+ * would ever see it.
406
+ * `{`/`}` are kept even though GitHub's branch-glob cheat sheet itself
407
+ * does not list them as glob metacharacters — they exist for the
408
+ * SEPARATE `${{...}}` expression-injection concern above, not the glob
409
+ * syntax, and the explicit `${{` substring check just below already
410
+ * covers that case more precisely than a bare `{`/`}` would.
411
+ */
412
+ const GITHUB_ACTIONS_UNSAFE_BRANCH_CHARS = ["!", "*", "?", "+", "[", "]", "{", "}"];
413
+ /** Returns the first GitHub-Actions-unsafe character (or the `${{`
414
+ * expression-start sequence) found in `name`, or `null` if none is
415
+ * present. Checked SEPARATELY from — and in addition to —
416
+ * `isValidGitBranchName`: a name can pass git's own ref-format rules and
417
+ * still be unsafe to drop into a `branches:` filter or workflow
418
+ * expression. */
419
+ export function findGithubActionsUnsafeBranchChar(name) {
420
+ // Checked before the single-character scan below: `${{` on its own does
421
+ // not contain any character this function would otherwise flag (`$` is
422
+ // not in GITHUB_ACTIONS_UNSAFE_BRANCH_CHARS), yet it is the exact
423
+ // substring GitHub's workflow parser treats as the start of an
424
+ // expression — naming it specifically produces a far clearer error than
425
+ // reporting a bare `{`.
426
+ if (name.includes("${{"))
427
+ return "${{";
428
+ for (const ch of GITHUB_ACTIONS_UNSAFE_BRANCH_CHARS) {
429
+ if (name.includes(ch))
430
+ return ch;
431
+ }
432
+ return null;
433
+ }
434
+ /** Emits `s` as a single-quoted YAML scalar — the one escaping rule
435
+ * single-quoted YAML needs (embedded `'` doubled to `''`). Used for the
436
+ * branch name in `renderCodebusWorkflow` (round-12 review finding 7): a
437
+ * git branch name may legally contain a comma, `]`, or `'`, any of which
438
+ * would corrupt an unquoted flow-sequence `[...]` entry. */
439
+ function yamlSingleQuoted(s) {
440
+ return `'${s.replace(/'/g, "''")}'`;
441
+ }
442
+ /** The workflow content emitted by --write-workflow. Exported for a
443
+ * snapshot test. `pr-sync` calls B2's own reusable workflow
444
+ * (.github/workflows/codebus-pr-sync.yml, this repo,
445
+ * 0072_github_ingestion.sql) — B2 has now landed, so this job is emitted
446
+ * LIVE, per the plan's own unified-caller-workflow design (one caller
447
+ * workflow file per connected repo, one job per active track, never a
448
+ * second commented-out revision waiting on a later chunk).
449
+ *
450
+ * Round-11 review finding F2 (repo review a4-r1, BLOCKER, CONFIRMED,
451
+ * A4-era): a LIVE `pull_request:` trigger with no push-only guard on the
452
+ * `graphify-publish` job would have let a pull_request-triggered run
453
+ * invoke graph-publish's reusable workflow, handing PR-controlled code the
454
+ * ingest-scope token secret. `graphify-publish` has carried its own
455
+ * explicit `if: github.event_name == 'push'` guard as defense-in-depth
456
+ * ever since — kept unchanged now that `pull_request`/`pull_request_review`
457
+ * are themselves live triggers too, for the identical reason: a future
458
+ * edit to `on:` must never silently widen which events graphify-publish
459
+ * runs on merely by adding a new trigger alongside it. `pr-sync` (below)
460
+ * carries the mirror-image guard, `github.event_name == 'pull_request' ||
461
+ * github.event_name == 'pull_request_review'`, so the two jobs can never
462
+ * cross-fire onto each other's event either. */
463
+ export function renderCodebusWorkflow(opts) {
464
+ // Round-11 review finding F6 (CONFIRMED): the previous revision always
465
+ // emitted `branches: [main]` here regardless of what default_branch this
466
+ // repo was actually connected with — a repo using another default branch
467
+ // recorded that branch in the database but received CI that could never
468
+ // run (no push to `main` ever happens there). The push trigger now uses
469
+ // the SAME resolved default branch repo_connect itself was called with.
470
+ // Round-12 review finding 7 (CONFIRMED): always emitted as a single-quoted
471
+ // YAML scalar (never bare inside the flow sequence) — see
472
+ // `yamlSingleQuoted`'s own doc for why quoting alone (not merely the
473
+ // caller-side `isValidGitBranchName` check) is required here.
474
+ const branch = yamlSingleQuoted(opts.defaultBranch ?? "main");
475
+ // B2 landed: `includePrSyncStub` (the constructor parameter name is kept
476
+ // for call-site compatibility — every existing call site still passes
477
+ // `includePrSyncStub: opts.prSync !== false`, cli/src/commands/repo.ts's
478
+ // repoConnectFull — see below) now means "emit the pr-sync job LIVE",
479
+ // never "emit a commented-out stub". `--no-pr-sync` still fully omits
480
+ // both the trigger and the job, unchanged.
481
+ //
482
+ // Round-2 (Sol) review finding F4 (CONFIRMED, MAJOR): the original type
483
+ // list (opened/synchronize/reopened/closed/edited) omitted every event
484
+ // that changes reviewer presence or draft state —
485
+ // `review_requested`/`review_request_removed` (the ONLY events GitHub
486
+ // fires when a PR's requested-reviewers set changes at all) and
487
+ // `converted_to_draft`/`ready_for_review` (the draft flag's own
488
+ // transitions). Without these, `requested_reviewers_present`,
489
+ // `review_state`, and the derived `draft`/PR `status` (0064) never get a
490
+ // CI-push sync when ONLY one of these things changes with no other edit.
491
+ // github-sync's own reconcile poll now ALSO covers requested-reviewer
492
+ // presence (github-sync/logic.mjs's mapPullToPollPayload maps the
493
+ // pulls-list endpoint's `requested_reviewers`/`requested_teams`) AND, for
494
+ // a fork-origin PR specifically, `review_decision` too (PR review, Luna,
495
+ // comment 3839437819, F2 — github-sync/index.ts spends one extra,
496
+ // budget-bounded `/pulls/{number}/reviews` call per fork PR to derive
497
+ // it, since that class never gets a CI-push sync at all — see the
498
+ // fork-origin `if:` guard below). For a SAME-repo PR, these CI-only
499
+ // trigger types remain the richer, more timely path for `review_decision`
500
+ // — the poll only spends its bounded reviews-fetch budget on fork PRs,
501
+ // never on one already covered by CI-push. Added below — this is not
502
+ // "every possible pull_request type" (labeled/assigned/milestoned/etc.
503
+ // map to no CodeBus PR field and are deliberately still omitted), it is
504
+ // every type that changes a field pr_upsert_synced (0064) actually
505
+ // tracks.
506
+ const pullRequestTrigger = opts.includePrSyncStub
507
+ ? " pull_request:\n" +
508
+ " types: [opened, synchronize, reopened, closed, edited, ready_for_review, converted_to_draft, review_requested, review_request_removed]\n" +
509
+ " pull_request_review:\n" +
510
+ " types: [submitted, edited, dismissed]\n"
511
+ : "";
512
+ const graphify = `# codebus: emitted by \`codebus repo connect --write-workflow\`.
513
+ #
514
+ # Round-12 review finding F1 (CONFIRMED, MAJOR): the graphify-publish job
515
+ # below calls a REUSABLE workflow that lives in Bragi-GmbH/CodeBus, a
516
+ # PRIVATE repository. GitHub's own reusable-workflow access rules mean this
517
+ # file can only ever run if THIS repo is (a) owned by Bragi-GmbH and (b)
518
+ # itself private or internal — \`codebus repo connect --write-workflow\`
519
+ # already checked both of those before emitting this file, so if you are
520
+ # reading this file inside such a repo, this half already holds. The
521
+ # remaining half is NOT something the CLI can verify remotely: Bragi-GmbH/
522
+ # CodeBus's own Settings -> Actions -> General -> "Access" setting must
523
+ # ALSO be configured to "Accessible from repositories in the Bragi-GmbH
524
+ # organization" (or broader) — ask a CodeBus admin to confirm this if
525
+ # graphify-publish still fails to resolve on the first real push.
526
+ name: codebus
527
+
528
+ on:
529
+ push:
530
+ branches: [${branch}]
531
+ ${pullRequestTrigger}
532
+ permissions:
533
+ contents: read
534
+
535
+ jobs:
536
+ graphify-publish:
537
+ # Defense-in-depth (round-11 review finding F2): now that pull_request/
538
+ # pull_request_review are LIVE triggers alongside push (B2 landed),
539
+ # this guard is what actually keeps graphify-publish (and its
540
+ # ingest-token secret) from ever running on a PR-controlled event —
541
+ # never merely "always true because nothing else triggers this
542
+ # workflow" the way it was before B2's own triggers existed.
543
+ if: github.event_name == 'push'
544
+ # Cross-repo reusable-workflow reference, pinned to a ref. Round-13
545
+ # review finding 2 (CONFIRMED, BLOCKER, fixed): this job used to fail at
546
+ # \`npm ci\` for any repo other than CodeBus itself, since the graphify
547
+ # script/lockfile only existed in the CodeBus repo — the reusable
548
+ # workflow (.github/workflows/graphify-publish.yml, CodeBus side) now
549
+ # checks its OWN scripts/lockfiles out into \`.codebus-kit/\` for any
550
+ # caller, so this job can actually run once the operator has also added
551
+ # the required secrets to this repo's \`graph-publish\` environment
552
+ # (docs/plans/repo-intelligence.md A4, "Open questions" — RESOLVED,
553
+ # round-19 PR #55 review: the reusable workflow's own second checkout
554
+ # stays pinned to \`main\`, matching THIS \`uses:\` line just below —
555
+ # this CLI always emits \`@main\` here, so a caller already trusts
556
+ # CodeBus's \`main\` wholesale; SHA-pinning only the inner checkout would
557
+ # protect nothing while adding a silent-staleness hazard. See that
558
+ # checkout step's own comment in graphify-publish.yml for the full
559
+ # argument).
560
+ # Round-14 review finding 1 (CONFIRMED, BLOCKER): Bragi-GmbH/CodeBus is
561
+ # PRIVATE — the reusable workflow's own \`github.token\` cannot check it
562
+ # out for any caller other than CodeBus itself, so THIS (foreign)
563
+ # caller must supply CODEBUS_KIT_TOKEN below (a GitHub PAT/App token
564
+ # with read access to Bragi-GmbH/CodeBus) or the reusable workflow's
565
+ # own fail-fast check refuses with a clear, actionable error naming
566
+ # exactly this secret.
567
+ uses: Bragi-GmbH/CodeBus/.github/workflows/graphify-publish.yml@main
568
+ # Round-8 review finding (CONFIRMED, MAJOR): the reusable workflow's own
569
+ # branch gate is driven by this \`default-branch\` input, not a hardcoded
570
+ # \`main\` — pass the SAME resolved default branch this repo was
571
+ # connected with (the identical value used for \`on: push: branches:\`
572
+ # above) so a repo whose default branch isn't \`main\` can actually
573
+ # publish, instead of the reusable workflow silently no-op'ing forever.
574
+ with:
575
+ default-branch: ${branch}
576
+ secrets:
577
+ SUPABASE_URL: \${{ secrets.SUPABASE_URL }}
578
+ SUPABASE_ANON_KEY: \${{ secrets.SUPABASE_ANON_KEY }}
579
+ GRAPH_INGEST_TOKEN: \${{ secrets.GRAPH_INGEST_TOKEN }}
580
+ ANTHROPIC_API_KEY: \${{ secrets.ANTHROPIC_API_KEY }}
581
+ CODEBUS_KIT_TOKEN: \${{ secrets.CODEBUS_KIT_TOKEN }}
582
+ `;
583
+ const prSyncJob = opts.includePrSyncStub
584
+ ? `
585
+ pr-sync:
586
+ # Mirror-image guard of graphify-publish's own \`if\` above — this job
587
+ # must never fire on a push event either, even though only pull_request/
588
+ # pull_request_review currently trigger it. The second clause skips a
589
+ # fork-origin PR specifically: GitHub never grants \`id-token: write\` to
590
+ # a pull_request run whose head lives in a fork — a documented platform
591
+ # restriction, not something this job's own permissions block can
592
+ # override — so left ungated, every fork PR would reach codebus-pr-
593
+ # sync.yml's OIDC-minting step and fail it red permanently (never
594
+ # resolves by retrying, looks identical to a real incident). Skipping
595
+ # is safe for what the reconcile poller (github-sync) actually covers:
596
+ # it independently re-derives every PR's title/branches/head sha/draft/
597
+ # open-closed-merged state AND requested-reviewer presence, fork-origin
598
+ # included, within 15 minutes (github-sync/logic.mjs's
599
+ # mapPullToPollPayload). review_decision (approved/changes_requested)
600
+ # is also covered for exactly this class now (PR review, Luna,
601
+ # comment 3839437819, F2): the pulls-list endpoint itself can't return
602
+ # it, but for a PR the poll identifies as fork-origin, github-sync/
603
+ # index.ts spends one extra, budget-bounded \`/pulls/{number}/reviews\`
604
+ # call to derive it — a same-repo PR never spends that budget, since
605
+ # its CI-push sync (pull_request_review events, reaching this job
606
+ # normally) is the richer, more timely source already. The one
607
+ # remaining honest gap: that per-sweep budget is finite, so a
608
+ # fork-origin PR's review_decision update can lag by a poll cycle or
609
+ # two under heavy load rather than update instantly — never a
610
+ # permanent zero the way it used to be.
611
+ if: |
612
+ (github.event_name == 'pull_request' || github.event_name == 'pull_request_review') &&
613
+ github.event.pull_request.head.repo.full_name == github.repository
614
+ # id-token: write at the JOB level (not just the workflow-level
615
+ # \`permissions:\` above, which deliberately stays contents:read-only for
616
+ # graphify-publish's sake) is what actually lets codebus-pr-sync.yml's
617
+ # own OIDC-token-minting step succeed once this job calls it via
618
+ # \`uses:\` — a reusable-workflow-calling job only passes down the
619
+ # permissions IT ITSELF declares, not merely whatever the calling
620
+ # workflow's top-level \`permissions:\` block happens to list.
621
+ # pull-requests: read is equally load-bearing (proven live by the
622
+ # CodeBus dogfood caller's own first run, PR #67): the reusable
623
+ # workflow's \`gh pr view\` reads PR metadata with THIS caller's token,
624
+ # and caller permissions can never be elevated by the callee — without
625
+ # it the job dies with "Resource not accessible by integration".
626
+ permissions:
627
+ contents: read
628
+ id-token: write
629
+ pull-requests: read
630
+ checks: read
631
+ statuses: read
632
+ actions: read
633
+ # Cross-repo reusable-workflow reference, pinned to a ref — same \`uses:\`
634
+ # shape as graphify-publish above. No secrets at all (unlike
635
+ # graphify-publish's GRAPH_INGEST_TOKEN): this job's ENTIRE credential is
636
+ # the OIDC token it mints for itself inside codebus-pr-sync.yml — see
637
+ # that file's own header for the full auth model. B2's own "Blast
638
+ # radius" note (docs/plans/repo-intelligence.md) is explicit that OIDC
639
+ # tokens are minted per-request and never stored, which is exactly why
640
+ # this job needs no secrets block.
641
+ uses: Bragi-GmbH/CodeBus/.github/workflows/codebus-pr-sync.yml@main
642
+ `
643
+ : "";
644
+ return graphify + prSyncJob;
645
+ }
646
+ export async function repoConnectCore(ctx, url, opts) {
647
+ const parsed = parseGithubUrl(url);
648
+ if (!parsed) {
649
+ // Round-19 review finding (CONFIRMED, credential-leakage theme — Luna
650
+ // PR #55 review, comment 3826839028): `url` is the caller-supplied
651
+ // argument to `codebus repo connect <url>` — parseGithubUrl's own
652
+ // accepted forms include userinfo (`https://token@github.com/...`,
653
+ // one of its own documented https-form matches), so a URL that fails
654
+ // to parse for some OTHER reason (a malformed owner/repo tail, say)
655
+ // still had that credential echoed back here verbatim.
656
+ throw new CodebusError(`could not parse a GitHub owner/repo from '${redactUrlUserinfo(url)}' — expected an https://github.com/<owner>/<repo> URL, ` +
657
+ "a git@github.com:<owner>/<repo>.git SSH URL, or a bare <owner>/<repo>.");
658
+ }
659
+ // Round-15 review finding (CONFIRMED, MINOR): named `requestedSlug` (not
660
+ // `slug`) deliberately — this is only what the CALLER asked to connect
661
+ // as. repo_connect (0070) can resolve it, via the retired_into chain or
662
+ // the normalized-url fallback (round-11 review finding F5), onto a row
663
+ // whose OWN current slug differs (the real collapse script renames a
664
+ // retired loser's slug and frees the original name). The RPC's own
665
+ // returned `slug` field (spread from `result` below, never shadowed by
666
+ // this variable) is the CANONICAL slug the CLI must actually surface —
667
+ // `repo status` only ever resolves that one, never a stale request.
668
+ const requestedSlug = opts.slug ?? parsed.name;
669
+ const branch = opts.branch ?? "main";
670
+ // Round-12 review finding 7 (CONFIRMED): validated BEFORE anything is
671
+ // written — a syntactically illegal branch name must never reach
672
+ // `repos.default_branch` (repo_connect's own p_default_branch) even when
673
+ // no workflow is ever emitted for it.
674
+ if (!isValidGitBranchName(branch)) {
675
+ throw new CodebusError(`'${branch}' is not a legal git branch name (checked via 'git check-ref-format') — refusing to connect ` +
676
+ "with an unusable default branch.");
677
+ }
678
+ // Round-9 review finding (CONFIRMED, MINOR): a name can pass git's own
679
+ // ref-format check above and STILL be unsafe once dropped into the
680
+ // emitted workflow's `branches:` filter or interpolated as a raw
681
+ // expression by GitHub itself — see `findGithubActionsUnsafeBranchChar`'s
682
+ // own doc for the two concrete failure modes (`!release` silently never
683
+ // triggers; `${{github.actor}}` is expression-injected). Checked
684
+ // separately, before `repo_connect` ever writes `p_default_branch`.
685
+ const unsafeChar = findGithubActionsUnsafeBranchChar(branch);
686
+ if (unsafeChar) {
687
+ throw new CodebusError(`'${branch}' is a legal git branch name but contains '${unsafeChar}', which GitHub Actions treats specially ` +
688
+ "in a push-trigger branch filter or as the start of a workflow expression (not as a literal character) — " +
689
+ "refusing to connect with a default branch that would silently corrupt or hijack the emitted CI workflow.");
690
+ }
691
+ const ws = await workspaceId(ctx);
692
+ const { data, error } = await ctx.supabase.rpc("repo_connect", {
693
+ p_workspace: ws,
694
+ p_slug: requestedSlug,
695
+ p_remote_url: parsed.canonicalUrl,
696
+ p_default_branch: branch,
697
+ p_force: !!opts.force,
698
+ });
699
+ if (error)
700
+ throw new CodebusError(error.message);
701
+ const result = data;
702
+ // `...result` carries the RPC's own canonical `slug` — deliberately NOT
703
+ // overridden by `requestedSlug` here (see this function's own doc above).
704
+ return { ...result, canonicalUrl: parsed.canonicalUrl, owner: parsed.owner, name: parsed.name, requestedSlug };
705
+ }
706
+ /**
707
+ * Full `repo connect` side effects: the repo_connect RPC, `--write-workflow`,
708
+ * the pending -> ci_wired transition, the ingest-token mint (authority
709
+ * only), and `--build` — in that order. Exported for unit testing (round-11
710
+ * review findings F3/F4/F6 and round-12 review findings 1/2/5 regression
711
+ * coverage) and reuse.
712
+ *
713
+ * Deliberately returns a plain result object and does NOT print the
714
+ * connect/token/transition/workflow summary itself — `connectAction` below
715
+ * is a thin CLI wrapper that does ALL of that printing, exactly once, after
716
+ * this function has fully resolved. That split is what round-11 review
717
+ * finding F3 requires: in `--json` mode the caller must be able to print
718
+ * the finished result as a single JSON object with nothing before or after
719
+ * it, which is only possible if transitioning/writing/minting/building have
720
+ * already happened by the time anything is printed.
721
+ *
722
+ * Round-11 review finding F4 (CONFIRMED, still enforced): the previous
723
+ * revision transitioned pending -> ci_wired BEFORE validating the origin
724
+ * match and writing the workflow file, so a mismatched checkout or a
725
+ * filesystem failure could leave the database falsely claiming CI is wired
726
+ * with no file to show for it. Validate-then-write still fully precedes the
727
+ * transition below it, never in parallel with it and never ahead of it.
728
+ *
729
+ * Round-12 review finding 1 (CONFIRMED, BLOCKER): `--write-workflow` no
730
+ * longer makes the transition happen — a file landing on disk is not
731
+ * evidence CI actually ran, even now that round-13 review finding 2 fixed
732
+ * the underlying workflow so it CAN run for a foreign repo (see
733
+ * `renderCodebusWorkflow`'s own comment) — the operator's GitHub-side setup
734
+ * (secrets, the `graph-publish` environment) is still unverified at the
735
+ * moment this file is written. `--build`'s own transition-before-build
736
+ * state-machine requirement (plan's own "Blast radius" note — the
737
+ * attachment must not sit `pending` while a build has already run) is
738
+ * unaffected: `--build` transitions unconditionally before it runs, since a
739
+ * `--build` run is a real, local, end-to-end proof that ingest works, not
740
+ * merely that a file exists. (Round-13 review finding 3: a genuine CI
741
+ * ingest arriving while still `pending` now ALSO advances the attachment —
742
+ * see 0063_graph_content.sql's `graph_ingest_artifact` — which is what
743
+ * actually closes the loop for the foreign-repo case this fix targets.)
744
+ *
745
+ * Round-12 review finding 5 (CONFIRMED, MAJOR): the previous revision
746
+ * minted the ingest token FIRST, before the workflow validate/write and the
747
+ * transition — either one failing afterward threw past the point where the
748
+ * token could ever be printed, leaving an active, unrecoverable plaintext
749
+ * token that no caller ever saw. Fixed by moving the mint to LAST among the
750
+ * fallible steps that do not themselves need it — after the workflow write
751
+ * and the transition, both of which can fail for reasons having nothing to
752
+ * do with the token, so neither one can waste a mint anymore. The one step
753
+ * that still runs AFTER the mint is `--build` itself, which cannot run
754
+ * without a live token to authenticate as — an unavoidable ordering, not an
755
+ * oversight, so a `--build` failure past this point still throws with the
756
+ * token embedded directly in the thrown error's message (see below) rather
757
+ * than silently discarding the only copy the way a bare re-throw would.
758
+ */
759
+ export async function repoConnectFull(ctx, cfg, url, opts, visibilityChecker = realRepoVisibilityChecker()) {
760
+ const core = await repoConnectCore(ctx, url, opts);
761
+ const defaultBranch = opts.branch ?? "main";
762
+ const result = { ...core, defaultBranch };
763
+ if (!result.is_authority)
764
+ return result;
765
+ // (F4) Validate + write BEFORE transitioning — see the function doc above.
766
+ if (opts.writeWorkflow) {
767
+ // Round-12 review finding F1 (CONFIRMED, MAJOR): checked BEFORE the
768
+ // origin check below (and, transitively, before mint/transition) —
769
+ // this is a property of the CONNECTED repo alone (owner + GitHub
770
+ // visibility), independent of the local checkout, so it belongs with
771
+ // the other fallible-but-side-effect-free preflight checks per the
772
+ // round-12 "mint last" rule. See assertEligibleForCodebusReusableWorkflow's
773
+ // own doc for why an ineligible repo's emitted workflow can never
774
+ // actually run.
775
+ await assertEligibleForCodebusReusableWorkflow(result.owner, result.name, visibilityChecker);
776
+ validateOriginMatchesConnectedRepo(opts.repoRoot, result.owner, result.name, "--write-workflow");
777
+ const workflowPath = join(opts.repoRoot, ".github", "workflows", "codebus.yml");
778
+ mkdirSync(dirname(workflowPath), { recursive: true });
779
+ writeFileSync(workflowPath, renderCodebusWorkflow({
780
+ owner: result.owner,
781
+ name: result.name,
782
+ includePrSyncStub: opts.prSync !== false,
783
+ defaultBranch, // (F6) the workflow's push trigger uses the resolved default branch, not a hardcoded 'main'.
784
+ }));
785
+ result.workflowWritten = workflowPath;
786
+ }
787
+ // Round-16 review finding 1, part 2 (CONFIRMED, BLOCKER): --build gets
788
+ // the IDENTICAL origin check, independently of --write-workflow — see
789
+ // `validateOriginMatchesConnectedRepo`'s own doc for why this must not
790
+ // be conditioned on --write-workflow having also been passed. Runs here
791
+ // (before mint, like the --write-workflow check above) so a mismatched
792
+ // checkout never wastes a mint either.
793
+ //
794
+ // Round-17 review finding 1 (CONFIRMED, MAJOR): `resolveTrustedGraph
795
+ // PublishScript` used to run INSIDE the `if (opts.build)` block below —
796
+ // i.e. AFTER the mint and the ci_wired transition — so a CLI installation
797
+ // missing its shipped `dist/kit/graph-publish.mjs` artifact threw a
798
+ // plain, token-free `CodebusError` at that point, silently discarding the
799
+ // token the mint just above it had already produced (the exact class of
800
+ // bug round-14 finding 4 and round-15 finding 2 already closed for the
801
+ // transition/build-execution failure sites — this was the one remaining
802
+ // pre-mint-eligible check that hadn't been moved yet). It is a PURE
803
+ // filesystem check with no side effects of its own, so — like the origin
804
+ // check immediately above — it belongs here, with the other fallible-
805
+ // but-safe pre-flight validations, per the round-12 "mint last" rule
806
+ // (repoConnectFull's own doc above): nothing after this point up to the
807
+ // mint call itself can still fail for a reason having nothing to do with
808
+ // the token. `trustedScriptPath` is resolved ONCE here and reused by the
809
+ // `if (opts.build)` block further below — never re-resolved there — so
810
+ // there is exactly one call site, not two that could disagree.
811
+ let trustedScriptPath;
812
+ if (opts.build) {
813
+ validateOriginMatchesConnectedRepo(opts.repoRoot, result.owner, result.name, "--build");
814
+ trustedScriptPath = resolveTrustedGraphPublishScript(import.meta.url);
815
+ }
816
+ // Round-12 review finding 5 (CONFIRMED): mint the token BEFORE the
817
+ // transition below — see the function doc above for why the ordering
818
+ // among these fallible steps that don't yet need `--build` itself is
819
+ // (workflow write) -> (mint) -> (transition) -> (build). Round-13 review
820
+ // finding 3 (CONFIRMED, MAJOR) is exactly why mint now precedes the
821
+ // transition rather than following it as an earlier revision had it:
822
+ // minting is itself fallible, and transitioning to `ci_wired` BEFORE a
823
+ // mint that then fails would leave the database claiming CI is wired
824
+ // while the operator never received a token to prove it — a second,
825
+ // narrower instance of the same "don't claim ci_wired for something that
826
+ // didn't actually succeed" principle round-12 finding 1 established for
827
+ // `--write-workflow`. `--build` itself still needs a real token to run,
828
+ // so mint must precede it either way; the only question this fix
829
+ // answers is mint-vs-transition order, and the answer is mint first.
830
+ const { data: token, error: tokenErr } = await ctx.supabase.rpc("graph_ingest_token_mint", {
831
+ p_repo_id: result.repo_id,
832
+ p_scope: "ingest",
833
+ });
834
+ if (tokenErr)
835
+ throw new CodebusError(`repo connected, but minting an ingest token failed: ${tokenErr.message}`);
836
+ result.token = String(token);
837
+ // Round-12 review finding 1 (CONFIRMED): --write-workflow alone no longer
838
+ // drives this transition — only --build does, since only --build is real
839
+ // evidence CI (or a local stand-in for it) can actually run. See the
840
+ // function doc above. Round-13 review finding 3: now gated on the mint
841
+ // above having already succeeded (see that comment).
842
+ const wantsTransition = !!opts.build;
843
+ if (wantsTransition && result.attachment_state === "pending") {
844
+ const { error: transErr } = await ctx.supabase.rpc("graph_attachment_transition", {
845
+ p_repo_id: result.repo_id, p_to_state: "ci_wired", p_note: "codebus repo connect",
846
+ });
847
+ // Round-14 review finding 4 (CONFIRMED, MAJOR): this throw used to be a
848
+ // plain, token-free CodebusError — but it runs AFTER the mint above
849
+ // already succeeded, the exact same "a later failure must never lose
850
+ // the operator's only copy of the plaintext" hazard PostMintFailureError
851
+ // exists for. Every post-mint failure site uses it now, not just
852
+ // --build's.
853
+ if (transErr)
854
+ throw new PostMintFailureError(`repo connected and minted an ingest token, but the pending -> ci_wired transition failed: ${transErr.message}`, result.token, "transition");
855
+ result.transitioned = true;
856
+ }
857
+ if (opts.build) {
858
+ // Suppressed under --json (not one of the raw child-process bytes below,
859
+ // which stdio:'inherit' cannot redirect without a scope change): this is
860
+ // OUR OWN informational line, and a --json caller piping stdout to a
861
+ // parser must see nothing but the final JSON object.
862
+ if (!opts.json)
863
+ console.log("\nrunning graphify locally (trusted graph-publish.mjs)...");
864
+ // Round-16 review finding 1, part 1: the trusted, CLI-shipped script —
865
+ // never anything inside opts.repoRoot. Round-17 review finding 1:
866
+ // resolved BEFORE mint now (see above) — the non-null assertion here is
867
+ // safe by construction: `trustedScriptPath` is only ever left
868
+ // `undefined` when `opts.build` is falsy, and this whole block only
869
+ // runs when it's truthy (the SAME condition, checked just above).
870
+ const scriptPath = trustedScriptPath;
871
+ const buildEnv = {
872
+ ...process.env,
873
+ SUPABASE_URL: cfg.url,
874
+ SUPABASE_ANON_KEY: cfg.anonKey ?? "",
875
+ GRAPH_INGEST_TOKEN: result.token,
876
+ // Round-16 review finding 1: explicit, not inherited — the trusted
877
+ // script's own boundary/containment check (round-15 review finding 1)
878
+ // must be anchored at opts.repoRoot regardless of whatever
879
+ // GITHUB_WORKSPACE/GRAPHIFY_ROOT_BOUNDARY this PROCESS'S OWN
880
+ // environment happens to carry (`...process.env` above would
881
+ // otherwise let a stray inherited value leak through unchallenged).
882
+ // There is no separate "kit" directory to keep graphify away from in
883
+ // this LOCAL --build path — the trusted script itself lives inside
884
+ // the CLI's own installation, nowhere near opts.repoRoot — so
885
+ // workspace root, boundary, and target are all simply opts.repoRoot.
886
+ GITHUB_WORKSPACE: opts.repoRoot,
887
+ GRAPHIFY_ROOT_BOUNDARY: opts.repoRoot,
888
+ GRAPHIFY_REPO_ROOT: ".",
889
+ };
890
+ if (opts.json) {
891
+ // Round-12 review finding 2 (CONFIRMED, BLOCKER): stdio:'inherit' here
892
+ // let graph-publish.mjs's OWN stdout writes land directly on THIS
893
+ // process's stdout, ahead of (and mixed into) the single JSON object
894
+ // connectAction prints once repoConnectFull returns — breaking the
895
+ // "stdout is exactly one JSON object" contract F3 established the
896
+ // instant --build was combined with --json. Fixed by piping the
897
+ // child's stdio instead of inheriting it, capturing everything it
898
+ // writes, and re-emitting that capture on OUR OWN stderr (never
899
+ // stdout) — visible to a human watching the terminal, invisible to a
900
+ // parser reading stdout — while also attaching it to the result as
901
+ // `buildLog` so a --json caller gets it back structurally too.
902
+ //
903
+ // `spawnSync` (not `execFileSync`) deliberately: execFileSync's return
904
+ // value on a SUCCESSFUL run only ever carries stdout — anything the
905
+ // child wrote to stderr on a zero exit is not retrievable from it at
906
+ // all (only a THROWN execFileSync error carries `.stderr`, i.e. only
907
+ // on a non-zero exit). spawnSync instead always returns
908
+ // `{stdout, stderr, status}` uniformly regardless of exit code, so a
909
+ // future warning graph-publish.mjs writes to stderr on an otherwise
910
+ // successful run is captured here too, not silently dropped.
911
+ const spawnResult = spawnSync(process.execPath, [scriptPath], { cwd: opts.repoRoot, env: buildEnv, encoding: "utf-8" });
912
+ const captured = (spawnResult.stdout ?? "") + (spawnResult.stderr ?? "");
913
+ if (captured)
914
+ process.stderr.write(captured);
915
+ result.buildLog = captured || undefined;
916
+ // Round-13 review finding 7 (CONFIRMED, MAJOR): `spawnResult.status`
917
+ // is `null` — never `0` — when the child was terminated by a signal
918
+ // rather than exiting normally (Node sets `.signal` instead, and does
919
+ // NOT set `.error` for this case; `.error` is reserved for the SPAWN
920
+ // itself failing, e.g. ENOENT). The previous `(spawnResult.status ??
921
+ // 0) !== 0` check coalesced that `null` to `0`, so a build killed by
922
+ // SIGKILL/SIGTERM (OOM, a CI job timeout sending SIGTERM, an operator
923
+ // Ctrl-C) was reported as a SUCCESSFUL build with `buildRan = true`.
924
+ // Signal termination is exactly as much a failure as a non-zero exit
925
+ // code — checked explicitly here, never coalesced away.
926
+ const buildFailed = !!spawnResult.error || spawnResult.status === null || spawnResult.status !== 0;
927
+ if (buildFailed) {
928
+ const reason = spawnResult.error
929
+ ? spawnResult.error.message
930
+ : spawnResult.status === null
931
+ ? `terminated by signal ${spawnResult.signal ?? "unknown"}`
932
+ : `exited with status ${spawnResult.status}`;
933
+ // Round-13 review finding 8 (CONFIRMED, MAJOR): the token is no
934
+ // longer embedded in this error's `message` — see
935
+ // `PostMintFailureError`'s own doc (errors.ts) for why. The
936
+ // mint above already ran, so the token must not disappear silently
937
+ // just because --json mode's own success path (a single stdout
938
+ // object) never gets a chance to run; it is carried as structured
939
+ // data instead, for the caller to render deliberately.
940
+ throw new PostMintFailureError(`--build failed (${reason}) — see graphify output above (stderr).`, result.token, "build");
941
+ }
942
+ }
943
+ else {
944
+ try {
945
+ execFileSync(process.execPath, [scriptPath], { cwd: opts.repoRoot, stdio: "inherit", env: buildEnv });
946
+ }
947
+ catch (e) {
948
+ // graph-publish.mjs already printed its own error (quarantined /
949
+ // 25MB-reject / build failure) to inherited stdio — surface a plain
950
+ // non-zero exit rather than re-wrapping and hiding that detail.
951
+ // execFileSync itself already throws correctly for BOTH a non-zero
952
+ // exit AND signal termination (round-13 review finding 7 is a
953
+ // spawnSync-specific bug — execFileSync's own throw-on-any-abnormal-
954
+ // exit behavior was never affected by it), so no separate signal
955
+ // check is needed on this branch.
956
+ const err = e;
957
+ const reason = err.signal ? `terminated by signal ${err.signal}` : `exited with status ${err.status}`;
958
+ // (Round-13 review finding 8, see above.)
959
+ throw new PostMintFailureError(`--build failed (${reason}) — see graphify output above.`, result.token, "build");
960
+ }
961
+ }
962
+ result.buildRan = true;
963
+ }
964
+ return result;
965
+ }
966
+ /**
967
+ * All of `repo connect`'s console output, split out from `connectAction`
968
+ * (round-12 review finding 6 regression coverage) purely so it can be unit
969
+ * tested without a live `makeCtx`/network round-trip — `repoConnectFull`
970
+ * above is the piece that does real work; this is a pure function of an
971
+ * already-resolved result, exactly parallel to why `repoConnectFull` itself
972
+ * does no printing (round-11 review finding F3's doc above).
973
+ */
974
+ export function printConnectResult(result, cfg, json) {
975
+ // Round-11 review finding F3 (BLOCKER, CONFIRMED): the previous revision
976
+ // printed the JSON summary FIRST, then unconditionally printed the
977
+ // plaintext token + gh-secret-set instructions (and, on --build, the
978
+ // graphify wrapper line) AFTER it regardless of --json — contaminating
979
+ // machine-readable stdout with free text and never actually including the
980
+ // token in the JSON object it claimed to be. Fixed: in --json mode this is
981
+ // the ONLY line ever printed, the token (if any) lives INSIDE this single
982
+ // object, and nothing follows it. Human mode is entirely separate, below.
983
+ if (json) {
984
+ console.log(JSON.stringify(result, null, 2));
985
+ return;
986
+ }
987
+ console.log(`${result.created ? "created" : "connected"}: ${result.owner}/${result.name} (slug '${result.slug}', ` +
988
+ `attachment ${result.attachment_state})`);
989
+ // Round-15 review finding (CONFIRMED, MINOR): `result.slug` above is
990
+ // ALWAYS the canonical slug repo_connect actually resolved to (never the
991
+ // requested one echoed back verbatim — see RepoConnectRpcResult's own
992
+ // doc). When it differs from what was requested, `repo status
993
+ // <requestedSlug>` would silently find nothing — surface the resolution
994
+ // explicitly so the operator knows which slug to actually use.
995
+ if (result.requestedSlug !== result.slug) {
996
+ console.log(`(requested slug '${result.requestedSlug}' resolved to canonical slug '${result.slug}' — use '${result.slug}' with \`codebus repo status\`)`);
997
+ }
998
+ if (!result.is_authority) {
999
+ console.error("\nrepo connected, but you are not this repo's authority (workspace admin or its designated curator) — " +
1000
+ "cannot mint an ingest token or wire CI from here. Ask an admin to run `codebus repo connect` again, or to " +
1001
+ "grant you curator status, then re-run this command (it is safe to repeat).");
1002
+ return;
1003
+ }
1004
+ // One-time ingest token: printed here, in human mode, exactly once.
1005
+ // Round-12 review finding 6 (CONFIRMED, MAJOR): the previous revision
1006
+ // printed the SAME plaintext token a second time, inline inside the
1007
+ // `gh secret set --body '<token>'` command immediately below it — two
1008
+ // appearances in the same block, contradicting the "print once" contract
1009
+ // round-11 review finding F3 established (that finding was about
1010
+ // --json-vs-human duplication; this is a second, distinct duplication
1011
+ // entirely within human mode's own output). Fixed: the token is shown
1012
+ // exactly once, standalone, and the `gh secret set` line for it references
1013
+ // that value with a placeholder + a stdin pipe (`printf '%s' ... | gh
1014
+ // secret set ... --repo ... --env ...`, no `--body`) rather than repeating
1015
+ // the literal secret inline in a second command.
1016
+ // Round-14 review finding 1 (CONFIRMED, BLOCKER): Bragi-GmbH/CodeBus is a
1017
+ // PRIVATE repo, so the emitted workflow's kit checkout cannot rely on
1018
+ // github.token for any caller other than CodeBus itself — every operator
1019
+ // running --write-workflow against a foreign repo must ALSO set a
1020
+ // CODEBUS_KIT_TOKEN secret (a GitHub PAT/App token with read access to
1021
+ // Bragi-GmbH/CodeBus), or the reusable workflow's own fail-fast check
1022
+ // refuses the run outright. Named here alongside the other required
1023
+ // secrets so an operator sees the complete list in one place, not just
1024
+ // the subset the token-mint step itself happens to produce.
1025
+ console.log("\nOne-time ingest token (store it now — it cannot be retrieved again):\n" +
1026
+ ` ${result.token}\n\n` +
1027
+ "Add it (and the connection details below) as GitHub Environment secrets named 'graph-publish' — pipe the " +
1028
+ "token shown above in rather than pasting it into another command's argv:\n" +
1029
+ ` printf '%s' '<TOKEN-ABOVE>' | gh secret set GRAPH_INGEST_TOKEN --repo ${result.owner}/${result.name} --env graph-publish\n` +
1030
+ ` gh secret set SUPABASE_URL --body '${cfg.url}' --repo ${result.owner}/${result.name} --env graph-publish\n` +
1031
+ ` gh secret set SUPABASE_ANON_KEY --body '${cfg.anonKey ?? "<your anon key>"}' --repo ${result.owner}/${result.name} --env graph-publish\n\n` +
1032
+ "Bragi-GmbH/CodeBus is a PRIVATE repo, so the emitted workflow ALSO needs a GitHub PAT or App token with read " +
1033
+ "access to it (used only to check out CodeBus's own scripts/lockfiles) — set this too, or the workflow refuses to run:\n" +
1034
+ ` gh secret set CODEBUS_KIT_TOKEN --body '<a PAT/App token that can read Bragi-GmbH/CodeBus>' --repo ${result.owner}/${result.name} --env graph-publish`);
1035
+ if (result.transitioned)
1036
+ console.log("attachment: pending -> ci_wired");
1037
+ if (result.workflowWritten)
1038
+ console.log(`wrote ${result.workflowWritten}`);
1039
+ }
1040
+ /**
1041
+ * Round-13 review finding 8 (CONFIRMED, MAJOR): the ONLY place a
1042
+ * `PostMintFailureError`'s token may ever be rendered — deliberately,
1043
+ * exactly once, matching `printConnectResult`'s own print-once discipline.
1044
+ * If `e` is not this specific error type, it is returned UNCHANGED for the
1045
+ * caller to rethrow as-is (this function performs no printing in that
1046
+ * case, so the generic handler in `cli/src/index.ts` remains the ONLY
1047
+ * thing that ever prints an ordinary error's message).
1048
+ *
1049
+ * In `--json` mode the token still lands on stdout — but wrapped in a
1050
+ * fresh, single JSON object (never free text), so a caller piping stdout
1051
+ * to a JSON parser still sees exactly one parseable object, now shaped as
1052
+ * an error result instead of a success result. In human mode it prints as
1053
+ * a clearly-marked, standalone block, exactly like the success path's own
1054
+ * one-time token block above.
1055
+ *
1056
+ * Round-15 review finding 2 (CONFIRMED, MINOR): the human-mode block used
1057
+ * to hardcode "--build failed" regardless of which post-mint step actually
1058
+ * threw — wrong for the transition-failure site (round-14 finding 4 added
1059
+ * that site; it fails before any build ever starts). The diagnostic is now
1060
+ * built from `e.stage`.
1061
+ *
1062
+ * Always returns a plain `CodebusError` carrying the ORIGINAL message
1063
+ * (never the token) for the caller to throw — that is what reaches
1064
+ * `index.ts`'s generic `console.error(\`error: ${e.message}\`)` handler,
1065
+ * which must never see the plaintext token in anything it prints.
1066
+ */
1067
+ export function handleConnectError(e, json) {
1068
+ if (!(e instanceof PostMintFailureError))
1069
+ return e;
1070
+ const stageDescription = e.stage === "transition" ? "the pending -> ci_wired transition failed" : "--build failed";
1071
+ if (json) {
1072
+ console.log(JSON.stringify({ error: e.message, token: e.token, stage: e.stage }, null, 2));
1073
+ }
1074
+ else {
1075
+ console.log(`\n${stageDescription}, but an ingest token WAS minted for this connection before the failure — ` +
1076
+ "store it now, it cannot be retrieved again:\n" +
1077
+ ` ${e.token}\n`);
1078
+ }
1079
+ return new CodebusError(e.message, e.exitCode);
1080
+ }
1081
+ async function connectAction(url, o) {
1082
+ const cfg = resolveConfig(o);
1083
+ const ctx = await makeCtx(cfg);
1084
+ const repoRoot = o.repoRoot ?? process.cwd();
1085
+ try {
1086
+ const result = await repoConnectFull(ctx, cfg, url, { ...o, repoRoot });
1087
+ printConnectResult(result, cfg, !!o.json);
1088
+ }
1089
+ catch (e) {
1090
+ throw handleConnectError(e, !!o.json);
1091
+ }
1092
+ }
1093
+ // ── repo status ─────────────────────────────────────────────────────────
1094
+ async function statusAction(slug, o) {
1095
+ const ctx = await makeCtx(resolveConfig(o));
1096
+ const ws = await workspaceId(ctx);
1097
+ const rid = await resolveRepoIdBySlug(ctx, ws, slug);
1098
+ if (!rid)
1099
+ throw new CodebusError(`unknown repo: ${slug}`);
1100
+ const { data: statusRows, error: statusErr } = await ctx.supabase
1101
+ .from("graph_status")
1102
+ .select("*")
1103
+ .eq("project_id", rid)
1104
+ .maybeSingle();
1105
+ if (statusErr)
1106
+ throw new CodebusError(statusErr.message);
1107
+ const { data: prRows, error: prErr } = await ctx.supabase
1108
+ .from("pull_requests")
1109
+ .select("status")
1110
+ .eq("repo_id", rid);
1111
+ if (prErr)
1112
+ throw new CodebusError(prErr.message);
1113
+ const prCounts = {};
1114
+ for (const r of prRows ?? [])
1115
+ prCounts[r.status] = (prCounts[r.status] ?? 0) + 1;
1116
+ const result = { slug, graph: statusRows ?? null, pull_requests: prCounts };
1117
+ if (o.json) {
1118
+ console.log(JSON.stringify(result, null, 2));
1119
+ }
1120
+ else {
1121
+ console.log(render([{
1122
+ slug,
1123
+ attachment_state: statusRows?.attachment_state ?? "(no attachment)",
1124
+ freshness: statusRows?.freshness ?? "(no graph)",
1125
+ grade: statusRows?.grade ?? "(ungraded)",
1126
+ prs: Object.entries(prCounts).map(([k, v]) => `${k}:${v}`).join(" ") || "(none)",
1127
+ }], { json: false }));
1128
+ }
1129
+ }
1130
+ // ── repo list ───────────────────────────────────────────────────────────
1131
+ /**
1132
+ * Shapes one raw `repos` (+ joined `graph_attachments`) row into `repo
1133
+ * list`'s output row — used for BOTH --json and human-mode, since `render`
1134
+ * (below) feeds the exact same `rows` array to either presentation.
1135
+ *
1136
+ * Round-19 review finding (CONFIRMED, PR #55 comment 3827524459):
1137
+ * `remote_url` used to reach this row (and therefore a terminal or a
1138
+ * --json consumer) verbatim. 0070's own migration deliberately AUDITS
1139
+ * (reports) pre-existing userinfo-bearing remote_url rows rather than
1140
+ * rewriting them in place — a silent server-side rewrite of a column an
1141
+ * operator's own tooling may still expect verbatim was judged the worse
1142
+ * hazard — so its trigger only REJECTS *new* writes; a legacy
1143
+ * `https://token@github.com/...` row from before 0070 existed can
1144
+ * genuinely still be sitting in this table today. `repo list` was the one
1145
+ * remaining place that echoed such a row unredacted. Extracted to its own
1146
+ * function (rather than an inline `.map()` lambda) specifically so this
1147
+ * redaction has a direct unit test against a credential-bearing fixture —
1148
+ * `listAction` itself follows this file's usual convention of being
1149
+ * covered by the real-subprocess integration suite instead, since it does
1150
+ * nothing beyond wiring this shape to a live query and `render`.
1151
+ */
1152
+ export function shapeRepoListRow(r) {
1153
+ return {
1154
+ slug: r.slug,
1155
+ remote_url: r.remote_url ? redactUrlUserinfo(r.remote_url) : "(none)",
1156
+ default_branch: r.default_branch,
1157
+ attachment_state: (Array.isArray(r.graph_attachments) ? r.graph_attachments[0]?.state : r.graph_attachments?.state) ?? "(none)",
1158
+ };
1159
+ }
1160
+ async function listAction(o) {
1161
+ const ctx = await makeCtx(resolveConfig(o));
1162
+ const ws = await workspaceId(ctx);
1163
+ const { data, error } = await ctx.supabase
1164
+ .from("repos")
1165
+ .select("slug, remote_url, default_branch, retired_into, graph_attachments(state)")
1166
+ .eq("workspace_id", ws)
1167
+ .order("slug");
1168
+ if (error)
1169
+ throw new CodebusError(error.message);
1170
+ const rows = (data ?? [])
1171
+ .filter((r) => !r.retired_into)
1172
+ .map(shapeRepoListRow);
1173
+ console.log(render(rows, { json: !!o.json }));
1174
+ }
1175
+ // ── registration ────────────────────────────────────────────────────────
1176
+ export function registerRepo(program, g) {
1177
+ const repo = program.command("repo").description("connect and inspect GitHub repos (CB repo-intelligence)");
1178
+ g(repo.command("connect").argument("<github-url>", "https/ssh GitHub URL, or bare owner/repo"))
1179
+ .option("--slug <slug>", "repo slug within this workspace (default: the repo name)")
1180
+ // Round-11 review finding F6 (CONFIRMED): the plan's own file spec
1181
+ // (docs/plans/repo-intelligence.md A4) names this flag `--default-branch`
1182
+ // — matching the `p_default_branch` RPC parameter it feeds and the
1183
+ // `repos.default_branch` column it lands in — not `--branch`. `--branch`
1184
+ // is kept as a deprecated alias rather than a breaking rename; whichever
1185
+ // is given wins (both given: --default-branch wins), neither given
1186
+ // defaults to 'main' exactly as before.
1187
+ .option("--default-branch <branch>", "default branch (default: main)")
1188
+ .option("--branch <branch>", "deprecated alias for --default-branch")
1189
+ .option("--force", "overwrite an existing, different remote_url (requires workspace admin or curator)")
1190
+ .option("--write-workflow", "write .github/workflows/codebus.yml (only when cwd's origin matches) — does NOT by itself mark the attachment ci_wired; run --build (or a real successful CI ingest, once available) to earn that")
1191
+ .option("--build", "run graphify locally against the freshly minted token — the only thing that transitions the attachment to ci_wired")
1192
+ .option("--no-pr-sync", "omit the (currently disabled) pr-sync job stub from the emitted workflow")
1193
+ .action((url, o) => connectAction(url, { ...o, branch: o.defaultBranch ?? o.branch ?? "main" }));
1194
+ g(repo.command("status").argument("<slug>", "repo slug"))
1195
+ .action((slug, o) => statusAction(slug, o));
1196
+ g(repo.command("list")).action((o) => listAction(o));
1197
+ }
1198
+ //# sourceMappingURL=repo.js.map