@vibe-commit/capture 0.7.0 → 0.9.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/README.md +238 -7
- package/dist/account_not_here.d.ts +15 -0
- package/dist/account_not_here.js +25 -0
- package/dist/account_not_here.js.map +1 -0
- package/dist/agents/claude_code.js +13 -0
- package/dist/agents/claude_code.js.map +1 -1
- package/dist/agents/codex_cli.js +11 -0
- package/dist/agents/codex_cli.js.map +1 -1
- package/dist/agents/cursor.js +7 -1
- package/dist/agents/cursor.js.map +1 -1
- package/dist/agents/registry.d.ts +7 -0
- package/dist/agents/registry.js +9 -0
- package/dist/agents/registry.js.map +1 -1
- package/dist/agents/types.d.ts +32 -0
- package/dist/agents/types.js.map +1 -1
- package/dist/bounded_id.d.ts +29 -0
- package/dist/bounded_id.js +42 -0
- package/dist/bounded_id.js.map +1 -0
- package/dist/codex_folders.d.ts +340 -0
- package/dist/codex_folders.js +1448 -0
- package/dist/codex_folders.js.map +1 -0
- package/dist/commands/auth.d.ts +7 -0
- package/dist/commands/auth.js +21 -0
- package/dist/commands/auth.js.map +1 -1
- package/dist/commands/backfill.d.ts +29 -7
- package/dist/commands/backfill.js +387 -109
- package/dist/commands/backfill.js.map +1 -1
- package/dist/commands/browser_credential.d.ts +4 -2
- package/dist/commands/browser_credential.js +5 -2
- package/dist/commands/browser_credential.js.map +1 -1
- package/dist/commands/connect.d.ts +10 -1
- package/dist/commands/connect.js +14 -9
- package/dist/commands/connect.js.map +1 -1
- package/dist/commands/connect_account.d.ts +135 -0
- package/dist/commands/connect_account.js +321 -0
- package/dist/commands/connect_account.js.map +1 -0
- package/dist/commands/connect_picker.js +204 -29
- package/dist/commands/connect_picker.js.map +1 -1
- package/dist/commands/device_credential.d.ts +3 -1
- package/dist/commands/device_credential.js +1 -0
- package/dist/commands/device_credential.js.map +1 -1
- package/dist/commands/finalize.d.ts +67 -0
- package/dist/commands/finalize.js +450 -114
- package/dist/commands/finalize.js.map +1 -1
- package/dist/commands/finish_mint.d.ts +28 -0
- package/dist/commands/finish_mint.js +16 -4
- package/dist/commands/finish_mint.js.map +1 -1
- package/dist/commands/scan.d.ts +28 -0
- package/dist/commands/scan.js +11 -0
- package/dist/commands/scan.js.map +1 -1
- package/dist/commands/status.d.ts +19 -1
- package/dist/commands/status.js +123 -14
- package/dist/commands/status.js.map +1 -1
- package/dist/commands/status_machine.d.ts +12 -3
- package/dist/commands/status_machine.js +111 -13
- package/dist/commands/status_machine.js.map +1 -1
- package/dist/commands/sync_reason.d.ts +17 -0
- package/dist/commands/sync_reason.js +25 -0
- package/dist/commands/sync_reason.js.map +1 -0
- package/dist/consent.d.ts +131 -3
- package/dist/consent.js +248 -14
- package/dist/consent.js.map +1 -1
- package/dist/copy/strings.d.ts +165 -4
- package/dist/copy/strings.js +190 -7
- package/dist/copy/strings.js.map +1 -1
- package/dist/folder_grants.d.ts +44 -0
- package/dist/folder_grants.js +101 -0
- package/dist/folder_grants.js.map +1 -0
- package/dist/git.d.ts +26 -4
- package/dist/git.js +82 -11
- package/dist/git.js.map +1 -1
- package/dist/grants_sync.d.ts +165 -0
- package/dist/grants_sync.js +289 -0
- package/dist/grants_sync.js.map +1 -0
- package/dist/grants_verb.d.ts +70 -0
- package/dist/grants_verb.js +121 -0
- package/dist/grants_verb.js.map +1 -0
- package/dist/hooks/entry.d.ts +30 -50
- package/dist/hooks/entry.js +601 -561
- package/dist/hooks/entry.js.map +1 -1
- package/dist/hooks/fire_lock.d.ts +31 -0
- package/dist/hooks/fire_lock.js +120 -12
- package/dist/hooks/fire_lock.js.map +1 -1
- package/dist/hooks/session_grants.d.ts +59 -0
- package/dist/hooks/session_grants.js +91 -0
- package/dist/hooks/session_grants.js.map +1 -0
- package/dist/hooks/session_start.d.ts +28 -7
- package/dist/hooks/session_start.js +55 -11
- package/dist/hooks/session_start.js.map +1 -1
- package/dist/index.js +22 -5
- package/dist/index.js.map +1 -1
- package/dist/machine_state.d.ts +44 -1
- package/dist/machine_state.js +75 -12
- package/dist/machine_state.js.map +1 -1
- package/dist/oauth/discovery.js +2 -1
- package/dist/oauth/discovery.js.map +1 -1
- package/dist/oauth/pkce.js +2 -1
- package/dist/oauth/pkce.js.map +1 -1
- package/dist/oauth/retire.d.ts +10 -0
- package/dist/oauth/retire.js +13 -0
- package/dist/oauth/retire.js.map +1 -1
- package/dist/paths.d.ts +54 -1
- package/dist/paths.js +88 -12
- package/dist/paths.js.map +1 -1
- package/dist/pending_revoke.d.ts +117 -0
- package/dist/pending_revoke.js +233 -0
- package/dist/pending_revoke.js.map +1 -0
- package/dist/pending_revoke_file.d.ts +55 -0
- package/dist/pending_revoke_file.js +74 -0
- package/dist/pending_revoke_file.js.map +1 -0
- package/dist/physical_path.d.ts +46 -0
- package/dist/physical_path.js +151 -0
- package/dist/physical_path.js.map +1 -0
- package/dist/picker.d.ts +82 -5
- package/dist/picker.js +183 -10
- package/dist/picker.js.map +1 -1
- package/dist/picker_checklist.d.ts +27 -3
- package/dist/picker_checklist.js +81 -6
- package/dist/picker_checklist.js.map +1 -1
- package/dist/policy.d.ts +24 -1
- package/dist/policy.js +30 -2
- package/dist/policy.js.map +1 -1
- package/dist/post.d.ts +55 -2
- package/dist/post.js +72 -22
- package/dist/post.js.map +1 -1
- package/dist/project.d.ts +89 -0
- package/dist/project.js +123 -0
- package/dist/project.js.map +1 -1
- package/dist/redact.d.ts +77 -2
- package/dist/redact.js +713 -47
- package/dist/redact.js.map +1 -1
- package/dist/session_targets.d.ts +713 -0
- package/dist/session_targets.js +2602 -0
- package/dist/session_targets.js.map +1 -0
- package/dist/spool.d.ts +12 -0
- package/dist/spool.js +29 -1
- package/dist/spool.js.map +1 -1
- package/dist/spool_union.d.ts +51 -0
- package/dist/spool_union.js +57 -0
- package/dist/spool_union.js.map +1 -0
- package/dist/state.d.ts +5 -0
- package/dist/state.js +19 -1
- package/dist/state.js.map +1 -1
- package/dist/system_message.d.ts +25 -2
- package/dist/system_message.js +29 -5
- package/dist/system_message.js.map +1 -1
- package/dist/text_tail.d.ts +8 -0
- package/dist/text_tail.js +19 -0
- package/dist/text_tail.js.map +1 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +3 -3
package/dist/picker.d.ts
CHANGED
|
@@ -12,14 +12,19 @@
|
|
|
12
12
|
* typing `-N`, deletes the grant. It is not an exclusion (`off` is), so the repo can be chosen again.
|
|
13
13
|
* owners one group per owner NOT yet granted, its repos behind an `aN` "all, including repos you clone later"
|
|
14
14
|
* row (D9: at most 8 repos before `+ N more`).
|
|
15
|
+
* folders (any-folder capture, R3) folders outside every work tree that hold sessions, one `Folders` group. A folder grant
|
|
16
|
+
* captures the sessions there that touch no repository, under a `local:folder:` identity that only the uploader
|
|
17
|
+
* can read; its grant line SAYS so ("private to you", R7). Granted folders are `on now` rows, removable like any.
|
|
15
18
|
* turned off slugs in `excluded`. Choosing one is a re-include; the grant block names it on a `turn on` row, inside
|
|
16
19
|
* the same y/N (D7 · 3A), and only a yes clears the exclusion.
|
|
20
|
+
* on your account (slice 2) what the account grants that this machine has not named. Unticked until chosen, like any
|
|
21
|
+
* row: choosing one is an ordinary addition, so it goes through the same disclosure and y/N (R3-8, R3-9).
|
|
17
22
|
*
|
|
18
23
|
* ⛔ **A pre-tick is not a consent** (D34): the working folder's repo starts ticked, and Enter on it leads to the naming
|
|
19
24
|
* y/N, which defaults to N. It is an ADDITION to the diff, never part of the baseline.
|
|
20
25
|
*/
|
|
21
26
|
import { type Grants } from "./consent.js";
|
|
22
|
-
import type { ScanClone, ScanRepo } from "./commands/scan.js";
|
|
27
|
+
import type { ScanClone, ScanFolder, ScanRepo } from "./commands/scan.js";
|
|
23
28
|
import type { AgentId } from "./agents/types.js";
|
|
24
29
|
/** D9: each owner group shows at most this many repos before the `+ N more` row. */
|
|
25
30
|
export declare const GROUP_CAP = 8;
|
|
@@ -41,11 +46,22 @@ export interface PickRepo extends Counts {
|
|
|
41
46
|
/** Starts ticked in the checklist. Only ever the working folder's repo, and only when it is not `$HOME`. */
|
|
42
47
|
readonly pre: boolean;
|
|
43
48
|
}
|
|
49
|
+
/** A folder the person can grant: outside every work tree, with sessions. `label` is how the row names it (`~/Documents/work`). */
|
|
50
|
+
export interface PickFolder extends Counts {
|
|
51
|
+
/** The REAL path: the key of the grant. */
|
|
52
|
+
readonly path: string;
|
|
53
|
+
readonly label: string;
|
|
54
|
+
readonly latestMs: number;
|
|
55
|
+
/** The person turned this folder off (or declined it). It is offered again, and choosing it turns it back on. */
|
|
56
|
+
readonly excluded?: boolean;
|
|
57
|
+
}
|
|
44
58
|
/** One `on now` row. `kind` says what removing it deletes. */
|
|
45
59
|
export interface OnNowRow {
|
|
46
|
-
readonly kind: "owner" | "repo" | "clone";
|
|
47
|
-
/** `alexvibecommit` (owner) or a
|
|
60
|
+
readonly kind: "owner" | "repo" | "clone" | "folder";
|
|
61
|
+
/** `alexvibecommit` (owner), a slug, or a folder's REAL path. */
|
|
48
62
|
readonly key: string;
|
|
63
|
+
/** How a `folder` row is named on screen (its `key` is the path). */
|
|
64
|
+
readonly label?: string;
|
|
49
65
|
readonly reason: string;
|
|
50
66
|
readonly counts: Counts | null;
|
|
51
67
|
/** Repos found here under an owner row. */
|
|
@@ -66,10 +82,19 @@ export interface TurnedOffRow {
|
|
|
66
82
|
/** The working folder's repo, turned off: `connect` inside it pre-selects the `turn on` (D34 + the re-include rule). */
|
|
67
83
|
readonly pre: boolean;
|
|
68
84
|
}
|
|
85
|
+
/** One account grant this machine has not named (slice 2): an owner (`key` without its slash) or a repo (`key` is the slug). */
|
|
86
|
+
export interface AccountRow {
|
|
87
|
+
readonly kind: "owner" | "repo";
|
|
88
|
+
readonly key: string;
|
|
89
|
+
}
|
|
69
90
|
export interface PickerModel {
|
|
70
91
|
readonly onNow: readonly OnNowRow[];
|
|
71
92
|
readonly owners: readonly OwnerGroup[];
|
|
93
|
+
/** What the account grants that this machine has not named. Empty before a sync, or on a machine with no credential. */
|
|
94
|
+
readonly account: readonly AccountRow[];
|
|
72
95
|
readonly turnedOff: readonly TurnedOffRow[];
|
|
96
|
+
/** Folders outside every work tree that hold sessions and are not yet granted, newest first (R3). */
|
|
97
|
+
readonly folders: readonly PickFolder[];
|
|
73
98
|
/** The picker's `Not listed here:` lines (no remote, can't be matched, unreadable agents). */
|
|
74
99
|
readonly notListed: readonly string[];
|
|
75
100
|
/** Repos the scan listed, for the `Found N repositories` line. */
|
|
@@ -86,6 +111,9 @@ export interface PickerChange {
|
|
|
86
111
|
readonly stopRepos: readonly string[];
|
|
87
112
|
/** The slugs of per-clone consents to withdraw. */
|
|
88
113
|
readonly stopClones: readonly string[];
|
|
114
|
+
/** Folder grants to add / withdraw, by REAL path. Optional so a change built before folders existed still types. */
|
|
115
|
+
readonly addFolders?: readonly string[];
|
|
116
|
+
readonly stopFolders?: readonly string[];
|
|
89
117
|
}
|
|
90
118
|
export declare const NO_CHANGE: PickerChange;
|
|
91
119
|
export declare function isNoChange(change: PickerChange): boolean;
|
|
@@ -114,7 +142,34 @@ export interface ModelInput {
|
|
|
114
142
|
readonly notListed: readonly string[];
|
|
115
143
|
readonly sawNothing: boolean;
|
|
116
144
|
readonly here: HereRepo | null;
|
|
145
|
+
/** The account's grants this machine has not named, as `accountView` lists them: `owner/` for an owner, the slug for a repo. */
|
|
146
|
+
readonly accountNotHere?: readonly string[];
|
|
147
|
+
/** Folders outside every work tree that hold sessions (`ScanResult.folders`). */
|
|
148
|
+
readonly folders?: readonly ScanFolder[];
|
|
149
|
+
/** The REAL paths of the folders already granted (`folders.json`). */
|
|
150
|
+
readonly grantedFolders?: readonly string[];
|
|
151
|
+
/** The REAL paths of the folders turned off or declined: offered again, marked, so they can be granted later. */
|
|
152
|
+
readonly excludedFolders?: readonly string[];
|
|
153
|
+
/** How a folder path is shown (`~`-relative). Default: the path as is. */
|
|
154
|
+
readonly folderLabel?: (path: string) => string;
|
|
155
|
+
/**
|
|
156
|
+
* C34: may this agent's sessions be captured under a folder? Default `agentIsolatesRecords`, the registry value the resolver gates on.
|
|
157
|
+
* A seam so a cell can flip one agent and prove the picker reads the predicate rather than a list.
|
|
158
|
+
*/
|
|
159
|
+
readonly isolates?: (agent: AgentId) => boolean;
|
|
117
160
|
}
|
|
161
|
+
/**
|
|
162
|
+
* The folder as a FOLDER GRANT would capture it (C34): only the sessions of agents whose records can be isolated count, because a
|
|
163
|
+
* Cursor session (or a Codex one, until its isolation lands) under a granted folder is never captured, and a count or an offer that
|
|
164
|
+
* included it would promise what does not happen. `null` when no session passes. Times and counts are recomputed from the members
|
|
165
|
+
* when the scan kept them; a hand-built folder without members keeps its own times and zeroes the agents that fail.
|
|
166
|
+
*/
|
|
167
|
+
export declare function capturableFolder(folder: ScanFolder, isolates?: (agent: AgentId) => boolean): ScanFolder | null;
|
|
168
|
+
/**
|
|
169
|
+
* The agents a folder grant captures, as the copy names them: `Claude Code`, `Claude Code or Codex`, `Claude Code, Codex or Cursor`.
|
|
170
|
+
* Built from the same predicate as the offers (C34), so the sentence and the behaviour cannot disagree.
|
|
171
|
+
*/
|
|
172
|
+
export declare function folderCaptureAgents(isolates?: (agent: AgentId) => boolean): string;
|
|
118
173
|
export declare function buildModel(input: ModelInput): PickerModel;
|
|
119
174
|
/** What a number on screen stands for. On-now rows come first, then each owner's repos, then turned-off. */
|
|
120
175
|
export type RowRef = {
|
|
@@ -123,6 +178,12 @@ export type RowRef = {
|
|
|
123
178
|
} | {
|
|
124
179
|
readonly kind: "repo";
|
|
125
180
|
readonly repo: PickRepo;
|
|
181
|
+
} | {
|
|
182
|
+
readonly kind: "folder";
|
|
183
|
+
readonly folder: PickFolder;
|
|
184
|
+
} | {
|
|
185
|
+
readonly kind: "account";
|
|
186
|
+
readonly row: AccountRow;
|
|
126
187
|
} | {
|
|
127
188
|
readonly kind: "off";
|
|
128
189
|
readonly row: TurnedOffRow;
|
|
@@ -136,6 +197,10 @@ export type Parsed = {
|
|
|
136
197
|
readonly kind: "empty";
|
|
137
198
|
} | {
|
|
138
199
|
readonly kind: "more";
|
|
200
|
+
}
|
|
201
|
+
/** `g`, alone, where it was offered (slice 2, exception 3): use the repositories the account already grants. */
|
|
202
|
+
| {
|
|
203
|
+
readonly kind: "account";
|
|
139
204
|
} | {
|
|
140
205
|
readonly kind: "change";
|
|
141
206
|
readonly change: PickerChange;
|
|
@@ -148,13 +213,19 @@ export type Parsed = {
|
|
|
148
213
|
* Parse one typed answer (D8 · 4A). ⛔ **Never applies part of an answer:** any token that is not understood returns
|
|
149
214
|
* `error` naming each such token, and nothing is chosen. `owner/` and `owner/repo` are always accepted, a clone here or
|
|
150
215
|
* not, because a new laptop needs them; the grant block flags `(no clone here yet)`.
|
|
216
|
+
*
|
|
217
|
+
* ⛔ **`g` STANDS ALONE** (exception 3, R3-12): where `offerAccount` is true, `g` alone is the account's grants, and `g` mixed with
|
|
218
|
+
* 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
|
|
219
|
+
* an ordinary token nobody understands.
|
|
151
220
|
*/
|
|
152
|
-
export declare function parseTyped(input: string, model: PickerModel, showAll: boolean): Parsed;
|
|
221
|
+
export declare function parseTyped(input: string, model: PickerModel, showAll: boolean, offerAccount?: boolean): Parsed;
|
|
153
222
|
export interface RenderOptions {
|
|
154
223
|
readonly colour: boolean;
|
|
155
224
|
readonly nowMs: number;
|
|
156
225
|
/** Show every repo, numbered afresh (the `more` token). */
|
|
157
226
|
readonly showAll: boolean;
|
|
227
|
+
/** 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. */
|
|
228
|
+
readonly offerAccount?: boolean;
|
|
158
229
|
}
|
|
159
230
|
/** The `Not listed here:` block, muted, or nothing. */
|
|
160
231
|
export declare function notListedBlock(model: PickerModel, colour: boolean): string[];
|
|
@@ -168,13 +239,19 @@ export interface BlockFacts {
|
|
|
168
239
|
readonly reposHere: ReadonlyMap<string, number>;
|
|
169
240
|
/** Typed `owner/` and `owner/repo` with no clone here. */
|
|
170
241
|
readonly noClone: ReadonlySet<string>;
|
|
242
|
+
/** `turn on` slugs the ACCOUNT has turned off: the row says so, because the yes re-includes them on every machine. */
|
|
243
|
+
readonly accountOff?: ReadonlySet<string>;
|
|
244
|
+
/** How each folder in the change is named (`~`-relative); a folder with no entry shows its path. */
|
|
245
|
+
readonly folderLabels?: ReadonlyMap<string, string>;
|
|
171
246
|
}
|
|
172
247
|
/**
|
|
173
248
|
* 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
249
|
* change; an owner row says how many repos are here and that later clones are covered, and a typed name with no clone
|
|
175
250
|
* on this machine says so, so a typo shows BEFORE the question.
|
|
176
251
|
*/
|
|
177
|
-
export declare function renderGrantBlock(change: PickerChange, facts: BlockFacts, colour: boolean
|
|
252
|
+
export declare function renderGrantBlock(change: PickerChange, facts: BlockFacts, colour: boolean,
|
|
253
|
+
/** `You chose:` for the person's picks; the account's own list (`g`, screen 1b) is headed `Your account grants:`. */
|
|
254
|
+
heading?: string): string[];
|
|
178
255
|
/** The grant y/N: additions (with or without removals) get the upload question, removals alone the stop question. */
|
|
179
256
|
export declare function grantQuestion(change: PickerChange, colour: boolean, answers: string): string;
|
|
180
257
|
export {};
|
package/dist/picker.js
CHANGED
|
@@ -12,14 +12,20 @@
|
|
|
12
12
|
* typing `-N`, deletes the grant. It is not an exclusion (`off` is), so the repo can be chosen again.
|
|
13
13
|
* owners one group per owner NOT yet granted, its repos behind an `aN` "all, including repos you clone later"
|
|
14
14
|
* row (D9: at most 8 repos before `+ N more`).
|
|
15
|
+
* folders (any-folder capture, R3) folders outside every work tree that hold sessions, one `Folders` group. A folder grant
|
|
16
|
+
* captures the sessions there that touch no repository, under a `local:folder:` identity that only the uploader
|
|
17
|
+
* can read; its grant line SAYS so ("private to you", R7). Granted folders are `on now` rows, removable like any.
|
|
15
18
|
* turned off slugs in `excluded`. Choosing one is a re-include; the grant block names it on a `turn on` row, inside
|
|
16
19
|
* the same y/N (D7 · 3A), and only a yes clears the exclusion.
|
|
20
|
+
* on your account (slice 2) what the account grants that this machine has not named. Unticked until chosen, like any
|
|
21
|
+
* row: choosing one is an ordinary addition, so it goes through the same disclosure and y/N (R3-8, R3-9).
|
|
17
22
|
*
|
|
18
23
|
* ⛔ **A pre-tick is not a consent** (D34): the working folder's repo starts ticked, and Enter on it leads to the naming
|
|
19
24
|
* y/N, which defaults to N. It is an ADDITION to the diff, never part of the baseline.
|
|
20
25
|
*/
|
|
21
26
|
import { ownerOfSlug, normalizeSlug, normalizeOwner, isLocalSlug } from "./consent.js";
|
|
22
27
|
import { AGENT_HOOKS, SCAN, PICKER, shortDate } from "./copy/index.js";
|
|
28
|
+
import { DIALECTS, agentIsolatesRecords } from "./agents/registry.js";
|
|
23
29
|
import { LABEL_GUTTER, paint, truncateMiddle, wrap } from "./term.js";
|
|
24
30
|
/** D9: each owner group shows at most this many repos before the `+ N more` row. */
|
|
25
31
|
export const GROUP_CAP = 8;
|
|
@@ -42,7 +48,9 @@ export function isNoChange(change) {
|
|
|
42
48
|
change.turnOn.length === 0 &&
|
|
43
49
|
change.stopOwners.length === 0 &&
|
|
44
50
|
change.stopRepos.length === 0 &&
|
|
45
|
-
change.stopClones.length === 0
|
|
51
|
+
change.stopClones.length === 0 &&
|
|
52
|
+
(change.addFolders ?? []).length === 0 &&
|
|
53
|
+
(change.stopFolders ?? []).length === 0);
|
|
46
54
|
}
|
|
47
55
|
/**
|
|
48
56
|
* What Enter means when the working folder's repo is pre-selected (D34): choosing it, which leads to the y/N and never to
|
|
@@ -57,7 +65,48 @@ export function preChange(model) {
|
|
|
57
65
|
}
|
|
58
66
|
/** Anything the person is ADDING needs the D65 disclosure; removals alone do not. */
|
|
59
67
|
export function hasAdditions(change) {
|
|
60
|
-
return change.addOwners.length + change.addRepos.length + change.turnOn.length > 0;
|
|
68
|
+
return change.addOwners.length + change.addRepos.length + change.turnOn.length + (change.addFolders ?? []).length > 0;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* The folder as a FOLDER GRANT would capture it (C34): only the sessions of agents whose records can be isolated count, because a
|
|
72
|
+
* Cursor session (or a Codex one, until its isolation lands) under a granted folder is never captured, and a count or an offer that
|
|
73
|
+
* included it would promise what does not happen. `null` when no session passes. Times and counts are recomputed from the members
|
|
74
|
+
* when the scan kept them; a hand-built folder without members keeps its own times and zeroes the agents that fail.
|
|
75
|
+
*/
|
|
76
|
+
export function capturableFolder(folder, isolates = agentIsolatesRecords) {
|
|
77
|
+
const agents = emptyAgents();
|
|
78
|
+
if (folder.members !== undefined) {
|
|
79
|
+
const kept = folder.members.filter((member) => isolates(member.agent));
|
|
80
|
+
if (kept.length === 0)
|
|
81
|
+
return null;
|
|
82
|
+
let sinceMs = Infinity;
|
|
83
|
+
let latestMs = 0;
|
|
84
|
+
for (const member of kept) {
|
|
85
|
+
const at = member.session.startedMs ?? member.session.mtimeMs;
|
|
86
|
+
sinceMs = Math.min(sinceMs, at);
|
|
87
|
+
latestMs = Math.max(latestMs, at);
|
|
88
|
+
agents[member.agent] += 1;
|
|
89
|
+
}
|
|
90
|
+
return { path: folder.path, sessions: kept.length, sinceMs, latestMs, agents, members: kept };
|
|
91
|
+
}
|
|
92
|
+
let sessions = 0;
|
|
93
|
+
for (const agent of Object.keys(agents)) {
|
|
94
|
+
if (isolates(agent)) {
|
|
95
|
+
agents[agent] = folder.agents[agent] ?? 0;
|
|
96
|
+
sessions += agents[agent];
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return sessions === 0 ? null : { path: folder.path, sessions, sinceMs: folder.sinceMs, latestMs: folder.latestMs, agents };
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* The agents a folder grant captures, as the copy names them: `Claude Code`, `Claude Code or Codex`, `Claude Code, Codex or Cursor`.
|
|
103
|
+
* Built from the same predicate as the offers (C34), so the sentence and the behaviour cannot disagree.
|
|
104
|
+
*/
|
|
105
|
+
export function folderCaptureAgents(isolates = agentIsolatesRecords) {
|
|
106
|
+
const names = DIALECTS.filter((dialect) => isolates(dialect.id)).map((dialect) => SCAN.agent[dialect.id]);
|
|
107
|
+
if (names.length <= 1)
|
|
108
|
+
return names.join("");
|
|
109
|
+
return `${names.slice(0, -1).join(", ")} or ${names[names.length - 1]}`;
|
|
61
110
|
}
|
|
62
111
|
function splitSlug(slug) {
|
|
63
112
|
const owner = ownerOfSlug(slug);
|
|
@@ -134,6 +183,37 @@ export function buildModel(input) {
|
|
|
134
183
|
continue;
|
|
135
184
|
onNow.push({ kind: "clone", key: slug, reason: PICKER.reasonPerClone, counts: countsOf(slug), reposHere: 0, commonDirs });
|
|
136
185
|
}
|
|
186
|
+
// Folder grants (R3): a granted folder is an `on now` row (removable); every other folder with sessions is offered.
|
|
187
|
+
const folderLabel = input.folderLabel ?? ((path) => path);
|
|
188
|
+
const grantedFolders = new Set(input.grantedFolders ?? []);
|
|
189
|
+
// ⛔ C34: counts and offers cover only what a folder grant would capture. A folder with no passing session is not offered; a granted
|
|
190
|
+
// one stays a removable `on now` row, with no counts.
|
|
191
|
+
const isolates = input.isolates ?? agentIsolatesRecords;
|
|
192
|
+
const capturable = (input.folders ?? []).flatMap((folder) => {
|
|
193
|
+
const kept = capturableFolder(folder, isolates);
|
|
194
|
+
return kept === null ? [] : [kept];
|
|
195
|
+
});
|
|
196
|
+
const folderCounts = new Map(capturable.map((folder) => [folder.path, folder]));
|
|
197
|
+
for (const path of input.grantedFolders ?? []) {
|
|
198
|
+
onNow.push({ kind: "folder", key: path, label: folderLabel(path), reason: PICKER.reasonFolder, counts: folderCounts.get(path) ?? null, reposHere: 0, commonDirs: [] });
|
|
199
|
+
}
|
|
200
|
+
const excludedSet = new Set(input.excludedFolders ?? []);
|
|
201
|
+
const offered = capturable.filter((folder) => !grantedFolders.has(folder.path));
|
|
202
|
+
const folders = [
|
|
203
|
+
...offered.map((folder) => ({
|
|
204
|
+
path: folder.path,
|
|
205
|
+
label: folderLabel(folder.path),
|
|
206
|
+
sessions: folder.sessions,
|
|
207
|
+
sinceMs: folder.sinceMs,
|
|
208
|
+
latestMs: folder.latestMs,
|
|
209
|
+
agents: folder.agents,
|
|
210
|
+
...(excludedSet.has(folder.path) ? { excluded: true } : {}),
|
|
211
|
+
})),
|
|
212
|
+
// A folder turned off that no scan found any more is still a row, so it can be turned back on.
|
|
213
|
+
...[...excludedSet]
|
|
214
|
+
.filter((path) => !offered.some((folder) => folder.path === path))
|
|
215
|
+
.map((path) => ({ path, label: folderLabel(path), sessions: 0, sinceMs: 0, latestMs: 0, agents: emptyAgents(), excluded: true })),
|
|
216
|
+
];
|
|
137
217
|
const isOn = (slug) => coveredByGrant(slug) || perCloneSlugs.has(slug);
|
|
138
218
|
const byOwner = new Map();
|
|
139
219
|
for (const repo of repos.values()) {
|
|
@@ -152,10 +232,21 @@ export function buildModel(input) {
|
|
|
152
232
|
const turnedOff = [...excluded]
|
|
153
233
|
.sort()
|
|
154
234
|
.map((slug) => ({ slug, counts: countsOf(slug), pre: slug === hereSlug }));
|
|
235
|
+
// ⛔ THE SCAN ROW WINS: an owner that already has a group above, and a repo already listed under one, are not repeated under "on
|
|
236
|
+
// your account" — choosing the scan row is the same change, and one row per thing is what a person can read.
|
|
237
|
+
const shownOwners = new Set(owners.map((group) => group.owner));
|
|
238
|
+
const shownRepos = new Set(owners.flatMap((group) => group.repos.map((repo) => repo.slug)));
|
|
239
|
+
const account = (input.accountNotHere ?? [])
|
|
240
|
+
.map((name) => name.endsWith("/")
|
|
241
|
+
? { kind: "owner", key: normalizeOwner(name.slice(0, -1)) }
|
|
242
|
+
: { kind: "repo", key: normalizeSlug(name) })
|
|
243
|
+
.filter((row) => (row.kind === "owner" ? !shownOwners.has(row.key) : !shownRepos.has(row.key)));
|
|
155
244
|
return {
|
|
156
245
|
onNow,
|
|
157
246
|
owners,
|
|
247
|
+
account,
|
|
158
248
|
turnedOff,
|
|
249
|
+
folders,
|
|
159
250
|
notListed: input.notListed,
|
|
160
251
|
found: input.repos.length,
|
|
161
252
|
sawNothing: input.sawNothing,
|
|
@@ -172,6 +263,10 @@ export function numberedRows(model, showAll) {
|
|
|
172
263
|
for (const repo of shown)
|
|
173
264
|
rows.push({ kind: "repo", repo });
|
|
174
265
|
}
|
|
266
|
+
for (const folder of showAll ? model.folders : model.folders.slice(0, GROUP_CAP))
|
|
267
|
+
rows.push({ kind: "folder", folder });
|
|
268
|
+
for (const row of model.account)
|
|
269
|
+
rows.push({ kind: "account", row });
|
|
175
270
|
for (const row of model.turnedOff)
|
|
176
271
|
rows.push({ kind: "off", row });
|
|
177
272
|
return rows;
|
|
@@ -182,13 +277,20 @@ const REPO_TOKEN = /^[^\s/]+(?:\/[^\s/]+)*\/[^\s/]+$/u;
|
|
|
182
277
|
* Parse one typed answer (D8 · 4A). ⛔ **Never applies part of an answer:** any token that is not understood returns
|
|
183
278
|
* `error` naming each such token, and nothing is chosen. `owner/` and `owner/repo` are always accepted, a clone here or
|
|
184
279
|
* not, because a new laptop needs them; the grant block flags `(no clone here yet)`.
|
|
280
|
+
*
|
|
281
|
+
* ⛔ **`g` STANDS ALONE** (exception 3, R3-12): where `offerAccount` is true, `g` alone is the account's grants, and `g` mixed with
|
|
282
|
+
* 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
|
|
283
|
+
* an ordinary token nobody understands.
|
|
185
284
|
*/
|
|
186
|
-
export function parseTyped(input, model, showAll) {
|
|
285
|
+
export function parseTyped(input, model, showAll, offerAccount = false) {
|
|
187
286
|
const tokens = input.split(/[\s,]+/u).filter((t) => t !== "");
|
|
188
287
|
if (tokens.length === 0)
|
|
189
288
|
return { kind: "empty" };
|
|
190
289
|
if (tokens.length === 1 && tokens[0].toLowerCase() === "more")
|
|
191
290
|
return { kind: "more" };
|
|
291
|
+
if (offerAccount && tokens.some((t) => t.toLowerCase() === "g")) {
|
|
292
|
+
return tokens.length === 1 ? { kind: "account" } : { kind: "error", tokens: [PICKER.gMixedToken] };
|
|
293
|
+
}
|
|
192
294
|
const rows = numberedRows(model, showAll);
|
|
193
295
|
const bad = [];
|
|
194
296
|
const addOwners = new Set();
|
|
@@ -197,6 +299,8 @@ export function parseTyped(input, model, showAll) {
|
|
|
197
299
|
const stopOwners = new Set();
|
|
198
300
|
const stopRepos = new Set();
|
|
199
301
|
const stopClones = new Set();
|
|
302
|
+
const addFolders = new Set();
|
|
303
|
+
const stopFolders = new Set();
|
|
200
304
|
const grantedOwners = new Set(model.onNow.filter((r) => r.kind === "owner").map((r) => r.key));
|
|
201
305
|
const grantedRepos = new Set(model.onNow.filter((r) => r.kind === "repo").map((r) => r.key));
|
|
202
306
|
const cloneSlugs = new Set(model.onNow.filter((r) => r.kind === "clone").map((r) => r.key));
|
|
@@ -206,6 +310,8 @@ export function parseTyped(input, model, showAll) {
|
|
|
206
310
|
stopOwners.add(row.key);
|
|
207
311
|
else if (row.kind === "repo")
|
|
208
312
|
stopRepos.add(row.key);
|
|
313
|
+
else if (row.kind === "folder")
|
|
314
|
+
stopFolders.add(row.key);
|
|
209
315
|
else
|
|
210
316
|
stopClones.add(row.key);
|
|
211
317
|
};
|
|
@@ -236,6 +342,14 @@ export function parseTyped(input, model, showAll) {
|
|
|
236
342
|
bad.push(PICKER.alreadyOn(token, `-${token}`));
|
|
237
343
|
else if (row.kind === "off")
|
|
238
344
|
turnOn.add(row.row.slug);
|
|
345
|
+
else if (row.kind === "folder")
|
|
346
|
+
addFolders.add(row.folder.path);
|
|
347
|
+
else if (row.kind === "account") {
|
|
348
|
+
if (row.row.kind === "owner")
|
|
349
|
+
addOwners.add(row.row.key);
|
|
350
|
+
else
|
|
351
|
+
addSlug(row.row.key, token);
|
|
352
|
+
}
|
|
239
353
|
else
|
|
240
354
|
addSlug(row.repo.slug, token);
|
|
241
355
|
}
|
|
@@ -300,6 +414,9 @@ export function parseTyped(input, model, showAll) {
|
|
|
300
414
|
stopOwners: [...stopOwners],
|
|
301
415
|
stopRepos: [...stopRepos],
|
|
302
416
|
stopClones: [...stopClones],
|
|
417
|
+
// Present only when something was chosen, so a repo-only answer is the object it always was.
|
|
418
|
+
...(addFolders.size > 0 ? { addFolders: [...addFolders] } : {}),
|
|
419
|
+
...(stopFolders.size > 0 ? { stopFolders: [...stopFolders] } : {}),
|
|
303
420
|
},
|
|
304
421
|
};
|
|
305
422
|
}
|
|
@@ -322,11 +439,19 @@ function onNowDisplay(row, nowMs) {
|
|
|
322
439
|
return { name: `${row.key}/`, meta: joinMeta(row.reason, PICKER.ownerOnReposHere(row.reposHere)), agents: "" };
|
|
323
440
|
}
|
|
324
441
|
return {
|
|
325
|
-
name: row.key,
|
|
442
|
+
name: row.kind === "folder" ? (row.label ?? row.key) : row.key,
|
|
326
443
|
meta: joinMeta(row.reason, countsText(row.counts, nowMs)),
|
|
327
444
|
agents: agentsText(row.counts),
|
|
328
445
|
};
|
|
329
446
|
}
|
|
447
|
+
function folderDisplay(folder, nowMs) {
|
|
448
|
+
const counts = folder.sessions === 0 ? null : folder;
|
|
449
|
+
return {
|
|
450
|
+
name: folder.label,
|
|
451
|
+
meta: joinMeta(folder.excluded === true ? PICKER.turnOnHint : "", countsText(counts, nowMs)),
|
|
452
|
+
agents: agentsText(counts),
|
|
453
|
+
};
|
|
454
|
+
}
|
|
330
455
|
function repoDisplay(repo, nowMs) {
|
|
331
456
|
const flag = repo.here === "repo" ? PICKER.thisRepo : repo.here === "folder" ? PICKER.thisFolder : "";
|
|
332
457
|
return {
|
|
@@ -377,7 +502,12 @@ export function notListedBlock(model, colour) {
|
|
|
377
502
|
export function renderTyped(model, options) {
|
|
378
503
|
const { colour, nowMs, showAll } = options;
|
|
379
504
|
const lines = [""];
|
|
380
|
-
if (model.found === 0 &&
|
|
505
|
+
if (model.found === 0 &&
|
|
506
|
+
model.onNow.length === 0 &&
|
|
507
|
+
model.turnedOff.length === 0 &&
|
|
508
|
+
model.owners.length === 0 &&
|
|
509
|
+
model.folders.length === 0 &&
|
|
510
|
+
model.account.length === 0) {
|
|
381
511
|
return [
|
|
382
512
|
...lines,
|
|
383
513
|
...wrap(model.sawNothing ? PICKER.emptyNone : SCAN.noneListable, 2).map((l) => paint(colour, "strong", l)),
|
|
@@ -385,6 +515,7 @@ export function renderTyped(model, options) {
|
|
|
385
515
|
...notListedBlock(model, colour),
|
|
386
516
|
"",
|
|
387
517
|
...wrap(model.sawNothing ? PICKER.emptyType : PICKER.emptyListable, 2),
|
|
518
|
+
...(options.offerAccount === true ? wrap(PICKER.gLine, 2) : []),
|
|
388
519
|
` ${PICKER.enterNothing}`,
|
|
389
520
|
];
|
|
390
521
|
}
|
|
@@ -392,12 +523,19 @@ export function renderTyped(model, options) {
|
|
|
392
523
|
const onNow = model.onNow.map((row) => onNowDisplay(row, nowMs));
|
|
393
524
|
const shown = (g) => (showAll ? g.repos : g.repos.slice(0, GROUP_CAP));
|
|
394
525
|
const repoRows = model.owners.flatMap((g) => shown(g).map((r) => repoDisplay(r, nowMs)));
|
|
526
|
+
const shownFolders = showAll ? model.folders : model.folders.slice(0, GROUP_CAP);
|
|
527
|
+
const folderRows = shownFolders.map((f) => folderDisplay(f, nowMs));
|
|
528
|
+
const account = model.account.map((row) => ({
|
|
529
|
+
name: row.kind === "owner" ? `${row.key}/` : row.key,
|
|
530
|
+
meta: PICKER.onAccountHint,
|
|
531
|
+
agents: "",
|
|
532
|
+
}));
|
|
395
533
|
const off = model.turnedOff.map((row) => ({
|
|
396
534
|
name: row.slug,
|
|
397
535
|
meta: PICKER.turnOnHint,
|
|
398
536
|
agents: "",
|
|
399
537
|
}));
|
|
400
|
-
const { cut, column, withAgents } = layout([...onNow, ...repoRows, ...off]);
|
|
538
|
+
const { cut, column, withAgents } = layout([...onNow, ...repoRows, ...folderRows, ...account, ...off]);
|
|
401
539
|
const row = (n, display) => `${String(n).padStart(6)} ${paint(colour, "text", cut(display.name).padEnd(column))}${" ".repeat(GAP)}${paint(colour, "muted", withAgents(display))}`;
|
|
402
540
|
let n = 0;
|
|
403
541
|
if (model.onNow.length > 0) {
|
|
@@ -414,6 +552,19 @@ export function renderTyped(model, options) {
|
|
|
414
552
|
lines.push(paint(colour, "muted", ` + ${PICKER.moreTyped(hidden, group.owner)}`));
|
|
415
553
|
}
|
|
416
554
|
}
|
|
555
|
+
if (folderRows.length > 0) {
|
|
556
|
+
lines.push(paint(colour, "muted", ` ${PICKER.folders}`));
|
|
557
|
+
for (const d of folderRows)
|
|
558
|
+
lines.push(row(++n, d));
|
|
559
|
+
const hiddenFolders = model.folders.length - shownFolders.length;
|
|
560
|
+
if (hiddenFolders > 0)
|
|
561
|
+
lines.push(paint(colour, "muted", ` + ${PICKER.moreFoldersTyped(hiddenFolders)}`));
|
|
562
|
+
}
|
|
563
|
+
if (account.length > 0) {
|
|
564
|
+
lines.push(paint(colour, "muted", ` ${PICKER.onAccount}`));
|
|
565
|
+
for (const d of account)
|
|
566
|
+
lines.push(row(++n, d));
|
|
567
|
+
}
|
|
417
568
|
if (model.turnedOff.length > 0) {
|
|
418
569
|
lines.push(paint(colour, "muted", ` ${PICKER.turnedOff}`));
|
|
419
570
|
for (const d of off)
|
|
@@ -421,6 +572,8 @@ export function renderTyped(model, options) {
|
|
|
421
572
|
}
|
|
422
573
|
lines.push(...notListedBlock(model, colour), "");
|
|
423
574
|
lines.push(` ${paint(colour, "text", PICKER.choose(model.owners.length > 0))}`);
|
|
575
|
+
if (options.offerAccount === true)
|
|
576
|
+
lines.push(...wrap(PICKER.gLine, 2).map((l) => paint(colour, "text", l)));
|
|
424
577
|
if (model.onNow.length > 0)
|
|
425
578
|
lines.push(` ${paint(colour, "text", PICKER.removeHint)}`);
|
|
426
579
|
const pre = preChange(model);
|
|
@@ -437,8 +590,10 @@ export function notUnderstoodLines(tokens) {
|
|
|
437
590
|
* change; an owner row says how many repos are here and that later clones are covered, and a typed name with no clone
|
|
438
591
|
* on this machine says so, so a typo shows BEFORE the question.
|
|
439
592
|
*/
|
|
440
|
-
export function renderGrantBlock(change, facts, colour
|
|
441
|
-
|
|
593
|
+
export function renderGrantBlock(change, facts, colour,
|
|
594
|
+
/** `You chose:` for the person's picks; the account's own list (`g`, screen 1b) is headed `Your account grants:`. */
|
|
595
|
+
heading = PICKER.chose) {
|
|
596
|
+
const lines = [` ${paint(colour, "strong", heading)}`];
|
|
442
597
|
const rows = [
|
|
443
598
|
[
|
|
444
599
|
PICKER.labelOwners,
|
|
@@ -451,13 +606,23 @@ export function renderGrantBlock(change, facts, colour) {
|
|
|
451
606
|
PICKER.labelRepos,
|
|
452
607
|
change.addRepos.map((r) => (facts.noClone.has(r) ? `${r} ${PICKER.noCloneHere}` : r)),
|
|
453
608
|
],
|
|
454
|
-
[
|
|
609
|
+
[
|
|
610
|
+
PICKER.labelTurnOn,
|
|
611
|
+
change.turnOn.map((slug) => (facts.accountOff?.has(slug) === true ? `${slug} ${PICKER.turnedOffOnAccount}` : slug)),
|
|
612
|
+
],
|
|
613
|
+
// ⛔ THE FOLDER GRANT LINE SAYS WHO CAN READ IT (R7 · D9): the server makes a folder repository private to its uploader, and the
|
|
614
|
+
// person is told so at the one moment they are asked. Nothing here claims more than that.
|
|
615
|
+
[
|
|
616
|
+
PICKER.labelFolders,
|
|
617
|
+
(change.addFolders ?? []).map((path) => `${facts.folderLabels?.get(path) ?? path} ${PICKER.folderPrivate}`),
|
|
618
|
+
],
|
|
455
619
|
[
|
|
456
620
|
PICKER.labelStop,
|
|
457
621
|
[
|
|
458
622
|
...change.stopOwners.map((o) => `${o}/`),
|
|
459
623
|
...change.stopRepos,
|
|
460
624
|
...change.stopClones,
|
|
625
|
+
...(change.stopFolders ?? []).map((path) => facts.folderLabels?.get(path) ?? path),
|
|
461
626
|
],
|
|
462
627
|
],
|
|
463
628
|
];
|
|
@@ -471,7 +636,15 @@ export function renderGrantBlock(change, facts, colour) {
|
|
|
471
636
|
}
|
|
472
637
|
/** The grant y/N: additions (with or without removals) get the upload question, removals alone the stop question. */
|
|
473
638
|
export function grantQuestion(change, colour, answers) {
|
|
474
|
-
const
|
|
639
|
+
const folders = (change.addFolders ?? []).length > 0 || (change.stopFolders ?? []).length > 0;
|
|
640
|
+
const repos = change.addOwners.length + change.addRepos.length + change.turnOn.length + change.stopOwners.length + change.stopRepos.length + change.stopClones.length > 0;
|
|
641
|
+
const q = hasAdditions(change)
|
|
642
|
+
? folders
|
|
643
|
+
? repos ? PICKER.questionGrantMixed : PICKER.questionGrantFolders
|
|
644
|
+
: PICKER.questionGrant
|
|
645
|
+
: folders
|
|
646
|
+
? repos ? PICKER.questionStopMixed : PICKER.questionStopFolders
|
|
647
|
+
: PICKER.questionStop;
|
|
475
648
|
return ` ${paint(colour, "text", q)} ${paint(colour, "muted", answers)} `;
|
|
476
649
|
}
|
|
477
650
|
//# sourceMappingURL=picker.js.map
|