nawabari 0.1.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +347 -5
- package/dist/cli.js +770 -51
- package/dist/cli.js.map +1 -1
- package/dist/contract.d.ts +9 -0
- package/dist/contract.js +291 -0
- package/dist/contract.js.map +1 -0
- package/dist/domain/doctor.d.ts +1 -1
- package/dist/domain/doctor.js +99 -90
- package/dist/domain/doctor.js.map +1 -1
- package/dist/domain/errors.d.ts +1 -1
- package/dist/domain/errors.js +5 -1
- package/dist/domain/errors.js.map +1 -1
- package/dist/domain/session-backend.d.ts +15 -3
- package/dist/domain/session-backend.js +348 -7
- package/dist/domain/session-backend.js.map +1 -1
- package/dist/domain/session.d.ts +247 -2
- package/dist/domain/session.js +42 -0
- package/dist/domain/session.js.map +1 -1
- package/dist/errors.d.ts +2 -2
- package/dist/errors.js.map +1 -1
- package/dist/git.d.ts +102 -0
- package/dist/git.js +714 -30
- package/dist/git.js.map +1 -1
- package/dist/operation-authorization.d.ts +76 -0
- package/dist/operation-authorization.js +41 -0
- package/dist/operation-authorization.js.map +1 -0
- 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 +76 -22
- package/dist/presentation.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 +87 -0
- package/dist/resource-claims.js +450 -0
- package/dist/resource-claims.js.map +1 -0
- package/dist/session-registry.d.ts +251 -1
- package/dist/session-registry.js +2094 -257
- package/dist/session-registry.js.map +1 -1
- package/package.json +2 -1
package/dist/cli.js
CHANGED
|
@@ -1,53 +1,345 @@
|
|
|
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";
|
|
8
|
+
import { MACHINE_CONTRACT_ID, MACHINE_CONTRACT_SCHEMA_VERSION, machineContract } from "./contract.js";
|
|
6
9
|
const CLI_NAME = "nawabari";
|
|
7
10
|
const packageMetadata = createRequire(import.meta.url)("../package.json");
|
|
8
11
|
const VERSION = packageMetadata.version;
|
|
9
|
-
const
|
|
10
|
-
|
|
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
|
-
"session
|
|
42
|
-
"
|
|
43
|
-
|
|
44
|
-
"
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
12
|
+
const GLOBAL_HELP_OPTIONS = [
|
|
13
|
+
{ name: "--json", description: "Emit one stable JSON document on stdout" },
|
|
14
|
+
{ name: "--help", description: "Show command-specific help" },
|
|
15
|
+
{ name: "--version", description: "Print the installed version" },
|
|
16
|
+
];
|
|
17
|
+
const option = (name, description, options = {}) => ({ name, description, ...options });
|
|
18
|
+
const HELP_COMMANDS = [
|
|
19
|
+
{
|
|
20
|
+
name: "session create",
|
|
21
|
+
summary: "Request a new Nawabari session",
|
|
22
|
+
usage: `${CLI_NAME} session create [options]`,
|
|
23
|
+
options: [
|
|
24
|
+
option("--branch", "Branch to create; omitted uses the generated session branch", {
|
|
25
|
+
value: "<name>",
|
|
26
|
+
default: "nawabari/session/<session_id>",
|
|
27
|
+
}),
|
|
28
|
+
option("--worktree", "Managed worktree path; omitted uses the resolved repository-local root", {
|
|
29
|
+
value: "<path>",
|
|
30
|
+
default: "<managed_worktree_root>/<repository>-<session_id>",
|
|
31
|
+
}),
|
|
32
|
+
option("--base", "Commit-resolving base ref for the new worktree", { value: "<ref>", default: "HEAD" }),
|
|
33
|
+
option("--label", "Optional display label; never used as an identity", { value: "<text>", default: "omitted" }),
|
|
34
|
+
],
|
|
35
|
+
notes: ["All create options are optional. Use status --json to discover managed_worktree_root."],
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
name: "session id",
|
|
39
|
+
summary: "Resolve the current session identity",
|
|
40
|
+
usage: `${CLI_NAME} session id`,
|
|
41
|
+
options: [],
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
name: "session show",
|
|
45
|
+
summary: "Show the current or selected session",
|
|
46
|
+
usage: `${CLI_NAME} session show [--session <id>]`,
|
|
47
|
+
options: [option("--session", "Select a session instead of the current worktree owner", { value: "<id>" })],
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
name: "session list",
|
|
51
|
+
summary: "List bounded repository session records",
|
|
52
|
+
usage: `${CLI_NAME} session list [--all|--history]`,
|
|
53
|
+
options: [
|
|
54
|
+
option("--all", "Include closed history; explicit unbounded history view"),
|
|
55
|
+
option("--history", "Alias for --all"),
|
|
56
|
+
],
|
|
57
|
+
notes: ["Default output excludes closed records and is limited to 64 records."],
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
name: "session claim",
|
|
61
|
+
summary: "Add a canonical resource claim",
|
|
62
|
+
usage: `${CLI_NAME} session claim --resource <path-or-glob> --mode <read|write|exclusive-write> [--session <id>]`,
|
|
63
|
+
options: [
|
|
64
|
+
option("--resource", "Repository-relative resource", { value: "<path-or-glob>", required: true }),
|
|
65
|
+
option("--mode", "Granted claim mode", { value: "<read|write|exclusive-write>", required: true }),
|
|
66
|
+
option("--session", "Target active session; omitted resolves the current owner", { value: "<id>" }),
|
|
67
|
+
option("--repository", "Expected repository identity", { value: "<id>" }),
|
|
68
|
+
],
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
name: "session update",
|
|
72
|
+
summary: "Replace a session's resource claims",
|
|
73
|
+
usage: `${CLI_NAME} session update --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 claims",
|
|
83
|
+
summary: "List canonical resource claims",
|
|
84
|
+
usage: `${CLI_NAME} session claims [--session <id>]`,
|
|
85
|
+
options: [option("--session", "Select a session; omitted lists all claims", { value: "<id>" })],
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
name: "session release",
|
|
89
|
+
summary: "Release resource claims",
|
|
90
|
+
usage: `${CLI_NAME} session release [--session <id>] [--claim-id <id>]`,
|
|
91
|
+
options: [
|
|
92
|
+
option("--session", "Target session; omitted resolves the current owner", { value: "<id>" }),
|
|
93
|
+
option("--claim-id", "Release only one claim; omitted releases all owned claims", { value: "<id>" }),
|
|
94
|
+
],
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
name: "resource claim",
|
|
98
|
+
summary: "Add a canonical resource claim (alias)",
|
|
99
|
+
usage: `${CLI_NAME} resource claim --resource <path-or-glob> --mode <read|write|exclusive-write>`,
|
|
100
|
+
options: [],
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
name: "resource update",
|
|
104
|
+
summary: "Replace a session's resource claims (alias)",
|
|
105
|
+
usage: `${CLI_NAME} resource update --resource <path-or-glob> --mode <read|write|exclusive-write>`,
|
|
106
|
+
options: [],
|
|
107
|
+
},
|
|
108
|
+
{
|
|
109
|
+
name: "resource list",
|
|
110
|
+
summary: "List canonical resource claims (alias)",
|
|
111
|
+
usage: `${CLI_NAME} resource list [--session <id>]`,
|
|
112
|
+
options: [option("--session", "Select a session; omitted lists all claims", { value: "<id>" })],
|
|
113
|
+
},
|
|
114
|
+
{
|
|
115
|
+
name: "resource release",
|
|
116
|
+
summary: "Release resource claims (alias)",
|
|
117
|
+
usage: `${CLI_NAME} resource release [--session <id>] [--claim-id <id>]`,
|
|
118
|
+
options: [
|
|
119
|
+
option("--session", "Target session; omitted resolves the current owner", { value: "<id>" }),
|
|
120
|
+
option("--claim-id", "Release only one claim; omitted releases all owned claims", { value: "<id>" }),
|
|
121
|
+
],
|
|
122
|
+
},
|
|
123
|
+
{
|
|
124
|
+
name: "session close",
|
|
125
|
+
summary: "Close the current or selected session",
|
|
126
|
+
usage: `${CLI_NAME} session close [--session <id>]`,
|
|
127
|
+
options: [option("--session", "Select a session instead of the current worktree owner", { value: "<id>" })],
|
|
128
|
+
},
|
|
129
|
+
{
|
|
130
|
+
name: "authorize",
|
|
131
|
+
summary: "Authorize an operation against concrete claims",
|
|
132
|
+
usage: `${CLI_NAME} authorize --operation <name> --resource <path> [--resource <path>] [--session <id>]`,
|
|
133
|
+
options: [
|
|
134
|
+
option("--session", "Assert the current session identity", { value: "<id>" }),
|
|
135
|
+
option("--operation", "Operation vocabulary entry", { value: "<name>", required: true }),
|
|
136
|
+
option("--resource", "Concrete repository-relative path; repeatable", { value: "<path>", required: true }),
|
|
137
|
+
],
|
|
138
|
+
},
|
|
139
|
+
{
|
|
140
|
+
name: "checkpoint",
|
|
141
|
+
summary: "Capture bounded Git execution evidence",
|
|
142
|
+
usage: `${CLI_NAME} checkpoint [--session <id>]`,
|
|
143
|
+
options: [option("--session", "Assert the current session identity", { value: "<id>" })],
|
|
144
|
+
},
|
|
145
|
+
{
|
|
146
|
+
name: "evidence snapshot",
|
|
147
|
+
summary: "Capture bounded read-only evidence for one owned session",
|
|
148
|
+
usage: `${CLI_NAME} evidence snapshot --session <id>`,
|
|
149
|
+
options: [option("--session", "Explicit owned session to observe", { value: "<id>", required: true })],
|
|
150
|
+
notes: ["The result is Git-observable physical evidence only; it contains no task or semantic interpretation."],
|
|
151
|
+
},
|
|
152
|
+
{
|
|
153
|
+
name: "diff",
|
|
154
|
+
summary: "Inspect bounded Git evidence for explicit paths",
|
|
155
|
+
usage: `${CLI_NAME} diff --session <id> --path <path> [options]`,
|
|
156
|
+
options: [
|
|
157
|
+
option("--session", "Explicit owned session to observe", { value: "<id>", required: true }),
|
|
158
|
+
option("--path", "Concrete repository-relative path; repeatable", { value: "<path>", required: true }),
|
|
159
|
+
option("--from", "Commit/ref at the start of the range", { value: "<ref>", default: "HEAD" }),
|
|
160
|
+
option("--to", "Commit/ref at the end of the range; omitted means worktree", { value: "<ref>" }),
|
|
161
|
+
option("--patch", "Include patch text; requires the bounded byte/hunk limits"),
|
|
162
|
+
option("--max-bytes", "Maximum UTF-8 patch bytes", { value: "<n>", default: String(EVIDENCE_MAX_DIFF_BYTES) }),
|
|
163
|
+
option("--max-hunks", "Maximum patch hunks", { value: "<n>", default: String(EVIDENCE_MAX_DIFF_HUNKS) }),
|
|
164
|
+
],
|
|
165
|
+
},
|
|
166
|
+
{
|
|
167
|
+
name: "commit",
|
|
168
|
+
summary: "Commit explicit claim-authorized resources",
|
|
169
|
+
usage: `${CLI_NAME} commit --message <final-message> --resource <path> [--resource <path>] [--session <id>] [--message-pattern <regex>]`,
|
|
170
|
+
options: [
|
|
171
|
+
option("--session", "Assert the current session identity", { value: "<id>" }),
|
|
172
|
+
option("--message", "Caller-decided final commit message", { value: "<final-message>", required: true }),
|
|
173
|
+
option("--resource", "Claim-covered concrete path; repeatable", { value: "<path>", required: true }),
|
|
174
|
+
option("--message-pattern", "Caller-declared commit-message rule; validated only when supplied", {
|
|
175
|
+
value: "<regex>",
|
|
176
|
+
}),
|
|
177
|
+
],
|
|
178
|
+
},
|
|
179
|
+
{
|
|
180
|
+
name: "push",
|
|
181
|
+
summary: "Push the owned branch to an explicit target",
|
|
182
|
+
usage: `${CLI_NAME} push --remote <name> --branch <name> --resource <path> [options]`,
|
|
183
|
+
options: [
|
|
184
|
+
option("--session", "Assert the current session identity", { value: "<id>" }),
|
|
185
|
+
option("--resource", "Claim-covered concrete path; repeatable", { value: "<path>", required: true }),
|
|
186
|
+
option("--remote", "Explicit Git remote", { value: "<name>", required: true }),
|
|
187
|
+
option("--branch", "Explicit target branch", { value: "<name>", required: true }),
|
|
188
|
+
option("--remote-branch", "Explicit remote branch alias for --branch", { value: "<name>" }),
|
|
189
|
+
option("--force", "Allow force-with-lease when relation requires it"),
|
|
190
|
+
option("--create-upstream", "Allow creation of a missing upstream"),
|
|
191
|
+
],
|
|
192
|
+
},
|
|
193
|
+
{
|
|
194
|
+
name: "status",
|
|
195
|
+
summary: "Show repository context and bounded session status",
|
|
196
|
+
usage: `${CLI_NAME} status [--all|--history]`,
|
|
197
|
+
options: [
|
|
198
|
+
option("--all", "Include closed history; explicit unbounded history view"),
|
|
199
|
+
option("--history", "Alias for --all"),
|
|
200
|
+
],
|
|
201
|
+
notes: ["The default machine result exposes managed_worktree_root for session-create path discovery."],
|
|
202
|
+
},
|
|
203
|
+
{
|
|
204
|
+
name: "guard",
|
|
205
|
+
summary: "Authorize the current worktree or operation",
|
|
206
|
+
usage: `${CLI_NAME} guard [--session <id>] [--operation <name> --resource <path>]`,
|
|
207
|
+
options: [
|
|
208
|
+
option("--session", "Assert the current session identity", { value: "<id>" }),
|
|
209
|
+
option("--operation", "Authorize an operation when resources are supplied", { value: "<name>" }),
|
|
210
|
+
option("--resource", "Concrete resource; repeatable with --operation", { value: "<path>" }),
|
|
211
|
+
],
|
|
212
|
+
},
|
|
213
|
+
{
|
|
214
|
+
name: "gc",
|
|
215
|
+
summary: "Detect or clean eligible stale sessions",
|
|
216
|
+
usage: `${CLI_NAME} gc [--dry-run|--apply]`,
|
|
217
|
+
options: [
|
|
218
|
+
option("--apply", "Apply only cleanup that passes safety checks"),
|
|
219
|
+
option("--dry-run", "Preflight eligible stale cleanup without mutation"),
|
|
220
|
+
],
|
|
221
|
+
notes: [
|
|
222
|
+
"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.",
|
|
223
|
+
],
|
|
224
|
+
},
|
|
225
|
+
{
|
|
226
|
+
name: "doctor",
|
|
227
|
+
summary: "Check local Nawabari prerequisites and reconciliation",
|
|
228
|
+
usage: `${CLI_NAME} doctor`,
|
|
229
|
+
options: [],
|
|
230
|
+
},
|
|
231
|
+
{
|
|
232
|
+
name: "capabilities",
|
|
233
|
+
summary: "Describe the standalone CLI/JSON contract",
|
|
234
|
+
usage: `${CLI_NAME} capabilities`,
|
|
235
|
+
options: [],
|
|
236
|
+
},
|
|
237
|
+
];
|
|
238
|
+
const ROOT_HELP_SPEC = {
|
|
239
|
+
name: "root",
|
|
240
|
+
summary: "Standalone local Git/session ownership CLI",
|
|
241
|
+
usage: `${CLI_NAME} <command> [options]`,
|
|
242
|
+
options: GLOBAL_HELP_OPTIONS,
|
|
50
243
|
};
|
|
244
|
+
function helpSpecFor(commandArguments) {
|
|
245
|
+
if (commandArguments.length === 0)
|
|
246
|
+
return ROOT_HELP_SPEC;
|
|
247
|
+
let key = commandArguments[0] === "resource"
|
|
248
|
+
? `resource ${commandArguments[1] ?? "list"}`
|
|
249
|
+
: commandArguments.slice(0, 2).join(" ");
|
|
250
|
+
if (key === "resource claims")
|
|
251
|
+
key = "resource list";
|
|
252
|
+
const direct = HELP_COMMANDS.find((spec) => spec.name === key);
|
|
253
|
+
if (direct !== undefined && !direct.name.startsWith("resource "))
|
|
254
|
+
return direct;
|
|
255
|
+
const aliasTarget = direct?.name.replace(/^resource /u, "session ");
|
|
256
|
+
const target = aliasTarget === undefined ? undefined : HELP_COMMANDS.find((spec) => spec.name === aliasTarget);
|
|
257
|
+
if (direct !== undefined && target !== undefined) {
|
|
258
|
+
return {
|
|
259
|
+
name: direct.name,
|
|
260
|
+
summary: direct.summary,
|
|
261
|
+
usage: direct.usage,
|
|
262
|
+
options: direct.options,
|
|
263
|
+
notes: direct.notes,
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
return HELP_COMMANDS.find((spec) => spec.name === commandArguments[0]) ?? ROOT_HELP_SPEC;
|
|
267
|
+
}
|
|
268
|
+
function helpPayload(spec) {
|
|
269
|
+
if (spec.name === "root") {
|
|
270
|
+
const optionNames = (options) => options.map((candidate) => candidate.name);
|
|
271
|
+
const sessionOptions = HELP_COMMANDS.filter((command) => command.name.startsWith("session ")).flatMap((command) => command.options);
|
|
272
|
+
const unique = (values) => values.filter((value, index) => values.indexOf(value) === index);
|
|
273
|
+
const sessionListOnlyOptions = new Set(["--all", "--history"]);
|
|
274
|
+
const optionsFor = (name) => optionNames(HELP_COMMANDS.find((command) => command.name === name)?.options ?? []);
|
|
275
|
+
return {
|
|
276
|
+
usage: `Usage: ${spec.usage}`,
|
|
277
|
+
commands: HELP_COMMANDS.map((command) => command.name),
|
|
278
|
+
options: ["--json", "--help", "--version"],
|
|
279
|
+
session_options: unique(sessionOptions.map((option) => option.name).filter((name) => !sessionListOnlyOptions.has(name))),
|
|
280
|
+
authorization_options: optionsFor("authorize"),
|
|
281
|
+
checkpoint_options: optionsFor("checkpoint"),
|
|
282
|
+
commit_options: optionsFor("commit"),
|
|
283
|
+
push_options: optionsFor("push"),
|
|
284
|
+
gc_options: optionsFor("gc"),
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
const options = spec.options.map((candidate) => ({
|
|
288
|
+
name: candidate.name,
|
|
289
|
+
...(candidate.value === undefined ? {} : { value: candidate.value }),
|
|
290
|
+
required: candidate.required === true,
|
|
291
|
+
...(candidate.default === undefined ? {} : { default: candidate.default }),
|
|
292
|
+
description: candidate.description,
|
|
293
|
+
}));
|
|
294
|
+
return {
|
|
295
|
+
help_for: spec.name,
|
|
296
|
+
usage: spec.usage,
|
|
297
|
+
summary: spec.summary,
|
|
298
|
+
required_options: spec.options
|
|
299
|
+
.filter((candidate) => candidate.required === true)
|
|
300
|
+
.map((candidate) => candidate.name),
|
|
301
|
+
optional_options: spec.options
|
|
302
|
+
.filter((candidate) => candidate.required !== true)
|
|
303
|
+
.map((candidate) => candidate.name),
|
|
304
|
+
defaults: Object.fromEntries(spec.options
|
|
305
|
+
.filter((candidate) => candidate.default !== undefined)
|
|
306
|
+
.map((candidate) => [candidate.name, candidate.default])),
|
|
307
|
+
options,
|
|
308
|
+
...(spec.notes === undefined ? {} : { notes: [...spec.notes] }),
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
function helpText(spec) {
|
|
312
|
+
const lines = [`Usage: ${spec.usage}`, "", spec.summary];
|
|
313
|
+
if (spec.name === "root") {
|
|
314
|
+
lines.push("", "Commands:");
|
|
315
|
+
for (const command of HELP_COMMANDS)
|
|
316
|
+
lines.push(` ${command.name.padEnd(20)} ${command.summary}`);
|
|
317
|
+
lines.push("", "Global options:");
|
|
318
|
+
for (const candidate of GLOBAL_HELP_OPTIONS) {
|
|
319
|
+
const label = candidate.name === "--help" ? "-h, --help" : candidate.name;
|
|
320
|
+
lines.push(` ${label.padEnd(20)} ${candidate.description}`);
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
else {
|
|
324
|
+
lines.push("", "Options:");
|
|
325
|
+
if (spec.options.length === 0)
|
|
326
|
+
lines.push(" (none)");
|
|
327
|
+
for (const candidate of spec.options) {
|
|
328
|
+
const label = candidate.value === undefined ? candidate.name : `${candidate.name} ${candidate.value}`;
|
|
329
|
+
const qualifier = candidate.required === true
|
|
330
|
+
? "required"
|
|
331
|
+
: `optional${candidate.default === undefined ? "" : `; default: ${candidate.default}`}`;
|
|
332
|
+
lines.push(` ${label.padEnd(38)} ${qualifier}; ${candidate.description}`);
|
|
333
|
+
}
|
|
334
|
+
if (spec.notes !== undefined) {
|
|
335
|
+
lines.push("", "Notes:");
|
|
336
|
+
for (const note of spec.notes)
|
|
337
|
+
lines.push(` ${note}`);
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
return lines.join("\n");
|
|
341
|
+
}
|
|
342
|
+
const HELP_TEXT = helpText(ROOT_HELP_SPEC);
|
|
51
343
|
function usageError(code, message, details = null) {
|
|
52
344
|
return new DomainError(code, message, details);
|
|
53
345
|
}
|
|
@@ -91,7 +383,29 @@ function parseOptions(arguments_, allowed) {
|
|
|
91
383
|
worktree: null,
|
|
92
384
|
base: null,
|
|
93
385
|
label: null,
|
|
386
|
+
resource: null,
|
|
387
|
+
resources: [],
|
|
388
|
+
operation: null,
|
|
389
|
+
message: null,
|
|
390
|
+
message_pattern: null,
|
|
391
|
+
remote: null,
|
|
392
|
+
remote_branch: null,
|
|
393
|
+
mode: null,
|
|
394
|
+
claim_id: null,
|
|
395
|
+
repository: null,
|
|
94
396
|
apply: false,
|
|
397
|
+
force: false,
|
|
398
|
+
create_upstream: false,
|
|
399
|
+
all: false,
|
|
400
|
+
history: false,
|
|
401
|
+
limit: null,
|
|
402
|
+
offset: null,
|
|
403
|
+
paths: [],
|
|
404
|
+
from_revision: null,
|
|
405
|
+
to_revision: null,
|
|
406
|
+
patch: false,
|
|
407
|
+
max_bytes: null,
|
|
408
|
+
max_hunks: null,
|
|
95
409
|
};
|
|
96
410
|
let dryRun = false;
|
|
97
411
|
for (let index = 0; index < arguments_.length; index += 1) {
|
|
@@ -99,14 +413,30 @@ function parseOptions(arguments_, allowed) {
|
|
|
99
413
|
if (!allowed.has(name)) {
|
|
100
414
|
return failure(usageError("INVALID_ARGUMENT", `Unknown option: ${name}.`, { option: name }));
|
|
101
415
|
}
|
|
102
|
-
if (name === "--apply" ||
|
|
416
|
+
if (name === "--apply" ||
|
|
417
|
+
name === "--dry-run" ||
|
|
418
|
+
name === "--force" ||
|
|
419
|
+
name === "--create-upstream" ||
|
|
420
|
+
name === "--all" ||
|
|
421
|
+
name === "--history" ||
|
|
422
|
+
name === "--patch") {
|
|
103
423
|
if (inlineValue !== null) {
|
|
104
424
|
return failure(usageError("INVALID_ARGUMENT", `${name} does not accept a value.`, { option: name }));
|
|
105
425
|
}
|
|
106
426
|
if (name === "--apply")
|
|
107
427
|
options.apply = true;
|
|
108
|
-
else
|
|
428
|
+
else if (name === "--dry-run")
|
|
109
429
|
dryRun = true;
|
|
430
|
+
else if (name === "--force")
|
|
431
|
+
options.force = true;
|
|
432
|
+
else if (name === "--create-upstream")
|
|
433
|
+
options.create_upstream = true;
|
|
434
|
+
else if (name === "--all")
|
|
435
|
+
options.all = true;
|
|
436
|
+
else if (name === "--history")
|
|
437
|
+
options.history = true;
|
|
438
|
+
else
|
|
439
|
+
options.patch = true;
|
|
110
440
|
continue;
|
|
111
441
|
}
|
|
112
442
|
const value = inlineValue ?? arguments_[index + 1];
|
|
@@ -125,6 +455,40 @@ function parseOptions(arguments_, allowed) {
|
|
|
125
455
|
options.base = value;
|
|
126
456
|
else if (name === "--label")
|
|
127
457
|
options.label = value;
|
|
458
|
+
else if (name === "--resource") {
|
|
459
|
+
options.resource = value;
|
|
460
|
+
options.resources.push(value);
|
|
461
|
+
}
|
|
462
|
+
else if (name === "--operation")
|
|
463
|
+
options.operation = value;
|
|
464
|
+
else if (name === "--message")
|
|
465
|
+
options.message = value;
|
|
466
|
+
else if (name === "--message-pattern")
|
|
467
|
+
options.message_pattern = value;
|
|
468
|
+
else if (name === "--remote")
|
|
469
|
+
options.remote = value;
|
|
470
|
+
else if (name === "--remote-branch")
|
|
471
|
+
options.remote_branch = value;
|
|
472
|
+
else if (name === "--mode")
|
|
473
|
+
options.mode = value;
|
|
474
|
+
else if (name === "--claim-id")
|
|
475
|
+
options.claim_id = value;
|
|
476
|
+
else if (name === "--repository")
|
|
477
|
+
options.repository = value;
|
|
478
|
+
else if (name === "--limit")
|
|
479
|
+
options.limit = value;
|
|
480
|
+
else if (name === "--offset")
|
|
481
|
+
options.offset = value;
|
|
482
|
+
else if (name === "--path")
|
|
483
|
+
options.paths.push(value);
|
|
484
|
+
else if (name === "--from")
|
|
485
|
+
options.from_revision = value;
|
|
486
|
+
else if (name === "--to")
|
|
487
|
+
options.to_revision = value;
|
|
488
|
+
else if (name === "--max-bytes")
|
|
489
|
+
options.max_bytes = value;
|
|
490
|
+
else if (name === "--max-hunks")
|
|
491
|
+
options.max_hunks = value;
|
|
128
492
|
}
|
|
129
493
|
if (options.apply && dryRun) {
|
|
130
494
|
return failure(usageError("INVALID_ARGUMENT", "--apply and --dry-run cannot be used together."));
|
|
@@ -134,6 +498,52 @@ function parseOptions(arguments_, allowed) {
|
|
|
134
498
|
function noOptions(arguments_) {
|
|
135
499
|
return parseOptions(arguments_, new Set());
|
|
136
500
|
}
|
|
501
|
+
function sessionListingOptions(parsed) {
|
|
502
|
+
const parseInteger = (option, value) => {
|
|
503
|
+
if (value === null)
|
|
504
|
+
return { ok: true, value: undefined };
|
|
505
|
+
if (!/^\d+$/u.test(value)) {
|
|
506
|
+
return failure(usageError("INVALID_ARGUMENT", `${option} requires a non-negative integer.`, { option, value }));
|
|
507
|
+
}
|
|
508
|
+
const parsedValue = Number(value);
|
|
509
|
+
if (!Number.isSafeInteger(parsedValue)) {
|
|
510
|
+
return failure(usageError("INVALID_ARGUMENT", `${option} is outside the safe integer range.`, { option }));
|
|
511
|
+
}
|
|
512
|
+
return { ok: true, value: parsedValue };
|
|
513
|
+
};
|
|
514
|
+
const limit = parseInteger("--limit", parsed.limit);
|
|
515
|
+
if (!limit.ok)
|
|
516
|
+
return limit;
|
|
517
|
+
if (limit.value !== undefined && (limit.value < 1 || limit.value > MAX_SESSION_LIST_LIMIT)) {
|
|
518
|
+
return failure(usageError("INVALID_ARGUMENT", `--limit must be between 1 and ${MAX_SESSION_LIST_LIMIT}.`, {
|
|
519
|
+
option: "--limit",
|
|
520
|
+
max: MAX_SESSION_LIST_LIMIT,
|
|
521
|
+
}));
|
|
522
|
+
}
|
|
523
|
+
const offset = parseInteger("--offset", parsed.offset);
|
|
524
|
+
if (!offset.ok)
|
|
525
|
+
return offset;
|
|
526
|
+
return {
|
|
527
|
+
ok: true,
|
|
528
|
+
value: {
|
|
529
|
+
include_closed: parsed.all || parsed.history,
|
|
530
|
+
...(limit.value === undefined ? {} : { limit: limit.value }),
|
|
531
|
+
...(offset.value === undefined ? {} : { offset: offset.value }),
|
|
532
|
+
},
|
|
533
|
+
};
|
|
534
|
+
}
|
|
535
|
+
function boundedEvidenceInteger(option, value, max) {
|
|
536
|
+
if (value === null)
|
|
537
|
+
return { ok: true, value: undefined };
|
|
538
|
+
if (!/^\d+$/u.test(value)) {
|
|
539
|
+
return failure(usageError("INVALID_ARGUMENT", `${option} requires a positive integer.`, { option, value }));
|
|
540
|
+
}
|
|
541
|
+
const parsed = Number(value);
|
|
542
|
+
if (!Number.isSafeInteger(parsed) || parsed < 1 || parsed > max) {
|
|
543
|
+
return failure(usageError("INVALID_ARGUMENT", `${option} must be between 1 and ${max}.`, { option, max, value }));
|
|
544
|
+
}
|
|
545
|
+
return { ok: true, value: parsed };
|
|
546
|
+
}
|
|
137
547
|
function sessionContext(cwd) {
|
|
138
548
|
return { cwd };
|
|
139
549
|
}
|
|
@@ -147,6 +557,48 @@ async function executeCommand(commandArguments, dependencies) {
|
|
|
147
557
|
if (subcommand === undefined) {
|
|
148
558
|
return failure(usageError("MISSING_ARGUMENT", "session requires a subcommand."));
|
|
149
559
|
}
|
|
560
|
+
if (subcommand === "claim" || subcommand === "update") {
|
|
561
|
+
const parsed = parseOptions(rest, new Set(["--session", "--resource", "--mode", "--repository"]));
|
|
562
|
+
if (!parsed.ok)
|
|
563
|
+
return parsed;
|
|
564
|
+
if (parsed.value.resource === null) {
|
|
565
|
+
return failure(usageError("MISSING_ARGUMENT", "--resource requires a value.", { option: "--resource" }));
|
|
566
|
+
}
|
|
567
|
+
if (parsed.value.mode === null) {
|
|
568
|
+
return failure(usageError("MISSING_ARGUMENT", "--mode requires a value.", { option: "--mode" }));
|
|
569
|
+
}
|
|
570
|
+
const options = {
|
|
571
|
+
session_id: parsed.value.session_id,
|
|
572
|
+
repository: parsed.value.repository,
|
|
573
|
+
claims: [{ resource: parsed.value.resource, mode: parsed.value.mode }],
|
|
574
|
+
};
|
|
575
|
+
const operation = subcommand === "claim" ? dependencies.backend.claimResources : dependencies.backend.updateClaims;
|
|
576
|
+
if (operation === undefined)
|
|
577
|
+
return claimCapabilityUnavailable(subcommand);
|
|
578
|
+
const result = await operation.call(dependencies.backend, context, options);
|
|
579
|
+
return result.ok ? { ok: true, value: result.value } : result;
|
|
580
|
+
}
|
|
581
|
+
if (subcommand === "claims") {
|
|
582
|
+
const parsed = parseOptions(rest, new Set(["--session"]));
|
|
583
|
+
if (!parsed.ok)
|
|
584
|
+
return parsed;
|
|
585
|
+
if (dependencies.backend.listClaims === undefined)
|
|
586
|
+
return claimCapabilityUnavailable("claims");
|
|
587
|
+
const result = await dependencies.backend.listClaims(context, parsed.value.session_id);
|
|
588
|
+
return result.ok ? { ok: true, value: result.value } : result;
|
|
589
|
+
}
|
|
590
|
+
if (subcommand === "release") {
|
|
591
|
+
const parsed = parseOptions(rest, new Set(["--session", "--claim-id"]));
|
|
592
|
+
if (!parsed.ok)
|
|
593
|
+
return parsed;
|
|
594
|
+
if (dependencies.backend.releaseClaims === undefined)
|
|
595
|
+
return claimCapabilityUnavailable("release");
|
|
596
|
+
const result = await dependencies.backend.releaseClaims(context, {
|
|
597
|
+
session_id: parsed.value.session_id,
|
|
598
|
+
claim_ids: parsed.value.claim_id === null ? null : [parsed.value.claim_id],
|
|
599
|
+
});
|
|
600
|
+
return result.ok ? { ok: true, value: result.value } : result;
|
|
601
|
+
}
|
|
150
602
|
if (subcommand === "create") {
|
|
151
603
|
const parsed = parseOptions(rest, new Set(["--branch", "--worktree", "--base", "--label"]));
|
|
152
604
|
if (!parsed.ok)
|
|
@@ -182,25 +634,232 @@ async function executeCommand(commandArguments, dependencies) {
|
|
|
182
634
|
return selected.ok ? { ok: true, value: selected.value } : selected;
|
|
183
635
|
}
|
|
184
636
|
if (subcommand === "list") {
|
|
185
|
-
const parsed =
|
|
637
|
+
const parsed = parseOptions(rest, new Set(["--all", "--history", "--limit", "--offset"]));
|
|
186
638
|
if (!parsed.ok)
|
|
187
639
|
return parsed;
|
|
188
|
-
const
|
|
640
|
+
const options = sessionListingOptions(parsed.value);
|
|
641
|
+
if (!options.ok)
|
|
642
|
+
return options;
|
|
643
|
+
const result = await dependencies.backend.listSessions(context, options.value);
|
|
189
644
|
return result.ok ? { ok: true, value: result.value } : result;
|
|
190
645
|
}
|
|
191
646
|
return failure(new DomainError("UNKNOWN_COMMAND", `Unknown session subcommand: ${subcommand}.`, { subcommand }));
|
|
192
647
|
}
|
|
648
|
+
if (command === "resource") {
|
|
649
|
+
const resourceSubcommand = subcommand ?? "list";
|
|
650
|
+
if (resourceSubcommand === "claim" || resourceSubcommand === "update") {
|
|
651
|
+
const parsed = parseOptions(rest, new Set(["--session", "--resource", "--mode", "--repository"]));
|
|
652
|
+
if (!parsed.ok)
|
|
653
|
+
return parsed;
|
|
654
|
+
if (parsed.value.resource === null || parsed.value.mode === null) {
|
|
655
|
+
return failure(usageError("MISSING_ARGUMENT", "resource claim requires --resource and --mode."));
|
|
656
|
+
}
|
|
657
|
+
const operation = resourceSubcommand === "claim" ? dependencies.backend.claimResources : dependencies.backend.updateClaims;
|
|
658
|
+
if (operation === undefined)
|
|
659
|
+
return claimCapabilityUnavailable(resourceSubcommand);
|
|
660
|
+
const result = await operation.call(dependencies.backend, context, {
|
|
661
|
+
session_id: parsed.value.session_id,
|
|
662
|
+
repository: parsed.value.repository,
|
|
663
|
+
claims: [{ resource: parsed.value.resource, mode: parsed.value.mode }],
|
|
664
|
+
});
|
|
665
|
+
return result.ok ? { ok: true, value: result.value } : result;
|
|
666
|
+
}
|
|
667
|
+
if (resourceSubcommand === "list" || resourceSubcommand === "claims") {
|
|
668
|
+
const parsed = parseOptions(rest, new Set(["--session"]));
|
|
669
|
+
if (!parsed.ok)
|
|
670
|
+
return parsed;
|
|
671
|
+
if (dependencies.backend.listClaims === undefined)
|
|
672
|
+
return claimCapabilityUnavailable("list");
|
|
673
|
+
const result = await dependencies.backend.listClaims(context, parsed.value.session_id);
|
|
674
|
+
return result.ok ? { ok: true, value: result.value } : result;
|
|
675
|
+
}
|
|
676
|
+
if (resourceSubcommand === "release") {
|
|
677
|
+
const parsed = parseOptions(rest, new Set(["--session", "--claim-id"]));
|
|
678
|
+
if (!parsed.ok)
|
|
679
|
+
return parsed;
|
|
680
|
+
if (dependencies.backend.releaseClaims === undefined)
|
|
681
|
+
return claimCapabilityUnavailable("release");
|
|
682
|
+
const result = await dependencies.backend.releaseClaims(context, {
|
|
683
|
+
session_id: parsed.value.session_id,
|
|
684
|
+
claim_ids: parsed.value.claim_id === null ? null : [parsed.value.claim_id],
|
|
685
|
+
});
|
|
686
|
+
return result.ok ? { ok: true, value: result.value } : result;
|
|
687
|
+
}
|
|
688
|
+
return failure(new DomainError("UNKNOWN_COMMAND", `Unknown resource subcommand: ${resourceSubcommand}.`, {
|
|
689
|
+
subcommand: resourceSubcommand,
|
|
690
|
+
}));
|
|
691
|
+
}
|
|
692
|
+
if (command === "authorize") {
|
|
693
|
+
const parsed = parseOptions([subcommand, ...rest].filter((argument) => argument !== undefined), new Set(["--session", "--operation", "--resource"]));
|
|
694
|
+
if (!parsed.ok)
|
|
695
|
+
return parsed;
|
|
696
|
+
if (parsed.value.operation === null) {
|
|
697
|
+
return failure(usageError("MISSING_ARGUMENT", "--operation requires a value.", { option: "--operation" }));
|
|
698
|
+
}
|
|
699
|
+
if (parsed.value.resources.length === 0) {
|
|
700
|
+
return failure(usageError("MISSING_ARGUMENT", "--resource requires a value.", { option: "--resource" }));
|
|
701
|
+
}
|
|
702
|
+
if (dependencies.backend.authorizeOperation === undefined)
|
|
703
|
+
return authorizationCapabilityUnavailable();
|
|
704
|
+
const options = {
|
|
705
|
+
session_id: parsed.value.session_id,
|
|
706
|
+
operation: parsed.value.operation,
|
|
707
|
+
resources: parsed.value.resources,
|
|
708
|
+
};
|
|
709
|
+
const result = await dependencies.backend.authorizeOperation(context, options);
|
|
710
|
+
if (!result.ok)
|
|
711
|
+
return result;
|
|
712
|
+
return result.value.allowed
|
|
713
|
+
? { ok: true, value: result.value }
|
|
714
|
+
: deniedAuthorization(result.value);
|
|
715
|
+
}
|
|
716
|
+
if (command === "checkpoint") {
|
|
717
|
+
const parsed = parseOptions([subcommand, ...rest].filter((argument) => argument !== undefined), new Set(["--session"]));
|
|
718
|
+
if (!parsed.ok)
|
|
719
|
+
return parsed;
|
|
720
|
+
if (dependencies.backend.checkpoint === undefined)
|
|
721
|
+
return checkpointCapabilityUnavailable();
|
|
722
|
+
const options = { session_id: parsed.value.session_id };
|
|
723
|
+
const result = await dependencies.backend.checkpoint(context, options);
|
|
724
|
+
return result.ok ? { ok: true, value: result.value } : result;
|
|
725
|
+
}
|
|
726
|
+
if (command === "evidence" && subcommand === "snapshot") {
|
|
727
|
+
const parsed = parseOptions(rest, new Set(["--session"]));
|
|
728
|
+
if (!parsed.ok)
|
|
729
|
+
return parsed;
|
|
730
|
+
if (parsed.value.session_id === null) {
|
|
731
|
+
return failure(usageError("MISSING_ARGUMENT", "evidence snapshot requires --session.", { option: "--session" }));
|
|
732
|
+
}
|
|
733
|
+
if (dependencies.backend.repositoryEvidence === undefined) {
|
|
734
|
+
return repositoryEvidenceCapabilityUnavailable("evidence snapshot");
|
|
735
|
+
}
|
|
736
|
+
const options = { session_id: parsed.value.session_id };
|
|
737
|
+
const result = await dependencies.backend.repositoryEvidence(context, options);
|
|
738
|
+
return result.ok ? { ok: true, value: result.value } : result;
|
|
739
|
+
}
|
|
740
|
+
if (command === "diff") {
|
|
741
|
+
const parsed = parseOptions([subcommand, ...rest].filter((argument) => argument !== undefined), new Set(["--session", "--path", "--from", "--to", "--patch", "--max-bytes", "--max-hunks"]));
|
|
742
|
+
if (!parsed.ok)
|
|
743
|
+
return parsed;
|
|
744
|
+
if (parsed.value.session_id === null) {
|
|
745
|
+
return failure(usageError("MISSING_ARGUMENT", "diff requires --session.", { option: "--session" }));
|
|
746
|
+
}
|
|
747
|
+
if (parsed.value.paths.length === 0) {
|
|
748
|
+
return failure(usageError("MISSING_ARGUMENT", "diff requires at least one --path.", { option: "--path" }));
|
|
749
|
+
}
|
|
750
|
+
if (parsed.value.paths.length > EVIDENCE_MAX_DIFF_PATHS) {
|
|
751
|
+
return failure(usageError("INVALID_ARGUMENT", `diff accepts at most ${EVIDENCE_MAX_DIFF_PATHS} paths.`, {
|
|
752
|
+
option: "--path",
|
|
753
|
+
max: EVIDENCE_MAX_DIFF_PATHS,
|
|
754
|
+
}));
|
|
755
|
+
}
|
|
756
|
+
const maxBytes = boundedEvidenceInteger("--max-bytes", parsed.value.max_bytes, EVIDENCE_MAX_DIFF_BYTES);
|
|
757
|
+
if (!maxBytes.ok)
|
|
758
|
+
return maxBytes;
|
|
759
|
+
const maxHunks = boundedEvidenceInteger("--max-hunks", parsed.value.max_hunks, EVIDENCE_MAX_DIFF_HUNKS);
|
|
760
|
+
if (!maxHunks.ok)
|
|
761
|
+
return maxHunks;
|
|
762
|
+
if (dependencies.backend.repositoryDiff === undefined) {
|
|
763
|
+
return repositoryEvidenceCapabilityUnavailable("diff");
|
|
764
|
+
}
|
|
765
|
+
const options = {
|
|
766
|
+
session_id: parsed.value.session_id,
|
|
767
|
+
paths: parsed.value.paths,
|
|
768
|
+
from: parsed.value.from_revision,
|
|
769
|
+
to: parsed.value.to_revision,
|
|
770
|
+
include_patch: parsed.value.patch,
|
|
771
|
+
max_bytes: maxBytes.value,
|
|
772
|
+
max_hunks: maxHunks.value,
|
|
773
|
+
};
|
|
774
|
+
const result = await dependencies.backend.repositoryDiff(context, options);
|
|
775
|
+
return result.ok ? { ok: true, value: result.value } : result;
|
|
776
|
+
}
|
|
777
|
+
if (command === "commit") {
|
|
778
|
+
const parsed = parseOptions([subcommand, ...rest].filter((argument) => argument !== undefined), new Set(["--session", "--message", "--resource", "--message-pattern"]));
|
|
779
|
+
if (!parsed.ok)
|
|
780
|
+
return parsed;
|
|
781
|
+
if (parsed.value.message === null) {
|
|
782
|
+
return failure(usageError("MISSING_ARGUMENT", "--message requires a value.", { option: "--message" }));
|
|
783
|
+
}
|
|
784
|
+
if (parsed.value.resources.length === 0) {
|
|
785
|
+
return failure(usageError("MISSING_ARGUMENT", "--resource requires a value.", { option: "--resource" }));
|
|
786
|
+
}
|
|
787
|
+
if (dependencies.backend.commit === undefined)
|
|
788
|
+
return mutationCapabilityUnavailable("commit");
|
|
789
|
+
const result = await dependencies.backend.commit(context, {
|
|
790
|
+
session_id: parsed.value.session_id,
|
|
791
|
+
message: parsed.value.message,
|
|
792
|
+
resources: parsed.value.resources,
|
|
793
|
+
message_pattern: parsed.value.message_pattern,
|
|
794
|
+
});
|
|
795
|
+
return result.ok ? { ok: true, value: result.value } : result;
|
|
796
|
+
}
|
|
797
|
+
if (command === "push") {
|
|
798
|
+
const parsed = parseOptions([subcommand, ...rest].filter((argument) => argument !== undefined), new Set(["--session", "--resource", "--remote", "--branch", "--remote-branch", "--force", "--create-upstream"]));
|
|
799
|
+
if (!parsed.ok)
|
|
800
|
+
return parsed;
|
|
801
|
+
if (parsed.value.remote === null) {
|
|
802
|
+
return failure(usageError("MISSING_ARGUMENT", "--remote requires a value.", { option: "--remote" }));
|
|
803
|
+
}
|
|
804
|
+
if (parsed.value.branch === null && parsed.value.remote_branch === null) {
|
|
805
|
+
return failure(usageError("MISSING_ARGUMENT", "--branch requires a value.", { option: "--branch" }));
|
|
806
|
+
}
|
|
807
|
+
if (parsed.value.branch !== null &&
|
|
808
|
+
parsed.value.remote_branch !== null &&
|
|
809
|
+
parsed.value.branch !== parsed.value.remote_branch) {
|
|
810
|
+
return failure(usageError("INVALID_ARGUMENT", "--branch and --remote-branch must identify the same target."));
|
|
811
|
+
}
|
|
812
|
+
if (parsed.value.resources.length === 0) {
|
|
813
|
+
return failure(usageError("MISSING_ARGUMENT", "--resource requires a value.", { option: "--resource" }));
|
|
814
|
+
}
|
|
815
|
+
if (dependencies.backend.push === undefined)
|
|
816
|
+
return mutationCapabilityUnavailable("push");
|
|
817
|
+
const result = await dependencies.backend.push(context, {
|
|
818
|
+
session_id: parsed.value.session_id,
|
|
819
|
+
resources: parsed.value.resources,
|
|
820
|
+
remote: parsed.value.remote,
|
|
821
|
+
branch: parsed.value.branch ?? parsed.value.remote_branch,
|
|
822
|
+
force: parsed.value.force,
|
|
823
|
+
create_upstream: parsed.value.create_upstream,
|
|
824
|
+
});
|
|
825
|
+
return result.ok ? { ok: true, value: result.value } : result;
|
|
826
|
+
}
|
|
193
827
|
if (command === "status") {
|
|
194
|
-
const parsed =
|
|
828
|
+
const parsed = parseOptions([subcommand, ...rest].filter((argument) => argument !== undefined), new Set(["--all", "--history", "--limit", "--offset"]));
|
|
195
829
|
if (!parsed.ok)
|
|
196
830
|
return parsed;
|
|
197
|
-
const
|
|
831
|
+
const options = sessionListingOptions(parsed.value);
|
|
832
|
+
if (!options.ok)
|
|
833
|
+
return options;
|
|
834
|
+
const result = await dependencies.backend.status(context, options.value);
|
|
198
835
|
return result.ok ? { ok: true, value: result.value } : result;
|
|
199
836
|
}
|
|
200
837
|
if (command === "guard") {
|
|
201
|
-
const parsed = parseOptions([subcommand, ...rest].filter((argument) => argument !== undefined), new Set(["--session"]));
|
|
838
|
+
const parsed = parseOptions([subcommand, ...rest].filter((argument) => argument !== undefined), new Set(["--session", "--operation", "--resource"]));
|
|
202
839
|
if (!parsed.ok)
|
|
203
840
|
return parsed;
|
|
841
|
+
if (parsed.value.resources.length > 0 && parsed.value.operation === null) {
|
|
842
|
+
return failure(usageError("MISSING_ARGUMENT", "--operation is required when --resource is provided.", {
|
|
843
|
+
option: "--operation",
|
|
844
|
+
}));
|
|
845
|
+
}
|
|
846
|
+
if (parsed.value.operation !== null) {
|
|
847
|
+
if (parsed.value.resources.length === 0) {
|
|
848
|
+
return failure(usageError("MISSING_ARGUMENT", "--resource requires a value.", { option: "--resource" }));
|
|
849
|
+
}
|
|
850
|
+
if (dependencies.backend.authorizeOperation === undefined)
|
|
851
|
+
return authorizationCapabilityUnavailable();
|
|
852
|
+
const result = await dependencies.backend.authorizeOperation(context, {
|
|
853
|
+
session_id: parsed.value.session_id,
|
|
854
|
+
operation: parsed.value.operation,
|
|
855
|
+
resources: parsed.value.resources,
|
|
856
|
+
});
|
|
857
|
+
if (!result.ok)
|
|
858
|
+
return result;
|
|
859
|
+
return result.value.allowed
|
|
860
|
+
? { ok: true, value: result.value }
|
|
861
|
+
: deniedAuthorization(result.value);
|
|
862
|
+
}
|
|
204
863
|
const result = await dependencies.backend.guard(context, { session_id: parsed.value.session_id });
|
|
205
864
|
if (!result.ok)
|
|
206
865
|
return result;
|
|
@@ -248,8 +907,56 @@ async function executeCommand(commandArguments, dependencies) {
|
|
|
248
907
|
function commandName(commandArguments) {
|
|
249
908
|
if (commandArguments[0] === "session")
|
|
250
909
|
return commandArguments.slice(0, 2).join(" ");
|
|
910
|
+
if (commandArguments[0] === "resource")
|
|
911
|
+
return commandArguments.slice(0, 2).join(" ");
|
|
912
|
+
if (commandArguments[0] === "evidence")
|
|
913
|
+
return commandArguments.slice(0, 2).join(" ");
|
|
251
914
|
return commandArguments[0] ?? "cli";
|
|
252
915
|
}
|
|
916
|
+
function claimCapabilityUnavailable(operation) {
|
|
917
|
+
return failure(new DomainError("BACKEND_UNAVAILABLE", "Resource claim capability is not available.", { operation }));
|
|
918
|
+
}
|
|
919
|
+
function authorizationCapabilityUnavailable() {
|
|
920
|
+
return failure(new DomainError("BACKEND_UNAVAILABLE", "Operation authorization capability is not available.", {
|
|
921
|
+
operation: "authorize",
|
|
922
|
+
}));
|
|
923
|
+
}
|
|
924
|
+
function checkpointCapabilityUnavailable() {
|
|
925
|
+
return failure(new DomainError("BACKEND_UNAVAILABLE", "Checkpoint evidence capability is not available.", {
|
|
926
|
+
operation: "checkpoint",
|
|
927
|
+
}));
|
|
928
|
+
}
|
|
929
|
+
function repositoryEvidenceCapabilityUnavailable(operation) {
|
|
930
|
+
return failure(new DomainError("BACKEND_UNAVAILABLE", "Repository evidence capability is not available.", { operation }));
|
|
931
|
+
}
|
|
932
|
+
function mutationCapabilityUnavailable(operation) {
|
|
933
|
+
return failure(new DomainError("BACKEND_UNAVAILABLE", "Governed Git mutation capability is not available.", { operation }));
|
|
934
|
+
}
|
|
935
|
+
function deniedAuthorization(decision) {
|
|
936
|
+
const code = decision.code === "ALLOWED" ? "OPERATION_REJECTED" : decision.code;
|
|
937
|
+
const diagnosticDetails = code === "INSUFFICIENT_CLAIM_MODE"
|
|
938
|
+
? {
|
|
939
|
+
resource: decision.details.resource,
|
|
940
|
+
granted_modes: decision.details.grantedModes,
|
|
941
|
+
}
|
|
942
|
+
: {};
|
|
943
|
+
return failure(new DomainError(code, `Operation denied: ${code}.`, {
|
|
944
|
+
allowed: false,
|
|
945
|
+
schema_version: decision.schema_version,
|
|
946
|
+
operation: decision.operation,
|
|
947
|
+
required_access: decision.required_access,
|
|
948
|
+
repository: decision.repository,
|
|
949
|
+
worktree: decision.worktree,
|
|
950
|
+
branch: decision.branch,
|
|
951
|
+
session_id: decision.session_id,
|
|
952
|
+
owner_session_id: decision.owner_session_id,
|
|
953
|
+
requested_session_id: decision.requested_session_id,
|
|
954
|
+
state: decision.state,
|
|
955
|
+
resources: decision.resources,
|
|
956
|
+
...diagnosticDetails,
|
|
957
|
+
details: decision.details,
|
|
958
|
+
}));
|
|
959
|
+
}
|
|
253
960
|
function emitFailure(mode, command, error, io) {
|
|
254
961
|
io[mode === "json" ? "stdout" : "stderr"](renderFailure(mode, command, error));
|
|
255
962
|
return error.exitCode;
|
|
@@ -261,14 +968,19 @@ export async function runCli(argv, dependencies = {}) {
|
|
|
261
968
|
if (!parsed.ok)
|
|
262
969
|
return emitFailure(mode, "cli", parsed.error, io);
|
|
263
970
|
if (parsed.value.help) {
|
|
971
|
+
const spec = helpSpecFor(parsed.value.commandArguments);
|
|
264
972
|
if (mode === "json")
|
|
265
|
-
io.stdout(renderSuccess(mode, "help",
|
|
973
|
+
io.stdout(renderSuccess(mode, "help", helpPayload(spec)));
|
|
266
974
|
else
|
|
267
|
-
io.stdout(
|
|
975
|
+
io.stdout(helpText(spec));
|
|
268
976
|
return EXIT_CODES.success;
|
|
269
977
|
}
|
|
270
978
|
if (parsed.value.version) {
|
|
271
|
-
const payload = {
|
|
979
|
+
const payload = {
|
|
980
|
+
version: dependencies.version ?? VERSION,
|
|
981
|
+
contract_id: MACHINE_CONTRACT_ID,
|
|
982
|
+
contract_schema_version: MACHINE_CONTRACT_SCHEMA_VERSION,
|
|
983
|
+
};
|
|
272
984
|
if (mode === "json")
|
|
273
985
|
io.stdout(renderSuccess(mode, "version", payload));
|
|
274
986
|
else
|
|
@@ -281,6 +993,13 @@ export async function runCli(argv, dependencies = {}) {
|
|
|
281
993
|
io.stdout(HELP_TEXT);
|
|
282
994
|
return EXIT_CODES.usage;
|
|
283
995
|
}
|
|
996
|
+
if (parsed.value.commandArguments[0] === "capabilities") {
|
|
997
|
+
const capabilityOptions = noOptions(parsed.value.commandArguments.slice(1));
|
|
998
|
+
if (!capabilityOptions.ok)
|
|
999
|
+
return emitFailure(mode, "capabilities", capabilityOptions.error, io);
|
|
1000
|
+
io.stdout(renderSuccess(mode, "capabilities", machineContract(dependencies.version ?? VERSION)));
|
|
1001
|
+
return EXIT_CODES.success;
|
|
1002
|
+
}
|
|
284
1003
|
const command = commandName(parsed.value.commandArguments);
|
|
285
1004
|
const cwd = dependencies.cwd ?? process.cwd();
|
|
286
1005
|
try {
|