@vibe-commit/capture 0.2.3 → 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.
- package/dist/agent_hooks.d.ts +53 -8
- package/dist/agent_hooks.js +180 -19
- package/dist/agent_hooks.js.map +1 -1
- package/dist/agent_install.d.ts +0 -17
- package/dist/agent_install.js +1 -24
- package/dist/agent_install.js.map +1 -1
- package/dist/agents/cursor.js +54 -18
- package/dist/agents/cursor.js.map +1 -1
- package/dist/commands/auth.d.ts +24 -9
- package/dist/commands/auth.js +38 -10
- package/dist/commands/auth.js.map +1 -1
- package/dist/commands/browser_credential.d.ts +89 -0
- package/dist/commands/browser_credential.js +181 -0
- package/dist/commands/browser_credential.js.map +1 -0
- package/dist/commands/connect.d.ts +26 -9
- package/dist/commands/connect.js +49 -90
- package/dist/commands/connect.js.map +1 -1
- package/dist/commands/status.js +35 -2
- package/dist/commands/status.js.map +1 -1
- package/dist/copy/strings.d.ts +42 -2
- package/dist/copy/strings.js +52 -1
- package/dist/copy/strings.js.map +1 -1
- package/dist/hooks/entry.js +106 -24
- package/dist/hooks/entry.js.map +1 -1
- package/dist/index.js +9 -1
- package/dist/index.js.map +1 -1
- package/dist/oauth/mint.d.ts +97 -0
- package/dist/oauth/mint.js +120 -0
- package/dist/oauth/mint.js.map +1 -0
- package/dist/oauth/signin.d.ts +25 -0
- package/dist/oauth/signin.js +30 -0
- package/dist/oauth/signin.js.map +1 -1
- package/dist/post.d.ts +21 -0
- package/dist/post.js +30 -3
- package/dist/post.js.map +1 -1
- package/dist/spool.d.ts +20 -0
- package/dist/spool.js +37 -0
- package/dist/spool.js.map +1 -1
- package/dist/state.d.ts +77 -0
- package/dist/state.js +111 -2
- package/dist/state.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
package/dist/commands/auth.js
CHANGED
|
@@ -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
|
-
* ##
|
|
39
|
+
* ## ⚠ Browser FIRST since `CR-226`; the paste is the fallback, not gone
|
|
40
40
|
*
|
|
41
|
-
*
|
|
42
|
-
* `vibecommit-mcp`
|
|
43
|
-
* plaintext
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
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
|
-
*
|
|
49
|
-
*
|
|
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":"AAAA
|
|
1
|
+
{"version":3,"file":"auth.js","sourceRoot":"","sources":["../../src/commands/auth.ts"],"names":[],"mappings":"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"}
|
|
@@ -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`
|
|
16
|
-
* token for the READ lane) behind `--sign-in
|
|
17
|
-
*
|
|
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
|
-
*
|
|
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
|
-
|
|
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>;
|
package/dist/commands/connect.js
CHANGED
|
@@ -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`
|
|
16
|
-
* token for the READ lane) behind `--sign-in
|
|
17
|
-
*
|
|
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
|
-
*
|
|
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,
|
|
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 {
|
|
41
|
+
import { credentialsPath } from "../paths.js";
|
|
42
|
+
import { paint, renderErrorBlock, tildePath, truncatePath, wrap } from "../term.js";
|
|
41
43
|
import { writeLines } from "./context.js";
|
|
42
|
-
|
|
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
|
|
271
|
-
*
|
|
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,
|