nawabari 0.7.1 → 0.9.2
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 +264 -29
- package/dist/cli-command-registry.d.ts +50 -0
- package/dist/cli-command-registry.js +509 -0
- package/dist/cli-command-registry.js.map +1 -0
- package/dist/cli.d.ts +18 -0
- package/dist/cli.js +302 -583
- package/dist/cli.js.map +1 -1
- package/dist/contract.d.ts +6 -0
- package/dist/contract.js +296 -192
- package/dist/contract.js.map +1 -1
- package/dist/domain/cgroups-v2.d.ts +77 -0
- package/dist/domain/cgroups-v2.js +434 -0
- package/dist/domain/cgroups-v2.js.map +1 -0
- package/dist/domain/doctor.d.ts +4 -1
- package/dist/domain/doctor.js +64 -13
- package/dist/domain/doctor.js.map +1 -1
- package/dist/domain/errors.d.ts +1 -1
- package/dist/domain/errors.js.map +1 -1
- package/dist/domain/landlock.d.ts +55 -0
- package/dist/domain/landlock.js +279 -0
- package/dist/domain/landlock.js.map +1 -0
- package/dist/domain/runtime.d.ts +12 -0
- package/dist/domain/runtime.js +50 -0
- package/dist/domain/runtime.js.map +1 -0
- package/dist/domain/sandbox-launcher.d.ts +26 -0
- package/dist/domain/sandbox-launcher.js +312 -23
- package/dist/domain/sandbox-launcher.js.map +1 -1
- package/dist/domain/sandbox-seccomp.d.ts +46 -0
- package/dist/domain/sandbox-seccomp.js +302 -0
- package/dist/domain/sandbox-seccomp.js.map +1 -0
- package/dist/domain/sandbox.d.ts +45 -1
- package/dist/domain/sandbox.js +176 -25
- package/dist/domain/sandbox.js.map +1 -1
- package/dist/domain/session-backend.d.ts +4 -1
- package/dist/domain/session-backend.js +256 -3
- package/dist/domain/session-backend.js.map +1 -1
- package/dist/domain/session.d.ts +207 -3
- package/dist/domain/session.js.map +1 -1
- package/dist/errors.d.ts +1 -1
- package/dist/failure-code-vocabulary.d.ts +20 -0
- package/dist/failure-code-vocabulary.js +216 -0
- package/dist/failure-code-vocabulary.js.map +1 -0
- package/dist/git.js +92 -6
- package/dist/git.js.map +1 -1
- package/dist/operation-authorization.d.ts +1 -1
- package/dist/product-state-manifest.d.ts +152 -0
- package/dist/product-state-manifest.js +240 -0
- package/dist/product-state-manifest.js.map +1 -0
- package/dist/public-contract.d.ts +25 -0
- package/dist/public-contract.js +29 -0
- package/dist/public-contract.js.map +1 -0
- package/dist/public-state.d.ts +75 -0
- package/dist/public-state.js +60 -0
- package/dist/public-state.js.map +1 -0
- package/dist/registry/lock.d.ts +8 -0
- package/dist/registry/lock.js +38 -20
- package/dist/registry/lock.js.map +1 -1
- package/dist/repository-evidence.d.ts +16 -0
- package/dist/repository-evidence.js.map +1 -1
- package/dist/resource-claims.d.ts +2 -2
- package/dist/resource-claims.js +96 -27
- package/dist/resource-claims.js.map +1 -1
- package/dist/session-lifecycle-actions.d.ts +64 -0
- package/dist/session-lifecycle-actions.js +117 -0
- package/dist/session-lifecycle-actions.js.map +1 -0
- package/dist/session-lifecycle-classification.d.ts +120 -0
- package/dist/session-lifecycle-classification.js +88 -0
- package/dist/session-lifecycle-classification.js.map +1 -0
- package/dist/session-registry.d.ts +210 -2
- package/dist/session-registry.js +1330 -87
- package/dist/session-registry.js.map +1 -1
- package/dist/state/index.d.ts +5 -0
- package/dist/state/index.js +4 -0
- package/dist/state/index.js.map +1 -0
- package/dist/state/session/actors.d.ts +71 -0
- package/dist/state/session/actors.js +259 -0
- package/dist/state/session/actors.js.map +1 -0
- package/dist/state/session/guards.d.ts +25 -0
- package/dist/state/session/guards.js +108 -0
- package/dist/state/session/guards.js.map +1 -0
- package/dist/state/session/index.d.ts +6 -0
- package/dist/state/session/index.js +5 -0
- package/dist/state/session/index.js.map +1 -0
- package/dist/state/session/machine.d.ts +185 -0
- package/dist/state/session/machine.js +408 -0
- package/dist/state/session/machine.js.map +1 -0
- package/dist/state/session/types.d.ts +94 -0
- package/dist/state/session/types.js +14 -0
- package/dist/state/session/types.js.map +1 -0
- package/package.json +46 -26
package/dist/cli.js
CHANGED
|
@@ -4,6 +4,7 @@ import { DomainError, EXIT_CODES, failure } from "./domain/errors.js";
|
|
|
4
4
|
import { MAX_SESSION_LIST_LIMIT, } from "./domain/session.js";
|
|
5
5
|
import { EVIDENCE_MAX_DIFF_BYTES, EVIDENCE_MAX_DIFF_HUNKS, EVIDENCE_MAX_DIFF_PATHS } from "./repository-evidence.js";
|
|
6
6
|
import { isResourceClaimMode } from "./resource-claims.js";
|
|
7
|
+
import { OPERATION_VOCABULARY } from "./operation-authorization.js";
|
|
7
8
|
import { createLocalSessionBackend } from "./domain/session-backend.js";
|
|
8
9
|
import { defaultCliIO, renderFailure, renderSuccess } from "./presentation.js";
|
|
9
10
|
import { MACHINE_CONTRACT_ID, MACHINE_CONTRACT_SCHEMA_VERSION, machineContract } from "./contract.js";
|
|
@@ -11,521 +12,173 @@ import { resolveSandboxExecutionRequest, runSandboxedCommand, } from "./domain/s
|
|
|
11
12
|
const CLI_NAME = "nawabari";
|
|
12
13
|
const packageMetadata = createRequire(import.meta.url)("../package.json");
|
|
13
14
|
const VERSION = packageMetadata.version;
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
],
|
|
57
|
-
},
|
|
58
|
-
{
|
|
59
|
-
name: "session inspect",
|
|
60
|
-
summary: "Report side-effect-free close/cleanup readiness for a session",
|
|
61
|
-
usage: `${CLI_NAME} session inspect [<session-id>|--session <id>] [--integrated-revision <rev>]`,
|
|
62
|
-
options: [
|
|
63
|
-
option("--session", "Select a session instead of the current worktree owner", { value: "<id>" }),
|
|
64
|
-
option("--integrated-revision", "Externally evidenced revision to test for non-ancestry (squash/rebase) integration; independently re-verified via exact Git tree-object equivalence, never trusted blindly", { value: "<rev>" }),
|
|
65
|
-
],
|
|
66
|
-
notes: [
|
|
67
|
-
"Read-only: never mutates session, claim, Git, worktree, branch, or registry state. Repeated calls are idempotent.",
|
|
68
|
-
"Derived from the same authoritative close/cleanup Git evidence as session close; does not duplicate or diverge from that logic.",
|
|
69
|
-
"Nawabari never queries GitHub or any remote provider; --integrated-revision only names a local revision for Nawabari to independently verify.",
|
|
70
|
-
"Target grammar: optional first positional <session-id> is an alias for --session <id>; do not supply both.",
|
|
71
|
-
],
|
|
72
|
-
},
|
|
73
|
-
{
|
|
74
|
-
name: "session run",
|
|
75
|
-
summary: "Run one command inside the protected session sandbox",
|
|
76
|
-
usage: `${CLI_NAME} session run [--session <id>] -- <command> [args...]`,
|
|
77
|
-
options: [option("--session", "Select the active owned session", { value: "<id>" })],
|
|
78
|
-
notes: [
|
|
79
|
-
"The -- terminator is mandatory. The command is passed as argv without a shell, and protected execution is fail-closed.",
|
|
80
|
-
"session exec is an alias.",
|
|
81
|
-
],
|
|
82
|
-
},
|
|
83
|
-
{
|
|
84
|
-
name: "session list",
|
|
85
|
-
summary: "List bounded repository session records",
|
|
86
|
-
usage: `${CLI_NAME} session list [--all|--history]`,
|
|
87
|
-
options: [
|
|
88
|
-
option("--all", "Include closed history; explicit unbounded history view"),
|
|
89
|
-
option("--history", "Alias for --all"),
|
|
90
|
-
],
|
|
91
|
-
notes: ["Default output excludes closed records and is limited to 64 records."],
|
|
92
|
-
},
|
|
93
|
-
{
|
|
94
|
-
name: "session claim",
|
|
95
|
-
summary: "Add a canonical resource claim",
|
|
96
|
-
usage: `${CLI_NAME} session claim [<session-id>] --resource <path-or-glob> --mode <read|write|exclusive-write>`,
|
|
97
|
-
options: [
|
|
98
|
-
option("--resource", "Repository-relative resource", { value: "<path-or-glob>", required: true }),
|
|
99
|
-
option("--mode", "Granted claim mode", { value: "<read|write|exclusive-write>", required: true }),
|
|
100
|
-
option("--session", "Target active session; omitted resolves the current owner", { value: "<id>" }),
|
|
101
|
-
option("--repository", "Expected repository identity", { value: "<id>" }),
|
|
102
|
-
],
|
|
103
|
-
notes: [
|
|
104
|
-
"Target grammar: optional first positional <session-id> is an alias for --session <id>; do not supply both.",
|
|
105
|
-
],
|
|
106
|
-
},
|
|
107
|
-
{
|
|
108
|
-
name: "session update",
|
|
109
|
-
summary: "Atomically replace a session's complete resource claim set",
|
|
110
|
-
usage: `${CLI_NAME} session update [<session-id>] --resource <path-or-glob> --mode <read|write|exclusive-write> [--resource <path-or-glob> --mode <read|write|exclusive-write> ...]`,
|
|
111
|
-
options: [
|
|
112
|
-
option("--resource", "Repository-relative resource; repeatable, each paired with the --mode immediately after it", {
|
|
113
|
-
value: "<path-or-glob>",
|
|
114
|
-
required: true,
|
|
115
|
-
}),
|
|
116
|
-
option("--mode", "Mode for the --resource immediately before it; repeatable", {
|
|
117
|
-
value: "<read|write|exclusive-write>",
|
|
118
|
-
required: true,
|
|
119
|
-
}),
|
|
120
|
-
option("--if-generation", "Expected claim-set generation for CAS; mutually exclusive with --force", {
|
|
121
|
-
value: "<non-negative-integer>",
|
|
122
|
-
}),
|
|
123
|
-
option("--force", "Explicitly permit unconditional complete replacement; mutually exclusive with --if-generation"),
|
|
124
|
-
option("--session", "Target active session; omitted resolves the current owner", { value: "<id>" }),
|
|
125
|
-
option("--repository", "Expected repository identity", { value: "<id>" }),
|
|
126
|
-
],
|
|
127
|
-
notes: [
|
|
128
|
-
"The desired claim set is a full replacement performed atomically in one updateClaims() transaction; " +
|
|
129
|
-
"use exactly one concurrency intent: --if-generation <non-negative-integer> for claim-set generation CAS, " +
|
|
130
|
-
"or explicit --force for unconditional replacement. On any invalid, conflicting, or stale claim the prior set is left unchanged.",
|
|
131
|
-
"Each --resource must be immediately followed by its own --mode; pairing is positional adjacency, not flag order.",
|
|
132
|
-
"Target grammar: optional first positional <session-id> is an alias for --session <id>; do not supply both.",
|
|
133
|
-
],
|
|
134
|
-
},
|
|
135
|
-
{
|
|
136
|
-
name: "session mutate",
|
|
137
|
-
summary: "Atomically apply exact-resource claim additions, changes, and releases",
|
|
138
|
-
usage: `${CLI_NAME} session mutate [<session>|--session <id>] [--repository <id>] ` +
|
|
139
|
-
`(--upsert-resource <path-or-glob> --mode <read|write|exclusive-write> | --release-resource <path-or-glob>)+ ` +
|
|
140
|
-
`(--if-generation <non-negative-safe-int> | --force)`,
|
|
141
|
-
options: [
|
|
142
|
-
option("--upsert-resource", "Exact repository-relative resource to add or change; each occurrence must be immediately followed by --mode", { value: "<path-or-glob>" }),
|
|
143
|
-
option("--mode", "Mode for the immediately preceding --upsert-resource", {
|
|
144
|
-
value: "<read|write|exclusive-write>",
|
|
145
|
-
}),
|
|
146
|
-
option("--release-resource", "Exact repository-relative resource to release; repeatable", {
|
|
147
|
-
value: "<path-or-glob>",
|
|
148
|
-
}),
|
|
149
|
-
option("--if-generation", "Expected claim-set generation for CAS; mutually exclusive with --force", {
|
|
150
|
-
value: "<non-negative-safe-int>",
|
|
151
|
-
}),
|
|
152
|
-
option("--force", "Explicitly permit unconditional atomic mutation; mutually exclusive with --if-generation"),
|
|
153
|
-
option("--session", "Target active session; omitted resolves the current owner", { value: "<id>" }),
|
|
154
|
-
option("--repository", "Expected repository identity", { value: "<id>" }),
|
|
155
|
-
],
|
|
156
|
-
notes: [
|
|
157
|
-
"Apply one or more ordered upsert/release deltas in one backend transaction; exactly one concurrency intent is required.",
|
|
158
|
-
"Every --upsert-resource must be immediately followed by its own --mode; --release-resource takes exactly one value.",
|
|
159
|
-
"Target grammar: optional first positional <session> is an alias for --session <id>; do not supply both.",
|
|
160
|
-
],
|
|
161
|
-
},
|
|
162
|
-
{
|
|
163
|
-
name: "session transition",
|
|
164
|
-
summary: "Atomically transition one exact resource claim mode",
|
|
165
|
-
usage: `${CLI_NAME} session transition [<session>|--session <id>] [--repository <id>] ` +
|
|
166
|
-
`--resource <path-or-glob> --mode <read|write|exclusive-write> ` +
|
|
167
|
-
`(--if-generation <non-negative-safe-int> | --force)`,
|
|
168
|
-
options: [
|
|
169
|
-
option("--resource", "Exact repository-relative resource to acquire or change", {
|
|
170
|
-
value: "<path-or-glob>",
|
|
171
|
-
required: true,
|
|
172
|
-
}),
|
|
173
|
-
option("--mode", "Target mode for the immediately preceding --resource", {
|
|
174
|
-
value: "<read|write|exclusive-write>",
|
|
175
|
-
required: true,
|
|
176
|
-
}),
|
|
177
|
-
option("--if-generation", "Expected claim-set generation for CAS; mutually exclusive with --force", {
|
|
178
|
-
value: "<non-negative-safe-int>",
|
|
179
|
-
}),
|
|
180
|
-
option("--force", "Explicitly permit unconditional atomic mutation; mutually exclusive with --if-generation"),
|
|
181
|
-
option("--session", "Target active session; omitted resolves the current owner", { value: "<id>" }),
|
|
182
|
-
option("--repository", "Expected repository identity", { value: "<id>" }),
|
|
183
|
-
],
|
|
184
|
-
notes: [
|
|
185
|
-
"Exactly one resource/mode pair is projected as one atomic upsert delta; same mode is an idempotent no-op.",
|
|
186
|
-
"Use exactly one concurrency intent: --if-generation <non-negative-safe-int> or explicit --force.",
|
|
187
|
-
"The target grammar accepts an optional first positional <session> or --session <id>, but not both.",
|
|
188
|
-
],
|
|
189
|
-
},
|
|
190
|
-
{
|
|
191
|
-
name: "session claims",
|
|
192
|
-
summary: "List canonical resource claims",
|
|
193
|
-
usage: `${CLI_NAME} session claims [<session-id>|--session <id>]`,
|
|
194
|
-
options: [option("--session", "Select a session; omitted lists all claims", { value: "<id>" })],
|
|
195
|
-
notes: [
|
|
196
|
-
"Target grammar: optional first positional <session-id> is an alias for --session <id>; do not supply both.",
|
|
197
|
-
],
|
|
198
|
-
},
|
|
199
|
-
{
|
|
200
|
-
name: "session release",
|
|
201
|
-
summary: "Release resource claims",
|
|
202
|
-
usage: `${CLI_NAME} session release [<session-id>|--session <id>] (--resource <path-or-glob> ... | --claim-id <id> ... | --all) (--if-generation <n> | --force)`,
|
|
203
|
-
options: [
|
|
204
|
-
option("--session", "Target session; omitted resolves the current owner", { value: "<id>" }),
|
|
205
|
-
option("--resource", "Release one exact canonical resource; repeatable", { value: "<path-or-glob>" }),
|
|
206
|
-
option("--claim-id", "Release one owned claim ID; repeatable", { value: "<id>" }),
|
|
207
|
-
option("--all", "Explicitly release all claims owned by the target session"),
|
|
208
|
-
option("--if-generation", "Require the expected claim-set generation; mutually exclusive with --force", {
|
|
209
|
-
value: "<non-negative-safe-int>",
|
|
210
|
-
}),
|
|
211
|
-
option("--force", "Explicitly allow unconditional release; mutually exclusive with --if-generation"),
|
|
212
|
-
],
|
|
213
|
-
notes: [
|
|
214
|
-
"Target grammar: optional first positional <session-id> is an alias for --session <id>; do not supply both.",
|
|
215
|
-
"Exactly one selector family is required: repeated --resource, repeated --claim-id, or explicit --all.",
|
|
216
|
-
"Exactly one destructive concurrency intent is required: --if-generation <non-negative-safe-int> or --force.",
|
|
217
|
-
],
|
|
218
|
-
},
|
|
219
|
-
{
|
|
220
|
-
name: "resource claim",
|
|
221
|
-
summary: "Add a canonical resource claim (alias)",
|
|
222
|
-
usage: `${CLI_NAME} resource claim [<session-id>|--session <id>] --resource <path-or-glob> --mode <read|write|exclusive-write>`,
|
|
223
|
-
options: [],
|
|
224
|
-
notes: [
|
|
225
|
-
"Target grammar matches session claim: optional first positional <session-id> is an alias for --session <id>; do not supply both.",
|
|
226
|
-
],
|
|
227
|
-
},
|
|
228
|
-
{
|
|
229
|
-
name: "resource update",
|
|
230
|
-
summary: "Atomically replace a session's complete resource claim set (alias)",
|
|
231
|
-
usage: `${CLI_NAME} resource update [<session-id>|--session <id>] --resource <path-or-glob> --mode <read|write|exclusive-write> [--resource <path-or-glob> --mode <read|write|exclusive-write> ...]`,
|
|
232
|
-
options: [
|
|
233
|
-
option("--resource", "Repository-relative resource; repeatable, each paired with the --mode immediately after it", {
|
|
234
|
-
value: "<path-or-glob>",
|
|
235
|
-
required: true,
|
|
236
|
-
}),
|
|
237
|
-
option("--mode", "Mode for the --resource immediately before it; repeatable", {
|
|
238
|
-
value: "<read|write|exclusive-write>",
|
|
239
|
-
required: true,
|
|
240
|
-
}),
|
|
241
|
-
option("--if-generation", "Expected claim-set generation for CAS; mutually exclusive with --force", {
|
|
242
|
-
value: "<non-negative-integer>",
|
|
243
|
-
}),
|
|
244
|
-
option("--force", "Explicitly permit unconditional complete replacement; mutually exclusive with --if-generation"),
|
|
245
|
-
option("--session", "Target active session; omitted resolves the current owner", { value: "<id>" }),
|
|
246
|
-
option("--repository", "Expected repository identity", { value: "<id>" }),
|
|
247
|
-
],
|
|
248
|
-
notes: [
|
|
249
|
-
"The desired claim set is a full replacement performed atomically in one updateClaims() transaction; " +
|
|
250
|
-
"use exactly one concurrency intent: --if-generation <non-negative-integer> for claim-set generation CAS, " +
|
|
251
|
-
"or explicit --force for unconditional replacement. On any invalid, conflicting, or stale claim the prior set is left unchanged.",
|
|
252
|
-
"Each --resource must be immediately followed by its own --mode; pairing is positional adjacency, not flag order.",
|
|
253
|
-
"Target grammar matches session update: optional first positional <session-id> is an alias for --session <id>; do not supply both.",
|
|
254
|
-
],
|
|
255
|
-
},
|
|
256
|
-
{
|
|
257
|
-
name: "resource mutate",
|
|
258
|
-
summary: "Atomically apply exact-resource claim additions, changes, and releases (alias)",
|
|
259
|
-
usage: `${CLI_NAME} resource mutate [<session>|--session <id>] [--repository <id>] ` +
|
|
260
|
-
`(--upsert-resource <path-or-glob> --mode <read|write|exclusive-write> | --release-resource <path-or-glob>)+ ` +
|
|
261
|
-
`(--if-generation <non-negative-safe-int> | --force)`,
|
|
262
|
-
options: [
|
|
263
|
-
option("--upsert-resource", "Exact repository-relative resource to add or change; each occurrence must be immediately followed by --mode", { value: "<path-or-glob>" }),
|
|
264
|
-
option("--mode", "Mode for the immediately preceding --upsert-resource", {
|
|
265
|
-
value: "<read|write|exclusive-write>",
|
|
266
|
-
}),
|
|
267
|
-
option("--release-resource", "Exact repository-relative resource to release; repeatable", {
|
|
268
|
-
value: "<path-or-glob>",
|
|
269
|
-
}),
|
|
270
|
-
option("--if-generation", "Expected claim-set generation for CAS; mutually exclusive with --force", {
|
|
271
|
-
value: "<non-negative-safe-int>",
|
|
272
|
-
}),
|
|
273
|
-
option("--force", "Explicitly permit unconditional atomic mutation; mutually exclusive with --if-generation"),
|
|
274
|
-
option("--session", "Target active session; omitted resolves the current owner", { value: "<id>" }),
|
|
275
|
-
option("--repository", "Expected repository identity", { value: "<id>" }),
|
|
276
|
-
],
|
|
277
|
-
notes: [
|
|
278
|
-
"Apply one or more ordered upsert/release deltas in one backend transaction; exactly one concurrency intent is required.",
|
|
279
|
-
"Every --upsert-resource must be immediately followed by its own --mode; --release-resource takes exactly one value.",
|
|
280
|
-
"Target grammar matches session mutate: optional first positional <session> is an alias for --session <id>; do not supply both.",
|
|
281
|
-
],
|
|
282
|
-
},
|
|
283
|
-
{
|
|
284
|
-
name: "resource transition",
|
|
285
|
-
summary: "Atomically transition one exact resource claim mode (alias)",
|
|
286
|
-
usage: `${CLI_NAME} resource transition [<session>|--session <id>] [--repository <id>] ` +
|
|
287
|
-
`--resource <path-or-glob> --mode <read|write|exclusive-write> ` +
|
|
288
|
-
`(--if-generation <non-negative-safe-int> | --force)`,
|
|
289
|
-
options: [
|
|
290
|
-
option("--resource", "Exact repository-relative resource to acquire or change", {
|
|
291
|
-
value: "<path-or-glob>",
|
|
292
|
-
required: true,
|
|
293
|
-
}),
|
|
294
|
-
option("--mode", "Target mode for the immediately preceding --resource", {
|
|
295
|
-
value: "<read|write|exclusive-write>",
|
|
296
|
-
required: true,
|
|
297
|
-
}),
|
|
298
|
-
option("--if-generation", "Expected claim-set generation for CAS; mutually exclusive with --force", {
|
|
299
|
-
value: "<non-negative-safe-int>",
|
|
300
|
-
}),
|
|
301
|
-
option("--force", "Explicitly permit unconditional atomic mutation; mutually exclusive with --if-generation"),
|
|
302
|
-
option("--session", "Target active session; omitted resolves the current owner", { value: "<id>" }),
|
|
303
|
-
option("--repository", "Expected repository identity", { value: "<id>" }),
|
|
304
|
-
],
|
|
305
|
-
notes: [
|
|
306
|
-
"Exactly one resource/mode pair is projected as one atomic upsert delta; same mode is an idempotent no-op.",
|
|
307
|
-
"Use exactly one concurrency intent: --if-generation <non-negative-safe-int> or explicit --force.",
|
|
308
|
-
"The target grammar accepts an optional first positional <session> or --session <id>, but not both.",
|
|
309
|
-
],
|
|
310
|
-
},
|
|
311
|
-
{
|
|
312
|
-
name: "resource list",
|
|
313
|
-
summary: "List canonical resource claims (alias)",
|
|
314
|
-
usage: `${CLI_NAME} resource list [<session-id>|--session <id>]`,
|
|
315
|
-
options: [option("--session", "Select a session; omitted lists all claims", { value: "<id>" })],
|
|
316
|
-
notes: [
|
|
317
|
-
"Target grammar: optional first positional <session-id> is an alias for --session <id>; do not supply both.",
|
|
318
|
-
],
|
|
319
|
-
},
|
|
320
|
-
{
|
|
321
|
-
name: "resource release",
|
|
322
|
-
summary: "Release resource claims (alias)",
|
|
323
|
-
usage: `${CLI_NAME} resource release [<session-id>|--session <id>] (--resource <path-or-glob> ... | --claim-id <id> ... | --all) (--if-generation <n> | --force)`,
|
|
324
|
-
options: [
|
|
325
|
-
option("--session", "Target session; omitted resolves the current owner", { value: "<id>" }),
|
|
326
|
-
option("--resource", "Release one exact canonical resource; repeatable", { value: "<path-or-glob>" }),
|
|
327
|
-
option("--claim-id", "Release one owned claim ID; repeatable", { value: "<id>" }),
|
|
328
|
-
option("--all", "Explicitly release all claims owned by the target session"),
|
|
329
|
-
option("--if-generation", "Require the expected claim-set generation; mutually exclusive with --force", {
|
|
330
|
-
value: "<non-negative-safe-int>",
|
|
331
|
-
}),
|
|
332
|
-
option("--force", "Explicitly allow unconditional release; mutually exclusive with --if-generation"),
|
|
333
|
-
],
|
|
334
|
-
notes: [
|
|
335
|
-
"Target grammar: optional first positional <session-id> is an alias for --session <id>; do not supply both.",
|
|
336
|
-
"Exactly one selector family is required: repeated --resource, repeated --claim-id, or explicit --all.",
|
|
337
|
-
"Exactly one destructive concurrency intent is required: --if-generation <non-negative-safe-int> or --force.",
|
|
338
|
-
],
|
|
339
|
-
},
|
|
340
|
-
{
|
|
341
|
-
name: "session close",
|
|
342
|
-
summary: "Close the current or selected session",
|
|
343
|
-
usage: `${CLI_NAME} session close [<session-id>|--session <id>] [--integrated-revision <rev>] [--fetch-remote <name> --fetch-branch <branch>]`,
|
|
344
|
-
options: [
|
|
345
|
-
option("--session", "Select a session instead of the current worktree owner", { value: "<id>" }),
|
|
346
|
-
option("--integrated-revision", "Externally evidenced revision proving non-ancestry (squash/rebase) integration; independently re-verified via exact Git tree-object equivalence, never trusted blindly", { value: "<rev>" }),
|
|
347
|
-
option("--fetch-remote", "Explicitly fetch one remote integration branch into a disposable proof ref", {
|
|
348
|
-
value: "<name>",
|
|
349
|
-
}),
|
|
350
|
-
option("--fetch-branch", "Integration branch to fetch; requires --fetch-remote", { value: "<branch>" }),
|
|
351
|
-
],
|
|
352
|
-
notes: [
|
|
353
|
-
"Ordinary ancestry-based close remains the cheap/default path and requires no flags.",
|
|
354
|
-
"Network access is never implicit; --fetch-remote and --fetch-branch are accepted only with a full lowercase --integrated-revision SHA.",
|
|
355
|
-
"Fetch updates only a disposable internal proof ref with no tags, FETCH_HEAD, tracking-ref, or local integration-branch changes; remote tip races fail closed.",
|
|
356
|
-
"Nawabari never calls provider APIs such as GitHub; only explicit --fetch-remote/--fetch-branch options connect to the configured Git remote.",
|
|
357
|
-
"Target grammar: optional first positional <session-id> is an alias for --session <id>; do not supply both.",
|
|
358
|
-
],
|
|
359
|
-
},
|
|
360
|
-
{
|
|
361
|
-
name: "session discard",
|
|
362
|
-
summary: "Explicitly discard one selected session and its owned resources",
|
|
363
|
-
usage: `${CLI_NAME} session discard <session-id>|--session <id>`,
|
|
364
|
-
options: [
|
|
365
|
-
option("--session", "Required explicit target; the current session is never inferred", {
|
|
366
|
-
value: "<id>",
|
|
367
|
-
required: true,
|
|
368
|
-
}),
|
|
369
|
-
],
|
|
370
|
-
notes: [
|
|
371
|
-
"Destructive and explicit: unintegrated commits and uncommitted work in the selected owned worktree may be destroyed.",
|
|
372
|
-
"Discard never changes close, gc, doctor, reconciliation, or integration-lineage proof behavior.",
|
|
373
|
-
"Use exactly one target form: positional <session-id> or --session <id>; do not supply both.",
|
|
374
|
-
"Machine JSON mode is non-interactive and deterministic.",
|
|
375
|
-
],
|
|
376
|
-
},
|
|
377
|
-
{
|
|
378
|
-
name: "authorize",
|
|
379
|
-
summary: "Authorize an operation against concrete claims",
|
|
380
|
-
usage: `${CLI_NAME} authorize --operation <name> --resource <path> [--resource <path>] [--session <id>]`,
|
|
381
|
-
options: [
|
|
382
|
-
option("--session", "Assert the current session identity", { value: "<id>" }),
|
|
383
|
-
option("--operation", "Operation vocabulary entry", { value: "<name>", required: true }),
|
|
384
|
-
option("--resource", "Concrete repository-relative path; repeatable", { value: "<path>", required: true }),
|
|
385
|
-
],
|
|
386
|
-
},
|
|
387
|
-
{
|
|
388
|
-
name: "checkpoint",
|
|
389
|
-
summary: "Capture bounded Git execution evidence",
|
|
390
|
-
usage: `${CLI_NAME} checkpoint [--session <id>]`,
|
|
391
|
-
options: [option("--session", "Assert the current session identity", { value: "<id>" })],
|
|
392
|
-
},
|
|
393
|
-
{
|
|
394
|
-
name: "evidence snapshot",
|
|
395
|
-
summary: "Capture bounded read-only evidence for one owned session",
|
|
396
|
-
usage: `${CLI_NAME} evidence snapshot --session <id>`,
|
|
397
|
-
options: [option("--session", "Explicit owned session to observe", { value: "<id>", required: true })],
|
|
398
|
-
notes: ["The result is Git-observable physical evidence only; it contains no task or semantic interpretation."],
|
|
399
|
-
},
|
|
400
|
-
{
|
|
401
|
-
name: "diff",
|
|
402
|
-
summary: "Inspect bounded Git evidence for explicit paths",
|
|
403
|
-
usage: `${CLI_NAME} diff --session <id> --path <path> [options]`,
|
|
404
|
-
options: [
|
|
405
|
-
option("--session", "Explicit owned session to observe", { value: "<id>", required: true }),
|
|
406
|
-
option("--path", "Concrete repository-relative path; repeatable", { value: "<path>", required: true }),
|
|
407
|
-
option("--from", "Commit/ref at the start of the range", { value: "<ref>", default: "HEAD" }),
|
|
408
|
-
option("--to", "Commit/ref at the end of the range; omitted means worktree", { value: "<ref>" }),
|
|
409
|
-
option("--patch", "Include patch text; requires the bounded byte/hunk limits"),
|
|
410
|
-
option("--max-bytes", "Maximum UTF-8 patch bytes", { value: "<n>", default: String(EVIDENCE_MAX_DIFF_BYTES) }),
|
|
411
|
-
option("--max-hunks", "Maximum patch hunks", { value: "<n>", default: String(EVIDENCE_MAX_DIFF_HUNKS) }),
|
|
412
|
-
],
|
|
413
|
-
},
|
|
414
|
-
{
|
|
415
|
-
name: "commit",
|
|
416
|
-
summary: "Commit explicit claim-authorized resources",
|
|
417
|
-
usage: `${CLI_NAME} commit --message <final-message> --resource <path> [--resource <path>] [--session <id>] [--message-pattern <regex>]`,
|
|
418
|
-
options: [
|
|
419
|
-
option("--session", "Assert the current session identity", { value: "<id>" }),
|
|
420
|
-
option("--message", "Caller-decided final commit message", { value: "<final-message>", required: true }),
|
|
421
|
-
option("--resource", "Claim-covered concrete path; repeatable", { value: "<path>", required: true }),
|
|
422
|
-
option("--message-pattern", "Caller-declared commit-message rule; validated only when supplied", {
|
|
423
|
-
value: "<regex>",
|
|
424
|
-
}),
|
|
425
|
-
],
|
|
426
|
-
},
|
|
427
|
-
{
|
|
428
|
-
name: "push",
|
|
429
|
-
summary: "Push the owned branch to an explicit target",
|
|
430
|
-
usage: `${CLI_NAME} push --remote <name> --branch <name> --resource <path> [options]`,
|
|
431
|
-
options: [
|
|
432
|
-
option("--session", "Assert the current session identity", { value: "<id>" }),
|
|
433
|
-
option("--resource", "Claim-covered concrete path; repeatable", { value: "<path>", required: true }),
|
|
434
|
-
option("--remote", "Explicit Git remote", { value: "<name>", required: true }),
|
|
435
|
-
option("--branch", "Explicit target branch", { value: "<name>", required: true }),
|
|
436
|
-
option("--remote-branch", "Explicit remote branch alias for --branch", { value: "<name>" }),
|
|
437
|
-
option("--force", "Allow force-with-lease when relation requires it"),
|
|
438
|
-
option("--create-upstream", "Allow creation of a missing upstream"),
|
|
439
|
-
],
|
|
440
|
-
},
|
|
441
|
-
{
|
|
442
|
-
name: "status",
|
|
443
|
-
summary: "Show repository context and bounded session status",
|
|
444
|
-
usage: `${CLI_NAME} status [--all|--history]`,
|
|
445
|
-
options: [
|
|
446
|
-
option("--all", "Include closed history; explicit unbounded history view"),
|
|
447
|
-
option("--history", "Alias for --all"),
|
|
448
|
-
],
|
|
449
|
-
notes: ["The default machine result exposes managed_worktree_root for session-create path discovery."],
|
|
450
|
-
},
|
|
451
|
-
{
|
|
452
|
-
name: "guard",
|
|
453
|
-
summary: "Authorize the current worktree or operation",
|
|
454
|
-
usage: `${CLI_NAME} guard [--session <id>] [--operation <name> --resource <path>]`,
|
|
455
|
-
options: [
|
|
456
|
-
option("--session", "Assert the current session identity", { value: "<id>" }),
|
|
457
|
-
option("--operation", "Authorize an operation when resources are supplied", { value: "<name>" }),
|
|
458
|
-
option("--resource", "Concrete resource; repeatable with --operation", { value: "<path>" }),
|
|
459
|
-
],
|
|
460
|
-
},
|
|
461
|
-
{
|
|
462
|
-
name: "gc",
|
|
463
|
-
summary: "Detect or clean eligible stale sessions",
|
|
464
|
-
usage: `${CLI_NAME} gc [--dry-run|--apply]`,
|
|
465
|
-
options: [
|
|
466
|
-
option("--apply", "Apply only cleanup that passes safety checks"),
|
|
467
|
-
option("--dry-run", "Preflight eligible stale cleanup without mutation"),
|
|
468
|
-
],
|
|
469
|
-
notes: [
|
|
470
|
-
"Default stale threshold is 24 hours (86,400,000 ms). Eligibility uses persisted state age or missing/prunable Git worktree physical state; closed history is not a stale candidate.",
|
|
471
|
-
],
|
|
472
|
-
},
|
|
473
|
-
{
|
|
474
|
-
name: "doctor",
|
|
475
|
-
summary: "Check local Nawabari prerequisites and reconciliation",
|
|
476
|
-
usage: `${CLI_NAME} doctor`,
|
|
477
|
-
options: [],
|
|
478
|
-
},
|
|
479
|
-
{
|
|
480
|
-
name: "capabilities",
|
|
481
|
-
summary: "Describe the standalone CLI/JSON contract",
|
|
482
|
-
usage: `${CLI_NAME} capabilities`,
|
|
483
|
-
options: [],
|
|
484
|
-
},
|
|
15
|
+
import { CLI_COMMAND_REGISTRY, GLOBAL_HELP_OPTIONS, ROOT_HELP_SPEC, publicCliCommandDefinitions, publicCliCommandNames, resolveCliCommandDefinition, canonicalCommandForName, } from "./cli-command-registry.js";
|
|
16
|
+
export { CLI_COMMAND_REGISTRY, COMMAND_REGISTRY, publicCliCommandDefinitions, publicCliCommandNames, resolveCliCommandDefinition, canonicalCommandForName, } from "./cli-command-registry.js";
|
|
17
|
+
/**
|
|
18
|
+
* The dispatcher deliberately keeps its command branches specialized, but
|
|
19
|
+
* this inventory makes the executable surface explicit for structural parity
|
|
20
|
+
* checks. Aliases are listed because they are public entry points too.
|
|
21
|
+
*/
|
|
22
|
+
export const DISPATCHER_COMMAND_INVENTORY = [
|
|
23
|
+
"session create",
|
|
24
|
+
"session id",
|
|
25
|
+
"session show",
|
|
26
|
+
"session inspect",
|
|
27
|
+
"session run",
|
|
28
|
+
"session exec",
|
|
29
|
+
"session list",
|
|
30
|
+
"session claim",
|
|
31
|
+
"resource claim",
|
|
32
|
+
"session update",
|
|
33
|
+
"resource update",
|
|
34
|
+
"session mutate",
|
|
35
|
+
"resource mutate",
|
|
36
|
+
"session transition",
|
|
37
|
+
"resource transition",
|
|
38
|
+
"session claims",
|
|
39
|
+
"resource list",
|
|
40
|
+
"resource claims",
|
|
41
|
+
"session release",
|
|
42
|
+
"resource release",
|
|
43
|
+
"session close",
|
|
44
|
+
"session discard",
|
|
45
|
+
"authorize",
|
|
46
|
+
"checkpoint",
|
|
47
|
+
"evidence snapshot",
|
|
48
|
+
"diff",
|
|
49
|
+
"commit",
|
|
50
|
+
"push",
|
|
51
|
+
"status",
|
|
52
|
+
"guard",
|
|
53
|
+
"gc",
|
|
54
|
+
"doctor",
|
|
55
|
+
"migrate",
|
|
56
|
+
"capabilities",
|
|
485
57
|
];
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
58
|
+
/** Dispatcher option inventory, keyed by canonical registry command identity. */
|
|
59
|
+
export const DISPATCHER_OPTION_INVENTORY = {
|
|
60
|
+
"session create": ["--branch", "--worktree", "--worktree-root", "--base", "--label"],
|
|
61
|
+
"session id": [],
|
|
62
|
+
"session show": ["--session"],
|
|
63
|
+
"session inspect": ["--session", "--integrated-revision"],
|
|
64
|
+
"session run": ["--session"],
|
|
65
|
+
"session list": ["--all", "--history", "--limit", "--offset"],
|
|
66
|
+
"session claim": ["--resource", "--mode", "--session", "--repository"],
|
|
67
|
+
"session update": ["--resource", "--mode", "--if-generation", "--force", "--session", "--repository"],
|
|
68
|
+
"session mutate": [
|
|
69
|
+
"--upsert-resource",
|
|
70
|
+
"--mode",
|
|
71
|
+
"--release-resource",
|
|
72
|
+
"--if-generation",
|
|
73
|
+
"--force",
|
|
74
|
+
"--session",
|
|
75
|
+
"--repository",
|
|
76
|
+
],
|
|
77
|
+
"session transition": ["--resource", "--mode", "--if-generation", "--force", "--session", "--repository"],
|
|
78
|
+
"session claims": ["--session"],
|
|
79
|
+
"session release": ["--session", "--resource", "--claim-id", "--all", "--if-generation", "--force"],
|
|
80
|
+
"session close": ["--session", "--integrated-revision", "--fetch-remote", "--fetch-branch"],
|
|
81
|
+
"session discard": ["--session", "--preview"],
|
|
82
|
+
authorize: ["--session", "--operation", "--resource"],
|
|
83
|
+
checkpoint: ["--session"],
|
|
84
|
+
"evidence snapshot": ["--session"],
|
|
85
|
+
diff: ["--session", "--path", "--from", "--to", "--patch", "--max-bytes", "--max-hunks"],
|
|
86
|
+
commit: ["--session", "--message", "--resource", "--all-claimed", "--message-pattern"],
|
|
87
|
+
push: [
|
|
88
|
+
"--session",
|
|
89
|
+
"--resource",
|
|
90
|
+
"--all-claimed",
|
|
91
|
+
"--remote",
|
|
92
|
+
"--branch",
|
|
93
|
+
"--remote-branch",
|
|
94
|
+
"--force",
|
|
95
|
+
"--create-upstream",
|
|
96
|
+
],
|
|
97
|
+
status: ["--all", "--history", "--limit", "--offset"],
|
|
98
|
+
guard: ["--session", "--operation", "--resource"],
|
|
99
|
+
gc: ["--apply", "--dry-run"],
|
|
100
|
+
doctor: [],
|
|
101
|
+
migrate: [],
|
|
102
|
+
capabilities: [],
|
|
491
103
|
};
|
|
104
|
+
function optionNames(definition) {
|
|
105
|
+
return definition.options.flatMap((candidate) => [candidate.name, ...(candidate.aliases ?? [])]);
|
|
106
|
+
}
|
|
107
|
+
function canonicalName(name) {
|
|
108
|
+
return CLI_COMMAND_REGISTRY.find((definition) => definition.name === name || definition.aliases?.includes(name))
|
|
109
|
+
?.name;
|
|
110
|
+
}
|
|
111
|
+
/** Return the registry-backed option set used by a specialized dispatcher parser. */
|
|
112
|
+
export function dispatcherAllowedOptions(command) {
|
|
113
|
+
const canonical = canonicalName(command);
|
|
114
|
+
if (canonical === undefined)
|
|
115
|
+
throw new Error(`Dispatcher command is not registered: ${command}`);
|
|
116
|
+
return new Set(DISPATCHER_OPTION_INVENTORY[canonical] ?? []);
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Verify both executable command/option inventory and registry metadata. This
|
|
120
|
+
* is intentionally structural: parser semantics remain in their command
|
|
121
|
+
* handlers, while drift becomes a deterministic failure in tests/startup.
|
|
122
|
+
*/
|
|
123
|
+
export function validateCliRegistryParity() {
|
|
124
|
+
const publicNames = publicCliCommandNames();
|
|
125
|
+
const publicSet = new Set(publicNames);
|
|
126
|
+
const dispatcherSet = new Set(DISPATCHER_COMMAND_INVENTORY);
|
|
127
|
+
const missingFromDispatcher = publicNames.filter((name) => !dispatcherSet.has(name));
|
|
128
|
+
const missingFromRegistry = DISPATCHER_COMMAND_INVENTORY.filter((name) => !publicSet.has(name));
|
|
129
|
+
if (missingFromDispatcher.length > 0 || missingFromRegistry.length > 0) {
|
|
130
|
+
throw new Error(`CLI command registry parity failure: missing_from_dispatcher=${missingFromDispatcher.join(",")}; ` +
|
|
131
|
+
`missing_from_registry=${missingFromRegistry.join(",")}`);
|
|
132
|
+
}
|
|
133
|
+
for (const name of publicNames) {
|
|
134
|
+
const canonical = canonicalName(name);
|
|
135
|
+
if (canonical === undefined)
|
|
136
|
+
throw new Error(`CLI command registry cannot resolve ${name}`);
|
|
137
|
+
const expected = new Set(DISPATCHER_OPTION_INVENTORY[canonical] ?? []);
|
|
138
|
+
const actual = new Set(optionNames(resolveCliCommandDefinition(name)));
|
|
139
|
+
const missing = [...actual].filter((option) => !expected.has(option));
|
|
140
|
+
const extra = [...expected].filter((option) => !actual.has(option));
|
|
141
|
+
if (missing.length > 0 || extra.length > 0) {
|
|
142
|
+
throw new Error(`CLI option registry parity failure for ${name}: missing_from_dispatcher=${missing.join(",")}; ` +
|
|
143
|
+
`missing_from_registry=${extra.join(",")}`);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
for (const command of ["authorize", "guard"]) {
|
|
147
|
+
const definition = resolveCliCommandDefinition(command);
|
|
148
|
+
const operation = definition?.options.find((candidate) => candidate.name === "--operation");
|
|
149
|
+
if (operation === undefined || operation.values === undefined) {
|
|
150
|
+
throw new Error(`CLI operation vocabulary parity failure for ${command}: missing --operation values`);
|
|
151
|
+
}
|
|
152
|
+
if (operation.values.join("\u0000") !== OPERATION_VOCABULARY.join("\u0000")) {
|
|
153
|
+
throw new Error(`CLI operation vocabulary parity failure for ${command}: expected=${OPERATION_VOCABULARY.join(",")}; ` +
|
|
154
|
+
`actual=${operation.values.join(",")}`);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
validateCliRegistryParity();
|
|
492
159
|
function helpSpecFor(commandArguments) {
|
|
493
160
|
if (commandArguments.length === 0)
|
|
494
161
|
return ROOT_HELP_SPEC;
|
|
495
|
-
|
|
162
|
+
const key = commandArguments[0] === "resource"
|
|
496
163
|
? `resource ${commandArguments[1] ?? "list"}`
|
|
497
|
-
: commandArguments
|
|
498
|
-
|
|
499
|
-
: commandArguments.slice(0, 2).join(" ");
|
|
500
|
-
if (key === "resource claims")
|
|
501
|
-
key = "resource list";
|
|
502
|
-
const direct = HELP_COMMANDS.find((spec) => spec.name === key);
|
|
503
|
-
if (direct !== undefined && !direct.name.startsWith("resource "))
|
|
504
|
-
return direct;
|
|
505
|
-
const aliasTarget = direct?.name.replace(/^resource /u, "session ");
|
|
506
|
-
const target = aliasTarget === undefined ? undefined : HELP_COMMANDS.find((spec) => spec.name === aliasTarget);
|
|
507
|
-
if (direct !== undefined && target !== undefined) {
|
|
508
|
-
return {
|
|
509
|
-
name: direct.name,
|
|
510
|
-
summary: direct.summary,
|
|
511
|
-
usage: direct.usage,
|
|
512
|
-
options: direct.options,
|
|
513
|
-
notes: direct.notes,
|
|
514
|
-
};
|
|
515
|
-
}
|
|
516
|
-
return HELP_COMMANDS.find((spec) => spec.name === commandArguments[0]) ?? ROOT_HELP_SPEC;
|
|
164
|
+
: commandArguments.slice(0, 2).join(" ");
|
|
165
|
+
return resolveCliCommandDefinition(key) ?? resolveCliCommandDefinition(commandArguments[0]) ?? ROOT_HELP_SPEC;
|
|
517
166
|
}
|
|
518
167
|
function helpPayload(spec) {
|
|
519
168
|
if (spec.name === "root") {
|
|
169
|
+
const publicCommands = publicCliCommandDefinitions();
|
|
520
170
|
const optionNames = (options) => options.map((candidate) => candidate.name);
|
|
521
|
-
const sessionOptions =
|
|
171
|
+
const sessionOptions = publicCommands
|
|
172
|
+
.filter((command) => command.name.startsWith("session "))
|
|
173
|
+
.flatMap((command) => command.options);
|
|
522
174
|
const unique = (values) => values.filter((value, index) => values.indexOf(value) === index);
|
|
523
175
|
const sessionListOnlyOptions = new Set(["--all", "--history"]);
|
|
524
|
-
const
|
|
176
|
+
const sessionTargetingCommands = CLI_COMMAND_REGISTRY.filter((command) => command.name.startsWith("session ") && command.usage.includes("<session")).map((command) => command.name.slice("session ".length));
|
|
177
|
+
const optionsFor = (name) => optionNames(publicCommands.find((command) => command.name === name)?.options ?? []);
|
|
525
178
|
return {
|
|
526
179
|
usage: `Usage: ${spec.usage}`,
|
|
527
|
-
commands:
|
|
528
|
-
options:
|
|
180
|
+
commands: publicCommands.map((command) => command.name),
|
|
181
|
+
options: GLOBAL_HELP_OPTIONS.map((candidate) => candidate.name),
|
|
529
182
|
session_options: unique(sessionOptions.map((option) => option.name).filter((name) => !sessionListOnlyOptions.has(name))),
|
|
530
183
|
authorization_options: optionsFor("authorize"),
|
|
531
184
|
checkpoint_options: optionsFor("checkpoint"),
|
|
@@ -535,18 +188,7 @@ function helpPayload(spec) {
|
|
|
535
188
|
session_targeting: {
|
|
536
189
|
canonical: "--session <id>",
|
|
537
190
|
positional_alias: "<session-id> as the first argument after a session-scoped subcommand",
|
|
538
|
-
commands:
|
|
539
|
-
"show",
|
|
540
|
-
"inspect",
|
|
541
|
-
"claim",
|
|
542
|
-
"claims",
|
|
543
|
-
"release",
|
|
544
|
-
"update",
|
|
545
|
-
"mutate",
|
|
546
|
-
"transition",
|
|
547
|
-
"close",
|
|
548
|
-
"discard",
|
|
549
|
-
],
|
|
191
|
+
commands: sessionTargetingCommands,
|
|
550
192
|
ambiguity: "supplying both positional and --session is rejected",
|
|
551
193
|
discard_requires_explicit_target: true,
|
|
552
194
|
},
|
|
@@ -554,13 +196,26 @@ function helpPayload(spec) {
|
|
|
554
196
|
}
|
|
555
197
|
const options = spec.options.map((candidate) => ({
|
|
556
198
|
name: candidate.name,
|
|
199
|
+
...(candidate.aliases === undefined ? {} : { aliases: [...candidate.aliases] }),
|
|
200
|
+
...(candidate.alias_of === undefined ? {} : { alias_of: candidate.alias_of }),
|
|
557
201
|
...(candidate.value === undefined ? {} : { value: candidate.value }),
|
|
558
202
|
required: candidate.required === true,
|
|
203
|
+
...(candidate.repeatable === undefined ? {} : { repeatable: candidate.repeatable }),
|
|
559
204
|
...(candidate.default === undefined ? {} : { default: candidate.default }),
|
|
205
|
+
...(candidate.minimum === undefined ? {} : { minimum: candidate.minimum }),
|
|
206
|
+
...(candidate.maximum === undefined ? {} : { maximum: candidate.maximum }),
|
|
207
|
+
...(candidate.values === undefined ? {} : { values: [...candidate.values] }),
|
|
208
|
+
...(candidate.required_unless === undefined ? {} : { required_unless: [...candidate.required_unless] }),
|
|
209
|
+
...(candidate.mutually_exclusive_with === undefined
|
|
210
|
+
? {}
|
|
211
|
+
: { mutually_exclusive_with: [...candidate.mutually_exclusive_with] }),
|
|
560
212
|
description: candidate.description,
|
|
561
213
|
}));
|
|
214
|
+
const canonical = canonicalCommandForName(spec.name);
|
|
562
215
|
return {
|
|
563
216
|
help_for: spec.name,
|
|
217
|
+
...(canonical === undefined || canonical.name === spec.name ? {} : { canonical_command: canonical.name }),
|
|
218
|
+
...(canonical?.aliases === undefined || canonical.name !== spec.name ? {} : { aliases: [...canonical.aliases] }),
|
|
564
219
|
usage: spec.usage,
|
|
565
220
|
summary: spec.summary,
|
|
566
221
|
required_options: spec.options
|
|
@@ -580,8 +235,9 @@ function helpText(spec) {
|
|
|
580
235
|
const lines = [`Usage: ${spec.usage}`, "", spec.summary];
|
|
581
236
|
if (spec.name === "root") {
|
|
582
237
|
lines.push("", "Commands:");
|
|
583
|
-
for (const command of
|
|
238
|
+
for (const command of publicCliCommandDefinitions()) {
|
|
584
239
|
lines.push(` ${command.name.padEnd(20)} ${command.summary}`);
|
|
240
|
+
}
|
|
585
241
|
lines.push("", "Global options:");
|
|
586
242
|
for (const candidate of GLOBAL_HELP_OPTIONS) {
|
|
587
243
|
const label = candidate.name === "--help" ? "-h, --help" : candidate.name;
|
|
@@ -597,7 +253,15 @@ function helpText(spec) {
|
|
|
597
253
|
const qualifier = candidate.required === true
|
|
598
254
|
? "required"
|
|
599
255
|
: `optional${candidate.default === undefined ? "" : `; default: ${candidate.default}`}`;
|
|
600
|
-
|
|
256
|
+
const bounds = candidate.minimum === undefined && candidate.maximum === undefined
|
|
257
|
+
? ""
|
|
258
|
+
: `; bounds: ${candidate.minimum ?? "-∞"}..${candidate.maximum ?? "∞"}`;
|
|
259
|
+
const values = candidate.values === undefined ? "" : `; values: ${candidate.values.join(", ")}`;
|
|
260
|
+
const requiredUnless = candidate.required_unless === undefined ? "" : `; required unless: ${candidate.required_unless.join(", ")}`;
|
|
261
|
+
const mutuallyExclusive = candidate.mutually_exclusive_with === undefined
|
|
262
|
+
? ""
|
|
263
|
+
: `; mutually exclusive with: ${candidate.mutually_exclusive_with.join(", ")}`;
|
|
264
|
+
lines.push(` ${label.padEnd(38)} ${qualifier}${bounds}${values}${requiredUnless}${mutuallyExclusive}; ${candidate.description}`);
|
|
601
265
|
}
|
|
602
266
|
if (spec.notes !== undefined) {
|
|
603
267
|
lines.push("", "Notes:");
|
|
@@ -675,6 +339,7 @@ function parseOptions(arguments_, allowed) {
|
|
|
675
339
|
apply: false,
|
|
676
340
|
force: false,
|
|
677
341
|
create_upstream: false,
|
|
342
|
+
all_claimed: false,
|
|
678
343
|
all: false,
|
|
679
344
|
history: false,
|
|
680
345
|
limit: null,
|
|
@@ -688,6 +353,7 @@ function parseOptions(arguments_, allowed) {
|
|
|
688
353
|
integrated_revision: null,
|
|
689
354
|
fetch_remote: null,
|
|
690
355
|
fetch_branch: null,
|
|
356
|
+
preview: false,
|
|
691
357
|
};
|
|
692
358
|
let dryRun = false;
|
|
693
359
|
for (let index = 0; index < arguments_.length; index += 1) {
|
|
@@ -699,9 +365,11 @@ function parseOptions(arguments_, allowed) {
|
|
|
699
365
|
name === "--dry-run" ||
|
|
700
366
|
name === "--force" ||
|
|
701
367
|
name === "--create-upstream" ||
|
|
368
|
+
name === "--all-claimed" ||
|
|
702
369
|
name === "--all" ||
|
|
703
370
|
name === "--history" ||
|
|
704
|
-
name === "--patch"
|
|
371
|
+
name === "--patch" ||
|
|
372
|
+
name === "--preview") {
|
|
705
373
|
if (inlineValue !== null) {
|
|
706
374
|
return failure(usageError("INVALID_ARGUMENT", `${name} does not accept a value.`, { option: name }));
|
|
707
375
|
}
|
|
@@ -713,12 +381,16 @@ function parseOptions(arguments_, allowed) {
|
|
|
713
381
|
options.force = true;
|
|
714
382
|
else if (name === "--create-upstream")
|
|
715
383
|
options.create_upstream = true;
|
|
384
|
+
else if (name === "--all-claimed")
|
|
385
|
+
options.all_claimed = true;
|
|
716
386
|
else if (name === "--all")
|
|
717
387
|
options.all = true;
|
|
718
388
|
else if (name === "--history")
|
|
719
389
|
options.history = true;
|
|
720
|
-
else
|
|
390
|
+
else if (name === "--patch")
|
|
721
391
|
options.patch = true;
|
|
392
|
+
else
|
|
393
|
+
options.preview = true;
|
|
722
394
|
continue;
|
|
723
395
|
}
|
|
724
396
|
const value = inlineValue ?? arguments_[index + 1];
|
|
@@ -895,7 +567,7 @@ function parseReleaseSelection(arguments_) {
|
|
|
895
567
|
expected_claim_set_generation: null,
|
|
896
568
|
force: false,
|
|
897
569
|
};
|
|
898
|
-
const recognized =
|
|
570
|
+
const recognized = dispatcherAllowedOptions("session release");
|
|
899
571
|
const targetArguments = positional.value.arguments;
|
|
900
572
|
for (let index = 0; index < targetArguments.length; index += 1) {
|
|
901
573
|
const { name, inlineValue } = optionParts(targetArguments[index]);
|
|
@@ -985,14 +657,7 @@ function parseClaimDeltaMutation(arguments_) {
|
|
|
985
657
|
expected_claim_set_generation: null,
|
|
986
658
|
force: false,
|
|
987
659
|
};
|
|
988
|
-
const recognized =
|
|
989
|
-
"--session",
|
|
990
|
-
"--repository",
|
|
991
|
-
"--upsert-resource",
|
|
992
|
-
"--mode",
|
|
993
|
-
"--release-resource",
|
|
994
|
-
...CLAIM_CONCURRENCY_OPTION_NAMES,
|
|
995
|
-
]);
|
|
660
|
+
const recognized = dispatcherAllowedOptions("session mutate");
|
|
996
661
|
const rejectPending = (name) => failure(usageError("INVALID_ARGUMENT", `${name} cannot appear between --upsert-resource and its --mode; only --mode is permitted immediately after --upsert-resource.`, { option: name }));
|
|
997
662
|
const targetArguments = positional.value.arguments;
|
|
998
663
|
for (let index = 0; index < targetArguments.length; index += 1) {
|
|
@@ -1086,7 +751,7 @@ function parseClaimDeltaMutation(arguments_) {
|
|
|
1086
751
|
* constrains its public projection to exactly one upsert pair.
|
|
1087
752
|
*/
|
|
1088
753
|
function parseClaimTransition(arguments_) {
|
|
1089
|
-
const parsed = parseClaimReplacementPairs(arguments_);
|
|
754
|
+
const parsed = parseClaimReplacementPairs(arguments_, true, "session transition");
|
|
1090
755
|
if (!parsed.ok)
|
|
1091
756
|
return parsed;
|
|
1092
757
|
if (parsed.value.pairs.length !== 1) {
|
|
@@ -1121,7 +786,7 @@ function parseClaimTransition(arguments_) {
|
|
|
1121
786
|
* parallel-array position, so argv order can never associate a resource
|
|
1122
787
|
* with the wrong mode.
|
|
1123
788
|
*/
|
|
1124
|
-
function parseClaimReplacementPairs(arguments_, requireConcurrencyIntent = true) {
|
|
789
|
+
function parseClaimReplacementPairs(arguments_, requireConcurrencyIntent = true, command = "session update") {
|
|
1125
790
|
const positional = splitPositionalSessionTarget(arguments_);
|
|
1126
791
|
if (!positional.ok)
|
|
1127
792
|
return positional;
|
|
@@ -1135,14 +800,11 @@ function parseClaimReplacementPairs(arguments_, requireConcurrencyIntent = true)
|
|
|
1135
800
|
};
|
|
1136
801
|
sessionId = positional.value.sessionId;
|
|
1137
802
|
const targetArguments = positional.value.arguments;
|
|
803
|
+
const recognized = dispatcherAllowedOptions(command);
|
|
1138
804
|
for (let index = 0; index < targetArguments.length; index += 1) {
|
|
1139
805
|
const { name, inlineValue } = optionParts(targetArguments[index]);
|
|
1140
806
|
const isConcurrencyOption = isClaimConcurrencyOption(name);
|
|
1141
|
-
if (name
|
|
1142
|
-
name !== "--repository" &&
|
|
1143
|
-
name !== "--resource" &&
|
|
1144
|
-
name !== "--mode" &&
|
|
1145
|
-
!(requireConcurrencyIntent && isConcurrencyOption)) {
|
|
807
|
+
if (!recognized.has(name) || (!requireConcurrencyIntent && isConcurrencyOption)) {
|
|
1146
808
|
return failure(usageError("INVALID_ARGUMENT", `Unknown option: ${name}.`, { option: name }));
|
|
1147
809
|
}
|
|
1148
810
|
if (pendingResource !== null && name !== "--mode") {
|
|
@@ -1210,7 +872,7 @@ function parseClaimReplacementPairs(arguments_, requireConcurrencyIntent = true)
|
|
|
1210
872
|
* first pair (last-wins).
|
|
1211
873
|
*/
|
|
1212
874
|
function parseSingleClaimPair(arguments_) {
|
|
1213
|
-
const parsed = parseClaimReplacementPairs(arguments_, false);
|
|
875
|
+
const parsed = parseClaimReplacementPairs(arguments_, false, "session claim");
|
|
1214
876
|
if (!parsed.ok)
|
|
1215
877
|
return parsed;
|
|
1216
878
|
if (parsed.value.pairs.length > 1) {
|
|
@@ -1333,6 +995,39 @@ async function executeRelease(arguments_, backend, context, operation) {
|
|
|
1333
995
|
const result = await backend.releaseClaims(context, options);
|
|
1334
996
|
return result.ok ? { ok: true, value: result.value } : result;
|
|
1335
997
|
}
|
|
998
|
+
/**
|
|
999
|
+
* The one dispatcher boundary for protected managed execution.
|
|
1000
|
+
*
|
|
1001
|
+
* The public registry owns the command identity (including aliases); this
|
|
1002
|
+
* function owns only the routing contract. Resolution is always enforced
|
|
1003
|
+
* and the resolved request is handed to the #145 launcher. In particular,
|
|
1004
|
+
* there is no ambient child-process path to select when resolution or launch
|
|
1005
|
+
* fails.
|
|
1006
|
+
*/
|
|
1007
|
+
async function executeProtectedSessionCommand(arguments_, dependencies, context) {
|
|
1008
|
+
const delimiter = arguments_.indexOf("--");
|
|
1009
|
+
if (delimiter === -1) {
|
|
1010
|
+
return failure(usageError("MISSING_ARGUMENT", "session run requires a -- terminator before the command."));
|
|
1011
|
+
}
|
|
1012
|
+
const parsed = parseOptions(arguments_.slice(0, delimiter), new Set(["--session"]));
|
|
1013
|
+
if (!parsed.ok)
|
|
1014
|
+
return parsed;
|
|
1015
|
+
const command = arguments_[delimiter + 1];
|
|
1016
|
+
if (command === undefined || command.length === 0) {
|
|
1017
|
+
return failure(usageError("MISSING_ARGUMENT", "session run requires a command after --."));
|
|
1018
|
+
}
|
|
1019
|
+
const request = await resolveSandboxExecutionRequest(dependencies.backend, context, { session_id: parsed.value.session_id, enforce: true }, dependencies.sandboxProbe, dependencies.sandboxRuntimeLayout);
|
|
1020
|
+
if (!request.ok)
|
|
1021
|
+
return request;
|
|
1022
|
+
// `sandboxRunner` is an injection seam for unit tests. Production always
|
|
1023
|
+
// reaches the canonical protected launcher exported by #145.
|
|
1024
|
+
const runner = dependencies.sandboxRunner ?? runSandboxedCommand;
|
|
1025
|
+
const result = await runner(request.value, {
|
|
1026
|
+
command,
|
|
1027
|
+
args: arguments_.slice(delimiter + 2),
|
|
1028
|
+
});
|
|
1029
|
+
return result.ok ? { ok: true, value: result.value } : result;
|
|
1030
|
+
}
|
|
1336
1031
|
async function executeCommand(commandArguments, dependencies) {
|
|
1337
1032
|
const [command, subcommand, ...rest] = commandArguments;
|
|
1338
1033
|
const context = sessionContext(dependencies.cwd);
|
|
@@ -1340,6 +1035,12 @@ async function executeCommand(commandArguments, dependencies) {
|
|
|
1340
1035
|
if (subcommand === undefined) {
|
|
1341
1036
|
return failure(usageError("MISSING_ARGUMENT", "session requires a subcommand."));
|
|
1342
1037
|
}
|
|
1038
|
+
// Resolve the subcommand through the canonical registry before dispatch.
|
|
1039
|
+
// This keeps `session exec` an alias of the same protected route instead
|
|
1040
|
+
// of creating a second launch path.
|
|
1041
|
+
if (canonicalCommandForName(`session ${subcommand}`)?.name === "session run") {
|
|
1042
|
+
return executeProtectedSessionCommand(rest, dependencies, context);
|
|
1043
|
+
}
|
|
1343
1044
|
if (subcommand === "claim") {
|
|
1344
1045
|
const parsed = parseSingleClaimPair(rest);
|
|
1345
1046
|
if (!parsed.ok)
|
|
@@ -1380,7 +1081,7 @@ async function executeCommand(commandArguments, dependencies) {
|
|
|
1380
1081
|
return executeClaimTransition(rest, dependencies.backend, context, "session transition");
|
|
1381
1082
|
}
|
|
1382
1083
|
if (subcommand === "claims") {
|
|
1383
|
-
const parsed = parseTargetedOptions(rest,
|
|
1084
|
+
const parsed = parseTargetedOptions(rest, dispatcherAllowedOptions("session claims"));
|
|
1384
1085
|
if (!parsed.ok)
|
|
1385
1086
|
return parsed;
|
|
1386
1087
|
if (dependencies.backend.listClaims === undefined)
|
|
@@ -1392,7 +1093,7 @@ async function executeCommand(commandArguments, dependencies) {
|
|
|
1392
1093
|
return executeRelease(rest, dependencies.backend, context, "session release");
|
|
1393
1094
|
}
|
|
1394
1095
|
if (subcommand === "create") {
|
|
1395
|
-
const parsed = parseOptions(rest,
|
|
1096
|
+
const parsed = parseOptions(rest, dispatcherAllowedOptions("session create"));
|
|
1396
1097
|
if (!parsed.ok)
|
|
1397
1098
|
return parsed;
|
|
1398
1099
|
if (parsed.value.worktree !== null && parsed.value.worktree_root !== null) {
|
|
@@ -1408,30 +1109,8 @@ async function executeCommand(commandArguments, dependencies) {
|
|
|
1408
1109
|
const result = await dependencies.backend.createSession(context, options);
|
|
1409
1110
|
return result.ok ? { ok: true, value: result.value } : result;
|
|
1410
1111
|
}
|
|
1411
|
-
if (subcommand === "run" || subcommand === "exec") {
|
|
1412
|
-
const delimiter = rest.indexOf("--");
|
|
1413
|
-
if (delimiter === -1) {
|
|
1414
|
-
return failure(usageError("MISSING_ARGUMENT", "session run requires a -- terminator before the command."));
|
|
1415
|
-
}
|
|
1416
|
-
const parsed = parseOptions(rest.slice(0, delimiter), new Set(["--session"]));
|
|
1417
|
-
if (!parsed.ok)
|
|
1418
|
-
return parsed;
|
|
1419
|
-
const command = rest[delimiter + 1];
|
|
1420
|
-
if (command === undefined || command.length === 0) {
|
|
1421
|
-
return failure(usageError("MISSING_ARGUMENT", "session run requires a command after --."));
|
|
1422
|
-
}
|
|
1423
|
-
const request = await resolveSandboxExecutionRequest(dependencies.backend, context, { session_id: parsed.value.session_id, enforce: true }, dependencies.sandboxProbe, dependencies.sandboxRuntimeLayout);
|
|
1424
|
-
if (!request.ok)
|
|
1425
|
-
return request;
|
|
1426
|
-
const runner = dependencies.sandboxRunner ?? runSandboxedCommand;
|
|
1427
|
-
const result = await runner(request.value, {
|
|
1428
|
-
command,
|
|
1429
|
-
args: rest.slice(delimiter + 2),
|
|
1430
|
-
});
|
|
1431
|
-
return result.ok ? { ok: true, value: result.value } : result;
|
|
1432
|
-
}
|
|
1433
1112
|
if (subcommand === "inspect") {
|
|
1434
|
-
const parsed = parseTargetedOptions(rest,
|
|
1113
|
+
const parsed = parseTargetedOptions(rest, dispatcherAllowedOptions("session inspect"));
|
|
1435
1114
|
if (!parsed.ok)
|
|
1436
1115
|
return parsed;
|
|
1437
1116
|
if (dependencies.backend.sessionDiagnostic === undefined)
|
|
@@ -1444,9 +1123,15 @@ async function executeCommand(commandArguments, dependencies) {
|
|
|
1444
1123
|
return result.ok ? { ok: true, value: result.value } : result;
|
|
1445
1124
|
}
|
|
1446
1125
|
if (subcommand === "discard") {
|
|
1447
|
-
const parsed = parseTargetedOptions(rest,
|
|
1126
|
+
const parsed = parseTargetedOptions(rest, dispatcherAllowedOptions("session discard"), true);
|
|
1448
1127
|
if (!parsed.ok)
|
|
1449
1128
|
return parsed;
|
|
1129
|
+
if (parsed.value.preview) {
|
|
1130
|
+
if (dependencies.backend.discardPreview === undefined)
|
|
1131
|
+
return sessionDiscardPreviewCapabilityUnavailable();
|
|
1132
|
+
const preview = await dependencies.backend.discardPreview(context, parsed.value.session_id);
|
|
1133
|
+
return preview.ok ? { ok: true, value: preview.value } : preview;
|
|
1134
|
+
}
|
|
1450
1135
|
if (dependencies.backend.discardSession === undefined)
|
|
1451
1136
|
return sessionDiscardCapabilityUnavailable();
|
|
1452
1137
|
const result = await dependencies.backend.discardSession(context, parsed.value.session_id);
|
|
@@ -1462,9 +1147,7 @@ async function executeCommand(commandArguments, dependencies) {
|
|
|
1462
1147
|
return selected;
|
|
1463
1148
|
return { ok: true, value: { session_id: selected.value.session_id } };
|
|
1464
1149
|
}
|
|
1465
|
-
const parsed = parseTargetedOptions(rest,
|
|
1466
|
-
? ["--session", "--integrated-revision", "--fetch-remote", "--fetch-branch"]
|
|
1467
|
-
: ["--session"]));
|
|
1150
|
+
const parsed = parseTargetedOptions(rest, dispatcherAllowedOptions(subcommand === "close" ? "session close" : "session show"));
|
|
1468
1151
|
if (!parsed.ok)
|
|
1469
1152
|
return parsed;
|
|
1470
1153
|
if (subcommand === "close") {
|
|
@@ -1491,7 +1174,7 @@ async function executeCommand(commandArguments, dependencies) {
|
|
|
1491
1174
|
return selected.ok ? { ok: true, value: selected.value } : selected;
|
|
1492
1175
|
}
|
|
1493
1176
|
if (subcommand === "list") {
|
|
1494
|
-
const parsed = parseOptions(rest,
|
|
1177
|
+
const parsed = parseOptions(rest, dispatcherAllowedOptions("session list"));
|
|
1495
1178
|
if (!parsed.ok)
|
|
1496
1179
|
return parsed;
|
|
1497
1180
|
const options = sessionListingOptions(parsed.value);
|
|
@@ -1544,7 +1227,7 @@ async function executeCommand(commandArguments, dependencies) {
|
|
|
1544
1227
|
return executeClaimTransition(rest, dependencies.backend, context, "resource transition");
|
|
1545
1228
|
}
|
|
1546
1229
|
if (resourceSubcommand === "list" || resourceSubcommand === "claims") {
|
|
1547
|
-
const parsed = parseTargetedOptions(rest,
|
|
1230
|
+
const parsed = parseTargetedOptions(rest, dispatcherAllowedOptions("session claims"));
|
|
1548
1231
|
if (!parsed.ok)
|
|
1549
1232
|
return parsed;
|
|
1550
1233
|
if (dependencies.backend.listClaims === undefined)
|
|
@@ -1560,7 +1243,7 @@ async function executeCommand(commandArguments, dependencies) {
|
|
|
1560
1243
|
}));
|
|
1561
1244
|
}
|
|
1562
1245
|
if (command === "authorize") {
|
|
1563
|
-
const parsed = parseOptions([subcommand, ...rest].filter((argument) => argument !== undefined),
|
|
1246
|
+
const parsed = parseOptions([subcommand, ...rest].filter((argument) => argument !== undefined), dispatcherAllowedOptions("authorize"));
|
|
1564
1247
|
if (!parsed.ok)
|
|
1565
1248
|
return parsed;
|
|
1566
1249
|
if (parsed.value.operation === null) {
|
|
@@ -1584,7 +1267,7 @@ async function executeCommand(commandArguments, dependencies) {
|
|
|
1584
1267
|
: deniedAuthorization(result.value);
|
|
1585
1268
|
}
|
|
1586
1269
|
if (command === "checkpoint") {
|
|
1587
|
-
const parsed = parseOptions([subcommand, ...rest].filter((argument) => argument !== undefined),
|
|
1270
|
+
const parsed = parseOptions([subcommand, ...rest].filter((argument) => argument !== undefined), dispatcherAllowedOptions("checkpoint"));
|
|
1588
1271
|
if (!parsed.ok)
|
|
1589
1272
|
return parsed;
|
|
1590
1273
|
if (dependencies.backend.checkpoint === undefined)
|
|
@@ -1594,7 +1277,7 @@ async function executeCommand(commandArguments, dependencies) {
|
|
|
1594
1277
|
return result.ok ? { ok: true, value: result.value } : result;
|
|
1595
1278
|
}
|
|
1596
1279
|
if (command === "evidence" && subcommand === "snapshot") {
|
|
1597
|
-
const parsed = parseOptions(rest,
|
|
1280
|
+
const parsed = parseOptions(rest, dispatcherAllowedOptions("evidence snapshot"));
|
|
1598
1281
|
if (!parsed.ok)
|
|
1599
1282
|
return parsed;
|
|
1600
1283
|
if (parsed.value.session_id === null) {
|
|
@@ -1608,7 +1291,7 @@ async function executeCommand(commandArguments, dependencies) {
|
|
|
1608
1291
|
return result.ok ? { ok: true, value: result.value } : result;
|
|
1609
1292
|
}
|
|
1610
1293
|
if (command === "diff") {
|
|
1611
|
-
const parsed = parseOptions([subcommand, ...rest].filter((argument) => argument !== undefined),
|
|
1294
|
+
const parsed = parseOptions([subcommand, ...rest].filter((argument) => argument !== undefined), dispatcherAllowedOptions("diff"));
|
|
1612
1295
|
if (!parsed.ok)
|
|
1613
1296
|
return parsed;
|
|
1614
1297
|
if (parsed.value.session_id === null) {
|
|
@@ -1645,13 +1328,18 @@ async function executeCommand(commandArguments, dependencies) {
|
|
|
1645
1328
|
return result.ok ? { ok: true, value: result.value } : result;
|
|
1646
1329
|
}
|
|
1647
1330
|
if (command === "commit") {
|
|
1648
|
-
const parsed = parseOptions([subcommand, ...rest].filter((argument) => argument !== undefined),
|
|
1331
|
+
const parsed = parseOptions([subcommand, ...rest].filter((argument) => argument !== undefined), dispatcherAllowedOptions("commit"));
|
|
1649
1332
|
if (!parsed.ok)
|
|
1650
1333
|
return parsed;
|
|
1651
1334
|
if (parsed.value.message === null) {
|
|
1652
1335
|
return failure(usageError("MISSING_ARGUMENT", "--message requires a value.", { option: "--message" }));
|
|
1653
1336
|
}
|
|
1654
|
-
if (parsed.value.resources.length
|
|
1337
|
+
if (parsed.value.all_claimed && parsed.value.resources.length > 0) {
|
|
1338
|
+
return failure(usageError("INVALID_ARGUMENT", "--resource and --all-claimed cannot be used together.", {
|
|
1339
|
+
options: ["--resource", "--all-claimed"],
|
|
1340
|
+
}));
|
|
1341
|
+
}
|
|
1342
|
+
if (!parsed.value.all_claimed && parsed.value.resources.length === 0) {
|
|
1655
1343
|
return failure(usageError("MISSING_ARGUMENT", "--resource requires a value.", { option: "--resource" }));
|
|
1656
1344
|
}
|
|
1657
1345
|
if (dependencies.backend.commit === undefined)
|
|
@@ -1660,12 +1348,13 @@ async function executeCommand(commandArguments, dependencies) {
|
|
|
1660
1348
|
session_id: parsed.value.session_id,
|
|
1661
1349
|
message: parsed.value.message,
|
|
1662
1350
|
resources: parsed.value.resources,
|
|
1351
|
+
...(parsed.value.all_claimed ? { all_claimed: true } : {}),
|
|
1663
1352
|
message_pattern: parsed.value.message_pattern,
|
|
1664
1353
|
});
|
|
1665
1354
|
return result.ok ? { ok: true, value: result.value } : result;
|
|
1666
1355
|
}
|
|
1667
1356
|
if (command === "push") {
|
|
1668
|
-
const parsed = parseOptions([subcommand, ...rest].filter((argument) => argument !== undefined),
|
|
1357
|
+
const parsed = parseOptions([subcommand, ...rest].filter((argument) => argument !== undefined), dispatcherAllowedOptions("push"));
|
|
1669
1358
|
if (!parsed.ok)
|
|
1670
1359
|
return parsed;
|
|
1671
1360
|
if (parsed.value.remote === null) {
|
|
@@ -1679,7 +1368,12 @@ async function executeCommand(commandArguments, dependencies) {
|
|
|
1679
1368
|
parsed.value.branch !== parsed.value.remote_branch) {
|
|
1680
1369
|
return failure(usageError("INVALID_ARGUMENT", "--branch and --remote-branch must identify the same target."));
|
|
1681
1370
|
}
|
|
1682
|
-
if (parsed.value.resources.length
|
|
1371
|
+
if (parsed.value.all_claimed && parsed.value.resources.length > 0) {
|
|
1372
|
+
return failure(usageError("INVALID_ARGUMENT", "--resource and --all-claimed cannot be used together.", {
|
|
1373
|
+
options: ["--resource", "--all-claimed"],
|
|
1374
|
+
}));
|
|
1375
|
+
}
|
|
1376
|
+
if (!parsed.value.all_claimed && parsed.value.resources.length === 0) {
|
|
1683
1377
|
return failure(usageError("MISSING_ARGUMENT", "--resource requires a value.", { option: "--resource" }));
|
|
1684
1378
|
}
|
|
1685
1379
|
if (dependencies.backend.push === undefined)
|
|
@@ -1687,6 +1381,7 @@ async function executeCommand(commandArguments, dependencies) {
|
|
|
1687
1381
|
const result = await dependencies.backend.push(context, {
|
|
1688
1382
|
session_id: parsed.value.session_id,
|
|
1689
1383
|
resources: parsed.value.resources,
|
|
1384
|
+
...(parsed.value.all_claimed ? { all_claimed: true } : {}),
|
|
1690
1385
|
remote: parsed.value.remote,
|
|
1691
1386
|
branch: parsed.value.branch ?? parsed.value.remote_branch,
|
|
1692
1387
|
force: parsed.value.force,
|
|
@@ -1695,7 +1390,7 @@ async function executeCommand(commandArguments, dependencies) {
|
|
|
1695
1390
|
return result.ok ? { ok: true, value: result.value } : result;
|
|
1696
1391
|
}
|
|
1697
1392
|
if (command === "status") {
|
|
1698
|
-
const parsed = parseOptions([subcommand, ...rest].filter((argument) => argument !== undefined),
|
|
1393
|
+
const parsed = parseOptions([subcommand, ...rest].filter((argument) => argument !== undefined), dispatcherAllowedOptions("status"));
|
|
1699
1394
|
if (!parsed.ok)
|
|
1700
1395
|
return parsed;
|
|
1701
1396
|
const options = sessionListingOptions(parsed.value);
|
|
@@ -1705,7 +1400,7 @@ async function executeCommand(commandArguments, dependencies) {
|
|
|
1705
1400
|
return result.ok ? { ok: true, value: result.value } : result;
|
|
1706
1401
|
}
|
|
1707
1402
|
if (command === "guard") {
|
|
1708
|
-
const parsed = parseOptions([subcommand, ...rest].filter((argument) => argument !== undefined),
|
|
1403
|
+
const parsed = parseOptions([subcommand, ...rest].filter((argument) => argument !== undefined), dispatcherAllowedOptions("guard"));
|
|
1709
1404
|
if (!parsed.ok)
|
|
1710
1405
|
return parsed;
|
|
1711
1406
|
if (parsed.value.resources.length > 0 && parsed.value.operation === null) {
|
|
@@ -1749,7 +1444,7 @@ async function executeCommand(commandArguments, dependencies) {
|
|
|
1749
1444
|
}));
|
|
1750
1445
|
}
|
|
1751
1446
|
if (command === "gc") {
|
|
1752
|
-
const parsed = parseOptions([subcommand, ...rest].filter((argument) => argument !== undefined),
|
|
1447
|
+
const parsed = parseOptions([subcommand, ...rest].filter((argument) => argument !== undefined), dispatcherAllowedOptions("gc"));
|
|
1753
1448
|
if (!parsed.ok)
|
|
1754
1449
|
return parsed;
|
|
1755
1450
|
const options = { apply: parsed.value.apply };
|
|
@@ -1760,7 +1455,7 @@ async function executeCommand(commandArguments, dependencies) {
|
|
|
1760
1455
|
const parsed = noOptions([subcommand, ...rest].filter((argument) => argument !== undefined));
|
|
1761
1456
|
if (!parsed.ok)
|
|
1762
1457
|
return parsed;
|
|
1763
|
-
const report = await runDoctor(dependencies.cwd);
|
|
1458
|
+
const report = await runDoctor(dependencies.cwd, dependencies.sandboxProbe);
|
|
1764
1459
|
if (!report.ok)
|
|
1765
1460
|
return report;
|
|
1766
1461
|
if (report.value.ok)
|
|
@@ -1768,8 +1463,18 @@ async function executeCommand(commandArguments, dependencies) {
|
|
|
1768
1463
|
return failure(new DomainError("DOCTOR_FAILED", "One or more local Nawabari checks failed.", {
|
|
1769
1464
|
checks: report.value.checks,
|
|
1770
1465
|
repository: report.value.repository,
|
|
1466
|
+
sandbox: report.value.sandbox,
|
|
1771
1467
|
}));
|
|
1772
1468
|
}
|
|
1469
|
+
if (command === "migrate") {
|
|
1470
|
+
const parsed = noOptions([subcommand, ...rest].filter((argument) => argument !== undefined));
|
|
1471
|
+
if (!parsed.ok)
|
|
1472
|
+
return parsed;
|
|
1473
|
+
if (dependencies.backend.migrate === undefined)
|
|
1474
|
+
return migrationCapabilityUnavailable();
|
|
1475
|
+
const result = await dependencies.backend.migrate(context);
|
|
1476
|
+
return result.ok ? { ok: true, value: result.value } : result;
|
|
1477
|
+
}
|
|
1773
1478
|
if (command === undefined)
|
|
1774
1479
|
return failure(new DomainError("NO_COMMAND", "A command is required."));
|
|
1775
1480
|
return failure(new DomainError("UNKNOWN_COMMAND", `Unknown command: ${command}.`, { command }));
|
|
@@ -1777,8 +1482,9 @@ async function executeCommand(commandArguments, dependencies) {
|
|
|
1777
1482
|
function commandName(commandArguments) {
|
|
1778
1483
|
if (commandArguments[0] === "session")
|
|
1779
1484
|
return commandArguments.slice(0, 2).join(" ");
|
|
1780
|
-
if (commandArguments[0] === "resource")
|
|
1781
|
-
return commandArguments.slice(0, 2).join(" ");
|
|
1485
|
+
if (commandArguments[0] === "resource") {
|
|
1486
|
+
return commandArguments[1] === undefined ? "resource list" : commandArguments.slice(0, 2).join(" ");
|
|
1487
|
+
}
|
|
1782
1488
|
if (commandArguments[0] === "evidence")
|
|
1783
1489
|
return commandArguments.slice(0, 2).join(" ");
|
|
1784
1490
|
return commandArguments[0] ?? "cli";
|
|
@@ -1786,6 +1492,11 @@ function commandName(commandArguments) {
|
|
|
1786
1492
|
function claimCapabilityUnavailable(operation) {
|
|
1787
1493
|
return failure(new DomainError("BACKEND_UNAVAILABLE", "Resource claim capability is not available.", { operation }));
|
|
1788
1494
|
}
|
|
1495
|
+
function migrationCapabilityUnavailable() {
|
|
1496
|
+
return failure(new DomainError("BACKEND_UNAVAILABLE", "Registry migration capability is not available.", {
|
|
1497
|
+
operation: "migrate",
|
|
1498
|
+
}));
|
|
1499
|
+
}
|
|
1789
1500
|
function authorizationCapabilityUnavailable() {
|
|
1790
1501
|
return failure(new DomainError("BACKEND_UNAVAILABLE", "Operation authorization capability is not available.", {
|
|
1791
1502
|
operation: "authorize",
|
|
@@ -1806,6 +1517,11 @@ function sessionDiscardCapabilityUnavailable() {
|
|
|
1806
1517
|
operation: "session.discard",
|
|
1807
1518
|
}));
|
|
1808
1519
|
}
|
|
1520
|
+
function sessionDiscardPreviewCapabilityUnavailable() {
|
|
1521
|
+
return failure(new DomainError("BACKEND_UNAVAILABLE", "Session discard preview capability is not available.", {
|
|
1522
|
+
operation: "session.discard.preview",
|
|
1523
|
+
}));
|
|
1524
|
+
}
|
|
1809
1525
|
function repositoryEvidenceCapabilityUnavailable(operation) {
|
|
1810
1526
|
return failure(new DomainError("BACKEND_UNAVAILABLE", "Repository evidence capability is not available.", { operation }));
|
|
1811
1527
|
}
|
|
@@ -1948,12 +1664,15 @@ export async function runCli(argv, dependencies = {}) {
|
|
|
1948
1664
|
const enriched = await enrichInvalidSessionIdError(result.error, backend, sessionContext(cwd));
|
|
1949
1665
|
return emitFailure(mode, command, enriched, io);
|
|
1950
1666
|
}
|
|
1667
|
+
const childExitCode = result.value.exit_code;
|
|
1668
|
+
const childSignal = result.value.signal;
|
|
1951
1669
|
io.stdout(renderSuccess(mode, command, result.value));
|
|
1952
1670
|
if ((command === "session run" || command === "session exec") &&
|
|
1953
|
-
typeof
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
|
|
1671
|
+
(childSignal !== null || (typeof childExitCode === "number" && childExitCode !== 0))) {
|
|
1672
|
+
if (childSignal !== null)
|
|
1673
|
+
return EXIT_CODES.rejected;
|
|
1674
|
+
return typeof childExitCode === "number" && childExitCode >= 1 && childExitCode <= 255
|
|
1675
|
+
? childExitCode
|
|
1957
1676
|
: EXIT_CODES.rejected;
|
|
1958
1677
|
}
|
|
1959
1678
|
return EXIT_CODES.success;
|