@vibe-commit/capture 0.7.0 → 0.8.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 (85) hide show
  1. package/README.md +114 -7
  2. package/dist/account_not_here.d.ts +15 -0
  3. package/dist/account_not_here.js +25 -0
  4. package/dist/account_not_here.js.map +1 -0
  5. package/dist/commands/auth.d.ts +7 -0
  6. package/dist/commands/auth.js +21 -0
  7. package/dist/commands/auth.js.map +1 -1
  8. package/dist/commands/browser_credential.d.ts +4 -2
  9. package/dist/commands/browser_credential.js +5 -2
  10. package/dist/commands/browser_credential.js.map +1 -1
  11. package/dist/commands/connect.d.ts +9 -0
  12. package/dist/commands/connect.js +13 -8
  13. package/dist/commands/connect.js.map +1 -1
  14. package/dist/commands/connect_account.d.ts +135 -0
  15. package/dist/commands/connect_account.js +321 -0
  16. package/dist/commands/connect_account.js.map +1 -0
  17. package/dist/commands/connect_picker.js +160 -26
  18. package/dist/commands/connect_picker.js.map +1 -1
  19. package/dist/commands/device_credential.d.ts +3 -1
  20. package/dist/commands/device_credential.js +1 -0
  21. package/dist/commands/device_credential.js.map +1 -1
  22. package/dist/commands/finish_mint.d.ts +28 -0
  23. package/dist/commands/finish_mint.js +16 -4
  24. package/dist/commands/finish_mint.js.map +1 -1
  25. package/dist/commands/status.d.ts +19 -1
  26. package/dist/commands/status.js +123 -14
  27. package/dist/commands/status.js.map +1 -1
  28. package/dist/commands/status_machine.d.ts +12 -3
  29. package/dist/commands/status_machine.js +64 -3
  30. package/dist/commands/status_machine.js.map +1 -1
  31. package/dist/commands/sync_reason.d.ts +17 -0
  32. package/dist/commands/sync_reason.js +25 -0
  33. package/dist/commands/sync_reason.js.map +1 -0
  34. package/dist/consent.d.ts +109 -3
  35. package/dist/consent.js +210 -13
  36. package/dist/consent.js.map +1 -1
  37. package/dist/copy/strings.d.ts +122 -3
  38. package/dist/copy/strings.js +142 -3
  39. package/dist/copy/strings.js.map +1 -1
  40. package/dist/grants_sync.d.ts +165 -0
  41. package/dist/grants_sync.js +289 -0
  42. package/dist/grants_sync.js.map +1 -0
  43. package/dist/grants_verb.d.ts +70 -0
  44. package/dist/grants_verb.js +121 -0
  45. package/dist/grants_verb.js.map +1 -0
  46. package/dist/hooks/entry.js +8 -2
  47. package/dist/hooks/entry.js.map +1 -1
  48. package/dist/hooks/session_grants.d.ts +59 -0
  49. package/dist/hooks/session_grants.js +91 -0
  50. package/dist/hooks/session_grants.js.map +1 -0
  51. package/dist/hooks/session_start.d.ts +28 -7
  52. package/dist/hooks/session_start.js +55 -11
  53. package/dist/hooks/session_start.js.map +1 -1
  54. package/dist/index.js +22 -5
  55. package/dist/index.js.map +1 -1
  56. package/dist/oauth/retire.d.ts +10 -0
  57. package/dist/oauth/retire.js +13 -0
  58. package/dist/oauth/retire.js.map +1 -1
  59. package/dist/paths.d.ts +20 -0
  60. package/dist/paths.js +26 -0
  61. package/dist/paths.js.map +1 -1
  62. package/dist/pending_revoke.d.ts +117 -0
  63. package/dist/pending_revoke.js +233 -0
  64. package/dist/pending_revoke.js.map +1 -0
  65. package/dist/pending_revoke_file.d.ts +55 -0
  66. package/dist/pending_revoke_file.js +74 -0
  67. package/dist/pending_revoke_file.js.map +1 -0
  68. package/dist/picker.d.ts +30 -2
  69. package/dist/picker.js +55 -6
  70. package/dist/picker.js.map +1 -1
  71. package/dist/picker_checklist.d.ts +19 -3
  72. package/dist/picker_checklist.js +40 -4
  73. package/dist/picker_checklist.js.map +1 -1
  74. package/dist/policy.d.ts +16 -1
  75. package/dist/policy.js +21 -2
  76. package/dist/policy.js.map +1 -1
  77. package/dist/post.d.ts +41 -2
  78. package/dist/post.js +40 -12
  79. package/dist/post.js.map +1 -1
  80. package/dist/system_message.d.ts +25 -2
  81. package/dist/system_message.js +29 -5
  82. package/dist/system_message.js.map +1 -1
  83. package/dist/version.d.ts +1 -1
  84. package/dist/version.js +1 -1
  85. package/package.json +2 -2
@@ -0,0 +1,233 @@
1
+ /**
2
+ * Pending revokes (slice 2, S4, R3-11): the WRITE-AHEAD record of a credential `g` has made and not yet been told to keep.
3
+ *
4
+ * ```
5
+ * addPendingRevoke(home, credential, endpoint, now) at the mint, BEFORE anything else: refuses (never overwrites) when it cannot
6
+ * removePendingRevoke(home, token) after the revoke, or after the credential is kept: re-reads, removes ONE row
7
+ * revokeHeld(home, token, fetch) one best-effort revoke (3 s) of a row, to ITS recorded origin
8
+ * retryPendingRevokes(home, env, fetch, now) every user-facing verb, once, at its start: one 3 s bound across all rows
9
+ * readPendingRevokes(home) how many are waiting (for `status`'s `pending` count)
10
+ * ```
11
+ *
12
+ * ## ⛔ WHY IT IS WRITE-AHEAD
13
+ *
14
+ * `g` is the one place a credential is made ahead of consent (exception 3). So the credential lives in MEMORY until the yes:
15
+ * `credentials.json` does not exist before it, and the row below is written the instant the server hands the token over. A kill, a
16
+ * second Ctrl+C, a SIGHUP at the y/N or an unexpected throw therefore leaves a ROW (which the next verb revokes), never a live
17
+ * credential on disk that nothing remembers. The yes commits in this order: the grants, then `credentials.json`, then the row's
18
+ * deletion; a retry skips and drops any row whose token IS the current credential, so a kill between the last two never revokes what
19
+ * the user kept.
20
+ *
21
+ * ## ⛔ THE FILE HOLDS A BEARER TOKEN — the same exposure class as `credentials.json`, held to the same rules
22
+ * - written mode 0600 under the 0700 directory, temp + rename;
23
+ * - READ through `readSecretFile`: a file or folder others can read is refused, and an insecure or unreadable file is NEVER
24
+ * overwritten (the writer refuses, and `g` fails closed);
25
+ * - never printed, logged or returned: `status` counts the rows and shows nothing else, `--json` included;
26
+ * - a hook never touches it (only the CLI verbs import this module; `test/connect-account.test.ts` walks the hooks' import graph).
27
+ *
28
+ * ## ⛔ A ROW IS NEVER DROPPED UNREVOKED, except by the clock
29
+ * - it leaves on a 2xx, a 401 (the server does not accept it: already dead) or a 404 (a server that cannot revoke);
30
+ * - or at `minted_at` + 91 days: a token that was never on disk was never used, and an ingest credential ends 90 days unused;
31
+ * - there is no row cap that drops one: at the cap `addPendingRevoke` REFUSES, and `g` fails closed.
32
+ *
33
+ * ## ⛔ THE ORIGIN IS THE ROW'S
34
+ *
35
+ * A row records the issuing server's origin and a retry goes ONLY there (https, or http on loopback: `isAllowedIngestUrl`), never to
36
+ * wherever `VIBECOMMIT_MCP_URL` points today.
37
+ */
38
+ import { chmodSync, mkdirSync, readFileSync, unlinkSync } from "node:fs";
39
+ import { writeFileAtomic } from "./atomic_write.js";
40
+ import { IngestCredential, INGEST_TOKEN_PREFIX } from "./credential.js";
41
+ import { LEASE_MS, leased, readPendingRevokes, readQueue } from "./pending_revoke_file.js";
42
+ import { REVOKE_TIMEOUT_MS, selfRevokeOutcome } from "./oauth/retire.js";
43
+ import { credentialsPath, pendingRevokePath, rootDir } from "./paths.js";
44
+ import { isAllowedIngestUrl } from "./post.js";
45
+ export { readPendingRevokes } from "./pending_revoke_file.js";
46
+ /** More rows than `g` can produce in earnest: past it `g` refuses rather than drop one. */
47
+ export const MAX_ROWS = 8;
48
+ /** A token never used ends 90 days after it is made; one day of slack. */
49
+ const MAX_AGE_MS = 91 * 24 * 60 * 60 * 1000;
50
+ /** One best-effort revoke, and the bound on a verb's whole retry. */
51
+ export { REVOKE_TIMEOUT_MS };
52
+ function writeRows(home, rows) {
53
+ if (rows.length === 0) {
54
+ try {
55
+ unlinkSync(pendingRevokePath(home));
56
+ }
57
+ catch {
58
+ /* already gone */
59
+ }
60
+ return;
61
+ }
62
+ mkdirSync(rootDir(home), { recursive: true, mode: 0o700 });
63
+ chmodSync(rootDir(home), 0o700);
64
+ writeFileAtomic(pendingRevokePath(home), `${JSON.stringify({ revokes: rows }, null, 2)}\n`, 0o600);
65
+ }
66
+ /**
67
+ * Write the row for a credential that has just been made. Never overwrites a file it cannot read, never drops a row.
68
+ * `lease: true` is `g` holding the credential at its y/N: the row carries `{ pid, lease_until }` (30 minutes) so another verb's retry
69
+ * leaves it alone while it is kept; every other writer (a test, an orphaned row) has no lease and is retried at once.
70
+ */
71
+ export function addPendingRevoke(home, credential, endpoint, now, options = {}) {
72
+ try {
73
+ const token = credential.expose();
74
+ if (!token.startsWith(INGEST_TOKEN_PREFIX))
75
+ return { kind: "failed" };
76
+ const queue = readQueue(home);
77
+ if (queue.kind === "refused")
78
+ return { kind: "refused", read: queue.read };
79
+ const rows = queue.kind === "ok" ? queue.rows : [];
80
+ if (rows.some((row) => row.token === token))
81
+ return { kind: "written" };
82
+ if (rows.length >= MAX_ROWS)
83
+ return { kind: "full" };
84
+ const lease = options.lease === true ? { pid: process.pid, lease_until: new Date(now.getTime() + LEASE_MS).toISOString() } : null;
85
+ writeRows(home, [...rows, { token, minted_at: now.toISOString(), origin: new URL(endpoint).origin, lease }]);
86
+ return { kind: "written" };
87
+ }
88
+ catch {
89
+ return { kind: "failed" };
90
+ }
91
+ }
92
+ /** Remove ONE row, re-reading just before the write so a row queued in between survives. Deletes the file when it is empty. Never throws. */
93
+ export function removePendingRevoke(home, token) {
94
+ removeRows(home, new Set([token]));
95
+ }
96
+ /** Give the row up to the retry: `g` is no longer holding the credential, so the next verb may revoke it. Re-reads before it writes. */
97
+ export function releaseLease(home, token) {
98
+ try {
99
+ const queue = readQueue(home);
100
+ if (queue.kind !== "ok" || !queue.rows.some((row) => row.token === token && row.lease !== null))
101
+ return;
102
+ writeRows(home, queue.rows.map((row) => (row.token === token ? { ...row, lease: null } : row)));
103
+ }
104
+ catch {
105
+ /* the lease runs out by itself */
106
+ }
107
+ }
108
+ /**
109
+ * Is the row for `token` still there? `g` asks just before it commits, and the three answers are different things:
110
+ * - `held`: the row is in a queue this client can use;
111
+ * - `gone`: the queue is readable and the row is not in it — another verb settled (revoked) it;
112
+ * - `unusable`: the queue is insecure or unreadable. Nobody revoked anything; this client can neither keep nor settle the record.
113
+ */
114
+ export function holdsRow(home, token) {
115
+ const queue = readQueue(home);
116
+ if (queue.kind === "refused")
117
+ return "unusable";
118
+ return queue.kind === "ok" && queue.rows.some((row) => row.token === token) ? "held" : "gone";
119
+ }
120
+ function removeRows(home, tokens) {
121
+ try {
122
+ const queue = readQueue(home);
123
+ if (queue.kind !== "ok")
124
+ return;
125
+ const rows = queue.rows.filter((row) => !tokens.has(row.token));
126
+ if (rows.length !== queue.rows.length)
127
+ writeRows(home, rows);
128
+ }
129
+ catch {
130
+ /* the row stays; the next verb retries */
131
+ }
132
+ }
133
+ /** Revoke a token, with itself as the bearer, at `origin` — and only where a credential may be sent at all. */
134
+ async function revokeToken(origin, token, fetchImpl, timeoutMs) {
135
+ if (!isAllowedIngestUrl(origin))
136
+ return null;
137
+ const outcome = await selfRevokeOutcome(origin, new IngestCredential(token, "file"), { fetch: fetchImpl, timeoutMs });
138
+ return outcome.kind === "retired" || outcome.kind === "gone" ? "revoked" : outcome.kind === "unknown" ? "unrevocable" : null;
139
+ }
140
+ /** The tokens this machine is using right now: `credentials.json`'s, read RAW (a mode problem does not hide it) and the environment's. */
141
+ function currentTokens(home, env) {
142
+ const out = new Set();
143
+ try {
144
+ const token = JSON.parse(readFileSync(credentialsPath(home), "utf8")).token;
145
+ if (typeof token === "string" && token !== "")
146
+ out.add(token);
147
+ }
148
+ catch {
149
+ /* no file, or not ours: nothing to protect */
150
+ }
151
+ const envToken = env.VIBECOMMIT_TOKEN?.trim();
152
+ if (envToken !== undefined && envToken !== "")
153
+ out.add(envToken);
154
+ return out;
155
+ }
156
+ /**
157
+ * `g`'s every no-grant exit: one revoke within 3 s. With a readable row it goes to the row's recorded origin and removes the row on a
158
+ * 2xx, 401 or 404 (a failure gives the lease up so the next verb retries). With NO usable row — missing, or the queue refused — it
159
+ * revokes with the in-memory credential at the MINTED origin: a missing row is never taken for "already revoked".
160
+ */
161
+ export async function revokeHeld(home, credential, mintedOrigin, fetchImpl) {
162
+ const token = credential.expose();
163
+ let insecure = null;
164
+ try {
165
+ const queue = readQueue(home);
166
+ const row = queue.kind === "ok" ? queue.rows.find((r) => r.token === token) : undefined;
167
+ if (queue.kind === "refused" && queue.read.kind === "insecure-file")
168
+ insecure = queue.read;
169
+ if (row !== undefined) {
170
+ const ended = await revokeToken(row.origin, token, fetchImpl, REVOKE_TIMEOUT_MS);
171
+ if (ended !== null) {
172
+ removePendingRevoke(home, token);
173
+ return { kind: ended, insecure };
174
+ }
175
+ releaseLease(home, token);
176
+ return { kind: "waiting", insecure };
177
+ }
178
+ const ended = await revokeToken(mintedOrigin, token, fetchImpl, REVOKE_TIMEOUT_MS);
179
+ return { kind: ended ?? "orphan", insecure };
180
+ }
181
+ catch {
182
+ return { kind: "orphan", insecure };
183
+ }
184
+ }
185
+ /**
186
+ * Every user-facing verb, once, at its start: one attempt at each row, and ONE 3 s bound across all of them (a row gets what is
187
+ * left of the bound, and none is attempted once it is spent). Silent, and needs no credential.
188
+ *
189
+ * Before EACH request it re-reads the queue and the current credential (a credential committed, or a row settled, after the loop
190
+ * began is never revoked on a stale view), and it
191
+ * - skips and DROPS a row whose token is the current credential (the user kept it; the kill came between the credential's write
192
+ * and the row's deletion) — compared on the file's raw token, so a mode problem does not hide a kept credential;
193
+ * - skips (and keeps) a LEASED row: `g` is holding it at its y/N in another process;
194
+ * - drops a row past `minted_at` + 91 days.
195
+ * Removes each row it settles as it goes, re-reading just before it writes. Returns how many rows still wait. Never throws.
196
+ */
197
+ export async function retryPendingRevokes(home, env, fetchImpl, now, deadlineMs = REVOKE_TIMEOUT_MS) {
198
+ try {
199
+ const first = readQueue(home);
200
+ if (first.kind !== "ok")
201
+ return 0;
202
+ const started = Date.now();
203
+ for (const listed of first.rows) {
204
+ const fresh = readQueue(home);
205
+ if (fresh.kind !== "ok")
206
+ break;
207
+ const row = fresh.rows.find((r) => r.token === listed.token);
208
+ if (row === undefined)
209
+ continue;
210
+ if (currentTokens(home, env).has(row.token)) {
211
+ removePendingRevoke(home, row.token);
212
+ continue;
213
+ }
214
+ if (leased(row, now))
215
+ continue;
216
+ const age = now.getTime() - Date.parse(row.minted_at);
217
+ if (Number.isFinite(age) && age > MAX_AGE_MS) {
218
+ removePendingRevoke(home, row.token);
219
+ continue;
220
+ }
221
+ const left = deadlineMs - (Date.now() - started);
222
+ if (left <= 0)
223
+ break;
224
+ if ((await revokeToken(row.origin, row.token, fetchImpl, left)) !== null)
225
+ removePendingRevoke(home, row.token);
226
+ }
227
+ return readPendingRevokes(home).length;
228
+ }
229
+ catch {
230
+ return readPendingRevokes(home).length;
231
+ }
232
+ }
233
+ //# sourceMappingURL=pending_revoke.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pending_revoke.js","sourceRoot":"","sources":["../src/pending_revoke.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoCG;AAEH,OAAO,EAAE,SAAS,EAAE,SAAS,EAAE,YAAY,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AACzE,OAAO,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC;AACpD,OAAO,EAAE,gBAAgB,EAAE,mBAAmB,EAAuB,MAAM,iBAAiB,CAAC;AAC7F,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAE,kBAAkB,EAAE,SAAS,EAAkC,MAAM,0BAA0B,CAAC;AAC3H,OAAO,EAAE,iBAAiB,EAAE,iBAAiB,EAAE,MAAM,mBAAmB,CAAC;AACzE,OAAO,EAAE,eAAe,EAAE,iBAAiB,EAAE,OAAO,EAAE,MAAM,YAAY,CAAC;AACzE,OAAO,EAAE,kBAAkB,EAAE,MAAM,WAAW,CAAC;AAE/C,OAAO,EAAE,kBAAkB,EAAsB,MAAM,0BAA0B,CAAC;AAElF,2FAA2F;AAC3F,MAAM,CAAC,MAAM,QAAQ,GAAG,CAAC,CAAC;AAC1B,0EAA0E;AAC1E,MAAM,UAAU,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,IAAI,CAAC;AAC5C,qEAAqE;AACrE,OAAO,EAAE,iBAAiB,EAAE,CAAC;AAE7B,SAAS,SAAS,CAAC,IAAY,EAAE,IAA8B;IAC7D,IAAI,IAAI,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACtB,IAAI,CAAC;YACH,UAAU,CAAC,iBAAiB,CAAC,IAAI,CAAC,CAAC,CAAC;QACtC,CAAC;QAAC,MAAM,CAAC;YACP,kBAAkB;QACpB,CAAC;QACD,OAAO;IACT,CAAC;IACD,SAAS,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC;IAC3D,SAAS,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,KAAK,CAAC,CAAC;IAChC,eAAe,CAAC,iBAAiB,CAAC,IAAI,CAAC,EAAE,GAAG,IAAI,CAAC,SAAS,CAAC,EAAE,OAAO,EAAE,IAAI,EAAE,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC;AACrG,CAAC;AAWD;;;;GAIG;AACH,MAAM,UAAU,gBAAgB,CAC9B,IAAY,EACZ,UAA4B,EAC5B,QAAgB,EAChB,GAAS,EACT,UAAwC,EAAE;IAE1C,IAAI,CAAC;QACH,MAAM,KAAK,GAAG,UAAU,CAAC,MAAM,EAAE,CAAC;QAClC,IAAI,CAAC,KAAK,CAAC,UAAU,CAAC,mBAAmB,CAAC;YAAE,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,CAAC;QACtE,MAAM,KAAK,GAAG,SAAS,CAAC,IAAI,CAAC,CAAC;QAC9B,IAAI,KAAK,CAAC,IAAI,KAAK,SAAS;YAAE,OAAO,EAAE,IAAI,EAAE,SAAS,EAAE,IAAI,EAAE,KAAK,CAAC,IAAI,EAAE,CAAC;QAC3E,MAAM,IAAI,GAAG,KAAK,CAAC,IAAI,KAAK,IAAI,CAAC,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC;QACnD,IAAI,IAAI,CAAC,IAAI,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,GAAG,CAAC,KAAK,KAAK,KAAK,CAAC;YAAE,OAAO,EAAE,IAAI,EAAE,SAAS,EAAE,CAAC;QACxE,IAAI,IAAI,CAAC,MAAM,IAAI,QAAQ;YAAE,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC;QACrD,MAAM,KAAK,GAAG,OAAO,CAAC,KAAK,KAAK,IAAI,CAAC,CAAC,CAAC,EAAE,GAAG,EAAE,OAAO,CAAC,GAAG,EAAE,WAAW,EAAE,IAAI,IAAI,CAAC,GAAG,CAAC,OAAO,EAAE,GAAG,QAAQ,CAAC,CAAC,WAAW,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC;QAClI,SAAS,CAAC,IAAI,EAAE,CAAC,GAAG,IAAI,EAAE,EAAE,KAAK,EAAE,SAAS,EAAE,GAAG,CAAC,WAAW,EAAE,EAAE,MAAM,EAAE,IAAI,GAAG,CAAC,QAAQ,CAAC,CAAC,MAAM,EAAE,KAAK,EAAE,CAAC,CAAC,CAAC;QAC7G,OAAO,EAAE,IAAI,EAAE,SAAS,EAAE,CAAC;IAC7B,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,CAAC;IAC5B,CAAC;AACH,CAAC;AAED,6IAA6I;AAC7I,MAAM,UAAU,mBAAmB,CAAC,IAAY,EAAE,KAAa;IAC7D,UAAU,CAAC,IAAI,EAAE,IAAI,GAAG,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;AACrC,CAAC;AAED,wIAAwI;AACxI,MAAM,UAAU,YAAY,CAAC,IAAY,EAAE,KAAa;IACtD,IAAI,CAAC;QACH,MAAM,KAAK,GAAG,SAAS,CAAC,IAAI,CAAC,CAAC;QAC9B,IAAI,KAAK,CAAC,IAAI,KAAK,IAAI,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,GAAG,CAAC,KAAK,KAAK,KAAK,IAAI,GAAG,CAAC,KAAK,KAAK,IAAI,CAAC;YAAE,OAAO;QACxG,SAAS,CAAC,IAAI,EAAE,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC,GAAG,CAAC,KAAK,KAAK,KAAK,CAAC,CAAC,CAAC,EAAE,GAAG,GAAG,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;IAClG,CAAC;IAAC,MAAM,CAAC;QACP,kCAAkC;IACpC,CAAC;AACH,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,QAAQ,CAAC,IAAY,EAAE,KAAa;IAClD,MAAM,KAAK,GAAG,SAAS,CAAC,IAAI,CAAC,CAAC;IAC9B,IAAI,KAAK,CAAC,IAAI,KAAK,SAAS;QAAE,OAAO,UAAU,CAAC;IAChD,OAAO,KAAK,CAAC,IAAI,KAAK,IAAI,IAAI,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,GAAG,CAAC,KAAK,KAAK,KAAK,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC;AAChG,CAAC;AAED,SAAS,UAAU,CAAC,IAAY,EAAE,MAA2B;IAC3D,IAAI,CAAC;QACH,MAAM,KAAK,GAAG,SAAS,CAAC,IAAI,CAAC,CAAC;QAC9B,IAAI,KAAK,CAAC,IAAI,KAAK,IAAI;YAAE,OAAO;QAChC,MAAM,IAAI,GAAG,KAAK,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,CAAC;QAChE,IAAI,IAAI,CAAC,MAAM,KAAK,KAAK,CAAC,IAAI,CAAC,MAAM;YAAE,SAAS,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC;IAC/D,CAAC;IAAC,MAAM,CAAC;QACP,0CAA0C;IAC5C,CAAC;AACH,CAAC;AAWD,+GAA+G;AAC/G,KAAK,UAAU,WAAW,CAAC,MAAc,EAAE,KAAa,EAAE,SAAuB,EAAE,SAAiB;IAClG,IAAI,CAAC,kBAAkB,CAAC,MAAM,CAAC;QAAE,OAAO,IAAI,CAAC;IAC7C,MAAM,OAAO,GAAG,MAAM,iBAAiB,CAAC,MAAM,EAAE,IAAI,gBAAgB,CAAC,KAAK,EAAE,MAAM,CAAC,EAAE,EAAE,KAAK,EAAE,SAAS,EAAE,SAAS,EAAE,CAAC,CAAC;IACtH,OAAO,OAAO,CAAC,IAAI,KAAK,SAAS,IAAI,OAAO,CAAC,IAAI,KAAK,MAAM,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,OAAO,CAAC,IAAI,KAAK,SAAS,CAAC,CAAC,CAAC,aAAa,CAAC,CAAC,CAAC,IAAI,CAAC;AAC/H,CAAC;AAED,0IAA0I;AAC1I,SAAS,aAAa,CAAC,IAAY,EAAE,GAAsB;IACzD,MAAM,GAAG,GAAG,IAAI,GAAG,EAAU,CAAC;IAC9B,IAAI,CAAC;QACH,MAAM,KAAK,GAAI,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,eAAe,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC,CAAyB,CAAC,KAAK,CAAC;QACrG,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,KAAK,EAAE;YAAE,GAAG,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC;IAChE,CAAC;IAAC,MAAM,CAAC;QACP,8CAA8C;IAChD,CAAC;IACD,MAAM,QAAQ,GAAG,GAAG,CAAC,gBAAgB,EAAE,IAAI,EAAE,CAAC;IAC9C,IAAI,QAAQ,KAAK,SAAS,IAAI,QAAQ,KAAK,EAAE;QAAE,GAAG,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;IACjE,OAAO,GAAG,CAAC;AACb,CAAC;AAiBD;;;;GAIG;AACH,MAAM,CAAC,KAAK,UAAU,UAAU,CAAC,IAAY,EAAE,UAA4B,EAAE,YAAoB,EAAE,SAAuB;IACxH,MAAM,KAAK,GAAG,UAAU,CAAC,MAAM,EAAE,CAAC;IAClC,IAAI,QAAQ,GAA2B,IAAI,CAAC;IAC5C,IAAI,CAAC;QACH,MAAM,KAAK,GAAG,SAAS,CAAC,IAAI,CAAC,CAAC;QAC9B,MAAM,GAAG,GAAG,KAAK,CAAC,IAAI,KAAK,IAAI,CAAC,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,KAAK,KAAK,KAAK,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC;QACxF,IAAI,KAAK,CAAC,IAAI,KAAK,SAAS,IAAI,KAAK,CAAC,IAAI,CAAC,IAAI,KAAK,eAAe;YAAE,QAAQ,GAAG,KAAK,CAAC,IAAI,CAAC;QAC3F,IAAI,GAAG,KAAK,SAAS,EAAE,CAAC;YACtB,MAAM,KAAK,GAAG,MAAM,WAAW,CAAC,GAAG,CAAC,MAAM,EAAE,KAAK,EAAE,SAAS,EAAE,iBAAiB,CAAC,CAAC;YACjF,IAAI,KAAK,KAAK,IAAI,EAAE,CAAC;gBACnB,mBAAmB,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC;gBACjC,OAAO,EAAE,IAAI,EAAE,KAAK,EAAE,QAAQ,EAAE,CAAC;YACnC,CAAC;YACD,YAAY,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC;YAC1B,OAAO,EAAE,IAAI,EAAE,SAAS,EAAE,QAAQ,EAAE,CAAC;QACvC,CAAC;QACD,MAAM,KAAK,GAAG,MAAM,WAAW,CAAC,YAAY,EAAE,KAAK,EAAE,SAAS,EAAE,iBAAiB,CAAC,CAAC;QACnF,OAAO,EAAE,IAAI,EAAE,KAAK,IAAI,QAAQ,EAAE,QAAQ,EAAE,CAAC;IAC/C,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,QAAQ,EAAE,CAAC;IACtC,CAAC;AACH,CAAC;AAED;;;;;;;;;;;GAWG;AACH,MAAM,CAAC,KAAK,UAAU,mBAAmB,CACvC,IAAY,EACZ,GAAsB,EACtB,SAAuB,EACvB,GAAS,EACT,UAAU,GAAG,iBAAiB;IAE9B,IAAI,CAAC;QACH,MAAM,KAAK,GAAG,SAAS,CAAC,IAAI,CAAC,CAAC;QAC9B,IAAI,KAAK,CAAC,IAAI,KAAK,IAAI;YAAE,OAAO,CAAC,CAAC;QAClC,MAAM,OAAO,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;QAC3B,KAAK,MAAM,MAAM,IAAI,KAAK,CAAC,IAAI,EAAE,CAAC;YAChC,MAAM,KAAK,GAAG,SAAS,CAAC,IAAI,CAAC,CAAC;YAC9B,IAAI,KAAK,CAAC,IAAI,KAAK,IAAI;gBAAE,MAAM;YAC/B,MAAM,GAAG,GAAG,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,KAAK,KAAK,MAAM,CAAC,KAAK,CAAC,CAAC;YAC7D,IAAI,GAAG,KAAK,SAAS;gBAAE,SAAS;YAChC,IAAI,aAAa,CAAC,IAAI,EAAE,GAAG,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC,KAAK,CAAC,EAAE,CAAC;gBAC5C,mBAAmB,CAAC,IAAI,EAAE,GAAG,CAAC,KAAK,CAAC,CAAC;gBACrC,SAAS;YACX,CAAC;YACD,IAAI,MAAM,CAAC,GAAG,EAAE,GAAG,CAAC;gBAAE,SAAS;YAC/B,MAAM,GAAG,GAAG,GAAG,CAAC,OAAO,EAAE,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC;YACtD,IAAI,MAAM,CAAC,QAAQ,CAAC,GAAG,CAAC,IAAI,GAAG,GAAG,UAAU,EAAE,CAAC;gBAC7C,mBAAmB,CAAC,IAAI,EAAE,GAAG,CAAC,KAAK,CAAC,CAAC;gBACrC,SAAS;YACX,CAAC;YACD,MAAM,IAAI,GAAG,UAAU,GAAG,CAAC,IAAI,CAAC,GAAG,EAAE,GAAG,OAAO,CAAC,CAAC;YACjD,IAAI,IAAI,IAAI,CAAC;gBAAE,MAAM;YACrB,IAAI,CAAC,MAAM,WAAW,CAAC,GAAG,CAAC,MAAM,EAAE,GAAG,CAAC,KAAK,EAAE,SAAS,EAAE,IAAI,CAAC,CAAC,KAAK,IAAI;gBAAE,mBAAmB,CAAC,IAAI,EAAE,GAAG,CAAC,KAAK,CAAC,CAAC;QACjH,CAAC;QACD,OAAO,kBAAkB,CAAC,IAAI,CAAC,CAAC,MAAM,CAAC;IACzC,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,kBAAkB,CAAC,IAAI,CAAC,CAAC,MAAM,CAAC;IACzC,CAAC;AACH,CAAC"}
@@ -0,0 +1,55 @@
1
+ /**
2
+ * The pending-revoke queue's FILE: its path's format, and reading it (slice 2, S4). Split from `pending_revoke.ts` so that what
3
+ * `status` needs — a COUNT — does not drag the writer and the network revoke onto every process's load path.
4
+ *
5
+ * Read through `readSecretFile` (the same mode checks as `credentials.json`): a file or folder others can read, a file that is not
6
+ * what this client wrote, or one with a row it cannot read all come back `refused`, and `pending_revoke.ts` never overwrites those.
7
+ * Nothing here writes, sends or prints a token; the only thing it hands `status` is how many rows wait.
8
+ */
9
+ import { type SecretFileRead } from "./credential.js";
10
+ export interface PendingRevoke {
11
+ readonly token: string;
12
+ /** ISO 8601, when the server made the credential. */
13
+ readonly minted_at: string;
14
+ /** The issuing server's origin (`https://api.vibecommit.ai`): the only place a retry sends this token. */
15
+ readonly origin: string;
16
+ /**
17
+ * While `g` is holding the credential (waiting at its y/N) the row carries the holder's lease: a retry run by ANOTHER verb in the
18
+ * meantime must not revoke a credential that is about to be kept. A row with no lease is an orphan. See `leased`.
19
+ */
20
+ readonly lease: {
21
+ readonly pid: number;
22
+ readonly lease_until: string;
23
+ } | null;
24
+ }
25
+ /** How long a holder's lease lasts: a `g` whose question stays open longer may be revoked by another verb, and its commit then refuses. */
26
+ export declare const LEASE_MS: number;
27
+ /**
28
+ * Is the row leased at `now`? BOTH must hold: the holder's pid is alive (`kill(pid, 0)`: EPERM counts as alive, ESRCH as dead) and
29
+ * `now` is before the EARLIER of the row's own `lease_until` and `minted_at` + 30 minutes. The lease is capped at its mint: a
30
+ * `lease_until` written far in the future (a bug, or an edit) does not keep a live pid's row out of the retry for ever. A dead holder or
31
+ * an expired lease is an orphan, which any verb's retry revokes.
32
+ */
33
+ export declare function leased(row: PendingRevoke, now: Date): boolean;
34
+ export type Queue = {
35
+ readonly kind: "ok";
36
+ readonly rows: PendingRevoke[];
37
+ } | {
38
+ readonly kind: "absent";
39
+ }
40
+ /** Present but not safe to read or write: never read, never overwritten. */
41
+ | {
42
+ readonly kind: "refused";
43
+ readonly read: Exclude<SecretFileRead, {
44
+ kind: "ok";
45
+ } | {
46
+ kind: "absent";
47
+ }> | {
48
+ readonly kind: "unreadable";
49
+ };
50
+ };
51
+ export declare function readQueue(home: string): Queue;
52
+ /** The rows waiting, for a count. An absent, insecure or unreadable file reads as none (it is never sent from). */
53
+ export declare function readPendingRevokes(home: string): PendingRevoke[];
54
+ /** How many rows wait: all `status` shows of the queue. */
55
+ export declare function countPendingRevokes(home: string): number;
@@ -0,0 +1,74 @@
1
+ /**
2
+ * The pending-revoke queue's FILE: its path's format, and reading it (slice 2, S4). Split from `pending_revoke.ts` so that what
3
+ * `status` needs — a COUNT — does not drag the writer and the network revoke onto every process's load path.
4
+ *
5
+ * Read through `readSecretFile` (the same mode checks as `credentials.json`): a file or folder others can read, a file that is not
6
+ * what this client wrote, or one with a row it cannot read all come back `refused`, and `pending_revoke.ts` never overwrites those.
7
+ * Nothing here writes, sends or prints a token; the only thing it hands `status` is how many rows wait.
8
+ */
9
+ import { INGEST_TOKEN_PREFIX, readSecretFile } from "./credential.js";
10
+ import { pendingRevokePath } from "./paths.js";
11
+ /** How long a holder's lease lasts: a `g` whose question stays open longer may be revoked by another verb, and its commit then refuses. */
12
+ export const LEASE_MS = 30 * 60 * 1000;
13
+ /**
14
+ * Is the row leased at `now`? BOTH must hold: the holder's pid is alive (`kill(pid, 0)`: EPERM counts as alive, ESRCH as dead) and
15
+ * `now` is before the EARLIER of the row's own `lease_until` and `minted_at` + 30 minutes. The lease is capped at its mint: a
16
+ * `lease_until` written far in the future (a bug, or an edit) does not keep a live pid's row out of the retry for ever. A dead holder or
17
+ * an expired lease is an orphan, which any verb's retry revokes.
18
+ */
19
+ export function leased(row, now) {
20
+ if (row.lease === null)
21
+ return false;
22
+ const until = Math.min(Date.parse(row.lease.lease_until), Date.parse(row.minted_at) + LEASE_MS);
23
+ if (!(until > now.getTime()))
24
+ return false;
25
+ try {
26
+ process.kill(row.lease.pid, 0);
27
+ return true;
28
+ }
29
+ catch (error) {
30
+ return error.code === "EPERM";
31
+ }
32
+ }
33
+ export function readQueue(home) {
34
+ const read = readSecretFile(home, pendingRevokePath(home));
35
+ if (read.kind === "absent")
36
+ return { kind: "absent" };
37
+ if (read.kind !== "ok")
38
+ return { kind: "refused", read };
39
+ try {
40
+ const doc = JSON.parse(read.raw);
41
+ if (doc === null || typeof doc !== "object" || !Array.isArray(doc.revokes))
42
+ return { kind: "refused", read: { kind: "unreadable" } };
43
+ const rows = [];
44
+ for (const row of doc.revokes) {
45
+ if (row === null || typeof row !== "object")
46
+ return { kind: "refused", read: { kind: "unreadable" } };
47
+ const r = row;
48
+ if (typeof r.token !== "string" || !r.token.startsWith(INGEST_TOKEN_PREFIX) || typeof r.minted_at !== "string" || typeof r.origin !== "string") {
49
+ // A row this client cannot read is a row it must not lose: refuse the whole file rather than rewrite it without the row.
50
+ return { kind: "refused", read: { kind: "unreadable" } };
51
+ }
52
+ // A lease this client cannot read is NO lease: the row is an orphan, which errs toward revoking.
53
+ const l = r.lease;
54
+ const lease = l !== null && l !== undefined && typeof l === "object" && typeof l.pid === "number" && Number.isInteger(l.pid) && typeof l.lease_until === "string"
55
+ ? { pid: l.pid, lease_until: l.lease_until }
56
+ : null;
57
+ rows.push({ token: r.token, minted_at: r.minted_at, origin: r.origin, lease });
58
+ }
59
+ return { kind: "ok", rows };
60
+ }
61
+ catch {
62
+ return { kind: "refused", read: { kind: "unreadable" } };
63
+ }
64
+ }
65
+ /** The rows waiting, for a count. An absent, insecure or unreadable file reads as none (it is never sent from). */
66
+ export function readPendingRevokes(home) {
67
+ const queue = readQueue(home);
68
+ return queue.kind === "ok" ? queue.rows : [];
69
+ }
70
+ /** How many rows wait: all `status` shows of the queue. */
71
+ export function countPendingRevokes(home) {
72
+ return readPendingRevokes(home).length;
73
+ }
74
+ //# sourceMappingURL=pending_revoke_file.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pending_revoke_file.js","sourceRoot":"","sources":["../src/pending_revoke_file.ts"],"names":[],"mappings":"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"}
package/dist/picker.d.ts CHANGED
@@ -14,6 +14,8 @@
14
14
  * row (D9: at most 8 repos before `+ N more`).
15
15
  * turned off slugs in `excluded`. Choosing one is a re-include; the grant block names it on a `turn on` row, inside
16
16
  * the same y/N (D7 · 3A), and only a yes clears the exclusion.
17
+ * on your account (slice 2) what the account grants that this machine has not named. Unticked until chosen, like any
18
+ * row: choosing one is an ordinary addition, so it goes through the same disclosure and y/N (R3-8, R3-9).
17
19
  *
18
20
  * ⛔ **A pre-tick is not a consent** (D34): the working folder's repo starts ticked, and Enter on it leads to the naming
19
21
  * y/N, which defaults to N. It is an ADDITION to the diff, never part of the baseline.
@@ -66,9 +68,16 @@ export interface TurnedOffRow {
66
68
  /** The working folder's repo, turned off: `connect` inside it pre-selects the `turn on` (D34 + the re-include rule). */
67
69
  readonly pre: boolean;
68
70
  }
71
+ /** One account grant this machine has not named (slice 2): an owner (`key` without its slash) or a repo (`key` is the slug). */
72
+ export interface AccountRow {
73
+ readonly kind: "owner" | "repo";
74
+ readonly key: string;
75
+ }
69
76
  export interface PickerModel {
70
77
  readonly onNow: readonly OnNowRow[];
71
78
  readonly owners: readonly OwnerGroup[];
79
+ /** What the account grants that this machine has not named. Empty before a sync, or on a machine with no credential. */
80
+ readonly account: readonly AccountRow[];
72
81
  readonly turnedOff: readonly TurnedOffRow[];
73
82
  /** The picker's `Not listed here:` lines (no remote, can't be matched, unreadable agents). */
74
83
  readonly notListed: readonly string[];
@@ -114,6 +123,8 @@ export interface ModelInput {
114
123
  readonly notListed: readonly string[];
115
124
  readonly sawNothing: boolean;
116
125
  readonly here: HereRepo | null;
126
+ /** The account's grants this machine has not named, as `accountView` lists them: `owner/` for an owner, the slug for a repo. */
127
+ readonly accountNotHere?: readonly string[];
117
128
  }
118
129
  export declare function buildModel(input: ModelInput): PickerModel;
119
130
  /** What a number on screen stands for. On-now rows come first, then each owner's repos, then turned-off. */
@@ -123,6 +134,9 @@ export type RowRef = {
123
134
  } | {
124
135
  readonly kind: "repo";
125
136
  readonly repo: PickRepo;
137
+ } | {
138
+ readonly kind: "account";
139
+ readonly row: AccountRow;
126
140
  } | {
127
141
  readonly kind: "off";
128
142
  readonly row: TurnedOffRow;
@@ -136,6 +150,10 @@ export type Parsed = {
136
150
  readonly kind: "empty";
137
151
  } | {
138
152
  readonly kind: "more";
153
+ }
154
+ /** `g`, alone, where it was offered (slice 2, exception 3): use the repositories the account already grants. */
155
+ | {
156
+ readonly kind: "account";
139
157
  } | {
140
158
  readonly kind: "change";
141
159
  readonly change: PickerChange;
@@ -148,13 +166,19 @@ export type Parsed = {
148
166
  * Parse one typed answer (D8 · 4A). ⛔ **Never applies part of an answer:** any token that is not understood returns
149
167
  * `error` naming each such token, and nothing is chosen. `owner/` and `owner/repo` are always accepted, a clone here or
150
168
  * not, because a new laptop needs them; the grant block flags `(no clone here yet)`.
169
+ *
170
+ * ⛔ **`g` STANDS ALONE** (exception 3, R3-12): where `offerAccount` is true, `g` alone is the account's grants, and `g` mixed with
171
+ * any other token is refused with nothing applied, so the y/N after it always names a single list. Where it was not offered `g` is
172
+ * an ordinary token nobody understands.
151
173
  */
152
- export declare function parseTyped(input: string, model: PickerModel, showAll: boolean): Parsed;
174
+ export declare function parseTyped(input: string, model: PickerModel, showAll: boolean, offerAccount?: boolean): Parsed;
153
175
  export interface RenderOptions {
154
176
  readonly colour: boolean;
155
177
  readonly nowMs: number;
156
178
  /** Show every repo, numbered afresh (the `more` token). */
157
179
  readonly showAll: boolean;
180
+ /** Slice 2: the prompt block's `g` line (screen 1a). Only on a machine with no credential, a terminal and a browser to sign in with. */
181
+ readonly offerAccount?: boolean;
158
182
  }
159
183
  /** The `Not listed here:` block, muted, or nothing. */
160
184
  export declare function notListedBlock(model: PickerModel, colour: boolean): string[];
@@ -168,13 +192,17 @@ export interface BlockFacts {
168
192
  readonly reposHere: ReadonlyMap<string, number>;
169
193
  /** Typed `owner/` and `owner/repo` with no clone here. */
170
194
  readonly noClone: ReadonlySet<string>;
195
+ /** `turn on` slugs the ACCOUNT has turned off: the row says so, because the yes re-includes them on every machine. */
196
+ readonly accountOff?: ReadonlySet<string>;
171
197
  }
172
198
  /**
173
199
  * The `You chose:` block — the thing the y/N is asking about (D7 · 3A), on the §13.5 label gutter. One label per kind of
174
200
  * change; an owner row says how many repos are here and that later clones are covered, and a typed name with no clone
175
201
  * on this machine says so, so a typo shows BEFORE the question.
176
202
  */
177
- export declare function renderGrantBlock(change: PickerChange, facts: BlockFacts, colour: boolean): string[];
203
+ export declare function renderGrantBlock(change: PickerChange, facts: BlockFacts, colour: boolean,
204
+ /** `You chose:` for the person's picks; the account's own list (`g`, screen 1b) is headed `Your account grants:`. */
205
+ heading?: string): string[];
178
206
  /** The grant y/N: additions (with or without removals) get the upload question, removals alone the stop question. */
179
207
  export declare function grantQuestion(change: PickerChange, colour: boolean, answers: string): string;
180
208
  export {};
package/dist/picker.js CHANGED
@@ -14,6 +14,8 @@
14
14
  * row (D9: at most 8 repos before `+ N more`).
15
15
  * turned off slugs in `excluded`. Choosing one is a re-include; the grant block names it on a `turn on` row, inside
16
16
  * the same y/N (D7 · 3A), and only a yes clears the exclusion.
17
+ * on your account (slice 2) what the account grants that this machine has not named. Unticked until chosen, like any
18
+ * row: choosing one is an ordinary addition, so it goes through the same disclosure and y/N (R3-8, R3-9).
17
19
  *
18
20
  * ⛔ **A pre-tick is not a consent** (D34): the working folder's repo starts ticked, and Enter on it leads to the naming
19
21
  * y/N, which defaults to N. It is an ADDITION to the diff, never part of the baseline.
@@ -152,9 +154,19 @@ export function buildModel(input) {
152
154
  const turnedOff = [...excluded]
153
155
  .sort()
154
156
  .map((slug) => ({ slug, counts: countsOf(slug), pre: slug === hereSlug }));
157
+ // ⛔ THE SCAN ROW WINS: an owner that already has a group above, and a repo already listed under one, are not repeated under "on
158
+ // your account" — choosing the scan row is the same change, and one row per thing is what a person can read.
159
+ const shownOwners = new Set(owners.map((group) => group.owner));
160
+ const shownRepos = new Set(owners.flatMap((group) => group.repos.map((repo) => repo.slug)));
161
+ const account = (input.accountNotHere ?? [])
162
+ .map((name) => name.endsWith("/")
163
+ ? { kind: "owner", key: normalizeOwner(name.slice(0, -1)) }
164
+ : { kind: "repo", key: normalizeSlug(name) })
165
+ .filter((row) => (row.kind === "owner" ? !shownOwners.has(row.key) : !shownRepos.has(row.key)));
155
166
  return {
156
167
  onNow,
157
168
  owners,
169
+ account,
158
170
  turnedOff,
159
171
  notListed: input.notListed,
160
172
  found: input.repos.length,
@@ -172,6 +184,8 @@ export function numberedRows(model, showAll) {
172
184
  for (const repo of shown)
173
185
  rows.push({ kind: "repo", repo });
174
186
  }
187
+ for (const row of model.account)
188
+ rows.push({ kind: "account", row });
175
189
  for (const row of model.turnedOff)
176
190
  rows.push({ kind: "off", row });
177
191
  return rows;
@@ -182,13 +196,20 @@ const REPO_TOKEN = /^[^\s/]+(?:\/[^\s/]+)*\/[^\s/]+$/u;
182
196
  * Parse one typed answer (D8 · 4A). ⛔ **Never applies part of an answer:** any token that is not understood returns
183
197
  * `error` naming each such token, and nothing is chosen. `owner/` and `owner/repo` are always accepted, a clone here or
184
198
  * not, because a new laptop needs them; the grant block flags `(no clone here yet)`.
199
+ *
200
+ * ⛔ **`g` STANDS ALONE** (exception 3, R3-12): where `offerAccount` is true, `g` alone is the account's grants, and `g` mixed with
201
+ * any other token is refused with nothing applied, so the y/N after it always names a single list. Where it was not offered `g` is
202
+ * an ordinary token nobody understands.
185
203
  */
186
- export function parseTyped(input, model, showAll) {
204
+ export function parseTyped(input, model, showAll, offerAccount = false) {
187
205
  const tokens = input.split(/[\s,]+/u).filter((t) => t !== "");
188
206
  if (tokens.length === 0)
189
207
  return { kind: "empty" };
190
208
  if (tokens.length === 1 && tokens[0].toLowerCase() === "more")
191
209
  return { kind: "more" };
210
+ if (offerAccount && tokens.some((t) => t.toLowerCase() === "g")) {
211
+ return tokens.length === 1 ? { kind: "account" } : { kind: "error", tokens: [PICKER.gMixedToken] };
212
+ }
192
213
  const rows = numberedRows(model, showAll);
193
214
  const bad = [];
194
215
  const addOwners = new Set();
@@ -236,6 +257,12 @@ export function parseTyped(input, model, showAll) {
236
257
  bad.push(PICKER.alreadyOn(token, `-${token}`));
237
258
  else if (row.kind === "off")
238
259
  turnOn.add(row.row.slug);
260
+ else if (row.kind === "account") {
261
+ if (row.row.kind === "owner")
262
+ addOwners.add(row.row.key);
263
+ else
264
+ addSlug(row.row.key, token);
265
+ }
239
266
  else
240
267
  addSlug(row.repo.slug, token);
241
268
  }
@@ -377,7 +404,11 @@ export function notListedBlock(model, colour) {
377
404
  export function renderTyped(model, options) {
378
405
  const { colour, nowMs, showAll } = options;
379
406
  const lines = [""];
380
- if (model.found === 0 && model.onNow.length === 0 && model.turnedOff.length === 0 && model.owners.length === 0) {
407
+ if (model.found === 0 &&
408
+ model.onNow.length === 0 &&
409
+ model.turnedOff.length === 0 &&
410
+ model.owners.length === 0 &&
411
+ model.account.length === 0) {
381
412
  return [
382
413
  ...lines,
383
414
  ...wrap(model.sawNothing ? PICKER.emptyNone : SCAN.noneListable, 2).map((l) => paint(colour, "strong", l)),
@@ -385,6 +416,7 @@ export function renderTyped(model, options) {
385
416
  ...notListedBlock(model, colour),
386
417
  "",
387
418
  ...wrap(model.sawNothing ? PICKER.emptyType : PICKER.emptyListable, 2),
419
+ ...(options.offerAccount === true ? wrap(PICKER.gLine, 2) : []),
388
420
  ` ${PICKER.enterNothing}`,
389
421
  ];
390
422
  }
@@ -392,12 +424,17 @@ export function renderTyped(model, options) {
392
424
  const onNow = model.onNow.map((row) => onNowDisplay(row, nowMs));
393
425
  const shown = (g) => (showAll ? g.repos : g.repos.slice(0, GROUP_CAP));
394
426
  const repoRows = model.owners.flatMap((g) => shown(g).map((r) => repoDisplay(r, nowMs)));
427
+ const account = model.account.map((row) => ({
428
+ name: row.kind === "owner" ? `${row.key}/` : row.key,
429
+ meta: PICKER.onAccountHint,
430
+ agents: "",
431
+ }));
395
432
  const off = model.turnedOff.map((row) => ({
396
433
  name: row.slug,
397
434
  meta: PICKER.turnOnHint,
398
435
  agents: "",
399
436
  }));
400
- const { cut, column, withAgents } = layout([...onNow, ...repoRows, ...off]);
437
+ const { cut, column, withAgents } = layout([...onNow, ...repoRows, ...account, ...off]);
401
438
  const row = (n, display) => `${String(n).padStart(6)} ${paint(colour, "text", cut(display.name).padEnd(column))}${" ".repeat(GAP)}${paint(colour, "muted", withAgents(display))}`;
402
439
  let n = 0;
403
440
  if (model.onNow.length > 0) {
@@ -414,6 +451,11 @@ export function renderTyped(model, options) {
414
451
  lines.push(paint(colour, "muted", ` + ${PICKER.moreTyped(hidden, group.owner)}`));
415
452
  }
416
453
  }
454
+ if (account.length > 0) {
455
+ lines.push(paint(colour, "muted", ` ${PICKER.onAccount}`));
456
+ for (const d of account)
457
+ lines.push(row(++n, d));
458
+ }
417
459
  if (model.turnedOff.length > 0) {
418
460
  lines.push(paint(colour, "muted", ` ${PICKER.turnedOff}`));
419
461
  for (const d of off)
@@ -421,6 +463,8 @@ export function renderTyped(model, options) {
421
463
  }
422
464
  lines.push(...notListedBlock(model, colour), "");
423
465
  lines.push(` ${paint(colour, "text", PICKER.choose(model.owners.length > 0))}`);
466
+ if (options.offerAccount === true)
467
+ lines.push(...wrap(PICKER.gLine, 2).map((l) => paint(colour, "text", l)));
424
468
  if (model.onNow.length > 0)
425
469
  lines.push(` ${paint(colour, "text", PICKER.removeHint)}`);
426
470
  const pre = preChange(model);
@@ -437,8 +481,10 @@ export function notUnderstoodLines(tokens) {
437
481
  * change; an owner row says how many repos are here and that later clones are covered, and a typed name with no clone
438
482
  * on this machine says so, so a typo shows BEFORE the question.
439
483
  */
440
- export function renderGrantBlock(change, facts, colour) {
441
- const lines = [` ${paint(colour, "strong", PICKER.chose)}`];
484
+ export function renderGrantBlock(change, facts, colour,
485
+ /** `You chose:` for the person's picks; the account's own list (`g`, screen 1b) is headed `Your account grants:`. */
486
+ heading = PICKER.chose) {
487
+ const lines = [` ${paint(colour, "strong", heading)}`];
442
488
  const rows = [
443
489
  [
444
490
  PICKER.labelOwners,
@@ -451,7 +497,10 @@ export function renderGrantBlock(change, facts, colour) {
451
497
  PICKER.labelRepos,
452
498
  change.addRepos.map((r) => (facts.noClone.has(r) ? `${r} ${PICKER.noCloneHere}` : r)),
453
499
  ],
454
- [PICKER.labelTurnOn, [...change.turnOn]],
500
+ [
501
+ PICKER.labelTurnOn,
502
+ change.turnOn.map((slug) => (facts.accountOff?.has(slug) === true ? `${slug} ${PICKER.turnedOffOnAccount}` : slug)),
503
+ ],
455
504
  [
456
505
  PICKER.labelStop,
457
506
  [