@vibe-commit/capture 0.1.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/LICENSE +21 -0
- package/README.md +66 -0
- package/dist/agents/claude_code.d.ts +11 -0
- package/dist/agents/claude_code.js +162 -0
- package/dist/agents/claude_code.js.map +1 -0
- package/dist/agents/codex_cli.d.ts +15 -0
- package/dist/agents/codex_cli.js +67 -0
- package/dist/agents/codex_cli.js.map +1 -0
- package/dist/agents/cursor.d.ts +21 -0
- package/dist/agents/cursor.js +89 -0
- package/dist/agents/cursor.js.map +1 -0
- package/dist/agents/registry.d.ts +184 -0
- package/dist/agents/registry.js +212 -0
- package/dist/agents/registry.js.map +1 -0
- package/dist/agents/types.d.ts +279 -0
- package/dist/agents/types.js +67 -0
- package/dist/agents/types.js.map +1 -0
- package/dist/commands/connect.d.ts +36 -0
- package/dist/commands/connect.js +499 -0
- package/dist/commands/connect.js.map +1 -0
- package/dist/commands/context.d.ts +45 -0
- package/dist/commands/context.js +14 -0
- package/dist/commands/context.js.map +1 -0
- package/dist/commands/report.d.ts +105 -0
- package/dist/commands/report.js +430 -0
- package/dist/commands/report.js.map +1 -0
- package/dist/commands/status.d.ts +29 -0
- package/dist/commands/status.js +236 -0
- package/dist/commands/status.js.map +1 -0
- package/dist/commands/why.d.ts +349 -0
- package/dist/commands/why.js +937 -0
- package/dist/commands/why.js.map +1 -0
- package/dist/consent.d.ts +59 -0
- package/dist/consent.js +111 -0
- package/dist/consent.js.map +1 -0
- package/dist/copy/claims.d.ts +79 -0
- package/dist/copy/claims.js +343 -0
- package/dist/copy/claims.js.map +1 -0
- package/dist/copy/grades.d.ts +117 -0
- package/dist/copy/grades.js +96 -0
- package/dist/copy/grades.js.map +1 -0
- package/dist/copy/index.d.ts +12 -0
- package/dist/copy/index.js +13 -0
- package/dist/copy/index.js.map +1 -0
- package/dist/copy/strings.d.ts +1328 -0
- package/dist/copy/strings.js +1243 -0
- package/dist/copy/strings.js.map +1 -0
- package/dist/copy/time.d.ts +27 -0
- package/dist/copy/time.js +43 -0
- package/dist/copy/time.js.map +1 -0
- package/dist/credential.d.ts +211 -0
- package/dist/credential.js +298 -0
- package/dist/credential.js.map +1 -0
- package/dist/exit.d.ts +59 -0
- package/dist/exit.js +60 -0
- package/dist/exit.js.map +1 -0
- package/dist/git.d.ts +299 -0
- package/dist/git.js +653 -0
- package/dist/git.js.map +1 -0
- package/dist/hooks/entry.d.ts +277 -0
- package/dist/hooks/entry.js +1092 -0
- package/dist/hooks/entry.js.map +1 -0
- package/dist/hooks/post_commit.d.ts +59 -0
- package/dist/hooks/post_commit.js +160 -0
- package/dist/hooks/post_commit.js.map +1 -0
- package/dist/hooks/post_rewrite.d.ts +59 -0
- package/dist/hooks/post_rewrite.js +161 -0
- package/dist/hooks/post_rewrite.js.map +1 -0
- package/dist/hooks/session_end.d.ts +101 -0
- package/dist/hooks/session_end.js +120 -0
- package/dist/hooks/session_end.js.map +1 -0
- package/dist/index.d.ts +34 -0
- package/dist/index.js +360 -0
- package/dist/index.js.map +1 -0
- package/dist/install.d.ts +153 -0
- package/dist/install.js +284 -0
- package/dist/install.js.map +1 -0
- package/dist/json.d.ts +100 -0
- package/dist/json.js +94 -0
- package/dist/json.js.map +1 -0
- package/dist/oauth/discovery.d.ts +96 -0
- package/dist/oauth/discovery.js +163 -0
- package/dist/oauth/discovery.js.map +1 -0
- package/dist/oauth/lock.d.ts +100 -0
- package/dist/oauth/lock.js +287 -0
- package/dist/oauth/lock.js.map +1 -0
- package/dist/oauth/loopback.d.ts +85 -0
- package/dist/oauth/loopback.js +124 -0
- package/dist/oauth/loopback.js.map +1 -0
- package/dist/oauth/pkce.d.ts +58 -0
- package/dist/oauth/pkce.js +77 -0
- package/dist/oauth/pkce.js.map +1 -0
- package/dist/oauth/read.d.ts +120 -0
- package/dist/oauth/read.js +144 -0
- package/dist/oauth/read.js.map +1 -0
- package/dist/oauth/session.d.ts +80 -0
- package/dist/oauth/session.js +112 -0
- package/dist/oauth/session.js.map +1 -0
- package/dist/oauth/signin.d.ts +178 -0
- package/dist/oauth/signin.js +225 -0
- package/dist/oauth/signin.js.map +1 -0
- package/dist/oauth/token.d.ts +139 -0
- package/dist/oauth/token.js +183 -0
- package/dist/oauth/token.js.map +1 -0
- package/dist/paths.d.ts +366 -0
- package/dist/paths.js +460 -0
- package/dist/paths.js.map +1 -0
- package/dist/policy.d.ts +195 -0
- package/dist/policy.js +244 -0
- package/dist/policy.js.map +1 -0
- package/dist/post.d.ts +302 -0
- package/dist/post.js +384 -0
- package/dist/post.js.map +1 -0
- package/dist/project.d.ts +99 -0
- package/dist/project.js +192 -0
- package/dist/project.js.map +1 -0
- package/dist/redact.d.ts +175 -0
- package/dist/redact.js +700 -0
- package/dist/redact.js.map +1 -0
- package/dist/runtime.d.ts +30 -0
- package/dist/runtime.js +37 -0
- package/dist/runtime.js.map +1 -0
- package/dist/spawn_budget.d.ts +101 -0
- package/dist/spawn_budget.js +124 -0
- package/dist/spawn_budget.js.map +1 -0
- package/dist/spool.d.ts +390 -0
- package/dist/spool.js +556 -0
- package/dist/spool.js.map +1 -0
- package/dist/state.d.ts +109 -0
- package/dist/state.js +217 -0
- package/dist/state.js.map +1 -0
- package/dist/system_message.d.ts +101 -0
- package/dist/system_message.js +141 -0
- package/dist/system_message.js.map +1 -0
- package/dist/term.d.ts +112 -0
- package/dist/term.js +134 -0
- package/dist/term.js.map +1 -0
- package/dist/version.d.ts +16 -0
- package/dist/version.js +17 -0
- package/dist/version.js.map +1 -0
- package/package.json +50 -0
package/dist/git.js
ADDED
|
@@ -0,0 +1,653 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What "this repository" means ON THE WIRE — `CR-019d`, the client half of
|
|
3
|
+
* `CR-019`.
|
|
4
|
+
*
|
|
5
|
+
* ⚠ **This is not `project.ts`'s question and the two answers are not
|
|
6
|
+
* interchangeable.** `resolveProjectKey()` answers "which work tree is this?"
|
|
7
|
+
* with a local filesystem path, and that path is (since `CR-017d`) half the
|
|
8
|
+
* local state key — it never leaves the machine. `resolveRepoSlug()` answers
|
|
9
|
+
* "which repository is this, in terms the SERVER can resolve?" and its value
|
|
10
|
+
* goes on the wire as `X-Repo-Slug`. Conflating them would put an absolute path
|
|
11
|
+
* from a developer's laptop into a tenant-scoped lookup, so they stay separate
|
|
12
|
+
* functions with separate return shapes.
|
|
13
|
+
*
|
|
14
|
+
* ⛔ **THIS PARAGRAPH USED TO CALL THAT PATH "THE CONSENT KEY" TOO, AND `D184`
|
|
15
|
+
* MADE THAT FALSE.** The consent gate now keys on the canonicalised git COMMON
|
|
16
|
+
* DIR — `resolveProjectKeys(dir).consent` — and `resolveProjectKey`'s own
|
|
17
|
+
* docblock says so in terms (`project.ts:197`). Corrected here rather than left
|
|
18
|
+
* standing, because this is the paragraph a reader meets first and it was
|
|
19
|
+
* teaching the retired model.
|
|
20
|
+
*
|
|
21
|
+
* ## The contract this must satisfy, read from the merged server, not inferred
|
|
22
|
+
*
|
|
23
|
+
* `CR-019b` shipped and `X-Repo-Slug` is REQUIRED. `parseIngestHeaders`
|
|
24
|
+
* (`vibecommit-mcp/src/conversation/ingest_session.ts`) rejects an absent header
|
|
25
|
+
* — and a present one that is empty after `.trim()` — with a 400 whose reason is
|
|
26
|
+
* `missing X-Repo-Slug`, and rejects anything over 256 characters. So there is
|
|
27
|
+
* no lenient path to fall back to: this module always returns a non-empty slug
|
|
28
|
+
* for a directory that has a git toplevel at all.
|
|
29
|
+
*
|
|
30
|
+
* D57 §OV1 is why the server is strict: *"A NULL `repository_id` is
|
|
31
|
+
* fail-closed-invisible per `org_access_model.sql`, so without this every hook
|
|
32
|
+
* capture would be hidden from its own author."* `readable_repository_ids` is a
|
|
33
|
+
* POSITIVE list, so a capture with no repository renders to nobody — including
|
|
34
|
+
* the person who just made it. A 400 is strictly better than that silence.
|
|
35
|
+
*
|
|
36
|
+
* ## The two keys have different GRAIN, and that is correct in both directions
|
|
37
|
+
*
|
|
38
|
+
* This is where the local and wire identities line up, so it is where the
|
|
39
|
+
* mismatch between them belongs on the record. A linked `git worktree` has its
|
|
40
|
+
* own toplevel — verified, `rev-parse --show-toplevel` returns the worktree
|
|
41
|
+
* directory, not the main clone — so two worktrees of one repository are two
|
|
42
|
+
* `repoKey`s and therefore two local offset ledgers, while sharing one `origin`
|
|
43
|
+
* makes them one `repoSlug` and one server `repository_id`.
|
|
44
|
+
*
|
|
45
|
+
* Both halves of that are the behaviour you want. Locally, separate ledgers are
|
|
46
|
+
* the safe direction: the state key exists to keep two work trees from applying
|
|
47
|
+
* each other's byte offsets (`CR-017d`, D58), and over-separating costs a resync
|
|
48
|
+
* at worst. On the wire, one identity is simply true — two worktrees of one
|
|
49
|
+
* repository ARE one repository, and minting two would fragment the audit record
|
|
50
|
+
* of a single repo.
|
|
51
|
+
*
|
|
52
|
+
* ⛔ **THE SPLIT IS NOW THE STATE BUCKET'S ALONE, AND IT USED TO BE EVERYTHING'S
|
|
53
|
+
* (`D184`).** Read the paragraph above as being about the OFFSET LEDGER, because
|
|
54
|
+
* that is all it still governs. Consent no longer follows the toplevel: it keys
|
|
55
|
+
* on the git common dir, which a main clone and every worktree linked to it
|
|
56
|
+
* SHARE. So the two statements a reader is likely to run together are now
|
|
57
|
+
* opposites — two worktrees are **two** state buckets and **one** consent
|
|
58
|
+
* grant — and the `CR-017d`/D58 reasoning above is kept verbatim because it is
|
|
59
|
+
* still exactly right about the half it was written for.
|
|
60
|
+
*
|
|
61
|
+
* ⚠ **The reason they moved apart is the same fact seen from two sides.** A
|
|
62
|
+
* per-worktree consent key made an agent session inside a `git worktree` capture
|
|
63
|
+
* NOTHING, silently, while `D19` makes worktree-per-builder the house convention
|
|
64
|
+
* — so sharing was the fix. A per-worktree state key is what stops two work
|
|
65
|
+
* trees applying each other's byte offsets — so separating is the fix. One
|
|
66
|
+
* value could not do both, and `project.ts` is where the two are produced
|
|
67
|
+
* together precisely so taking the wrong one has to be deliberate.
|
|
68
|
+
*
|
|
69
|
+
* The one reachable oddity: two worktrees driven by ONE Claude Code session (so,
|
|
70
|
+
* one `session_id`) would keep two independent client ledgers against a single
|
|
71
|
+
* server stream key. The server is idempotent on `(session, offset)` and treats
|
|
72
|
+
* a disagreement as a resync, so the cost is re-sent bytes, never a cross-repo
|
|
73
|
+
* bleed. Not designed around; recorded so the next person meets it as a known
|
|
74
|
+
* property rather than a surprise.
|
|
75
|
+
*
|
|
76
|
+
* ## Case, and why this does not lowercase
|
|
77
|
+
*
|
|
78
|
+
* `resolve_repository` canonicalises with `lower(trim(coalesce(slug, '')))` and
|
|
79
|
+
* the App-installation oracle compares `lower(r.repo_full_name) = v_slug`, so
|
|
80
|
+
* both sides are lowercased and case cannot affect resolution. What case DOES
|
|
81
|
+
* affect is the row the server writes: `display_name` is
|
|
82
|
+
* `trim(p_source_repo_full_name)` — the raw value sent here. Preserving the
|
|
83
|
+
* remote's own case therefore gives a correctly-cased display name for free,
|
|
84
|
+
* and lowercasing would throw that away for no gain.
|
|
85
|
+
*
|
|
86
|
+
* @provenance vibecommit-mcp src/conversation/ingest_session.ts — parseIngestHeaders, read
|
|
87
|
+
* @provenance vibecommit-schema org_access_model.sql — readable_repository_ids, read
|
|
88
|
+
*/
|
|
89
|
+
import { execFileSync } from "node:child_process";
|
|
90
|
+
import { createHash } from "node:crypto";
|
|
91
|
+
import { spawnTimeoutMs } from "./spawn_budget.js";
|
|
92
|
+
/**
|
|
93
|
+
* Per-spawn CEILINGS — the most any one call may take when no hook budget is
|
|
94
|
+
* armed (a CLI verb). ⚠ **A ceiling is not a bound**: four of these on one hook
|
|
95
|
+
* path summed to 20s against a 5s watchdog, which is `TODOS[87]`. What bounds
|
|
96
|
+
* them is `spawnTimeoutMs`, which hands back the SMALLER of the ceiling and what
|
|
97
|
+
* is left of one shared window.
|
|
98
|
+
*/
|
|
99
|
+
const GIT_TIMEOUT_MS = 5_000;
|
|
100
|
+
/** Tighter than the git probes: `ssh -G` is pure config evaluation, measured ~4ms. */
|
|
101
|
+
const SSH_TIMEOUT_MS = 2_000;
|
|
102
|
+
/**
|
|
103
|
+
* Hosts whose URLs are confidently GitHub.com — and therefore the ONLY hosts
|
|
104
|
+
* that emit an UNQUALIFIED two-segment `owner/repo`.
|
|
105
|
+
*
|
|
106
|
+
* `ssh.github.com` is GitHub's documented port-443 SSH endpoint, used by anyone
|
|
107
|
+
* behind a firewall that blocks 22. It is the same host in every sense that
|
|
108
|
+
* matters here, and dropping those users to `local:` would silently cost them
|
|
109
|
+
* repository attribution for a config GitHub itself tells them to use.
|
|
110
|
+
*
|
|
111
|
+
* ⚠ **THIS SET MUST NOT GROW TO INCLUDE GHES OR ANY OTHER FORGE** (D157). It is
|
|
112
|
+
* not "hosts we support" — it is "hosts whose slug the App-installation oracle
|
|
113
|
+
* can resolve". `resolve_repository` matches the oracle on
|
|
114
|
+
* `github_installation_repos.repo_full_name`, which is `owner/repo`, so a host
|
|
115
|
+
* added here starts claiming the github.com namespace. Every other host is
|
|
116
|
+
* host-qualified instead — see `parseRemoteSlug`.
|
|
117
|
+
*/
|
|
118
|
+
const GITHUB_HOSTS = new Set(["github.com", "ssh.github.com"]);
|
|
119
|
+
/**
|
|
120
|
+
* A hostname we are willing to hand to `ssh` and to put in a slug.
|
|
121
|
+
*
|
|
122
|
+
* ⚠ **THIS IS AN ARGUMENT-INJECTION GUARD, NOT A VALIDATOR.** `execFileSync`
|
|
123
|
+
* closes the shell-injection hole but NOT the option-injection one: a host is
|
|
124
|
+
* attacker-influenced (it comes from `.git/config`, which travels with a clone),
|
|
125
|
+
* and `fromScpLike`'s host group happily matches a leading `-`. Measured:
|
|
126
|
+
* `ssh -G "-oProxyCommand=…"` is parsed as FLAGS and prints ssh's usage. So a
|
|
127
|
+
* host that is not plainly a hostname never reaches `ssh` — and `--` below is
|
|
128
|
+
* the second, independent guard, because two cheap guards on a spawn beat one.
|
|
129
|
+
*
|
|
130
|
+
* No leading or trailing `-` or `.`, and nothing outside the LDH set, which is
|
|
131
|
+
* what a real DNS name is anyway.
|
|
132
|
+
*/
|
|
133
|
+
const HOSTNAME = /^[A-Za-z0-9](?:[A-Za-z0-9.-]*[A-Za-z0-9])?$/;
|
|
134
|
+
/**
|
|
135
|
+
* A GitHub account name: alphanumerics and hyphens, no leading or trailing
|
|
136
|
+
* hyphen. Deliberately GitHub's own rule rather than "anything without a
|
|
137
|
+
* slash" — see the confidence note on `resolveRepoSlug`.
|
|
138
|
+
*/
|
|
139
|
+
const OWNER = /^[A-Za-z0-9](?:[A-Za-z0-9-]*[A-Za-z0-9])?$/;
|
|
140
|
+
/** A GitHub repository name. `.` and `..` are excluded below, not here. */
|
|
141
|
+
const REPO = /^[A-Za-z0-9._-]+$/;
|
|
142
|
+
/**
|
|
143
|
+
* `owner/repo` for a work tree whose `origin` is GitHub, `host/owner/repo` for
|
|
144
|
+
* any other host, else `local:<sha256(abspath)[0:12]>`.
|
|
145
|
+
*
|
|
146
|
+
* **Takes the TOPLEVEL, not a `cwd`.** Every caller already has it: the consent
|
|
147
|
+
* gate resolves `resolveProjectKey(input.cwd)` before anything else runs and
|
|
148
|
+
* refuses when it is null, so re-deriving it here would be a second
|
|
149
|
+
* `git rev-parse` spent against the hook's wall-clock budget (DESIGN.md §13.7)
|
|
150
|
+
* to recompute a value the caller is holding — and a second call could disagree
|
|
151
|
+
* with the first, which is the same reason `CR-017d` reuses it for the state key
|
|
152
|
+
* rather than recomputing.
|
|
153
|
+
*
|
|
154
|
+
* **Never returns empty.** A work tree always has an identity: with a remote it
|
|
155
|
+
* is the remote's, without one it is the path's. That is what lets the server
|
|
156
|
+
* require the header (D58 §ER25).
|
|
157
|
+
*
|
|
158
|
+
* ## ⚠ Why `local:` had to stop being the answer for a real remote (D157)
|
|
159
|
+
*
|
|
160
|
+
* `localSlug` hashes the developer's OWN ABSOLUTE PATH. That is correct for a
|
|
161
|
+
* work tree with no remote — there is nothing else to key on, and two such trees
|
|
162
|
+
* genuinely are two repositories. It is WRONG for a tree that has a remote:
|
|
163
|
+
* every colleague, and every second clone on one machine, mints a DIFFERENT
|
|
164
|
+
* repository for what is provably one repo. Captures never join, the graph
|
|
165
|
+
* cannot span them, and **nothing errors** — the failure is silent by
|
|
166
|
+
* construction, which is what made it survive this long.
|
|
167
|
+
*
|
|
168
|
+
* So `local:` now survives only where it is true: **no remote at all**, or a
|
|
169
|
+
* remote whose path is not `owner/repo` and therefore cannot be named without
|
|
170
|
+
* guessing (`parseRemoteSlug` refuses rather than guesses — `CR-019d`).
|
|
171
|
+
*/
|
|
172
|
+
export function resolveRepoSlug(toplevel) {
|
|
173
|
+
const remote = gitRemoteUrl(toplevel);
|
|
174
|
+
const slug = remote === null ? null : parseRemoteSlug(remote);
|
|
175
|
+
return slug ?? localSlug(toplevel);
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* `origin`'s URL, or null when there is no `origin` (or git cannot say).
|
|
179
|
+
*
|
|
180
|
+
* `execFileSync` with an argument array, never a shell string — a repository
|
|
181
|
+
* path containing a space or a `;` is ordinary on a developer's machine and
|
|
182
|
+
* would be a command-injection sink through `execSync`. Same discipline as
|
|
183
|
+
* `project.ts`'s `gitToplevel`, for the same reason.
|
|
184
|
+
*/
|
|
185
|
+
export function gitRemoteUrl(dir) {
|
|
186
|
+
const timeout = spawnTimeoutMs(GIT_TIMEOUT_MS);
|
|
187
|
+
if (timeout === null)
|
|
188
|
+
return null;
|
|
189
|
+
try {
|
|
190
|
+
const out = execFileSync("git", ["-C", dir, "remote", "get-url", "origin"], {
|
|
191
|
+
encoding: "utf8",
|
|
192
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
193
|
+
timeout,
|
|
194
|
+
});
|
|
195
|
+
const url = out.trim();
|
|
196
|
+
return url === "" ? null : url;
|
|
197
|
+
}
|
|
198
|
+
catch {
|
|
199
|
+
// No `origin`, not a work tree, git missing, timeout — all the same answer:
|
|
200
|
+
// we cannot name a remote, so the caller falls back to the local identity.
|
|
201
|
+
return null;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
/**
|
|
205
|
+
* Run one git command, or return null — the shared probe.
|
|
206
|
+
*
|
|
207
|
+
* ⚠ **AN ARGUMENT ARRAY, NEVER A SHELL STRING.** A repository path containing a
|
|
208
|
+
* space or a `;` is ordinary on a developer's machine and a shell string is a
|
|
209
|
+
* command-injection sink. Identical shape to `gitRemoteUrl` above and
|
|
210
|
+
* `project.ts`'s `gitToplevel`: `-C <dir>`, utf8, stderr discarded, a
|
|
211
|
+
* five-second bound, and a failure that resolves to a VALUE rather than
|
|
212
|
+
* throwing.
|
|
213
|
+
*
|
|
214
|
+
* ⚠ **MOVED HERE FROM `src/commands/why.ts` BY `CR-108`**, because `report`
|
|
215
|
+
* needs the same probe and a second copy is how the two drift. `why`'s screens
|
|
216
|
+
* are byte-identical afterwards — `test/why.test.ts` is the oracle for that,
|
|
217
|
+
* and the move changes no behaviour: this is the same function.
|
|
218
|
+
*/
|
|
219
|
+
export function gitProbe(dir, args) {
|
|
220
|
+
const timeout = spawnTimeoutMs(GIT_TIMEOUT_MS);
|
|
221
|
+
if (timeout === null)
|
|
222
|
+
return null;
|
|
223
|
+
try {
|
|
224
|
+
return execFileSync("git", ["-C", dir, ...args], {
|
|
225
|
+
encoding: "utf8",
|
|
226
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
227
|
+
timeout,
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
catch {
|
|
231
|
+
return null;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
/**
|
|
235
|
+
* Is this a shallow clone?
|
|
236
|
+
*
|
|
237
|
+
* ⚠ **LOAD-BEARING RATHER THAN DEFENSIVE** (D67's posture), and it is the same
|
|
238
|
+
* hazard for two different verbs. `git blame` on a shallow clone attributes
|
|
239
|
+
* every line older than the graft boundary to the boundary commit; a
|
|
240
|
+
* REACHABILITY question on a shallow clone is wrong the same way, because the
|
|
241
|
+
* commits that would answer it may simply not be in the clone. Both return a
|
|
242
|
+
* PLAUSIBLE WRONG ANSWER — D98's hazard class — and shallow clones live in CI
|
|
243
|
+
* containers, which is where nobody is reading the output.
|
|
244
|
+
*
|
|
245
|
+
* ⚠ **FAILS CLOSED.** If git cannot answer, we do not know the clone is
|
|
246
|
+
* complete, and an answer we cannot stand behind is worse than a refusal.
|
|
247
|
+
*/
|
|
248
|
+
export function isShallowClone(dir) {
|
|
249
|
+
const out = gitProbe(dir, ["rev-parse", "--is-shallow-repository"]);
|
|
250
|
+
return out === null || out.trim() !== "false";
|
|
251
|
+
}
|
|
252
|
+
/**
|
|
253
|
+
* Is `sha` an ancestor of `ref` — i.e. still reachable from it?
|
|
254
|
+
*
|
|
255
|
+
* ⚠ **THE ONE THING THE SERVER CANNOT ANSWER.** `commit_coverage` returns the
|
|
256
|
+
* shas we hold an edge for and deliberately no rate, because reachability is a
|
|
257
|
+
* property of the user's clone and that process has no repository (D67). This
|
|
258
|
+
* is the client half of that split.
|
|
259
|
+
*
|
|
260
|
+
* ⚠ **FAILS CLOSED, and the direction matters.** `merge-base --is-ancestor`
|
|
261
|
+
* exits 0 for yes and 1 for no, so `gitProbe` returns `""` for reachable and
|
|
262
|
+
* `null` for BOTH "not reachable" and "git could not answer". Those are
|
|
263
|
+
* different facts, and collapsing them would silently count an unanswerable
|
|
264
|
+
* probe as unreachable — deflating the rate rather than refusing it. So the
|
|
265
|
+
* caller is handed `null` for "unknown" and must decide, and `report` refuses.
|
|
266
|
+
*/
|
|
267
|
+
export function isReachable(dir, sha, ref) {
|
|
268
|
+
// `--quiet` so the exit code is the whole answer and nothing reaches stdout.
|
|
269
|
+
if (gitProbe(dir, ["merge-base", "--is-ancestor", sha, ref]) !== null)
|
|
270
|
+
return true;
|
|
271
|
+
// ⚠ A REFUSAL AND A NEGATIVE ARE DIFFERENT ANSWERS. If `ref` does not resolve,
|
|
272
|
+
// or the sha is not in this clone at all, the question was never asked — so
|
|
273
|
+
// report `null` rather than folding it into a `false` the caller would count.
|
|
274
|
+
//
|
|
275
|
+
// ⚠ And note the phrasing above avoids the token `from` before a double
|
|
276
|
+
// quote: `test/provenance.test.ts`'s linkage wall is a REGEX over raw text,
|
|
277
|
+
// `/(?:from|import)\s*\(?\s*"([^"]+)"/`, so ordinary prose can read as an
|
|
278
|
+
// import specifier escaping the package. `why.ts` carries the same warning.
|
|
279
|
+
// This comment cost one red run to learn, in the direction that is safe.
|
|
280
|
+
if (gitProbe(dir, ["rev-parse", "--verify", "--quiet", `${ref}^{commit}`]) === null)
|
|
281
|
+
return null;
|
|
282
|
+
return gitProbe(dir, ["cat-file", "-e", `${sha}^{commit}`]) === null ? null : false;
|
|
283
|
+
}
|
|
284
|
+
/**
|
|
285
|
+
* The `git patch-id` of one commit — the CLIENT-ASSERTED half of a successor
|
|
286
|
+
* pair (`CR-133d`, D111 §1).
|
|
287
|
+
*
|
|
288
|
+
* ⚠ **THE CLIENT IS THE ONLY SIDE THAT CAN COMPUTE THIS.** The server provably
|
|
289
|
+
* cannot: no git dependency, no clone, and its only `child_process` is a
|
|
290
|
+
* best-effort `git show` on no live path (D108). A patch id is a fact about a
|
|
291
|
+
* DIFF, so it survives the rewrite that changes the sha — which is what lets a
|
|
292
|
+
* successor pair resolve through a rebase or GitHub's squash-merge button.
|
|
293
|
+
*
|
|
294
|
+
* ⚠ **IT IS EVIDENCE THE CLIENT ASSERTS, NOT SOMETHING WE WITNESSED** (D111 §1
|
|
295
|
+
* condition 3, the same posture as `cli_read` in D73). Whatever consumes this
|
|
296
|
+
* must record it as client-asserted; nothing here establishes that the caller's
|
|
297
|
+
* repository is the one the server thinks it is.
|
|
298
|
+
*
|
|
299
|
+
* ⚠ **NULL RATHER THAN A GUESS, AND THE REASON IS PERMANENCE.** `match_kind` is
|
|
300
|
+
* in the primary key and `reject_history_mutation` raises on every UPDATE
|
|
301
|
+
* including `service_role` (D105, D108), so **a wrong row can never be
|
|
302
|
+
* retracted**. That makes guessing not a degraded-but-acceptable mode. Every
|
|
303
|
+
* unanswerable case below therefore returns `null`, exactly like `isReachable`
|
|
304
|
+
* beside it, and the caller must be able to express "no patch id" as a state
|
|
305
|
+
* rather than as a value.
|
|
306
|
+
*
|
|
307
|
+
* MEASURED cases that return `null` — none of them is an error:
|
|
308
|
+
* - a MERGE commit — `diff-tree` emits no patch for one, and a merge has no
|
|
309
|
+
* single diff to identify;
|
|
310
|
+
* - an EMPTY commit — nothing to hash;
|
|
311
|
+
* - a sha absent from this clone, which is the ordinary SHALLOW-clone case
|
|
312
|
+
* (`isShallowClone` above exists for the same reason);
|
|
313
|
+
* - git missing, a timeout, or output that is not a hex id.
|
|
314
|
+
*
|
|
315
|
+
* ⚠ `--root` is deliberate: without it the INITIAL commit of a repository has
|
|
316
|
+
* no patch id at all, which would silently exclude the one commit every
|
|
317
|
+
* repository has.
|
|
318
|
+
*
|
|
319
|
+
* ⚠ `--no-ext-diff` is load-bearing, not tidiness: a developer's configured
|
|
320
|
+
* external differ would otherwise rewrite the bytes being hashed, and the
|
|
321
|
+
* resulting id would be stable only on that machine.
|
|
322
|
+
*
|
|
323
|
+
* ⚠ **NO SHELL, so no pipe.** `git patch-id` normally reads a pipe; feeding it
|
|
324
|
+
* through `input` keeps the argument-array rule `gitProbe` documents above — a
|
|
325
|
+
* repository path containing a space or a `;` is ordinary.
|
|
326
|
+
*/
|
|
327
|
+
export function gitPatchId(dir, ref) {
|
|
328
|
+
const diff = gitProbe(dir, ["diff-tree", "-p", "--root", "--no-color", "--no-ext-diff", ref]);
|
|
329
|
+
// ⚠ An empty diff is NOT a failure — it is a merge, or an empty commit. Both
|
|
330
|
+
// are "no patch id exists", which is the same answer as "we could not ask".
|
|
331
|
+
if (diff === null || diff.trim() === "")
|
|
332
|
+
return null;
|
|
333
|
+
// ⚠ A SECOND SPAWN IN ONE FUNCTION, so it re-reads the budget rather than
|
|
334
|
+
// reusing `gitProbe`'s number — the `diff-tree` above may have consumed most
|
|
335
|
+
// of the window, and a stale timeout would let the pair overrun together.
|
|
336
|
+
const timeout = spawnTimeoutMs(GIT_TIMEOUT_MS);
|
|
337
|
+
if (timeout === null)
|
|
338
|
+
return null;
|
|
339
|
+
let out;
|
|
340
|
+
try {
|
|
341
|
+
out = execFileSync("git", ["-C", dir, "patch-id", "--stable"], {
|
|
342
|
+
input: diff,
|
|
343
|
+
encoding: "utf8",
|
|
344
|
+
stdio: ["pipe", "pipe", "ignore"],
|
|
345
|
+
timeout,
|
|
346
|
+
});
|
|
347
|
+
}
|
|
348
|
+
catch {
|
|
349
|
+
return null;
|
|
350
|
+
}
|
|
351
|
+
// `patch-id` prints "<patch-id> <commit-ish>". Take the id and PROVE it is one
|
|
352
|
+
// — anything else is a git we do not understand, and half an id is worse than
|
|
353
|
+
// none. 40 hex for sha1 repositories, 64 for sha256.
|
|
354
|
+
const id = out.trim().split(/\s+/)[0] ?? "";
|
|
355
|
+
return /^[0-9a-f]{40}$|^[0-9a-f]{64}$/.test(id) ? id : null;
|
|
356
|
+
}
|
|
357
|
+
/** A 40-hex sha1 or 64-hex sha256 object name. Nothing else is a sha. */
|
|
358
|
+
const SHA = /^[0-9a-f]{40}$|^[0-9a-f]{64}$/;
|
|
359
|
+
/**
|
|
360
|
+
* `HEAD`'s sha and branch — **the ONE new spawn `CR-170` adds to the Claude Code
|
|
361
|
+
* hook path**, and it is for REF MOVES, not for commits.
|
|
362
|
+
*
|
|
363
|
+
* Commits are observed by `post-commit` (D154), which cannot see a `pull`, a
|
|
364
|
+
* `checkout` or a `reset --hard` — those move a ref **without creating a
|
|
365
|
+
* commit**, so no commit hook fires and only a direct look at `HEAD` records it.
|
|
366
|
+
*
|
|
367
|
+
* ⛔ **ONE SPAWN, BUT NOT COMBINED WITH `--show-toplevel`.** MEASURED
|
|
368
|
+
* 2026-08-17: `git rev-parse --show-toplevel nosuchref` prints the toplevel and
|
|
369
|
+
* THEN exits **128**, so `execFileSync` throws and the toplevel is lost with it —
|
|
370
|
+
* and on a repository with no commits `rev-parse HEAD` is exactly that case
|
|
371
|
+
* (measured: exit 128, `HEAD` on stdout, a usage line on stderr). Folding the two
|
|
372
|
+
* together would make an empty repo lose its project key, which is the `C3`
|
|
373
|
+
* regression. The toplevel keeps its own spawn; this is the second.
|
|
374
|
+
*
|
|
375
|
+
* ⛔ **THE OUTPUT IS VALIDATED, NEVER TRUSTED.** MEASURED, same day: when the rev
|
|
376
|
+
* argument is itself 40 hex, `rev-parse` exits **0 and ECHOES IT BACK** as though
|
|
377
|
+
* it had resolved something. Nothing distinguishes that from a real answer except
|
|
378
|
+
* checking the shape — the same reason `gitPatchId` tests its own return before
|
|
379
|
+
* believing it. ⚠ Marked as an inference: the echo-back is not reachable from
|
|
380
|
+
* THIS caller, which passes the literal `HEAD`, so the test below is defence
|
|
381
|
+
* against a future caller and against a git we do not understand, not a live path.
|
|
382
|
+
*/
|
|
383
|
+
export function headRef(dir) {
|
|
384
|
+
const out = gitProbe(dir, ["rev-parse", "HEAD", "--abbrev-ref", "HEAD"]);
|
|
385
|
+
if (out === null)
|
|
386
|
+
return null;
|
|
387
|
+
const lines = out.trim().split("\n");
|
|
388
|
+
if (lines.length < 2)
|
|
389
|
+
return null;
|
|
390
|
+
const sha = lines[0].trim();
|
|
391
|
+
if (!SHA.test(sha))
|
|
392
|
+
return null;
|
|
393
|
+
const ref = lines[1].trim();
|
|
394
|
+
// `HEAD` is what `--abbrev-ref` prints when detached. Not a branch name.
|
|
395
|
+
return { sha, branch: ref === "" || ref === "HEAD" ? null : ref };
|
|
396
|
+
}
|
|
397
|
+
/** The branch `HEAD` is on, or null on a detached HEAD or a failure. */
|
|
398
|
+
export function currentBranch(dir) {
|
|
399
|
+
const out = gitProbe(dir, ["rev-parse", "--abbrev-ref", "HEAD"]);
|
|
400
|
+
if (out === null)
|
|
401
|
+
return null;
|
|
402
|
+
const ref = out.trim();
|
|
403
|
+
// `HEAD` is what `--abbrev-ref` prints when detached. That is not a branch
|
|
404
|
+
// name, and reporting "coverage on HEAD" would name nothing the reader can
|
|
405
|
+
// check, so the caller asks for `--ref` instead of guessing.
|
|
406
|
+
return ref === "" || ref === "HEAD" ? null : ref;
|
|
407
|
+
}
|
|
408
|
+
/**
|
|
409
|
+
* `owner/repo` from a github.com remote, `host/owner/repo` from any other host,
|
|
410
|
+
* or null when the URL cannot be named without guessing.
|
|
411
|
+
*
|
|
412
|
+
* ⚠ **RENAMED FROM `parseGitHubSlug` BY `CR-167`**, because it stopped being
|
|
413
|
+
* about GitHub. A function that answers `gitlab.com/owner/repo` while calling
|
|
414
|
+
* itself `parseGitHubSlug` is the kind of name the next reader trusts and should
|
|
415
|
+
* not. Nothing outside this module and its test imported the old name.
|
|
416
|
+
*
|
|
417
|
+
* ## ⚠ SEGMENT COUNT IS THE DISCRIMINATOR, AND THE SERVER ALREADY PARSES IT
|
|
418
|
+
*
|
|
419
|
+
* `cr033a` §4 branches on the slash count: exactly one slash means the host is
|
|
420
|
+
* IMPLIED to be github.com and segment 1 is the owner; otherwise segment 1 is
|
|
421
|
+
* the host and segment 2 is the owner. This function emits into that grammar
|
|
422
|
+
* rather than inventing one.
|
|
423
|
+
*
|
|
424
|
+
* ⛔ **github.com therefore stays UNQUALIFIED at two segments.**
|
|
425
|
+
* `resolve_repository` matches the App-installation oracle on
|
|
426
|
+
* `github_installation_repos.repo_full_name`, which is `owner/repo`. Qualifying
|
|
427
|
+
* github.com would silently break every oracle join — the exact failure class
|
|
428
|
+
* this change exists to fix, re-introduced from the other side.
|
|
429
|
+
*
|
|
430
|
+
* ## Null is still the safe answer, and that asymmetry is still the design
|
|
431
|
+
*
|
|
432
|
+
* A wrong slug is worse than no slug. `local:` is designed to be safe: it can
|
|
433
|
+
* never collide with a real repository (neither a two- nor a three-segment slug
|
|
434
|
+
* can contain a `:`), and `resolve_repository` resolves it ungated because a
|
|
435
|
+
* local-only work tree has no external owner to check against. A
|
|
436
|
+
* confidently-wrong slug, by contrast, attributes a capture to a repository the
|
|
437
|
+
* user may not own. So every branch below still fails to null rather than
|
|
438
|
+
* guessing, and callers turn null into `local:`.
|
|
439
|
+
*
|
|
440
|
+
* ⚠ **HOST-QUALIFYING DOES NOT WEAKEN THE SUFFIX-ATTACK GUARD, IT RELOCATES IT.**
|
|
441
|
+
* `github.com.evil.example` no longer returns null — it returns
|
|
442
|
+
* `github.com.evil.example/owner/repo`. The security property was never "returns
|
|
443
|
+
* null"; it is **"is never mistaken for a github.com slug"**, and a three-segment
|
|
444
|
+
* slug provably cannot collide with the two-segment github.com namespace. The
|
|
445
|
+
* server agrees independently: `cr033a` §4 reads segment 1 as the HOST, so that
|
|
446
|
+
* slug is scoped to a namespace claim on `github.com.evil.example`, never to
|
|
447
|
+
* `owner`'s GitHub account.
|
|
448
|
+
*
|
|
449
|
+
* Exported for its own tests: the parsing is where the confidence lives, and it
|
|
450
|
+
* is pure apart from one `ssh -G`, so it is tested directly rather than through
|
|
451
|
+
* a spawned `git`.
|
|
452
|
+
*/
|
|
453
|
+
export function parseRemoteSlug(remoteUrl) {
|
|
454
|
+
const raw = remoteUrl.trim();
|
|
455
|
+
if (raw === "")
|
|
456
|
+
return null;
|
|
457
|
+
const parts = raw.includes("://") ? fromUrl(raw) : fromScpLike(raw);
|
|
458
|
+
if (parts === null)
|
|
459
|
+
return null;
|
|
460
|
+
const segments = parts.path
|
|
461
|
+
.replace(/\.git$/, "")
|
|
462
|
+
.split("/")
|
|
463
|
+
.filter((segment) => segment !== "");
|
|
464
|
+
// EXACTLY two. A URL with more is not a remote — it is a web URL someone
|
|
465
|
+
// pasted (`.../owner/repo/tree/main`), and silently taking its first two
|
|
466
|
+
// segments would be exactly the confident guess this function refuses to make.
|
|
467
|
+
//
|
|
468
|
+
// ⚠ This bound is checked BEFORE `ssh -G` runs: a URL we are going to refuse
|
|
469
|
+
// anyway must not cost a spawn on the hook's budget.
|
|
470
|
+
if (segments.length !== 2)
|
|
471
|
+
return null;
|
|
472
|
+
const [owner, repo] = segments;
|
|
473
|
+
if (!OWNER.test(owner))
|
|
474
|
+
return null;
|
|
475
|
+
if (!REPO.test(repo) || repo === "." || repo === "..")
|
|
476
|
+
return null;
|
|
477
|
+
// Hostnames are case-insensitive; `GitHub.com` is the same host.
|
|
478
|
+
const host = resolveSshAlias(parts).toLowerCase();
|
|
479
|
+
// A host we would not hand to `ssh` is a host we will not put on the wire
|
|
480
|
+
// either. Same guard, same reason: doubt resolves to `local:`.
|
|
481
|
+
if (!GITHUB_HOSTS.has(host) && !HOSTNAME.test(host))
|
|
482
|
+
return null;
|
|
483
|
+
const slug = GITHUB_HOSTS.has(host) ? `${owner}/${repo}` : `${host}/${owner}/${repo}`;
|
|
484
|
+
// ⚠ THE SERVER'S BOUND, ENFORCED WHERE THE VALUE IS BUILT. `parseIngestHeaders`
|
|
485
|
+
// 400s on a slug over `REPO_SLUG_MAX`, and a 400 loses the capture outright —
|
|
486
|
+
// strictly worse than `local:`, which is always 18 characters and always
|
|
487
|
+
// resolves. `OWNER` and `REPO` are unbounded `+` quantifiers by design (we do
|
|
488
|
+
// not hardcode any forge's limits), so nothing upstream caps this.
|
|
489
|
+
//
|
|
490
|
+
// ⚠ Measured, both halves, because they have different provenance: a 300-char
|
|
491
|
+
// HOST is a regression this change would otherwise introduce (such a remote
|
|
492
|
+
// used to return null and fall to `local:`), while a 200+200 `owner/repo` on
|
|
493
|
+
// github.com already returned 401 characters on `main`. One guard closes both.
|
|
494
|
+
return slug.length <= SLUG_MAX ? slug : null;
|
|
495
|
+
}
|
|
496
|
+
/**
|
|
497
|
+
* `parseIngestHeaders`'s upper bound on `X-Repo-Slug`, mirrored here.
|
|
498
|
+
*
|
|
499
|
+
* Duplicating the server's constant is deliberate: this module cannot import it
|
|
500
|
+
* across the repo boundary, and the alternative is discovering the limit as a
|
|
501
|
+
* 400 in the field. The lower bound needs no check — every branch above returns
|
|
502
|
+
* either `null` or a slug containing at least one non-empty segment.
|
|
503
|
+
*
|
|
504
|
+
* @provenance vibecommit-mcp src/conversation/ingest_session.ts — REPO_SLUG_MAX, read
|
|
505
|
+
*/
|
|
506
|
+
const SLUG_MAX = 256;
|
|
507
|
+
/**
|
|
508
|
+
* The remote's host, after resolving an `ssh_config` alias — the half of D157
|
|
509
|
+
* that fixes this project's OWN dogfooding.
|
|
510
|
+
*
|
|
511
|
+
* ⚠ **AN SSH ALIAS IS INVISIBLE TO EVERY OTHER READER OF THE URL.** Our own
|
|
512
|
+
* clones use `git@github-vibecommit:…`, mandatory because plain `git@github.com`
|
|
513
|
+
* resolves to the wrong key, so `github-vibecommit` is not a hostname at all —
|
|
514
|
+
* it is a key into the user's `~/.ssh/config`. Only `ssh` can expand it, so
|
|
515
|
+
* `ssh -G <host>` is consulted and its `hostname` line taken. Measured on
|
|
516
|
+
* OpenSSH 10.3: `ssh -G github-vibecommit` prints `hostname github.com`.
|
|
517
|
+
*
|
|
518
|
+
* ⚠ **ONLY FOR SSH TRANSPORTS, WHICH IS WHAT GIT ITSELF DOES.** An `https://`
|
|
519
|
+
* host is a DNS name that never passes through `ssh_config`; expanding it would
|
|
520
|
+
* let an unrelated `Host` stanza silently rewrite a web remote. So the flag
|
|
521
|
+
* comes from the parse — scp-like form and `ssh://` only, never `https`, `http`
|
|
522
|
+
* or `git://`.
|
|
523
|
+
*
|
|
524
|
+
* ⚠ **AND NEVER FOR A HOST THAT IS ALREADY KNOWN-GITHUB.** Measured identity
|
|
525
|
+
* (`ssh -G github.com` → `github.com`), so the spawn would buy nothing — and
|
|
526
|
+
* this is the common case, which means the overwhelming majority of hooks pay
|
|
527
|
+
* ZERO new process spawns for this change. Only alias and self-hosted users pay,
|
|
528
|
+
* and they pay ~4ms (measured: 10 runs in 41ms, no network I/O — `-G` evaluates
|
|
529
|
+
* config and does not connect or resolve DNS).
|
|
530
|
+
*
|
|
531
|
+
* ⚠ **EVERY FAILURE RETURNS THE HOST UNCHANGED, WHICH IS EXACTLY TODAY'S
|
|
532
|
+
* BEHAVIOUR.** `ssh` absent (a minimal container), too old for `-G` (pre-6.8),
|
|
533
|
+
* a `Match exec` stanza that errors, a timeout, or output with no `hostname`
|
|
534
|
+
* line — all mean "we could not expand this", and an unexpanded host is the
|
|
535
|
+
* status quo rather than a new failure mode. Nothing here can make a slug worse
|
|
536
|
+
* than it is today; it can only fail to improve it.
|
|
537
|
+
*
|
|
538
|
+
* ⚠ **THE BOUND IS 2s, TIGHTER THAN THE 5s ITS NEIGHBOURS USE.** This runs on
|
|
539
|
+
* the hook path, where a synchronous spawn blocks the event loop and therefore
|
|
540
|
+
* defeats the watchdog outright (`TODOS[87]`), so a NEW spawn there earns a
|
|
541
|
+
* tighter bound than the git probes that predate the concern. 2s is ~500x the
|
|
542
|
+
* measured cost; the only way to approach it is a user's own `Match exec`.
|
|
543
|
+
*/
|
|
544
|
+
function resolveSshAlias(parts) {
|
|
545
|
+
const { host, ssh } = parts;
|
|
546
|
+
if (!ssh)
|
|
547
|
+
return host;
|
|
548
|
+
if (GITHUB_HOSTS.has(host.toLowerCase()))
|
|
549
|
+
return host;
|
|
550
|
+
// ⛔ Never hand `ssh` an option-shaped string — see `HOSTNAME`.
|
|
551
|
+
if (!HOSTNAME.test(host))
|
|
552
|
+
return host;
|
|
553
|
+
// ⚠ THE FIFTH CONSUMER OF THE HOOK'S SPAWN BUDGET (`CR-167` added it after
|
|
554
|
+
// `TODOS[87]` was written). Its 2s ceiling was already tighter than the git
|
|
555
|
+
// probes' 5s precisely because it runs on the hook path — but a ceiling is not
|
|
556
|
+
// a bound, so it now derives from the same shared window as the rest.
|
|
557
|
+
const timeout = spawnTimeoutMs(SSH_TIMEOUT_MS);
|
|
558
|
+
// Budget spent: the host is returned UNEXPANDED, which is the same answer as
|
|
559
|
+
// ssh being absent — a slug that is merely unqualified, never a wrong one.
|
|
560
|
+
if (timeout === null)
|
|
561
|
+
return host;
|
|
562
|
+
let out;
|
|
563
|
+
try {
|
|
564
|
+
// `--` is the second guard: even if `HOSTNAME` were widened one day, ssh
|
|
565
|
+
// stops reading flags here. Verified on OpenSSH 10.3 — with `--`, an
|
|
566
|
+
// option-shaped host is rejected as a hostname rather than parsed as a flag.
|
|
567
|
+
out = execFileSync("ssh", ["-G", "--", host], {
|
|
568
|
+
encoding: "utf8",
|
|
569
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
570
|
+
timeout,
|
|
571
|
+
});
|
|
572
|
+
}
|
|
573
|
+
catch {
|
|
574
|
+
return host;
|
|
575
|
+
}
|
|
576
|
+
for (const line of out.split("\n")) {
|
|
577
|
+
// ⚠ `hostname`, NOT `host`. `ssh -G` prints BOTH, and `host` echoes the
|
|
578
|
+
// ALIAS we asked about — taking it would make this function a no-op that
|
|
579
|
+
// looks like it works. Anchored and tokenised so `hostname` cannot match
|
|
580
|
+
// the `host` line's prefix.
|
|
581
|
+
const found = /^hostname[ \t]+(\S+)[ \t]*$/.exec(line);
|
|
582
|
+
// Re-check the ANSWER, not just the question: `ssh_config` is user-supplied
|
|
583
|
+
// and its `HostName` can be anything, including a `%h` token that did not
|
|
584
|
+
// expand.
|
|
585
|
+
if (found !== null)
|
|
586
|
+
return HOSTNAME.test(found[1]) ? found[1] : host;
|
|
587
|
+
}
|
|
588
|
+
return host;
|
|
589
|
+
}
|
|
590
|
+
/**
|
|
591
|
+
* `https://`, `ssh://`, `git://` — anything with a scheme.
|
|
592
|
+
*
|
|
593
|
+
* `ssh` marks the ones git routes through `ssh_config`, and it is TRUE only for
|
|
594
|
+
* `ssh:`. `git:` is the git daemon protocol and `https:`/`http:` are web
|
|
595
|
+
* transports; none of them consults an ssh alias, so neither do we.
|
|
596
|
+
*/
|
|
597
|
+
function fromUrl(raw) {
|
|
598
|
+
let url;
|
|
599
|
+
try {
|
|
600
|
+
url = new URL(raw);
|
|
601
|
+
}
|
|
602
|
+
catch {
|
|
603
|
+
return null;
|
|
604
|
+
}
|
|
605
|
+
// `git+ssh://` and friends parse, but a scheme we have not reasoned about is
|
|
606
|
+
// a doubt, and doubt resolves to `local:`.
|
|
607
|
+
if (!["https:", "http:", "ssh:", "git:"].includes(url.protocol))
|
|
608
|
+
return null;
|
|
609
|
+
// `url.hostname` drops any userinfo and port for us, which is what makes
|
|
610
|
+
// `ssh://git@ssh.github.com:443/owner/repo.git` resolve correctly.
|
|
611
|
+
return { host: url.hostname, path: url.pathname, ssh: url.protocol === "ssh:" };
|
|
612
|
+
}
|
|
613
|
+
/**
|
|
614
|
+
* The scp-like form git accepts: `[user@]host:path`, e.g.
|
|
615
|
+
* `git@github.com:owner/repo.git`.
|
|
616
|
+
*
|
|
617
|
+
* Not a URL, so `new URL` cannot read it — and misreading it as one is a real
|
|
618
|
+
* hazard rather than a hypothetical: `new URL("git@github.com:owner/repo")`
|
|
619
|
+
* succeeds with protocol `git@github.com:`, which would sail past a naive
|
|
620
|
+
* scheme check.
|
|
621
|
+
*
|
|
622
|
+
* ⚠ **ALWAYS `ssh: true`** — this form is ssh-only in git, and it is the form
|
|
623
|
+
* every aliased remote takes, so it is the whole reason `resolveSshAlias`
|
|
624
|
+
* exists. `git@github-vibecommit:Vibe-Commit/vibecommit-capture.git` reaches
|
|
625
|
+
* here with host `github-vibecommit`, which is not a hostname at all.
|
|
626
|
+
*/
|
|
627
|
+
function fromScpLike(raw) {
|
|
628
|
+
const match = /^(?:[^@/]+@)?([^@/:]+):(.+)$/.exec(raw);
|
|
629
|
+
if (match === null)
|
|
630
|
+
return null;
|
|
631
|
+
const [, host, path] = match;
|
|
632
|
+
return { host, path, ssh: true };
|
|
633
|
+
}
|
|
634
|
+
/**
|
|
635
|
+
* The local-only identity: `local:` plus 12 hex of the toplevel's SHA-256.
|
|
636
|
+
*
|
|
637
|
+
* Shape and prefix are both fixed by the server (D58 §ER25) — `resolve_repository`
|
|
638
|
+
* branches on `v_slug not like 'local:%'` to skip the ownership oracle, because
|
|
639
|
+
* a work tree with no remote has no external owner and gating it would gate a
|
|
640
|
+
* developer against themselves. The prefix is emitted lowercase because this
|
|
641
|
+
* module constructs it: the server's own canonicalisation would also lowercase
|
|
642
|
+
* it, but relying on that for a literal we control would be relying on someone
|
|
643
|
+
* else's normalisation to make our own output correct.
|
|
644
|
+
*
|
|
645
|
+
* A digest rather than the path itself: the path is 1..256-bounded only by luck,
|
|
646
|
+
* it can exceed the server's `REPO_SLUG_MAX`, and it would put an absolute
|
|
647
|
+
* filesystem path — usernames included — into a server-side row and its
|
|
648
|
+
* `display_name`.
|
|
649
|
+
*/
|
|
650
|
+
function localSlug(toplevel) {
|
|
651
|
+
return `local:${createHash("sha256").update(toplevel).digest("hex").slice(0, 12)}`;
|
|
652
|
+
}
|
|
653
|
+
//# sourceMappingURL=git.js.map
|
package/dist/git.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"git.js","sourceRoot":"","sources":["../src/git.ts"],"names":[],"mappings":"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"}
|