nawabari 0.2.0 → 0.4.1
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 +230 -15
- package/dist/cli.d.ts +5 -0
- package/dist/cli.js +704 -115
- package/dist/cli.js.map +1 -1
- package/dist/contract.js +135 -2
- package/dist/contract.js.map +1 -1
- package/dist/domain/errors.d.ts +1 -1
- package/dist/domain/errors.js +4 -1
- package/dist/domain/errors.js.map +1 -1
- package/dist/domain/sandbox-launcher.d.ts +35 -0
- package/dist/domain/sandbox-launcher.js +679 -0
- package/dist/domain/sandbox-launcher.js.map +1 -0
- package/dist/domain/sandbox.d.ts +190 -0
- package/dist/domain/sandbox.js +363 -0
- package/dist/domain/sandbox.js.map +1 -0
- package/dist/domain/session-backend.d.ts +5 -3
- package/dist/domain/session-backend.js +102 -4
- package/dist/domain/session-backend.js.map +1 -1
- package/dist/domain/session.d.ts +116 -2
- package/dist/domain/session.js +36 -0
- package/dist/domain/session.js.map +1 -1
- package/dist/errors.d.ts +1 -1
- package/dist/errors.js.map +1 -1
- package/dist/git.d.ts +62 -0
- package/dist/git.js +299 -0
- package/dist/git.js.map +1 -1
- package/dist/operation-authorization.d.ts +5 -2
- package/dist/operation-authorization.js +6 -7
- package/dist/operation-authorization.js.map +1 -1
- package/dist/output-budget.d.ts +16 -0
- package/dist/output-budget.js +58 -0
- package/dist/output-budget.js.map +1 -0
- package/dist/presentation.js +28 -4
- package/dist/presentation.js.map +1 -1
- package/dist/registry/atomic.d.ts +24 -0
- package/dist/registry/atomic.js +35 -2
- package/dist/registry/atomic.js.map +1 -1
- package/dist/registry/errors.d.ts +1 -1
- package/dist/registry/errors.js +1 -0
- package/dist/registry/errors.js.map +1 -1
- package/dist/repository-evidence.d.ts +74 -0
- package/dist/repository-evidence.js +7 -0
- package/dist/repository-evidence.js.map +1 -0
- package/dist/resource-claims.d.ts +25 -3
- package/dist/resource-claims.js +57 -5
- package/dist/resource-claims.js.map +1 -1
- package/dist/session-registry.d.ts +68 -7
- package/dist/session-registry.js +422 -149
- package/dist/session-registry.js.map +1 -1
- package/package.json +5 -3
package/dist/cli.js
CHANGED
|
@@ -1,96 +1,385 @@
|
|
|
1
1
|
import { createRequire } from "node:module";
|
|
2
2
|
import { runDoctor } from "./domain/doctor.js";
|
|
3
3
|
import { DomainError, EXIT_CODES, failure } from "./domain/errors.js";
|
|
4
|
+
import { MAX_SESSION_LIST_LIMIT, } from "./domain/session.js";
|
|
5
|
+
import { EVIDENCE_MAX_DIFF_BYTES, EVIDENCE_MAX_DIFF_HUNKS, EVIDENCE_MAX_DIFF_PATHS } from "./repository-evidence.js";
|
|
4
6
|
import { createLocalSessionBackend } from "./domain/session-backend.js";
|
|
5
7
|
import { defaultCliIO, renderFailure, renderSuccess } from "./presentation.js";
|
|
6
8
|
import { MACHINE_CONTRACT_ID, MACHINE_CONTRACT_SCHEMA_VERSION, machineContract } from "./contract.js";
|
|
9
|
+
import { resolveSandboxExecutionRequest, runSandboxedCommand, } from "./domain/sandbox.js";
|
|
7
10
|
const CLI_NAME = "nawabari";
|
|
8
11
|
const packageMetadata = createRequire(import.meta.url)("../package.json");
|
|
9
12
|
const VERSION = packageMetadata.version;
|
|
10
|
-
const
|
|
11
|
-
|
|
12
|
-
"",
|
|
13
|
-
"
|
|
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
|
-
"session
|
|
59
|
-
"session
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
"
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
"
|
|
69
|
-
"
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
"
|
|
80
|
-
"
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
13
|
+
const GLOBAL_HELP_OPTIONS = [
|
|
14
|
+
{ name: "--json", description: "Emit one stable JSON document on stdout" },
|
|
15
|
+
{ name: "--help", description: "Show command-specific help" },
|
|
16
|
+
{ name: "--version", description: "Print the installed version" },
|
|
17
|
+
];
|
|
18
|
+
const option = (name, description, options = {}) => ({ name, description, ...options });
|
|
19
|
+
const HELP_COMMANDS = [
|
|
20
|
+
{
|
|
21
|
+
name: "session create",
|
|
22
|
+
summary: "Request a new Nawabari session",
|
|
23
|
+
usage: `${CLI_NAME} session create [options]`,
|
|
24
|
+
options: [
|
|
25
|
+
option("--branch", "Branch to create; omitted uses the generated session branch", {
|
|
26
|
+
value: "<name>",
|
|
27
|
+
default: "nawabari/session/<session_id>",
|
|
28
|
+
}),
|
|
29
|
+
option("--worktree", "Managed worktree path; omitted uses the resolved repository-local root", {
|
|
30
|
+
value: "<path>",
|
|
31
|
+
default: "<managed_worktree_root>/<repository>-<session_id>",
|
|
32
|
+
}),
|
|
33
|
+
option("--base", "Commit-resolving base ref for the new worktree", { value: "<ref>", default: "HEAD" }),
|
|
34
|
+
option("--label", "Optional display label; never used as an identity", { value: "<text>", default: "omitted" }),
|
|
35
|
+
],
|
|
36
|
+
notes: ["All create options are optional. Use status --json to discover managed_worktree_root."],
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
name: "session id",
|
|
40
|
+
summary: "Resolve the current session identity",
|
|
41
|
+
usage: `${CLI_NAME} session id`,
|
|
42
|
+
options: [],
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
name: "session show",
|
|
46
|
+
summary: "Show the current or selected session",
|
|
47
|
+
usage: `${CLI_NAME} session show [--session <id>]`,
|
|
48
|
+
options: [option("--session", "Select a session instead of the current worktree owner", { value: "<id>" })],
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
name: "session run",
|
|
52
|
+
summary: "Run one command inside the protected session sandbox",
|
|
53
|
+
usage: `${CLI_NAME} session run [--session <id>] -- <command> [args...]`,
|
|
54
|
+
options: [option("--session", "Select the active owned session", { value: "<id>" })],
|
|
55
|
+
notes: [
|
|
56
|
+
"The -- terminator is mandatory. The command is passed as argv without a shell, and protected execution is fail-closed.",
|
|
57
|
+
"session exec is an alias.",
|
|
58
|
+
],
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
name: "session list",
|
|
62
|
+
summary: "List bounded repository session records",
|
|
63
|
+
usage: `${CLI_NAME} session list [--all|--history]`,
|
|
64
|
+
options: [
|
|
65
|
+
option("--all", "Include closed history; explicit unbounded history view"),
|
|
66
|
+
option("--history", "Alias for --all"),
|
|
67
|
+
],
|
|
68
|
+
notes: ["Default output excludes closed records and is limited to 64 records."],
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
name: "session claim",
|
|
72
|
+
summary: "Add a canonical resource claim",
|
|
73
|
+
usage: `${CLI_NAME} session claim --resource <path-or-glob> --mode <read|write|exclusive-write> [--session <id>]`,
|
|
74
|
+
options: [
|
|
75
|
+
option("--resource", "Repository-relative resource", { value: "<path-or-glob>", required: true }),
|
|
76
|
+
option("--mode", "Granted claim mode", { value: "<read|write|exclusive-write>", required: true }),
|
|
77
|
+
option("--session", "Target active session; omitted resolves the current owner", { value: "<id>" }),
|
|
78
|
+
option("--repository", "Expected repository identity", { value: "<id>" }),
|
|
79
|
+
],
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
name: "session update",
|
|
83
|
+
summary: "Atomically replace a session's complete resource claim set",
|
|
84
|
+
usage: `${CLI_NAME} session update --resource <path-or-glob> --mode <read|write|exclusive-write> [--resource <path-or-glob> --mode <read|write|exclusive-write> ...] [--session <id>]`,
|
|
85
|
+
options: [
|
|
86
|
+
option("--resource", "Repository-relative resource; repeatable, each paired with the --mode immediately after it", {
|
|
87
|
+
value: "<path-or-glob>",
|
|
88
|
+
required: true,
|
|
89
|
+
}),
|
|
90
|
+
option("--mode", "Mode for the --resource immediately before it; repeatable", {
|
|
91
|
+
value: "<read|write|exclusive-write>",
|
|
92
|
+
required: true,
|
|
93
|
+
}),
|
|
94
|
+
option("--session", "Target active session; omitted resolves the current owner", { value: "<id>" }),
|
|
95
|
+
option("--repository", "Expected repository identity", { value: "<id>" }),
|
|
96
|
+
],
|
|
97
|
+
notes: [
|
|
98
|
+
"The desired claim set fully replaces the session's current claims in one updateClaims() transaction; " +
|
|
99
|
+
"on any invalid or conflicting claim the prior set is left unchanged.",
|
|
100
|
+
"Each --resource must be immediately followed by its own --mode; pairing is positional adjacency, not flag order.",
|
|
101
|
+
],
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
name: "session claims",
|
|
105
|
+
summary: "List canonical resource claims",
|
|
106
|
+
usage: `${CLI_NAME} session claims [--session <id>]`,
|
|
107
|
+
options: [option("--session", "Select a session; omitted lists all claims", { value: "<id>" })],
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
name: "session release",
|
|
111
|
+
summary: "Release resource claims",
|
|
112
|
+
usage: `${CLI_NAME} session release [--session <id>] [--claim-id <id>]`,
|
|
113
|
+
options: [
|
|
114
|
+
option("--session", "Target session; omitted resolves the current owner", { value: "<id>" }),
|
|
115
|
+
option("--claim-id", "Release only one claim; omitted releases all owned claims", { value: "<id>" }),
|
|
116
|
+
],
|
|
117
|
+
},
|
|
118
|
+
{
|
|
119
|
+
name: "resource claim",
|
|
120
|
+
summary: "Add a canonical resource claim (alias)",
|
|
121
|
+
usage: `${CLI_NAME} resource claim --resource <path-or-glob> --mode <read|write|exclusive-write>`,
|
|
122
|
+
options: [],
|
|
123
|
+
},
|
|
124
|
+
{
|
|
125
|
+
name: "resource update",
|
|
126
|
+
summary: "Atomically replace a session's complete resource claim set (alias)",
|
|
127
|
+
usage: `${CLI_NAME} resource update --resource <path-or-glob> --mode <read|write|exclusive-write> [--resource <path-or-glob> --mode <read|write|exclusive-write> ...] [--session <id>]`,
|
|
128
|
+
options: [
|
|
129
|
+
option("--resource", "Repository-relative resource; repeatable, each paired with the --mode immediately after it", {
|
|
130
|
+
value: "<path-or-glob>",
|
|
131
|
+
required: true,
|
|
132
|
+
}),
|
|
133
|
+
option("--mode", "Mode for the --resource immediately before it; repeatable", {
|
|
134
|
+
value: "<read|write|exclusive-write>",
|
|
135
|
+
required: true,
|
|
136
|
+
}),
|
|
137
|
+
option("--session", "Target active session; omitted resolves the current owner", { value: "<id>" }),
|
|
138
|
+
option("--repository", "Expected repository identity", { value: "<id>" }),
|
|
139
|
+
],
|
|
140
|
+
notes: [
|
|
141
|
+
"The desired claim set fully replaces the session's current claims in one updateClaims() transaction; " +
|
|
142
|
+
"on any invalid or conflicting claim the prior set is left unchanged.",
|
|
143
|
+
"Each --resource must be immediately followed by its own --mode; pairing is positional adjacency, not flag order.",
|
|
144
|
+
],
|
|
145
|
+
},
|
|
146
|
+
{
|
|
147
|
+
name: "resource list",
|
|
148
|
+
summary: "List canonical resource claims (alias)",
|
|
149
|
+
usage: `${CLI_NAME} resource list [--session <id>]`,
|
|
150
|
+
options: [option("--session", "Select a session; omitted lists all claims", { value: "<id>" })],
|
|
151
|
+
},
|
|
152
|
+
{
|
|
153
|
+
name: "resource release",
|
|
154
|
+
summary: "Release resource claims (alias)",
|
|
155
|
+
usage: `${CLI_NAME} resource release [--session <id>] [--claim-id <id>]`,
|
|
156
|
+
options: [
|
|
157
|
+
option("--session", "Target session; omitted resolves the current owner", { value: "<id>" }),
|
|
158
|
+
option("--claim-id", "Release only one claim; omitted releases all owned claims", { value: "<id>" }),
|
|
159
|
+
],
|
|
160
|
+
},
|
|
161
|
+
{
|
|
162
|
+
name: "session close",
|
|
163
|
+
summary: "Close the current or selected session",
|
|
164
|
+
usage: `${CLI_NAME} session close [--session <id>]`,
|
|
165
|
+
options: [option("--session", "Select a session instead of the current worktree owner", { value: "<id>" })],
|
|
166
|
+
},
|
|
167
|
+
{
|
|
168
|
+
name: "authorize",
|
|
169
|
+
summary: "Authorize an operation against concrete claims",
|
|
170
|
+
usage: `${CLI_NAME} authorize --operation <name> --resource <path> [--resource <path>] [--session <id>]`,
|
|
171
|
+
options: [
|
|
172
|
+
option("--session", "Assert the current session identity", { value: "<id>" }),
|
|
173
|
+
option("--operation", "Operation vocabulary entry", { value: "<name>", required: true }),
|
|
174
|
+
option("--resource", "Concrete repository-relative path; repeatable", { value: "<path>", required: true }),
|
|
175
|
+
],
|
|
176
|
+
},
|
|
177
|
+
{
|
|
178
|
+
name: "checkpoint",
|
|
179
|
+
summary: "Capture bounded Git execution evidence",
|
|
180
|
+
usage: `${CLI_NAME} checkpoint [--session <id>]`,
|
|
181
|
+
options: [option("--session", "Assert the current session identity", { value: "<id>" })],
|
|
182
|
+
},
|
|
183
|
+
{
|
|
184
|
+
name: "evidence snapshot",
|
|
185
|
+
summary: "Capture bounded read-only evidence for one owned session",
|
|
186
|
+
usage: `${CLI_NAME} evidence snapshot --session <id>`,
|
|
187
|
+
options: [option("--session", "Explicit owned session to observe", { value: "<id>", required: true })],
|
|
188
|
+
notes: ["The result is Git-observable physical evidence only; it contains no task or semantic interpretation."],
|
|
189
|
+
},
|
|
190
|
+
{
|
|
191
|
+
name: "diff",
|
|
192
|
+
summary: "Inspect bounded Git evidence for explicit paths",
|
|
193
|
+
usage: `${CLI_NAME} diff --session <id> --path <path> [options]`,
|
|
194
|
+
options: [
|
|
195
|
+
option("--session", "Explicit owned session to observe", { value: "<id>", required: true }),
|
|
196
|
+
option("--path", "Concrete repository-relative path; repeatable", { value: "<path>", required: true }),
|
|
197
|
+
option("--from", "Commit/ref at the start of the range", { value: "<ref>", default: "HEAD" }),
|
|
198
|
+
option("--to", "Commit/ref at the end of the range; omitted means worktree", { value: "<ref>" }),
|
|
199
|
+
option("--patch", "Include patch text; requires the bounded byte/hunk limits"),
|
|
200
|
+
option("--max-bytes", "Maximum UTF-8 patch bytes", { value: "<n>", default: String(EVIDENCE_MAX_DIFF_BYTES) }),
|
|
201
|
+
option("--max-hunks", "Maximum patch hunks", { value: "<n>", default: String(EVIDENCE_MAX_DIFF_HUNKS) }),
|
|
202
|
+
],
|
|
203
|
+
},
|
|
204
|
+
{
|
|
205
|
+
name: "commit",
|
|
206
|
+
summary: "Commit explicit claim-authorized resources",
|
|
207
|
+
usage: `${CLI_NAME} commit --message <final-message> --resource <path> [--resource <path>] [--session <id>] [--message-pattern <regex>]`,
|
|
208
|
+
options: [
|
|
209
|
+
option("--session", "Assert the current session identity", { value: "<id>" }),
|
|
210
|
+
option("--message", "Caller-decided final commit message", { value: "<final-message>", required: true }),
|
|
211
|
+
option("--resource", "Claim-covered concrete path; repeatable", { value: "<path>", required: true }),
|
|
212
|
+
option("--message-pattern", "Caller-declared commit-message rule; validated only when supplied", {
|
|
213
|
+
value: "<regex>",
|
|
214
|
+
}),
|
|
215
|
+
],
|
|
216
|
+
},
|
|
217
|
+
{
|
|
218
|
+
name: "push",
|
|
219
|
+
summary: "Push the owned branch to an explicit target",
|
|
220
|
+
usage: `${CLI_NAME} push --remote <name> --branch <name> --resource <path> [options]`,
|
|
221
|
+
options: [
|
|
222
|
+
option("--session", "Assert the current session identity", { value: "<id>" }),
|
|
223
|
+
option("--resource", "Claim-covered concrete path; repeatable", { value: "<path>", required: true }),
|
|
224
|
+
option("--remote", "Explicit Git remote", { value: "<name>", required: true }),
|
|
225
|
+
option("--branch", "Explicit target branch", { value: "<name>", required: true }),
|
|
226
|
+
option("--remote-branch", "Explicit remote branch alias for --branch", { value: "<name>" }),
|
|
227
|
+
option("--force", "Allow force-with-lease when relation requires it"),
|
|
228
|
+
option("--create-upstream", "Allow creation of a missing upstream"),
|
|
229
|
+
],
|
|
230
|
+
},
|
|
231
|
+
{
|
|
232
|
+
name: "status",
|
|
233
|
+
summary: "Show repository context and bounded session status",
|
|
234
|
+
usage: `${CLI_NAME} status [--all|--history]`,
|
|
235
|
+
options: [
|
|
236
|
+
option("--all", "Include closed history; explicit unbounded history view"),
|
|
237
|
+
option("--history", "Alias for --all"),
|
|
238
|
+
],
|
|
239
|
+
notes: ["The default machine result exposes managed_worktree_root for session-create path discovery."],
|
|
240
|
+
},
|
|
241
|
+
{
|
|
242
|
+
name: "guard",
|
|
243
|
+
summary: "Authorize the current worktree or operation",
|
|
244
|
+
usage: `${CLI_NAME} guard [--session <id>] [--operation <name> --resource <path>]`,
|
|
245
|
+
options: [
|
|
246
|
+
option("--session", "Assert the current session identity", { value: "<id>" }),
|
|
247
|
+
option("--operation", "Authorize an operation when resources are supplied", { value: "<name>" }),
|
|
248
|
+
option("--resource", "Concrete resource; repeatable with --operation", { value: "<path>" }),
|
|
249
|
+
],
|
|
250
|
+
},
|
|
251
|
+
{
|
|
252
|
+
name: "gc",
|
|
253
|
+
summary: "Detect or clean eligible stale sessions",
|
|
254
|
+
usage: `${CLI_NAME} gc [--dry-run|--apply]`,
|
|
255
|
+
options: [
|
|
256
|
+
option("--apply", "Apply only cleanup that passes safety checks"),
|
|
257
|
+
option("--dry-run", "Preflight eligible stale cleanup without mutation"),
|
|
258
|
+
],
|
|
259
|
+
notes: [
|
|
260
|
+
"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.",
|
|
261
|
+
],
|
|
262
|
+
},
|
|
263
|
+
{
|
|
264
|
+
name: "doctor",
|
|
265
|
+
summary: "Check local Nawabari prerequisites and reconciliation",
|
|
266
|
+
usage: `${CLI_NAME} doctor`,
|
|
267
|
+
options: [],
|
|
268
|
+
},
|
|
269
|
+
{
|
|
270
|
+
name: "capabilities",
|
|
271
|
+
summary: "Describe the standalone CLI/JSON contract",
|
|
272
|
+
usage: `${CLI_NAME} capabilities`,
|
|
273
|
+
options: [],
|
|
274
|
+
},
|
|
275
|
+
];
|
|
276
|
+
const ROOT_HELP_SPEC = {
|
|
277
|
+
name: "root",
|
|
278
|
+
summary: "Standalone local Git/session ownership CLI",
|
|
279
|
+
usage: `${CLI_NAME} <command> [options]`,
|
|
280
|
+
options: GLOBAL_HELP_OPTIONS,
|
|
93
281
|
};
|
|
282
|
+
function helpSpecFor(commandArguments) {
|
|
283
|
+
if (commandArguments.length === 0)
|
|
284
|
+
return ROOT_HELP_SPEC;
|
|
285
|
+
let key = commandArguments[0] === "resource"
|
|
286
|
+
? `resource ${commandArguments[1] ?? "list"}`
|
|
287
|
+
: commandArguments[0] === "session" && commandArguments[1] === "exec"
|
|
288
|
+
? "session run"
|
|
289
|
+
: commandArguments.slice(0, 2).join(" ");
|
|
290
|
+
if (key === "resource claims")
|
|
291
|
+
key = "resource list";
|
|
292
|
+
const direct = HELP_COMMANDS.find((spec) => spec.name === key);
|
|
293
|
+
if (direct !== undefined && !direct.name.startsWith("resource "))
|
|
294
|
+
return direct;
|
|
295
|
+
const aliasTarget = direct?.name.replace(/^resource /u, "session ");
|
|
296
|
+
const target = aliasTarget === undefined ? undefined : HELP_COMMANDS.find((spec) => spec.name === aliasTarget);
|
|
297
|
+
if (direct !== undefined && target !== undefined) {
|
|
298
|
+
return {
|
|
299
|
+
name: direct.name,
|
|
300
|
+
summary: direct.summary,
|
|
301
|
+
usage: direct.usage,
|
|
302
|
+
options: direct.options,
|
|
303
|
+
notes: direct.notes,
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
return HELP_COMMANDS.find((spec) => spec.name === commandArguments[0]) ?? ROOT_HELP_SPEC;
|
|
307
|
+
}
|
|
308
|
+
function helpPayload(spec) {
|
|
309
|
+
if (spec.name === "root") {
|
|
310
|
+
const optionNames = (options) => options.map((candidate) => candidate.name);
|
|
311
|
+
const sessionOptions = HELP_COMMANDS.filter((command) => command.name.startsWith("session ")).flatMap((command) => command.options);
|
|
312
|
+
const unique = (values) => values.filter((value, index) => values.indexOf(value) === index);
|
|
313
|
+
const sessionListOnlyOptions = new Set(["--all", "--history"]);
|
|
314
|
+
const optionsFor = (name) => optionNames(HELP_COMMANDS.find((command) => command.name === name)?.options ?? []);
|
|
315
|
+
return {
|
|
316
|
+
usage: `Usage: ${spec.usage}`,
|
|
317
|
+
commands: HELP_COMMANDS.map((command) => command.name),
|
|
318
|
+
options: ["--json", "--help", "--version"],
|
|
319
|
+
session_options: unique(sessionOptions.map((option) => option.name).filter((name) => !sessionListOnlyOptions.has(name))),
|
|
320
|
+
authorization_options: optionsFor("authorize"),
|
|
321
|
+
checkpoint_options: optionsFor("checkpoint"),
|
|
322
|
+
commit_options: optionsFor("commit"),
|
|
323
|
+
push_options: optionsFor("push"),
|
|
324
|
+
gc_options: optionsFor("gc"),
|
|
325
|
+
};
|
|
326
|
+
}
|
|
327
|
+
const options = spec.options.map((candidate) => ({
|
|
328
|
+
name: candidate.name,
|
|
329
|
+
...(candidate.value === undefined ? {} : { value: candidate.value }),
|
|
330
|
+
required: candidate.required === true,
|
|
331
|
+
...(candidate.default === undefined ? {} : { default: candidate.default }),
|
|
332
|
+
description: candidate.description,
|
|
333
|
+
}));
|
|
334
|
+
return {
|
|
335
|
+
help_for: spec.name,
|
|
336
|
+
usage: spec.usage,
|
|
337
|
+
summary: spec.summary,
|
|
338
|
+
required_options: spec.options
|
|
339
|
+
.filter((candidate) => candidate.required === true)
|
|
340
|
+
.map((candidate) => candidate.name),
|
|
341
|
+
optional_options: spec.options
|
|
342
|
+
.filter((candidate) => candidate.required !== true)
|
|
343
|
+
.map((candidate) => candidate.name),
|
|
344
|
+
defaults: Object.fromEntries(spec.options
|
|
345
|
+
.filter((candidate) => candidate.default !== undefined)
|
|
346
|
+
.map((candidate) => [candidate.name, candidate.default])),
|
|
347
|
+
options,
|
|
348
|
+
...(spec.notes === undefined ? {} : { notes: [...spec.notes] }),
|
|
349
|
+
};
|
|
350
|
+
}
|
|
351
|
+
function helpText(spec) {
|
|
352
|
+
const lines = [`Usage: ${spec.usage}`, "", spec.summary];
|
|
353
|
+
if (spec.name === "root") {
|
|
354
|
+
lines.push("", "Commands:");
|
|
355
|
+
for (const command of HELP_COMMANDS)
|
|
356
|
+
lines.push(` ${command.name.padEnd(20)} ${command.summary}`);
|
|
357
|
+
lines.push("", "Global options:");
|
|
358
|
+
for (const candidate of GLOBAL_HELP_OPTIONS) {
|
|
359
|
+
const label = candidate.name === "--help" ? "-h, --help" : candidate.name;
|
|
360
|
+
lines.push(` ${label.padEnd(20)} ${candidate.description}`);
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
else {
|
|
364
|
+
lines.push("", "Options:");
|
|
365
|
+
if (spec.options.length === 0)
|
|
366
|
+
lines.push(" (none)");
|
|
367
|
+
for (const candidate of spec.options) {
|
|
368
|
+
const label = candidate.value === undefined ? candidate.name : `${candidate.name} ${candidate.value}`;
|
|
369
|
+
const qualifier = candidate.required === true
|
|
370
|
+
? "required"
|
|
371
|
+
: `optional${candidate.default === undefined ? "" : `; default: ${candidate.default}`}`;
|
|
372
|
+
lines.push(` ${label.padEnd(38)} ${qualifier}; ${candidate.description}`);
|
|
373
|
+
}
|
|
374
|
+
if (spec.notes !== undefined) {
|
|
375
|
+
lines.push("", "Notes:");
|
|
376
|
+
for (const note of spec.notes)
|
|
377
|
+
lines.push(` ${note}`);
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
return lines.join("\n");
|
|
381
|
+
}
|
|
382
|
+
const HELP_TEXT = helpText(ROOT_HELP_SPEC);
|
|
94
383
|
function usageError(code, message, details = null) {
|
|
95
384
|
return new DomainError(code, message, details);
|
|
96
385
|
}
|
|
@@ -99,7 +388,17 @@ function parseGlobalArguments(argv) {
|
|
|
99
388
|
let help = false;
|
|
100
389
|
let version = false;
|
|
101
390
|
const commandArguments = [];
|
|
391
|
+
let passthrough = false;
|
|
102
392
|
for (const argument of argv) {
|
|
393
|
+
if (passthrough) {
|
|
394
|
+
commandArguments.push(argument);
|
|
395
|
+
continue;
|
|
396
|
+
}
|
|
397
|
+
if (argument === "--") {
|
|
398
|
+
passthrough = true;
|
|
399
|
+
commandArguments.push(argument);
|
|
400
|
+
continue;
|
|
401
|
+
}
|
|
103
402
|
if (argument === "--json") {
|
|
104
403
|
json = true;
|
|
105
404
|
}
|
|
@@ -138,6 +437,7 @@ function parseOptions(arguments_, allowed) {
|
|
|
138
437
|
resources: [],
|
|
139
438
|
operation: null,
|
|
140
439
|
message: null,
|
|
440
|
+
message_pattern: null,
|
|
141
441
|
remote: null,
|
|
142
442
|
remote_branch: null,
|
|
143
443
|
mode: null,
|
|
@@ -146,6 +446,16 @@ function parseOptions(arguments_, allowed) {
|
|
|
146
446
|
apply: false,
|
|
147
447
|
force: false,
|
|
148
448
|
create_upstream: false,
|
|
449
|
+
all: false,
|
|
450
|
+
history: false,
|
|
451
|
+
limit: null,
|
|
452
|
+
offset: null,
|
|
453
|
+
paths: [],
|
|
454
|
+
from_revision: null,
|
|
455
|
+
to_revision: null,
|
|
456
|
+
patch: false,
|
|
457
|
+
max_bytes: null,
|
|
458
|
+
max_hunks: null,
|
|
149
459
|
};
|
|
150
460
|
let dryRun = false;
|
|
151
461
|
for (let index = 0; index < arguments_.length; index += 1) {
|
|
@@ -153,7 +463,13 @@ function parseOptions(arguments_, allowed) {
|
|
|
153
463
|
if (!allowed.has(name)) {
|
|
154
464
|
return failure(usageError("INVALID_ARGUMENT", `Unknown option: ${name}.`, { option: name }));
|
|
155
465
|
}
|
|
156
|
-
if (name === "--apply" ||
|
|
466
|
+
if (name === "--apply" ||
|
|
467
|
+
name === "--dry-run" ||
|
|
468
|
+
name === "--force" ||
|
|
469
|
+
name === "--create-upstream" ||
|
|
470
|
+
name === "--all" ||
|
|
471
|
+
name === "--history" ||
|
|
472
|
+
name === "--patch") {
|
|
157
473
|
if (inlineValue !== null) {
|
|
158
474
|
return failure(usageError("INVALID_ARGUMENT", `${name} does not accept a value.`, { option: name }));
|
|
159
475
|
}
|
|
@@ -163,8 +479,14 @@ function parseOptions(arguments_, allowed) {
|
|
|
163
479
|
dryRun = true;
|
|
164
480
|
else if (name === "--force")
|
|
165
481
|
options.force = true;
|
|
166
|
-
else
|
|
482
|
+
else if (name === "--create-upstream")
|
|
167
483
|
options.create_upstream = true;
|
|
484
|
+
else if (name === "--all")
|
|
485
|
+
options.all = true;
|
|
486
|
+
else if (name === "--history")
|
|
487
|
+
options.history = true;
|
|
488
|
+
else
|
|
489
|
+
options.patch = true;
|
|
168
490
|
continue;
|
|
169
491
|
}
|
|
170
492
|
const value = inlineValue ?? arguments_[index + 1];
|
|
@@ -191,6 +513,8 @@ function parseOptions(arguments_, allowed) {
|
|
|
191
513
|
options.operation = value;
|
|
192
514
|
else if (name === "--message")
|
|
193
515
|
options.message = value;
|
|
516
|
+
else if (name === "--message-pattern")
|
|
517
|
+
options.message_pattern = value;
|
|
194
518
|
else if (name === "--remote")
|
|
195
519
|
options.remote = value;
|
|
196
520
|
else if (name === "--remote-branch")
|
|
@@ -201,6 +525,20 @@ function parseOptions(arguments_, allowed) {
|
|
|
201
525
|
options.claim_id = value;
|
|
202
526
|
else if (name === "--repository")
|
|
203
527
|
options.repository = value;
|
|
528
|
+
else if (name === "--limit")
|
|
529
|
+
options.limit = value;
|
|
530
|
+
else if (name === "--offset")
|
|
531
|
+
options.offset = value;
|
|
532
|
+
else if (name === "--path")
|
|
533
|
+
options.paths.push(value);
|
|
534
|
+
else if (name === "--from")
|
|
535
|
+
options.from_revision = value;
|
|
536
|
+
else if (name === "--to")
|
|
537
|
+
options.to_revision = value;
|
|
538
|
+
else if (name === "--max-bytes")
|
|
539
|
+
options.max_bytes = value;
|
|
540
|
+
else if (name === "--max-hunks")
|
|
541
|
+
options.max_hunks = value;
|
|
204
542
|
}
|
|
205
543
|
if (options.apply && dryRun) {
|
|
206
544
|
return failure(usageError("INVALID_ARGUMENT", "--apply and --dry-run cannot be used together."));
|
|
@@ -210,6 +548,129 @@ function parseOptions(arguments_, allowed) {
|
|
|
210
548
|
function noOptions(arguments_) {
|
|
211
549
|
return parseOptions(arguments_, new Set());
|
|
212
550
|
}
|
|
551
|
+
/**
|
|
552
|
+
* `update` accepts a complete desired claim set as repeated
|
|
553
|
+
* `--resource <path> --mode <mode>` pairs. Pairing is by strict local
|
|
554
|
+
* adjacency (each `--resource` must be immediately followed by its own
|
|
555
|
+
* `--mode`, with no other option able to intervene) rather than by
|
|
556
|
+
* parallel-array position, so argv order can never associate a resource
|
|
557
|
+
* with the wrong mode.
|
|
558
|
+
*/
|
|
559
|
+
function parseClaimReplacementPairs(arguments_) {
|
|
560
|
+
let sessionId = null;
|
|
561
|
+
let repository = null;
|
|
562
|
+
const pairs = [];
|
|
563
|
+
let pendingResource = null;
|
|
564
|
+
for (let index = 0; index < arguments_.length; index += 1) {
|
|
565
|
+
const { name, inlineValue } = optionParts(arguments_[index]);
|
|
566
|
+
if (name !== "--session" && name !== "--repository" && name !== "--resource" && name !== "--mode") {
|
|
567
|
+
return failure(usageError("INVALID_ARGUMENT", `Unknown option: ${name}.`, { option: name }));
|
|
568
|
+
}
|
|
569
|
+
if (pendingResource !== null && name !== "--mode") {
|
|
570
|
+
return failure(usageError("INVALID_ARGUMENT", `${name} cannot appear between --resource and its --mode; only --mode is permitted immediately after --resource.`, { option: name }));
|
|
571
|
+
}
|
|
572
|
+
const value = inlineValue ?? arguments_[index + 1];
|
|
573
|
+
if (value === undefined || value === "" || (inlineValue === null && value.startsWith("-"))) {
|
|
574
|
+
return failure(usageError("MISSING_ARGUMENT", `${name} requires a value.`, { option: name }));
|
|
575
|
+
}
|
|
576
|
+
if (inlineValue === null)
|
|
577
|
+
index += 1;
|
|
578
|
+
if (name === "--session")
|
|
579
|
+
sessionId = value;
|
|
580
|
+
else if (name === "--repository")
|
|
581
|
+
repository = value;
|
|
582
|
+
else if (name === "--resource")
|
|
583
|
+
pendingResource = value;
|
|
584
|
+
else {
|
|
585
|
+
if (pendingResource === null) {
|
|
586
|
+
return failure(usageError("INVALID_ARGUMENT", "--mode must be immediately preceded by its own --resource.", {
|
|
587
|
+
option: "--mode",
|
|
588
|
+
}));
|
|
589
|
+
}
|
|
590
|
+
pairs.push({ resource: pendingResource, mode: value });
|
|
591
|
+
pendingResource = null;
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
if (pendingResource !== null) {
|
|
595
|
+
return failure(usageError("MISSING_ARGUMENT", "--resource must be immediately followed by --mode.", { option: "--mode" }));
|
|
596
|
+
}
|
|
597
|
+
if (pairs.length === 0) {
|
|
598
|
+
return failure(usageError("MISSING_ARGUMENT", "--resource requires a value.", { option: "--resource" }));
|
|
599
|
+
}
|
|
600
|
+
return { ok: true, value: { session_id: sessionId, repository, pairs } };
|
|
601
|
+
}
|
|
602
|
+
/**
|
|
603
|
+
* `claim` accepts exactly one `--resource`/`--mode` pair. Reuses the same
|
|
604
|
+
* strict-adjacency scan as `update` so a second `--resource`/`--mode` (in
|
|
605
|
+
* any order) is rejected outright instead of silently overwriting the
|
|
606
|
+
* first pair (last-wins).
|
|
607
|
+
*/
|
|
608
|
+
function parseSingleClaimPair(arguments_) {
|
|
609
|
+
const parsed = parseClaimReplacementPairs(arguments_);
|
|
610
|
+
if (!parsed.ok)
|
|
611
|
+
return parsed;
|
|
612
|
+
if (parsed.value.pairs.length > 1) {
|
|
613
|
+
return failure(usageError("INVALID_ARGUMENT", "claim accepts exactly one --resource/--mode pair.", {
|
|
614
|
+
pair_count: parsed.value.pairs.length,
|
|
615
|
+
}));
|
|
616
|
+
}
|
|
617
|
+
const [pair] = parsed.value.pairs;
|
|
618
|
+
return {
|
|
619
|
+
ok: true,
|
|
620
|
+
value: {
|
|
621
|
+
session_id: parsed.value.session_id,
|
|
622
|
+
repository: parsed.value.repository,
|
|
623
|
+
resource: pair.resource,
|
|
624
|
+
mode: pair.mode,
|
|
625
|
+
},
|
|
626
|
+
};
|
|
627
|
+
}
|
|
628
|
+
function sessionListingOptions(parsed) {
|
|
629
|
+
const parseInteger = (option, value) => {
|
|
630
|
+
if (value === null)
|
|
631
|
+
return { ok: true, value: undefined };
|
|
632
|
+
if (!/^\d+$/u.test(value)) {
|
|
633
|
+
return failure(usageError("INVALID_ARGUMENT", `${option} requires a non-negative integer.`, { option, value }));
|
|
634
|
+
}
|
|
635
|
+
const parsedValue = Number(value);
|
|
636
|
+
if (!Number.isSafeInteger(parsedValue)) {
|
|
637
|
+
return failure(usageError("INVALID_ARGUMENT", `${option} is outside the safe integer range.`, { option }));
|
|
638
|
+
}
|
|
639
|
+
return { ok: true, value: parsedValue };
|
|
640
|
+
};
|
|
641
|
+
const limit = parseInteger("--limit", parsed.limit);
|
|
642
|
+
if (!limit.ok)
|
|
643
|
+
return limit;
|
|
644
|
+
if (limit.value !== undefined && (limit.value < 1 || limit.value > MAX_SESSION_LIST_LIMIT)) {
|
|
645
|
+
return failure(usageError("INVALID_ARGUMENT", `--limit must be between 1 and ${MAX_SESSION_LIST_LIMIT}.`, {
|
|
646
|
+
option: "--limit",
|
|
647
|
+
max: MAX_SESSION_LIST_LIMIT,
|
|
648
|
+
}));
|
|
649
|
+
}
|
|
650
|
+
const offset = parseInteger("--offset", parsed.offset);
|
|
651
|
+
if (!offset.ok)
|
|
652
|
+
return offset;
|
|
653
|
+
return {
|
|
654
|
+
ok: true,
|
|
655
|
+
value: {
|
|
656
|
+
include_closed: parsed.all || parsed.history,
|
|
657
|
+
...(limit.value === undefined ? {} : { limit: limit.value }),
|
|
658
|
+
...(offset.value === undefined ? {} : { offset: offset.value }),
|
|
659
|
+
},
|
|
660
|
+
};
|
|
661
|
+
}
|
|
662
|
+
function boundedEvidenceInteger(option, value, max) {
|
|
663
|
+
if (value === null)
|
|
664
|
+
return { ok: true, value: undefined };
|
|
665
|
+
if (!/^\d+$/u.test(value)) {
|
|
666
|
+
return failure(usageError("INVALID_ARGUMENT", `${option} requires a positive integer.`, { option, value }));
|
|
667
|
+
}
|
|
668
|
+
const parsed = Number(value);
|
|
669
|
+
if (!Number.isSafeInteger(parsed) || parsed < 1 || parsed > max) {
|
|
670
|
+
return failure(usageError("INVALID_ARGUMENT", `${option} must be between 1 and ${max}.`, { option, max, value }));
|
|
671
|
+
}
|
|
672
|
+
return { ok: true, value: parsed };
|
|
673
|
+
}
|
|
213
674
|
function sessionContext(cwd) {
|
|
214
675
|
return { cwd };
|
|
215
676
|
}
|
|
@@ -223,25 +684,33 @@ async function executeCommand(commandArguments, dependencies) {
|
|
|
223
684
|
if (subcommand === undefined) {
|
|
224
685
|
return failure(usageError("MISSING_ARGUMENT", "session requires a subcommand."));
|
|
225
686
|
}
|
|
226
|
-
if (subcommand === "claim"
|
|
227
|
-
const parsed =
|
|
687
|
+
if (subcommand === "claim") {
|
|
688
|
+
const parsed = parseSingleClaimPair(rest);
|
|
228
689
|
if (!parsed.ok)
|
|
229
690
|
return parsed;
|
|
230
|
-
if (
|
|
231
|
-
return
|
|
232
|
-
|
|
233
|
-
if (parsed.value.mode === null) {
|
|
234
|
-
return failure(usageError("MISSING_ARGUMENT", "--mode requires a value.", { option: "--mode" }));
|
|
235
|
-
}
|
|
236
|
-
const options = {
|
|
691
|
+
if (dependencies.backend.claimResources === undefined)
|
|
692
|
+
return claimCapabilityUnavailable(subcommand);
|
|
693
|
+
const result = await dependencies.backend.claimResources(context, {
|
|
237
694
|
session_id: parsed.value.session_id,
|
|
238
695
|
repository: parsed.value.repository,
|
|
239
696
|
claims: [{ resource: parsed.value.resource, mode: parsed.value.mode }],
|
|
240
|
-
};
|
|
241
|
-
|
|
242
|
-
|
|
697
|
+
});
|
|
698
|
+
return result.ok ? { ok: true, value: result.value } : result;
|
|
699
|
+
}
|
|
700
|
+
if (subcommand === "update") {
|
|
701
|
+
const parsed = parseClaimReplacementPairs(rest);
|
|
702
|
+
if (!parsed.ok)
|
|
703
|
+
return parsed;
|
|
704
|
+
if (dependencies.backend.updateClaims === undefined)
|
|
243
705
|
return claimCapabilityUnavailable(subcommand);
|
|
244
|
-
const result = await
|
|
706
|
+
const result = await dependencies.backend.updateClaims(context, {
|
|
707
|
+
session_id: parsed.value.session_id,
|
|
708
|
+
repository: parsed.value.repository,
|
|
709
|
+
claims: parsed.value.pairs.map((pair) => ({
|
|
710
|
+
resource: pair.resource,
|
|
711
|
+
mode: pair.mode,
|
|
712
|
+
})),
|
|
713
|
+
});
|
|
245
714
|
return result.ok ? { ok: true, value: result.value } : result;
|
|
246
715
|
}
|
|
247
716
|
if (subcommand === "claims") {
|
|
@@ -278,6 +747,28 @@ async function executeCommand(commandArguments, dependencies) {
|
|
|
278
747
|
const result = await dependencies.backend.createSession(context, options);
|
|
279
748
|
return result.ok ? { ok: true, value: result.value } : result;
|
|
280
749
|
}
|
|
750
|
+
if (subcommand === "run" || subcommand === "exec") {
|
|
751
|
+
const delimiter = rest.indexOf("--");
|
|
752
|
+
if (delimiter === -1) {
|
|
753
|
+
return failure(usageError("MISSING_ARGUMENT", "session run requires a -- terminator before the command."));
|
|
754
|
+
}
|
|
755
|
+
const parsed = parseOptions(rest.slice(0, delimiter), new Set(["--session"]));
|
|
756
|
+
if (!parsed.ok)
|
|
757
|
+
return parsed;
|
|
758
|
+
const command = rest[delimiter + 1];
|
|
759
|
+
if (command === undefined || command.length === 0) {
|
|
760
|
+
return failure(usageError("MISSING_ARGUMENT", "session run requires a command after --."));
|
|
761
|
+
}
|
|
762
|
+
const request = await resolveSandboxExecutionRequest(dependencies.backend, context, { session_id: parsed.value.session_id, enforce: true }, dependencies.sandboxProbe, dependencies.sandboxRuntimeLayout);
|
|
763
|
+
if (!request.ok)
|
|
764
|
+
return request;
|
|
765
|
+
const runner = dependencies.sandboxRunner ?? runSandboxedCommand;
|
|
766
|
+
const result = await runner(request.value, {
|
|
767
|
+
command,
|
|
768
|
+
args: rest.slice(delimiter + 2),
|
|
769
|
+
});
|
|
770
|
+
return result.ok ? { ok: true, value: result.value } : result;
|
|
771
|
+
}
|
|
281
772
|
if (subcommand === "id" || subcommand === "show" || subcommand === "close") {
|
|
282
773
|
if (subcommand === "id") {
|
|
283
774
|
const parsed = noOptions(rest);
|
|
@@ -300,33 +791,48 @@ async function executeCommand(commandArguments, dependencies) {
|
|
|
300
791
|
return selected.ok ? { ok: true, value: selected.value } : selected;
|
|
301
792
|
}
|
|
302
793
|
if (subcommand === "list") {
|
|
303
|
-
const parsed =
|
|
794
|
+
const parsed = parseOptions(rest, new Set(["--all", "--history", "--limit", "--offset"]));
|
|
304
795
|
if (!parsed.ok)
|
|
305
796
|
return parsed;
|
|
306
|
-
const
|
|
797
|
+
const options = sessionListingOptions(parsed.value);
|
|
798
|
+
if (!options.ok)
|
|
799
|
+
return options;
|
|
800
|
+
const result = await dependencies.backend.listSessions(context, options.value);
|
|
307
801
|
return result.ok ? { ok: true, value: result.value } : result;
|
|
308
802
|
}
|
|
309
803
|
return failure(new DomainError("UNKNOWN_COMMAND", `Unknown session subcommand: ${subcommand}.`, { subcommand }));
|
|
310
804
|
}
|
|
311
805
|
if (command === "resource") {
|
|
312
806
|
const resourceSubcommand = subcommand ?? "list";
|
|
313
|
-
if (resourceSubcommand === "claim"
|
|
314
|
-
const parsed =
|
|
807
|
+
if (resourceSubcommand === "claim") {
|
|
808
|
+
const parsed = parseSingleClaimPair(rest);
|
|
315
809
|
if (!parsed.ok)
|
|
316
810
|
return parsed;
|
|
317
|
-
if (
|
|
318
|
-
return failure(usageError("MISSING_ARGUMENT", "resource claim requires --resource and --mode."));
|
|
319
|
-
}
|
|
320
|
-
const operation = resourceSubcommand === "claim" ? dependencies.backend.claimResources : dependencies.backend.updateClaims;
|
|
321
|
-
if (operation === undefined)
|
|
811
|
+
if (dependencies.backend.claimResources === undefined)
|
|
322
812
|
return claimCapabilityUnavailable(resourceSubcommand);
|
|
323
|
-
const result = await
|
|
813
|
+
const result = await dependencies.backend.claimResources(context, {
|
|
324
814
|
session_id: parsed.value.session_id,
|
|
325
815
|
repository: parsed.value.repository,
|
|
326
816
|
claims: [{ resource: parsed.value.resource, mode: parsed.value.mode }],
|
|
327
817
|
});
|
|
328
818
|
return result.ok ? { ok: true, value: result.value } : result;
|
|
329
819
|
}
|
|
820
|
+
if (resourceSubcommand === "update") {
|
|
821
|
+
const parsed = parseClaimReplacementPairs(rest);
|
|
822
|
+
if (!parsed.ok)
|
|
823
|
+
return parsed;
|
|
824
|
+
if (dependencies.backend.updateClaims === undefined)
|
|
825
|
+
return claimCapabilityUnavailable(resourceSubcommand);
|
|
826
|
+
const result = await dependencies.backend.updateClaims(context, {
|
|
827
|
+
session_id: parsed.value.session_id,
|
|
828
|
+
repository: parsed.value.repository,
|
|
829
|
+
claims: parsed.value.pairs.map((pair) => ({
|
|
830
|
+
resource: pair.resource,
|
|
831
|
+
mode: pair.mode,
|
|
832
|
+
})),
|
|
833
|
+
});
|
|
834
|
+
return result.ok ? { ok: true, value: result.value } : result;
|
|
835
|
+
}
|
|
330
836
|
if (resourceSubcommand === "list" || resourceSubcommand === "claims") {
|
|
331
837
|
const parsed = parseOptions(rest, new Set(["--session"]));
|
|
332
838
|
if (!parsed.ok)
|
|
@@ -386,8 +892,59 @@ async function executeCommand(commandArguments, dependencies) {
|
|
|
386
892
|
const result = await dependencies.backend.checkpoint(context, options);
|
|
387
893
|
return result.ok ? { ok: true, value: result.value } : result;
|
|
388
894
|
}
|
|
895
|
+
if (command === "evidence" && subcommand === "snapshot") {
|
|
896
|
+
const parsed = parseOptions(rest, new Set(["--session"]));
|
|
897
|
+
if (!parsed.ok)
|
|
898
|
+
return parsed;
|
|
899
|
+
if (parsed.value.session_id === null) {
|
|
900
|
+
return failure(usageError("MISSING_ARGUMENT", "evidence snapshot requires --session.", { option: "--session" }));
|
|
901
|
+
}
|
|
902
|
+
if (dependencies.backend.repositoryEvidence === undefined) {
|
|
903
|
+
return repositoryEvidenceCapabilityUnavailable("evidence snapshot");
|
|
904
|
+
}
|
|
905
|
+
const options = { session_id: parsed.value.session_id };
|
|
906
|
+
const result = await dependencies.backend.repositoryEvidence(context, options);
|
|
907
|
+
return result.ok ? { ok: true, value: result.value } : result;
|
|
908
|
+
}
|
|
909
|
+
if (command === "diff") {
|
|
910
|
+
const parsed = parseOptions([subcommand, ...rest].filter((argument) => argument !== undefined), new Set(["--session", "--path", "--from", "--to", "--patch", "--max-bytes", "--max-hunks"]));
|
|
911
|
+
if (!parsed.ok)
|
|
912
|
+
return parsed;
|
|
913
|
+
if (parsed.value.session_id === null) {
|
|
914
|
+
return failure(usageError("MISSING_ARGUMENT", "diff requires --session.", { option: "--session" }));
|
|
915
|
+
}
|
|
916
|
+
if (parsed.value.paths.length === 0) {
|
|
917
|
+
return failure(usageError("MISSING_ARGUMENT", "diff requires at least one --path.", { option: "--path" }));
|
|
918
|
+
}
|
|
919
|
+
if (parsed.value.paths.length > EVIDENCE_MAX_DIFF_PATHS) {
|
|
920
|
+
return failure(usageError("INVALID_ARGUMENT", `diff accepts at most ${EVIDENCE_MAX_DIFF_PATHS} paths.`, {
|
|
921
|
+
option: "--path",
|
|
922
|
+
max: EVIDENCE_MAX_DIFF_PATHS,
|
|
923
|
+
}));
|
|
924
|
+
}
|
|
925
|
+
const maxBytes = boundedEvidenceInteger("--max-bytes", parsed.value.max_bytes, EVIDENCE_MAX_DIFF_BYTES);
|
|
926
|
+
if (!maxBytes.ok)
|
|
927
|
+
return maxBytes;
|
|
928
|
+
const maxHunks = boundedEvidenceInteger("--max-hunks", parsed.value.max_hunks, EVIDENCE_MAX_DIFF_HUNKS);
|
|
929
|
+
if (!maxHunks.ok)
|
|
930
|
+
return maxHunks;
|
|
931
|
+
if (dependencies.backend.repositoryDiff === undefined) {
|
|
932
|
+
return repositoryEvidenceCapabilityUnavailable("diff");
|
|
933
|
+
}
|
|
934
|
+
const options = {
|
|
935
|
+
session_id: parsed.value.session_id,
|
|
936
|
+
paths: parsed.value.paths,
|
|
937
|
+
from: parsed.value.from_revision,
|
|
938
|
+
to: parsed.value.to_revision,
|
|
939
|
+
include_patch: parsed.value.patch,
|
|
940
|
+
max_bytes: maxBytes.value,
|
|
941
|
+
max_hunks: maxHunks.value,
|
|
942
|
+
};
|
|
943
|
+
const result = await dependencies.backend.repositoryDiff(context, options);
|
|
944
|
+
return result.ok ? { ok: true, value: result.value } : result;
|
|
945
|
+
}
|
|
389
946
|
if (command === "commit") {
|
|
390
|
-
const parsed = parseOptions([subcommand, ...rest].filter((argument) => argument !== undefined), new Set(["--session", "--message", "--resource"]));
|
|
947
|
+
const parsed = parseOptions([subcommand, ...rest].filter((argument) => argument !== undefined), new Set(["--session", "--message", "--resource", "--message-pattern"]));
|
|
391
948
|
if (!parsed.ok)
|
|
392
949
|
return parsed;
|
|
393
950
|
if (parsed.value.message === null) {
|
|
@@ -402,6 +959,7 @@ async function executeCommand(commandArguments, dependencies) {
|
|
|
402
959
|
session_id: parsed.value.session_id,
|
|
403
960
|
message: parsed.value.message,
|
|
404
961
|
resources: parsed.value.resources,
|
|
962
|
+
message_pattern: parsed.value.message_pattern,
|
|
405
963
|
});
|
|
406
964
|
return result.ok ? { ok: true, value: result.value } : result;
|
|
407
965
|
}
|
|
@@ -436,10 +994,13 @@ async function executeCommand(commandArguments, dependencies) {
|
|
|
436
994
|
return result.ok ? { ok: true, value: result.value } : result;
|
|
437
995
|
}
|
|
438
996
|
if (command === "status") {
|
|
439
|
-
const parsed =
|
|
997
|
+
const parsed = parseOptions([subcommand, ...rest].filter((argument) => argument !== undefined), new Set(["--all", "--history", "--limit", "--offset"]));
|
|
440
998
|
if (!parsed.ok)
|
|
441
999
|
return parsed;
|
|
442
|
-
const
|
|
1000
|
+
const options = sessionListingOptions(parsed.value);
|
|
1001
|
+
if (!options.ok)
|
|
1002
|
+
return options;
|
|
1003
|
+
const result = await dependencies.backend.status(context, options.value);
|
|
443
1004
|
return result.ok ? { ok: true, value: result.value } : result;
|
|
444
1005
|
}
|
|
445
1006
|
if (command === "guard") {
|
|
@@ -517,6 +1078,8 @@ function commandName(commandArguments) {
|
|
|
517
1078
|
return commandArguments.slice(0, 2).join(" ");
|
|
518
1079
|
if (commandArguments[0] === "resource")
|
|
519
1080
|
return commandArguments.slice(0, 2).join(" ");
|
|
1081
|
+
if (commandArguments[0] === "evidence")
|
|
1082
|
+
return commandArguments.slice(0, 2).join(" ");
|
|
520
1083
|
return commandArguments[0] ?? "cli";
|
|
521
1084
|
}
|
|
522
1085
|
function claimCapabilityUnavailable(operation) {
|
|
@@ -532,11 +1095,20 @@ function checkpointCapabilityUnavailable() {
|
|
|
532
1095
|
operation: "checkpoint",
|
|
533
1096
|
}));
|
|
534
1097
|
}
|
|
1098
|
+
function repositoryEvidenceCapabilityUnavailable(operation) {
|
|
1099
|
+
return failure(new DomainError("BACKEND_UNAVAILABLE", "Repository evidence capability is not available.", { operation }));
|
|
1100
|
+
}
|
|
535
1101
|
function mutationCapabilityUnavailable(operation) {
|
|
536
1102
|
return failure(new DomainError("BACKEND_UNAVAILABLE", "Governed Git mutation capability is not available.", { operation }));
|
|
537
1103
|
}
|
|
538
1104
|
function deniedAuthorization(decision) {
|
|
539
1105
|
const code = decision.code === "ALLOWED" ? "OPERATION_REJECTED" : decision.code;
|
|
1106
|
+
const diagnosticDetails = code === "INSUFFICIENT_CLAIM_MODE"
|
|
1107
|
+
? {
|
|
1108
|
+
resource: decision.details.resource,
|
|
1109
|
+
granted_modes: decision.details.grantedModes,
|
|
1110
|
+
}
|
|
1111
|
+
: {};
|
|
540
1112
|
return failure(new DomainError(code, `Operation denied: ${code}.`, {
|
|
541
1113
|
allowed: false,
|
|
542
1114
|
schema_version: decision.schema_version,
|
|
@@ -550,6 +1122,7 @@ function deniedAuthorization(decision) {
|
|
|
550
1122
|
requested_session_id: decision.requested_session_id,
|
|
551
1123
|
state: decision.state,
|
|
552
1124
|
resources: decision.resources,
|
|
1125
|
+
...diagnosticDetails,
|
|
553
1126
|
details: decision.details,
|
|
554
1127
|
}));
|
|
555
1128
|
}
|
|
@@ -559,15 +1132,18 @@ function emitFailure(mode, command, error, io) {
|
|
|
559
1132
|
}
|
|
560
1133
|
export async function runCli(argv, dependencies = {}) {
|
|
561
1134
|
const io = dependencies.io ?? defaultCliIO();
|
|
562
|
-
const
|
|
1135
|
+
const delimiter = argv.indexOf("--");
|
|
1136
|
+
const globalArguments = delimiter === -1 ? argv : argv.slice(0, delimiter);
|
|
1137
|
+
const mode = globalArguments.includes("--json") ? "json" : "human";
|
|
563
1138
|
const parsed = parseGlobalArguments(argv);
|
|
564
1139
|
if (!parsed.ok)
|
|
565
1140
|
return emitFailure(mode, "cli", parsed.error, io);
|
|
566
1141
|
if (parsed.value.help) {
|
|
1142
|
+
const spec = helpSpecFor(parsed.value.commandArguments);
|
|
567
1143
|
if (mode === "json")
|
|
568
|
-
io.stdout(renderSuccess(mode, "help",
|
|
1144
|
+
io.stdout(renderSuccess(mode, "help", helpPayload(spec)));
|
|
569
1145
|
else
|
|
570
|
-
io.stdout(
|
|
1146
|
+
io.stdout(helpText(spec));
|
|
571
1147
|
return EXIT_CODES.success;
|
|
572
1148
|
}
|
|
573
1149
|
if (parsed.value.version) {
|
|
@@ -599,10 +1175,23 @@ export async function runCli(argv, dependencies = {}) {
|
|
|
599
1175
|
const cwd = dependencies.cwd ?? process.cwd();
|
|
600
1176
|
try {
|
|
601
1177
|
const backend = dependencies.backend ?? createLocalSessionBackend();
|
|
602
|
-
const result = await executeCommand(parsed.value.commandArguments, {
|
|
1178
|
+
const result = await executeCommand(parsed.value.commandArguments, {
|
|
1179
|
+
backend,
|
|
1180
|
+
cwd,
|
|
1181
|
+
sandboxRunner: dependencies.sandboxRunner,
|
|
1182
|
+
sandboxProbe: dependencies.sandboxProbe,
|
|
1183
|
+
sandboxRuntimeLayout: dependencies.sandboxRuntimeLayout,
|
|
1184
|
+
});
|
|
603
1185
|
if (!result.ok)
|
|
604
1186
|
return emitFailure(mode, command, result.error, io);
|
|
605
1187
|
io.stdout(renderSuccess(mode, command, result.value));
|
|
1188
|
+
if ((command === "session run" || command === "session exec") &&
|
|
1189
|
+
typeof result.value.exit_code === "number" &&
|
|
1190
|
+
result.value.exit_code !== 0) {
|
|
1191
|
+
return result.value.exit_code >= 1 && result.value.exit_code <= 255
|
|
1192
|
+
? result.value.exit_code
|
|
1193
|
+
: EXIT_CODES.rejected;
|
|
1194
|
+
}
|
|
606
1195
|
return EXIT_CODES.success;
|
|
607
1196
|
}
|
|
608
1197
|
catch {
|