nawabari 0.2.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 +145 -11
- package/dist/cli.js +497 -92
- package/dist/cli.js.map +1 -1
- package/dist/contract.js +121 -2
- package/dist/contract.js.map +1 -1
- package/dist/domain/errors.d.ts +1 -1
- package/dist/domain/errors.js.map +1 -1
- package/dist/domain/session-backend.d.ts +5 -3
- package/dist/domain/session-backend.js +101 -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/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 +43 -7
- package/dist/session-registry.js +375 -102
- package/dist/session-registry.js.map +1 -1
- package/package.json +2 -1
package/dist/cli.js
CHANGED
|
@@ -1,96 +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";
|
|
6
8
|
import { MACHINE_CONTRACT_ID, MACHINE_CONTRACT_SCHEMA_VERSION, machineContract } from "./contract.js";
|
|
7
9
|
const CLI_NAME = "nawabari";
|
|
8
10
|
const packageMetadata = createRequire(import.meta.url)("../package.json");
|
|
9
11
|
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
|
-
"
|
|
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
|
-
|
|
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,
|
|
93
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);
|
|
94
343
|
function usageError(code, message, details = null) {
|
|
95
344
|
return new DomainError(code, message, details);
|
|
96
345
|
}
|
|
@@ -138,6 +387,7 @@ function parseOptions(arguments_, allowed) {
|
|
|
138
387
|
resources: [],
|
|
139
388
|
operation: null,
|
|
140
389
|
message: null,
|
|
390
|
+
message_pattern: null,
|
|
141
391
|
remote: null,
|
|
142
392
|
remote_branch: null,
|
|
143
393
|
mode: null,
|
|
@@ -146,6 +396,16 @@ function parseOptions(arguments_, allowed) {
|
|
|
146
396
|
apply: false,
|
|
147
397
|
force: false,
|
|
148
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,
|
|
149
409
|
};
|
|
150
410
|
let dryRun = false;
|
|
151
411
|
for (let index = 0; index < arguments_.length; index += 1) {
|
|
@@ -153,7 +413,13 @@ function parseOptions(arguments_, allowed) {
|
|
|
153
413
|
if (!allowed.has(name)) {
|
|
154
414
|
return failure(usageError("INVALID_ARGUMENT", `Unknown option: ${name}.`, { option: name }));
|
|
155
415
|
}
|
|
156
|
-
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") {
|
|
157
423
|
if (inlineValue !== null) {
|
|
158
424
|
return failure(usageError("INVALID_ARGUMENT", `${name} does not accept a value.`, { option: name }));
|
|
159
425
|
}
|
|
@@ -163,8 +429,14 @@ function parseOptions(arguments_, allowed) {
|
|
|
163
429
|
dryRun = true;
|
|
164
430
|
else if (name === "--force")
|
|
165
431
|
options.force = true;
|
|
166
|
-
else
|
|
432
|
+
else if (name === "--create-upstream")
|
|
167
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;
|
|
168
440
|
continue;
|
|
169
441
|
}
|
|
170
442
|
const value = inlineValue ?? arguments_[index + 1];
|
|
@@ -191,6 +463,8 @@ function parseOptions(arguments_, allowed) {
|
|
|
191
463
|
options.operation = value;
|
|
192
464
|
else if (name === "--message")
|
|
193
465
|
options.message = value;
|
|
466
|
+
else if (name === "--message-pattern")
|
|
467
|
+
options.message_pattern = value;
|
|
194
468
|
else if (name === "--remote")
|
|
195
469
|
options.remote = value;
|
|
196
470
|
else if (name === "--remote-branch")
|
|
@@ -201,6 +475,20 @@ function parseOptions(arguments_, allowed) {
|
|
|
201
475
|
options.claim_id = value;
|
|
202
476
|
else if (name === "--repository")
|
|
203
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;
|
|
204
492
|
}
|
|
205
493
|
if (options.apply && dryRun) {
|
|
206
494
|
return failure(usageError("INVALID_ARGUMENT", "--apply and --dry-run cannot be used together."));
|
|
@@ -210,6 +498,52 @@ function parseOptions(arguments_, allowed) {
|
|
|
210
498
|
function noOptions(arguments_) {
|
|
211
499
|
return parseOptions(arguments_, new Set());
|
|
212
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
|
+
}
|
|
213
547
|
function sessionContext(cwd) {
|
|
214
548
|
return { cwd };
|
|
215
549
|
}
|
|
@@ -300,10 +634,13 @@ async function executeCommand(commandArguments, dependencies) {
|
|
|
300
634
|
return selected.ok ? { ok: true, value: selected.value } : selected;
|
|
301
635
|
}
|
|
302
636
|
if (subcommand === "list") {
|
|
303
|
-
const parsed =
|
|
637
|
+
const parsed = parseOptions(rest, new Set(["--all", "--history", "--limit", "--offset"]));
|
|
304
638
|
if (!parsed.ok)
|
|
305
639
|
return parsed;
|
|
306
|
-
const
|
|
640
|
+
const options = sessionListingOptions(parsed.value);
|
|
641
|
+
if (!options.ok)
|
|
642
|
+
return options;
|
|
643
|
+
const result = await dependencies.backend.listSessions(context, options.value);
|
|
307
644
|
return result.ok ? { ok: true, value: result.value } : result;
|
|
308
645
|
}
|
|
309
646
|
return failure(new DomainError("UNKNOWN_COMMAND", `Unknown session subcommand: ${subcommand}.`, { subcommand }));
|
|
@@ -386,8 +723,59 @@ async function executeCommand(commandArguments, dependencies) {
|
|
|
386
723
|
const result = await dependencies.backend.checkpoint(context, options);
|
|
387
724
|
return result.ok ? { ok: true, value: result.value } : result;
|
|
388
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
|
+
}
|
|
389
777
|
if (command === "commit") {
|
|
390
|
-
const parsed = parseOptions([subcommand, ...rest].filter((argument) => argument !== undefined), new Set(["--session", "--message", "--resource"]));
|
|
778
|
+
const parsed = parseOptions([subcommand, ...rest].filter((argument) => argument !== undefined), new Set(["--session", "--message", "--resource", "--message-pattern"]));
|
|
391
779
|
if (!parsed.ok)
|
|
392
780
|
return parsed;
|
|
393
781
|
if (parsed.value.message === null) {
|
|
@@ -402,6 +790,7 @@ async function executeCommand(commandArguments, dependencies) {
|
|
|
402
790
|
session_id: parsed.value.session_id,
|
|
403
791
|
message: parsed.value.message,
|
|
404
792
|
resources: parsed.value.resources,
|
|
793
|
+
message_pattern: parsed.value.message_pattern,
|
|
405
794
|
});
|
|
406
795
|
return result.ok ? { ok: true, value: result.value } : result;
|
|
407
796
|
}
|
|
@@ -436,10 +825,13 @@ async function executeCommand(commandArguments, dependencies) {
|
|
|
436
825
|
return result.ok ? { ok: true, value: result.value } : result;
|
|
437
826
|
}
|
|
438
827
|
if (command === "status") {
|
|
439
|
-
const parsed =
|
|
828
|
+
const parsed = parseOptions([subcommand, ...rest].filter((argument) => argument !== undefined), new Set(["--all", "--history", "--limit", "--offset"]));
|
|
440
829
|
if (!parsed.ok)
|
|
441
830
|
return parsed;
|
|
442
|
-
const
|
|
831
|
+
const options = sessionListingOptions(parsed.value);
|
|
832
|
+
if (!options.ok)
|
|
833
|
+
return options;
|
|
834
|
+
const result = await dependencies.backend.status(context, options.value);
|
|
443
835
|
return result.ok ? { ok: true, value: result.value } : result;
|
|
444
836
|
}
|
|
445
837
|
if (command === "guard") {
|
|
@@ -517,6 +909,8 @@ function commandName(commandArguments) {
|
|
|
517
909
|
return commandArguments.slice(0, 2).join(" ");
|
|
518
910
|
if (commandArguments[0] === "resource")
|
|
519
911
|
return commandArguments.slice(0, 2).join(" ");
|
|
912
|
+
if (commandArguments[0] === "evidence")
|
|
913
|
+
return commandArguments.slice(0, 2).join(" ");
|
|
520
914
|
return commandArguments[0] ?? "cli";
|
|
521
915
|
}
|
|
522
916
|
function claimCapabilityUnavailable(operation) {
|
|
@@ -532,11 +926,20 @@ function checkpointCapabilityUnavailable() {
|
|
|
532
926
|
operation: "checkpoint",
|
|
533
927
|
}));
|
|
534
928
|
}
|
|
929
|
+
function repositoryEvidenceCapabilityUnavailable(operation) {
|
|
930
|
+
return failure(new DomainError("BACKEND_UNAVAILABLE", "Repository evidence capability is not available.", { operation }));
|
|
931
|
+
}
|
|
535
932
|
function mutationCapabilityUnavailable(operation) {
|
|
536
933
|
return failure(new DomainError("BACKEND_UNAVAILABLE", "Governed Git mutation capability is not available.", { operation }));
|
|
537
934
|
}
|
|
538
935
|
function deniedAuthorization(decision) {
|
|
539
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
|
+
: {};
|
|
540
943
|
return failure(new DomainError(code, `Operation denied: ${code}.`, {
|
|
541
944
|
allowed: false,
|
|
542
945
|
schema_version: decision.schema_version,
|
|
@@ -550,6 +953,7 @@ function deniedAuthorization(decision) {
|
|
|
550
953
|
requested_session_id: decision.requested_session_id,
|
|
551
954
|
state: decision.state,
|
|
552
955
|
resources: decision.resources,
|
|
956
|
+
...diagnosticDetails,
|
|
553
957
|
details: decision.details,
|
|
554
958
|
}));
|
|
555
959
|
}
|
|
@@ -564,10 +968,11 @@ export async function runCli(argv, dependencies = {}) {
|
|
|
564
968
|
if (!parsed.ok)
|
|
565
969
|
return emitFailure(mode, "cli", parsed.error, io);
|
|
566
970
|
if (parsed.value.help) {
|
|
971
|
+
const spec = helpSpecFor(parsed.value.commandArguments);
|
|
567
972
|
if (mode === "json")
|
|
568
|
-
io.stdout(renderSuccess(mode, "help",
|
|
973
|
+
io.stdout(renderSuccess(mode, "help", helpPayload(spec)));
|
|
569
974
|
else
|
|
570
|
-
io.stdout(
|
|
975
|
+
io.stdout(helpText(spec));
|
|
571
976
|
return EXIT_CODES.success;
|
|
572
977
|
}
|
|
573
978
|
if (parsed.value.version) {
|