@vibe-commit/capture 0.2.2 → 0.3.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 (44) hide show
  1. package/dist/agent_hooks.d.ts +53 -8
  2. package/dist/agent_hooks.js +180 -19
  3. package/dist/agent_hooks.js.map +1 -1
  4. package/dist/agent_install.d.ts +0 -17
  5. package/dist/agent_install.js +1 -24
  6. package/dist/agent_install.js.map +1 -1
  7. package/dist/agents/cursor.js +54 -18
  8. package/dist/agents/cursor.js.map +1 -1
  9. package/dist/commands/auth.d.ts +24 -9
  10. package/dist/commands/auth.js +38 -10
  11. package/dist/commands/auth.js.map +1 -1
  12. package/dist/commands/browser_credential.d.ts +89 -0
  13. package/dist/commands/browser_credential.js +181 -0
  14. package/dist/commands/browser_credential.js.map +1 -0
  15. package/dist/commands/connect.d.ts +26 -9
  16. package/dist/commands/connect.js +49 -90
  17. package/dist/commands/connect.js.map +1 -1
  18. package/dist/commands/status.js +35 -2
  19. package/dist/commands/status.js.map +1 -1
  20. package/dist/copy/strings.d.ts +42 -2
  21. package/dist/copy/strings.js +52 -1
  22. package/dist/copy/strings.js.map +1 -1
  23. package/dist/hooks/entry.js +106 -24
  24. package/dist/hooks/entry.js.map +1 -1
  25. package/dist/index.js +9 -1
  26. package/dist/index.js.map +1 -1
  27. package/dist/oauth/mint.d.ts +97 -0
  28. package/dist/oauth/mint.js +120 -0
  29. package/dist/oauth/mint.js.map +1 -0
  30. package/dist/oauth/signin.d.ts +25 -0
  31. package/dist/oauth/signin.js +30 -0
  32. package/dist/oauth/signin.js.map +1 -1
  33. package/dist/post.d.ts +42 -0
  34. package/dist/post.js +64 -3
  35. package/dist/post.js.map +1 -1
  36. package/dist/spool.d.ts +20 -0
  37. package/dist/spool.js +37 -0
  38. package/dist/spool.js.map +1 -1
  39. package/dist/state.d.ts +77 -0
  40. package/dist/state.js +111 -2
  41. package/dist/state.js.map +1 -1
  42. package/dist/version.d.ts +1 -1
  43. package/dist/version.js +1 -1
  44. package/package.json +1 -1
@@ -36,29 +36,38 @@
36
36
  * that discipline theatre. It is refused loudly rather than quietly ignored,
37
37
  * because a user who typed it needs to be told it is now in their history.
38
38
  *
39
- * ## What this verb is NOT
39
+ * ## ⚠ Browser FIRST since `CR-226`; the paste is the fallback, not gone
40
40
  *
41
- * It is not sign-in, and it does not mint. `mintIngestCredential` exists in
42
- * `vibecommit-mcp` with no route in front of it, so on this side of the wire the
43
- * plaintext still comes from the web app's `POST /api/ingest-credentials`, shown
44
- * once, and the human carries it here. Putting a route in front of the minter is
45
- * the PKCE work `connect.ts` defers; this verb makes the paste path first-class,
46
- * it does not remove the paste.
41
+ * This note used to say the verb "is not sign-in, and it does not mint", because
42
+ * `mintIngestCredential` in `vibecommit-mcp` had no route in front of it and the
43
+ * only plaintext producer was the web app's `POST /api/ingest-credentials`. The
44
+ * route now exists (`POST /oauth/ingest-credential`), so on a terminal this verb
45
+ * signs in through the browser and mints the credential there
46
+ * (`browser_credential.ts`). The paste below is unchanged and still reached:
47
47
  *
48
- * @provenance vibecommit-mcp src/oauth/ingest_credential.ts — no route mints one, re-verified
49
- * @provenance vibecommit-web app/api/ingest-credentials/route.ts — the only producer, verified
48
+ * - `--stdin`, or no TTY — the CI shape. Never a browser, exactly as before.
49
+ * - `--paste` — a human who already holds a credential from the dashboard.
50
+ * - any browser step failing — its reason is printed, then the prompt runs.
51
+ *
52
+ * ⚠ So there is no path on which `CR-226` makes the pre-existing flow worse:
53
+ * every failure lands on the prompt that was the whole verb before it.
54
+ *
55
+ * @provenance vibecommit-mcp src/transport/server.ts — POST /oauth/ingest-credential is the mint route, verified
56
+ * @provenance vibecommit-web app/api/ingest-credentials/route.ts — the dashboard producer the paste path still serves, verified
50
57
  */
51
58
  import { AUTH, URLS } from "../copy/index.js";
52
59
  import { writeLines } from "./context.js";
53
60
  import { EXIT } from "../exit.js";
54
61
  import { INGEST_TOKEN_PREFIX, IngestCredential, saveCredential } from "../credential.js";
55
62
  import { credentialsPath } from "../paths.js";
56
- import { renderErrorBlock, tildePath } from "../term.js";
63
+ import { paint, renderErrorBlock, tildePath, wrap } from "../term.js";
57
64
  /**
58
65
  * `--stdin` forces the pipe even on a terminal — the CI shape, where a job may
59
66
  * well have a TTY attached and must not start prompting because of it.
60
67
  */
61
68
  const STDIN_FLAG = "--stdin";
69
+ /** `CR-226`. Skip the browser and go straight to the prompt. */
70
+ const PASTE_FLAG = "--paste";
62
71
  export async function auth(ctx, argv, deps = {}) {
63
72
  // ⛔ THE ARGV REFUSAL COMES FIRST, before stdin is read, resumed or opened.
64
73
  // Anything else would mean the secret in `argv` had already been accepted by
@@ -75,6 +84,21 @@ export async function auth(ctx, argv, deps = {}) {
75
84
  return EXIT.usage;
76
85
  }
77
86
  const piped = argv.includes(STDIN_FLAG) || !ctx.stdinIsTty;
87
+ // ── `CR-226`: the browser, first, on a terminal only. ──────────────────────
88
+ if (!piped && !argv.includes(PASTE_FLAG) && deps.browser !== undefined) {
89
+ const minted = await deps.browser(ctx, "auth");
90
+ if (minted.kind === "saved")
91
+ return saved(ctx);
92
+ // The reason is already on stderr. The documented fallback is the paste
93
+ // this verb has always had, with the page that mints one named first.
94
+ writeLines(ctx.stdout, [
95
+ "",
96
+ ...wrap(AUTH.pasteInsteadLabel, 2),
97
+ // `accent` on a URL — §13.1's one exception, one colour on the line.
98
+ ` ${paint(ctx.colour, "accent", URLS.settings)}`,
99
+ "",
100
+ ]);
101
+ }
78
102
  const raw = piped
79
103
  ? await (deps.readStdin ?? (() => Promise.resolve("")))()
80
104
  : await (deps.readSecret ?? (() => Promise.resolve("")))(AUTH.prompt);
@@ -107,6 +131,10 @@ export async function auth(ctx, argv, deps = {}) {
107
131
  return EXIT.failure;
108
132
  }
109
133
  saveCredential(ctx.home, new IngestCredential(secret, "file"));
134
+ return saved(ctx);
135
+ }
136
+ /** The one confirmation, whichever path wrote the file. */
137
+ function saved(ctx) {
110
138
  writeLines(ctx.stdout, renderErrorBlock({
111
139
  kind: "ok",
112
140
  what: AUTH.savedWhat,
@@ -1 +1 @@
1
- {"version":3,"file":"auth.js","sourceRoot":"","sources":["../../src/commands/auth.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"auth.js","sourceRoot":"","sources":["../../src/commands/auth.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAwDG;AAEH,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,MAAM,kBAAkB,CAAC;AAE9C,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAE1C,OAAO,EAAE,IAAI,EAAE,MAAM,YAAY,CAAC;AAClC,OAAO,EAAE,mBAAmB,EAAE,gBAAgB,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC;AACzF,OAAO,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAC9C,OAAO,EAAE,KAAK,EAAE,gBAAgB,EAAE,SAAS,EAAE,IAAI,EAAE,MAAM,YAAY,CAAC;AAyBtE;;;GAGG;AACH,MAAM,UAAU,GAAG,SAAS,CAAC;AAE7B,gEAAgE;AAChE,MAAM,UAAU,GAAG,SAAS,CAAC;AAE7B,MAAM,CAAC,KAAK,UAAU,IAAI,CACxB,GAAmB,EACnB,IAAuB,EACvB,OAAiB,EAAE;IAEnB,2EAA2E;IAC3E,6EAA6E;IAC7E,8BAA8B;IAC9B,MAAM,UAAU,GAAG,IAAI,CAAC,MAAM,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC,GAAG,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC,CAAC;IAC9D,IAAI,UAAU,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QAC1B,UAAU,CACR,GAAG,CAAC,MAAM,EACV,gBAAgB,CACd;YACE,IAAI,EAAE,KAAK;YACX,IAAI,EAAE,IAAI,CAAC,QAAQ;YACnB,GAAG,EAAE,CAAC,IAAI,CAAC,OAAO,CAAC;YACnB,QAAQ,EAAE,IAAI,CAAC,OAAO;YACtB,KAAK,EAAE,CAAC,IAAI,CAAC,aAAa,EAAE,IAAI,CAAC,YAAY,CAAC;SAC/C,EACD,GAAG,CAAC,MAAM,CACX,CACF,CAAC;QACF,OAAO,IAAI,CAAC,KAAK,CAAC;IACpB,CAAC;IAED,MAAM,KAAK,GAAG,IAAI,CAAC,QAAQ,CAAC,UAAU,CAAC,IAAI,CAAC,GAAG,CAAC,UAAU,CAAC;IAE3D,8EAA8E;IAC9E,IAAI,CAAC,KAAK,IAAI,CAAC,IAAI,CAAC,QAAQ,CAAC,UAAU,CAAC,IAAI,IAAI,CAAC,OAAO,KAAK,SAAS,EAAE,CAAC;QACvE,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,OAAO,CAAC,GAAG,EAAE,MAAM,CAAC,CAAC;QAC/C,IAAI,MAAM,CAAC,IAAI,KAAK,OAAO;YAAE,OAAO,KAAK,CAAC,GAAG,CAAC,CAAC;QAC/C,wEAAwE;QACxE,sEAAsE;QACtE,UAAU,CAAC,GAAG,CAAC,MAAM,EAAE;YACrB,EAAE;YACF,GAAG,IAAI,CAAC,IAAI,CAAC,iBAAiB,EAAE,CAAC,CAAC;YAClC,qEAAqE;YACrE,KAAK,KAAK,CAAC,GAAG,CAAC,MAAM,EAAE,QAAQ,EAAE,IAAI,CAAC,QAAQ,CAAC,EAAE;YACjD,EAAE;SACH,CAAC,CAAC;IACL,CAAC;IAED,MAAM,GAAG,GAAG,KAAK;QACf,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,SAAS,IAAI,CAAC,GAAG,EAAE,CAAC,OAAO,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE;QACzD,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,UAAU,IAAI,CAAC,GAAG,EAAE,CAAC,OAAO,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;IAExE,+EAA+E;IAC/E,+EAA+E;IAC/E,uEAAuE;IACvE,MAAM,MAAM,GAAG,GAAG,CAAC,IAAI,EAAE,CAAC;IAE1B,IAAI,MAAM,KAAK,EAAE,EAAE,CAAC;QAClB,UAAU,CACR,GAAG,CAAC,MAAM,EACV,gBAAgB,CACd;YACE,IAAI,EAAE,KAAK;YACX,IAAI,EAAE,IAAI,CAAC,SAAS;YACpB,GAAG,EAAE,CAAC,IAAI,CAAC,QAAQ,CAAC;YACpB,QAAQ,EAAE,IAAI,CAAC,QAAQ;YACvB,KAAK,EAAE,CAAC,IAAI,CAAC,QAAQ,CAAC;SACvB,EACD,GAAG,CAAC,MAAM,CACX,CACF,CAAC;QACF,OAAO,IAAI,CAAC,OAAO,CAAC;IACtB,CAAC;IAED,yEAAyE;IACzE,+EAA+E;IAC/E,+EAA+E;IAC/E,0CAA0C;IAC1C,IAAI,CAAC,MAAM,CAAC,UAAU,CAAC,mBAAmB,CAAC,EAAE,CAAC;QAC5C,UAAU,CACR,GAAG,CAAC,MAAM,EACV,gBAAgB,CACd;YACE,IAAI,EAAE,KAAK;YACX,IAAI,EAAE,IAAI,CAAC,cAAc;YACzB,GAAG,EAAE,CAAC,IAAI,CAAC,aAAa,CAAC;YACzB,QAAQ,EAAE,IAAI,CAAC,aAAa;YAC5B,KAAK,EAAE,CAAC,IAAI,CAAC,QAAQ,CAAC;SACvB,EACD,GAAG,CAAC,MAAM,CACX,CACF,CAAC;QACF,OAAO,IAAI,CAAC,OAAO,CAAC;IACtB,CAAC;IAED,cAAc,CAAC,GAAG,CAAC,IAAI,EAAE,IAAI,gBAAgB,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;IAC/D,OAAO,KAAK,CAAC,GAAG,CAAC,CAAC;AACpB,CAAC;AAED,2DAA2D;AAC3D,SAAS,KAAK,CAAC,GAAmB;IAChC,UAAU,CACR,GAAG,CAAC,MAAM,EACV,gBAAgB,CACd;QACE,IAAI,EAAE,IAAI;QACV,IAAI,EAAE,IAAI,CAAC,SAAS;QACpB,GAAG,EAAE,CAAC,IAAI,CAAC,QAAQ,CAAC,SAAS,CAAC,eAAe,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC;QACpE,QAAQ,EAAE,IAAI,CAAC,SAAS;QACxB,KAAK,EAAE,CAAC,IAAI,CAAC,WAAW,CAAC;KAC1B,EACD,GAAG,CAAC,MAAM,CACX,CACF,CAAC;IACF,OAAO,IAAI,CAAC,EAAE,CAAC;AACjB,CAAC"}
@@ -0,0 +1,89 @@
1
+ /**
2
+ * The browser beats — sign in, then buy this machine's ingest credential with
3
+ * that sign-in. `CR-084d` wrote the first; `CR-226` adds the second.
4
+ *
5
+ * ## Why these live together, and outside `connect`
6
+ *
7
+ * Sign-in used to be `connect`'s private beat, reachable only behind
8
+ * `--sign-in`, for a reason its own note gave: *"sign-in cannot mint the
9
+ * credential the rest of `connect` is about."* `CR-226` put a route in front of
10
+ * the minter, so that sentence stopped being true — and two verbs now need the
11
+ * pair. `auth` runs it first and falls back to the paste it has always had;
12
+ * `connect` runs it when a fresh machine reaches the credential beat with a
13
+ * human at the terminal. One copy of the sequence, two callers.
14
+ *
15
+ * ## ⚠ Browser FIRST, never browser ONLY (FOUNDER_OAUTH_PROPOSAL decision 2)
16
+ *
17
+ * Every failure here returns `failed` and leaves the machine exactly as it was:
18
+ * no half-written file, no deleted session. The caller then offers the paste
19
+ * path, which is the path that worked before this module existed. There is no
20
+ * outcome of this function that makes the pre-`CR-226` state worse, and that is
21
+ * the property that made it safe to wire a mechanism that had never run.
22
+ *
23
+ * ## ⚠ And never for a machine that already has a credential
24
+ *
25
+ * `VIBECOMMIT_TOKEN` is read before the file (D56) and still is; this module
26
+ * does not read either. It is the CALLER's job to reach it only when there is
27
+ * nothing to use — `connect` gates on `absent`, and `auth` is the verb a user
28
+ * types precisely to replace what is there.
29
+ */
30
+ import { type IngestCredential } from "../credential.js";
31
+ import { type CommandContext } from "./context.js";
32
+ /**
33
+ * The network and the browser, injectable. Defaults are the real ones; a test
34
+ * supplies a `fetch` aimed at a stand-in server and an `openBrowser` that follows
35
+ * the authorize redirect itself.
36
+ */
37
+ export interface BrowserDeps {
38
+ readonly fetch?: typeof fetch;
39
+ readonly openBrowser?: (url: string) => boolean;
40
+ /** The machine's name, for the credential's label. */
41
+ readonly hostname?: () => string;
42
+ }
43
+ /**
44
+ * Which verb a failed step tells the user to run again.
45
+ *
46
+ * ⚠ A NAME, NOT THE COMMAND STRING, and that is for `test/copy-width.test.ts`.
47
+ * Its fix-line width gate DERIVES every string that can reach a `fixes:` array
48
+ * by reading the array literals; a `fixes: [retry]` holding a runtime string is
49
+ * invisible to it, which would quietly drop `COMMANDS.signIn` out of the set it
50
+ * checks. The conditional in `signInError` names all three copy paths inline,
51
+ * where the gate can see them.
52
+ */
53
+ export type Retry = "auth" | "connect" | "sign-in";
54
+ export interface SignInBeatOptions extends BrowserDeps {
55
+ /** The verb a failed sign-in's fix line tells the user to run again. */
56
+ readonly retry: Retry;
57
+ /**
58
+ * Sign in even though a session file loads. Only for a session the server has
59
+ * already REFUSED to renew — see `browserCredential` — never to second-guess a
60
+ * working one.
61
+ */
62
+ readonly replace?: boolean;
63
+ }
64
+ /**
65
+ * The browser sign-in beat — `CR-084d`.
66
+ *
67
+ * Opt-in inside `connect` (`--sign-in`) for the read lane, and the first half of
68
+ * `browserCredential` below. When a session already exists it is a no-op that
69
+ * says so; it never replaces a working session.
70
+ */
71
+ export declare function signInBeat(ctx: CommandContext, options: SignInBeatOptions): Promise<number>;
72
+ export type BrowserCredential =
73
+ /** Minted AND written to `credentials.json`. The caller confirms it on screen. */
74
+ {
75
+ readonly kind: "saved";
76
+ readonly credential: IngestCredential;
77
+ }
78
+ /** Nothing was written. The reason is already on stderr; the caller offers the paste. */
79
+ | {
80
+ readonly kind: "failed";
81
+ };
82
+ /**
83
+ * Sign in if needed, mint this machine's credential, save it — `CR-226`.
84
+ *
85
+ * Prints its own progress and its own failure; prints NOTHING on success,
86
+ * because what comes next differs by caller (`auth` ends, `connect` goes on to
87
+ * capture) and a confirmation here would be a second one.
88
+ */
89
+ export declare function browserCredential(ctx: CommandContext, retry: Retry, deps?: BrowserDeps): Promise<BrowserCredential>;
@@ -0,0 +1,181 @@
1
+ /**
2
+ * The browser beats — sign in, then buy this machine's ingest credential with
3
+ * that sign-in. `CR-084d` wrote the first; `CR-226` adds the second.
4
+ *
5
+ * ## Why these live together, and outside `connect`
6
+ *
7
+ * Sign-in used to be `connect`'s private beat, reachable only behind
8
+ * `--sign-in`, for a reason its own note gave: *"sign-in cannot mint the
9
+ * credential the rest of `connect` is about."* `CR-226` put a route in front of
10
+ * the minter, so that sentence stopped being true — and two verbs now need the
11
+ * pair. `auth` runs it first and falls back to the paste it has always had;
12
+ * `connect` runs it when a fresh machine reaches the credential beat with a
13
+ * human at the terminal. One copy of the sequence, two callers.
14
+ *
15
+ * ## ⚠ Browser FIRST, never browser ONLY (FOUNDER_OAUTH_PROPOSAL decision 2)
16
+ *
17
+ * Every failure here returns `failed` and leaves the machine exactly as it was:
18
+ * no half-written file, no deleted session. The caller then offers the paste
19
+ * path, which is the path that worked before this module existed. There is no
20
+ * outcome of this function that makes the pre-`CR-226` state worse, and that is
21
+ * the property that made it safe to wire a mechanism that had never run.
22
+ *
23
+ * ## ⚠ And never for a machine that already has a credential
24
+ *
25
+ * `VIBECOMMIT_TOKEN` is read before the file (D56) and still is; this module
26
+ * does not read either. It is the CALLER's job to reach it only when there is
27
+ * nothing to use — `connect` gates on `absent`, and `auth` is the verb a user
28
+ * types precisely to replace what is there.
29
+ */
30
+ import { hostname as osHostname } from "node:os";
31
+ import { AUTH, COMMANDS, SIGNIN } from "../copy/index.js";
32
+ import { saveCredential } from "../credential.js";
33
+ import { EXIT } from "../exit.js";
34
+ import { mcpUrl } from "../oauth/discovery.js";
35
+ import { machineLabel } from "../oauth/mint.js";
36
+ import { loadSession } from "../oauth/session.js";
37
+ import { HTTP_TIMEOUT_MS, mintWithSession, openBrowser as platformOpenBrowser, signIn, } from "../oauth/signin.js";
38
+ import { paint, renderErrorBlock, wrap } from "../term.js";
39
+ import { writeLines } from "./context.js";
40
+ /**
41
+ * The browser sign-in beat — `CR-084d`.
42
+ *
43
+ * Opt-in inside `connect` (`--sign-in`) for the read lane, and the first half of
44
+ * `browserCredential` below. When a session already exists it is a no-op that
45
+ * says so; it never replaces a working session.
46
+ */
47
+ export async function signInBeat(ctx, options) {
48
+ if (options.replace !== true && loadSession(ctx.home).kind === "ok") {
49
+ writeLines(ctx.stdout, wrap(SIGNIN.alreadySignedIn, 2));
50
+ return EXIT.ok;
51
+ }
52
+ const endpoint = mcpUrl(ctx.env);
53
+ if (endpoint === null) {
54
+ writeLines(ctx.stderr, signInError(ctx, options.retry, SIGNIN.noServerWhat, SIGNIN.noServerRefusedWhy));
55
+ return EXIT.failure;
56
+ }
57
+ const outcome = await signIn({
58
+ home: ctx.home,
59
+ mcpEndpoint: endpoint,
60
+ fetch: options.fetch ?? fetch,
61
+ nowMs: () => ctx.now().getTime(),
62
+ openBrowser: options.openBrowser ?? platformOpenBrowser,
63
+ onAuthorizeUrl: (url, opened) => {
64
+ writeLines(ctx.stdout, [
65
+ "",
66
+ ...wrap(opened ? SIGNIN.opening : SIGNIN.manualLabel, 2),
67
+ // `accent` on a URL is §13.1's one exception, and the line carries no
68
+ // second colour. Printed on BOTH paths: a user who wants to open it in a
69
+ // different browser than the default should not have to guess it.
70
+ ` ${paint(ctx.colour, "accent", url)}`,
71
+ ...(opened ? wrap(SIGNIN.waiting, 2) : []),
72
+ ]);
73
+ },
74
+ page: { done: SIGNIN.browserDone, refused: SIGNIN.browserRefused },
75
+ });
76
+ if (outcome.kind === "ok") {
77
+ writeLines(ctx.stdout, ["", ...wrap(SIGNIN.done, 2)]);
78
+ return EXIT.ok;
79
+ }
80
+ writeLines(ctx.stderr, ["", ...signInFailure(ctx, options.retry, outcome)]);
81
+ return EXIT.failure;
82
+ }
83
+ /** §13.6's shape for a sign-in failure. One renderer, six outcomes. */
84
+ function signInFailure(ctx, retry, outcome) {
85
+ switch (outcome.kind) {
86
+ case "no-server":
87
+ return signInError(ctx, retry, SIGNIN.noServerWhat, outcome.detail === "unreachable"
88
+ ? SIGNIN.noServerUnreachableWhy
89
+ : SIGNIN.noServerMalformedWhy, SIGNIN.noServerFix);
90
+ case "denied":
91
+ return signInError(ctx, retry, SIGNIN.deniedWhat, SIGNIN.deniedWhy, SIGNIN.deniedFix);
92
+ case "timeout":
93
+ return signInError(ctx, retry, SIGNIN.timeoutWhat, SIGNIN.timeoutWhy, SIGNIN.timeoutFix);
94
+ case "rejected":
95
+ return signInError(ctx, retry, SIGNIN.rejectedWhat, SIGNIN.rejectedWhy, SIGNIN.rejectedFix);
96
+ case "unreachable":
97
+ return signInError(ctx, retry, SIGNIN.unreachableWhat, SIGNIN.unreachableWhy, SIGNIN.unreachableFix);
98
+ case "malformed":
99
+ return signInError(ctx, retry, SIGNIN.malformedWhat, SIGNIN.malformedWhy, SIGNIN.malformedFix);
100
+ }
101
+ }
102
+ function signInError(ctx, retry, what, why, fixLabel = SIGNIN.noServerFix) {
103
+ return renderErrorBlock({
104
+ kind: "bad",
105
+ what,
106
+ why: [why],
107
+ fixLabel,
108
+ fixes: [
109
+ retry === "auth" ? COMMANDS.auth : retry === "connect" ? COMMANDS.connect : COMMANDS.signIn,
110
+ ],
111
+ }, ctx.colour);
112
+ }
113
+ /**
114
+ * Sign in if needed, mint this machine's credential, save it — `CR-226`.
115
+ *
116
+ * Prints its own progress and its own failure; prints NOTHING on success,
117
+ * because what comes next differs by caller (`auth` ends, `connect` goes on to
118
+ * capture) and a confirmation here would be a second one.
119
+ */
120
+ export async function browserCredential(ctx, retry, deps = {}) {
121
+ // Resolved ONCE and handed to both halves, so the session and the credential
122
+ // cannot come from two different servers.
123
+ const endpoint = mcpUrl(ctx.env);
124
+ if (endpoint === null) {
125
+ writeLines(ctx.stderr, signInError(ctx, retry, SIGNIN.noServerWhat, SIGNIN.noServerRefusedWhy));
126
+ return { kind: "failed" };
127
+ }
128
+ if ((await signInBeat(ctx, { ...deps, retry })) !== EXIT.ok)
129
+ return { kind: "failed" };
130
+ const label = machineLabel((deps.hostname ?? osHostname)());
131
+ writeLines(ctx.stdout, ["", ...wrap(AUTH.minting(label), 2)]);
132
+ const fetchImpl = deps.fetch ?? fetch;
133
+ const mint = () => mintWithSession(endpoint, label, {
134
+ home: ctx.home,
135
+ fetch: fetchImpl,
136
+ nowMs: () => ctx.now().getTime(),
137
+ mint: { fetch: fetchImpl, timeoutMs: HTTP_TIMEOUT_MS },
138
+ });
139
+ let outcome = await mint();
140
+ // ⚠ A SESSION THE SERVER WILL NOT RENEW IS OVER, and a stored file that loads
141
+ // is not the same as a session that works. `signInBeat` above declines to
142
+ // replace any session that LOADS — correct for the read lane, and a dead end
143
+ // here: the user would be told to sign in by a verb that then says "already
144
+ // signed in". So an `expired` (the refresh grant was refused) gets exactly one
145
+ // fresh sign-in and one more mint. Nothing else does: a `busy` lock or an
146
+ // unreachable server is not fixed by a browser.
147
+ if (outcome.kind === "not-authorized" && outcome.authorized.kind === "expired") {
148
+ if ((await signInBeat(ctx, { ...deps, retry, replace: true })) !== EXIT.ok) {
149
+ return { kind: "failed" };
150
+ }
151
+ outcome = await mint();
152
+ }
153
+ if (outcome.kind === "ok") {
154
+ // Through the CR-216 writer, so the 0700/0600 double-chmod and the
155
+ // wrong-class refusal apply exactly as they do to a pasted credential.
156
+ saveCredential(ctx.home, outcome.credential);
157
+ return { kind: "saved", credential: outcome.credential };
158
+ }
159
+ writeLines(ctx.stderr, ["", ...renderErrorBlock({ kind: "bad", what: AUTH.mintFailedWhat, why: [mintWhy(outcome)] }, ctx.colour)]);
160
+ return { kind: "failed" };
161
+ }
162
+ /** One `why` per way the mint can fail. No status codes on screen (§13.6). */
163
+ function mintWhy(outcome) {
164
+ switch (outcome.kind) {
165
+ case "not-authorized":
166
+ case "unauthorized":
167
+ case "wrong-client":
168
+ return AUTH.mintSessionWhy;
169
+ case "refused":
170
+ if (outcome.error === "not_a_member")
171
+ return AUTH.mintNotMemberWhy;
172
+ if (outcome.error === "rate_limited")
173
+ return AUTH.mintRateLimitedWhy;
174
+ return AUTH.mintRefusedWhy;
175
+ case "unreachable":
176
+ return AUTH.mintUnreachableWhy;
177
+ case "malformed":
178
+ return AUTH.mintMalformedWhy;
179
+ }
180
+ }
181
+ //# sourceMappingURL=browser_credential.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"browser_credential.js","sourceRoot":"","sources":["../../src/commands/browser_credential.ts"],"names":[],"mappings":"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"}
@@ -12,17 +12,22 @@
12
12
  * - `CR-025` owns the ending: capture the current session and print the live
13
13
  * `/app/commits/<sha>` URL. The seam is marked below.
14
14
  *
15
- * `CR-084d` lands the browser sign-in beat (PKCE loopback → a user-principal
16
- * token for the READ lane) behind `--sign-in`. It is opt-in and not part of the
17
- * default install for one reason: **sign-in does not produce an ingest
18
- * credential**, so it cannot replace the beat below. No route in
19
- * `vibecommit-mcp` mints a `vcik_` — `mintIngestCredential` exists and has no
20
- * call site outside that repo's own tests — so `VIBECOMMIT_TOKEN` remains the
21
- * supported path for capture, exactly as before.
15
+ * `CR-084d` landed the browser sign-in beat (PKCE loopback → a user-principal
16
+ * token for the READ lane) behind `--sign-in`, opt-in because sign-in did not
17
+ * produce an ingest credential.
22
18
  *
23
- * @provenance vibecommit-mcp src/oauth/ingest_credential.ts — no route mints one, verified
19
+ * ⚠ `CR-226` CHANGED THAT, AND THIS NOTE USED TO SAY THE OPPOSITE. The server
20
+ * now has a route in front of `mintIngestCredential`, so a fresh machine that
21
+ * reaches the credential beat with a human at the terminal signs in through the
22
+ * browser and mints its own credential there (`browser_credential.ts`) — browser
23
+ * FIRST, with the old warning pointing at `vibecommit auth` as the fallback when
24
+ * any step fails. `--sign-in` keeps its meaning (sign in for the read lane even
25
+ * when a credential exists), and `VIBECOMMIT_TOKEN` still wins over everything.
26
+ *
27
+ * @provenance vibecommit-mcp src/transport/server.ts — POST /oauth/ingest-credential now mints one, verified
24
28
  */
25
29
  import { type CommandContext } from "./context.js";
30
+ import { type BrowserCredential, type Retry } from "./browser_credential.js";
26
31
  /**
27
32
  * What `connect` was asked to do beyond the default.
28
33
  *
@@ -33,4 +38,16 @@ export interface ConnectOptions {
33
38
  /** `--sign-in`. Off by default — see the module note for why. */
34
39
  readonly signIn?: boolean;
35
40
  }
36
- export declare function connect(ctx: CommandContext, options?: ConnectOptions): Promise<number>;
41
+ /**
42
+ * The browser beat, INJECTED — `CR-226`. `src/index.ts` passes the real
43
+ * `browserCredential`; left out, the credential beat behaves exactly as it did
44
+ * before the route existed and never opens a browser. That default is the same
45
+ * one `auth` takes for its readers, and for the same reason: a suite that
46
+ * forgot to stub it must not reach a real server or launch a real browser.
47
+ * `test/cli-wiring.test.ts` asserts the boot path supplies it, so "forgot" is
48
+ * caught where it matters.
49
+ */
50
+ export interface ConnectDeps {
51
+ readonly browser?: (ctx: CommandContext, retry: Retry) => Promise<BrowserCredential>;
52
+ }
53
+ export declare function connect(ctx: CommandContext, options?: ConnectOptions, deps?: ConnectDeps): Promise<number>;
@@ -12,17 +12,21 @@
12
12
  * - `CR-025` owns the ending: capture the current session and print the live
13
13
  * `/app/commits/<sha>` URL. The seam is marked below.
14
14
  *
15
- * `CR-084d` lands the browser sign-in beat (PKCE loopback → a user-principal
16
- * token for the READ lane) behind `--sign-in`. It is opt-in and not part of the
17
- * default install for one reason: **sign-in does not produce an ingest
18
- * credential**, so it cannot replace the beat below. No route in
19
- * `vibecommit-mcp` mints a `vcik_` — `mintIngestCredential` exists and has no
20
- * call site outside that repo's own tests — so `VIBECOMMIT_TOKEN` remains the
21
- * supported path for capture, exactly as before.
15
+ * `CR-084d` landed the browser sign-in beat (PKCE loopback → a user-principal
16
+ * token for the READ lane) behind `--sign-in`, opt-in because sign-in did not
17
+ * produce an ingest credential.
22
18
  *
23
- * @provenance vibecommit-mcp src/oauth/ingest_credential.ts — no route mints one, verified
19
+ * ⚠ `CR-226` CHANGED THAT, AND THIS NOTE USED TO SAY THE OPPOSITE. The server
20
+ * now has a route in front of `mintIngestCredential`, so a fresh machine that
21
+ * reaches the credential beat with a human at the terminal signs in through the
22
+ * browser and mints its own credential there (`browser_credential.ts`) — browser
23
+ * FIRST, with the old warning pointing at `vibecommit auth` as the fallback when
24
+ * any step fails. `--sign-in` keeps its meaning (sign in for the read lane even
25
+ * when a credential exists), and `VIBECOMMIT_TOKEN` still wins over everything.
26
+ *
27
+ * @provenance vibecommit-mcp src/transport/server.ts — POST /oauth/ingest-credential now mints one, verified
24
28
  */
25
- import { CONNECT, COMMANDS, AGENT_HOOKS, COMMIT_HOOK, ERRORS, HELP, PATH_CLASH, RUNTIME, SIGNIN, URLS, } from "../copy/index.js";
29
+ import { AUTH, CONNECT, COMMANDS, AGENT_HOOKS, COMMIT_HOOK, ERRORS, HELP, PATH_CLASH, RUNTIME, URLS, } from "../copy/index.js";
26
30
  import { dialectFor } from "../agents/registry.js";
27
31
  import { grantProject, isAffirmative, isProjectAllowed } from "../consent.js";
28
32
  import { loadCredential } from "../credential.js";
@@ -31,15 +35,14 @@ import { resolveRepoSlug } from "../git.js";
31
35
  import { confinementRoots, readSpan, DEFAULT_HOOK_BUDGET_MS } from "../hooks/entry.js";
32
36
  import { installAgentHooks } from "../agent_install.js";
33
37
  import { classifyInstall, installPostCommitHook } from "../install.js";
34
- import { mcpUrl } from "../oauth/discovery.js";
35
- import { loadSession } from "../oauth/session.js";
36
- import { openBrowser, signIn } from "../oauth/signin.js";
37
38
  import { CAPTURE_NOT_APPROVED, deliver, ingestUrl, } from "../post.js";
38
39
  import { resolveProjectKeys } from "../project.js";
39
40
  import { meetsNodeFloor, NODE_FLOOR_TEXT } from "../runtime.js";
40
- import { paint, renderErrorBlock, truncatePath, wrap } from "../term.js";
41
+ import { credentialsPath } from "../paths.js";
42
+ import { paint, renderErrorBlock, tildePath, truncatePath, wrap } from "../term.js";
41
43
  import { writeLines } from "./context.js";
42
- export async function connect(ctx, options = {}) {
44
+ import { signInBeat } from "./browser_credential.js";
45
+ export async function connect(ctx, options = {}, deps = {}) {
43
46
  // (1) The runtime floor refuses LOUDLY here, unlike in the hook: the user is
44
47
  // watching and can act, and D57 §DX11 asks for exactly that asymmetry.
45
48
  if (!meetsNodeFloor(ctx.nodeVersion)) {
@@ -72,7 +75,7 @@ export async function connect(ctx, options = {}) {
72
75
  }
73
76
  if (isProjectAllowed(ctx.home, keys.consent)) {
74
77
  writeLines(ctx.stdout, wrap(CONNECT.alreadyConnected, 2));
75
- return await afterConsent(ctx, keys, options);
78
+ return await afterConsent(ctx, keys, options, deps);
76
79
  }
77
80
  // (3) The disclosure, then the gate.
78
81
  writeLines(ctx.stdout, disclosure(ctx));
@@ -97,7 +100,7 @@ export async function connect(ctx, options = {}) {
97
100
  return EXIT.ok;
98
101
  }
99
102
  grantProject(ctx.home, keys.consent, ctx.now());
100
- return await afterConsent(ctx, keys, options);
103
+ return await afterConsent(ctx, keys, options, deps);
101
104
  }
102
105
  /**
103
106
  * Consent is settled. Sign in if asked, then do the credential beat.
@@ -108,81 +111,13 @@ export async function connect(ctx, options = {}) {
108
111
  * A failure in one says nothing about the other — which is why a failed sign-in
109
112
  * returns here rather than falling through to report the install as fine.
110
113
  */
111
- async function afterConsent(ctx, keys, options) {
114
+ async function afterConsent(ctx, keys, options, deps) {
112
115
  if (options.signIn === true) {
113
- const code = await signInBeat(ctx);
116
+ const code = await signInBeat(ctx, { retry: "sign-in" });
114
117
  if (code !== EXIT.ok)
115
118
  return code;
116
119
  }
117
- return await credentialBeat(ctx, keys);
118
- }
119
- /**
120
- * The browser sign-in beat — `CR-084d`.
121
- *
122
- * Reachable only behind `--sign-in`. That is deliberate and not timidity: an
123
- * unconditional beat here would open a browser during every `connect`, including
124
- * the ones run by a wrapper, and sign-in cannot mint the credential the rest of
125
- * `connect` is about. When a read verb lands (`why` — `CR-086`; `report` —
126
- * `CR-108`) it calls `readWithSession`, which drives the same session and needs
127
- * no beat here at all.
128
- */
129
- async function signInBeat(ctx) {
130
- if (loadSession(ctx.home).kind === "ok") {
131
- writeLines(ctx.stdout, wrap(SIGNIN.alreadySignedIn, 2));
132
- return EXIT.ok;
133
- }
134
- const endpoint = mcpUrl(ctx.env);
135
- if (endpoint === null) {
136
- writeLines(ctx.stderr, signInError(ctx, SIGNIN.noServerWhat, SIGNIN.noServerRefusedWhy));
137
- return EXIT.failure;
138
- }
139
- const outcome = await signIn({
140
- home: ctx.home,
141
- mcpEndpoint: endpoint,
142
- fetch,
143
- nowMs: () => ctx.now().getTime(),
144
- openBrowser,
145
- onAuthorizeUrl: (url, opened) => {
146
- writeLines(ctx.stdout, [
147
- "",
148
- ...wrap(opened ? SIGNIN.opening : SIGNIN.manualLabel, 2),
149
- // `accent` on a URL is §13.1's one exception, and the line carries no
150
- // second colour. Printed on BOTH paths: a user who wants to open it in a
151
- // different browser than the default should not have to guess it.
152
- ` ${paint(ctx.colour, "accent", url)}`,
153
- ...(opened ? wrap(SIGNIN.waiting, 2) : []),
154
- ]);
155
- },
156
- page: { done: SIGNIN.browserDone, refused: SIGNIN.browserRefused },
157
- });
158
- if (outcome.kind === "ok") {
159
- writeLines(ctx.stdout, ["", ...wrap(SIGNIN.done, 2)]);
160
- return EXIT.ok;
161
- }
162
- writeLines(ctx.stderr, ["", ...signInFailure(ctx, outcome)]);
163
- return EXIT.failure;
164
- }
165
- /** §13.6's shape for a sign-in failure. One renderer, six outcomes. */
166
- function signInFailure(ctx, outcome) {
167
- switch (outcome.kind) {
168
- case "no-server":
169
- return signInError(ctx, SIGNIN.noServerWhat, outcome.detail === "unreachable"
170
- ? SIGNIN.noServerUnreachableWhy
171
- : SIGNIN.noServerMalformedWhy, SIGNIN.noServerFix);
172
- case "denied":
173
- return signInError(ctx, SIGNIN.deniedWhat, SIGNIN.deniedWhy, SIGNIN.deniedFix);
174
- case "timeout":
175
- return signInError(ctx, SIGNIN.timeoutWhat, SIGNIN.timeoutWhy, SIGNIN.timeoutFix);
176
- case "rejected":
177
- return signInError(ctx, SIGNIN.rejectedWhat, SIGNIN.rejectedWhy, SIGNIN.rejectedFix);
178
- case "unreachable":
179
- return signInError(ctx, SIGNIN.unreachableWhat, SIGNIN.unreachableWhy, SIGNIN.unreachableFix);
180
- case "malformed":
181
- return signInError(ctx, SIGNIN.malformedWhat, SIGNIN.malformedWhy, SIGNIN.malformedFix);
182
- }
183
- }
184
- function signInError(ctx, what, why, fixLabel = SIGNIN.noServerFix) {
185
- return renderErrorBlock({ kind: "bad", what, why: [why], fixLabel, fixes: [COMMANDS.signIn] }, ctx.colour);
120
+ return await credentialBeat(ctx, keys, deps);
186
121
  }
187
122
  /**
188
123
  * The disclosure block — `CR-111d`, style guide §10.2 transcribed.
@@ -242,11 +177,33 @@ function disclosure(ctx) {
242
177
  * name, and on npm >= 7 the clash surfaces as EEXIST during the first install of
243
178
  * exactly the population that installs commit tooling.
244
179
  */
245
- async function credentialBeat(ctx, keys) {
180
+ async function credentialBeat(ctx, keys, deps) {
246
181
  const load = loadCredential({ env: ctx.env, home: ctx.home });
247
182
  if (load.kind === "ok") {
248
183
  return await captureBeat(ctx, keys, load.credential);
249
184
  }
185
+ // ⛔ ONLY FOR `absent`, AND ONLY WITH A HUMAN AT THE TERMINAL (`CR-226`).
186
+ // - `absent`: a credential that is present but wrong-class, unreadable or
187
+ // insecure is a FAULT, and a fault's fix is `vibecommit auth` (D206 §5,
188
+ // `credential-fix-copy.test.ts`). Minting over it would hide the fault.
189
+ // - `stdinIsTty`: a wrapper or a CI job runs `connect` too, and a browser
190
+ // opened there waits five minutes for nobody. The old warning is the
191
+ // right answer for them, and it is what they still get.
192
+ if (load.kind === "absent" && ctx.stdinIsTty && deps.browser !== undefined) {
193
+ const minted = await deps.browser(ctx, "connect");
194
+ if (minted.kind === "saved") {
195
+ writeLines(ctx.stdout, [
196
+ "",
197
+ ...renderErrorBlock({
198
+ kind: "ok",
199
+ what: AUTH.savedWhat,
200
+ why: [AUTH.savedWhy(tildePath(credentialsPath(ctx.home), ctx.home))],
201
+ }, ctx.colour),
202
+ ]);
203
+ return await captureBeat(ctx, keys, minted.credential);
204
+ }
205
+ // Failed: the reason is already on stderr. Fall through to the paste path.
206
+ }
250
207
  writeLines(ctx.stderr, renderErrorBlock({
251
208
  kind: "warn",
252
209
  what: CONNECT.credentialNeededWhat,
@@ -267,8 +224,10 @@ async function credentialBeat(ctx, keys) {
267
224
  * the client may render but may not analyze (D60 §D6). It is not obtainable
268
225
  * from the server either: `send()` returns a status and discards the body,
269
226
  * so no id, sha or count exists on this side of the wire.
270
- * - **`Opening your browser to sign in...`.** That beat is the oauth lane's.
271
- * Until PKCE lands, `VIBECOMMIT_TOKEN` is the supported path.
227
+ * - **`Opening your browser to sign in...`.** That beat runs BEFORE this one
228
+ * now (`credentialBeat` → `browser_credential.ts`, `CR-226`), and only when
229
+ * there was no credential to start with — by the time this function runs,
230
+ * the credential exists however it arrived.
272
231
  */
273
232
  async function captureBeat(ctx, keys, credential) {
274
233
  // ⚠ EVERY USE BELOW BUT ONE IS THE TOPLEVEL ROLE — locating the transcript,