nawabari 0.6.0 → 0.7.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/dist/cli.js +463 -12
- package/dist/cli.js.map +1 -1
- package/dist/contract.js +14 -0
- 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 +3 -1
- package/dist/domain/session-backend.js +51 -1
- package/dist/domain/session-backend.js.map +1 -1
- package/dist/domain/session.d.ts +55 -1
- package/dist/domain/session.js.map +1 -1
- package/dist/errors.d.ts +1 -1
- package/dist/errors.js.map +1 -1
- package/dist/operation-authorization.d.ts +98 -3
- package/dist/operation-authorization.js +48 -21
- package/dist/operation-authorization.js.map +1 -1
- package/dist/resource-claims.d.ts +17 -0
- package/dist/resource-claims.js +35 -0
- package/dist/resource-claims.js.map +1 -1
- package/dist/session-registry.d.ts +89 -2
- package/dist/session-registry.js +573 -49
- package/dist/session-registry.js.map +1 -1
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -3,6 +3,7 @@ import { runDoctor } from "./domain/doctor.js";
|
|
|
3
3
|
import { DomainError, EXIT_CODES, failure } from "./domain/errors.js";
|
|
4
4
|
import { MAX_SESSION_LIST_LIMIT, } from "./domain/session.js";
|
|
5
5
|
import { EVIDENCE_MAX_DIFF_BYTES, EVIDENCE_MAX_DIFF_HUNKS, EVIDENCE_MAX_DIFF_PATHS } from "./repository-evidence.js";
|
|
6
|
+
import { isResourceClaimMode } from "./resource-claims.js";
|
|
6
7
|
import { createLocalSessionBackend } from "./domain/session-backend.js";
|
|
7
8
|
import { defaultCliIO, renderFailure, renderSuccess } from "./presentation.js";
|
|
8
9
|
import { MACHINE_CONTRACT_ID, MACHINE_CONTRACT_SCHEMA_VERSION, machineContract } from "./contract.js";
|
|
@@ -116,16 +117,76 @@ const HELP_COMMANDS = [
|
|
|
116
117
|
value: "<read|write|exclusive-write>",
|
|
117
118
|
required: true,
|
|
118
119
|
}),
|
|
120
|
+
option("--if-generation", "Expected claim-set generation for CAS; mutually exclusive with --force", {
|
|
121
|
+
value: "<non-negative-integer>",
|
|
122
|
+
}),
|
|
123
|
+
option("--force", "Explicitly permit unconditional complete replacement; mutually exclusive with --if-generation"),
|
|
119
124
|
option("--session", "Target active session; omitted resolves the current owner", { value: "<id>" }),
|
|
120
125
|
option("--repository", "Expected repository identity", { value: "<id>" }),
|
|
121
126
|
],
|
|
122
127
|
notes: [
|
|
123
|
-
"The desired claim set
|
|
124
|
-
"
|
|
128
|
+
"The desired claim set is a full replacement performed atomically in one updateClaims() transaction; " +
|
|
129
|
+
"use exactly one concurrency intent: --if-generation <non-negative-integer> for claim-set generation CAS, " +
|
|
130
|
+
"or explicit --force for unconditional replacement. On any invalid, conflicting, or stale claim the prior set is left unchanged.",
|
|
125
131
|
"Each --resource must be immediately followed by its own --mode; pairing is positional adjacency, not flag order.",
|
|
126
132
|
"Target grammar: optional first positional <session-id> is an alias for --session <id>; do not supply both.",
|
|
127
133
|
],
|
|
128
134
|
},
|
|
135
|
+
{
|
|
136
|
+
name: "session mutate",
|
|
137
|
+
summary: "Atomically apply exact-resource claim additions, changes, and releases",
|
|
138
|
+
usage: `${CLI_NAME} session mutate [<session>|--session <id>] [--repository <id>] ` +
|
|
139
|
+
`(--upsert-resource <path-or-glob> --mode <read|write|exclusive-write> | --release-resource <path-or-glob>)+ ` +
|
|
140
|
+
`(--if-generation <non-negative-safe-int> | --force)`,
|
|
141
|
+
options: [
|
|
142
|
+
option("--upsert-resource", "Exact repository-relative resource to add or change; each occurrence must be immediately followed by --mode", { value: "<path-or-glob>" }),
|
|
143
|
+
option("--mode", "Mode for the immediately preceding --upsert-resource", {
|
|
144
|
+
value: "<read|write|exclusive-write>",
|
|
145
|
+
}),
|
|
146
|
+
option("--release-resource", "Exact repository-relative resource to release; repeatable", {
|
|
147
|
+
value: "<path-or-glob>",
|
|
148
|
+
}),
|
|
149
|
+
option("--if-generation", "Expected claim-set generation for CAS; mutually exclusive with --force", {
|
|
150
|
+
value: "<non-negative-safe-int>",
|
|
151
|
+
}),
|
|
152
|
+
option("--force", "Explicitly permit unconditional atomic mutation; mutually exclusive with --if-generation"),
|
|
153
|
+
option("--session", "Target active session; omitted resolves the current owner", { value: "<id>" }),
|
|
154
|
+
option("--repository", "Expected repository identity", { value: "<id>" }),
|
|
155
|
+
],
|
|
156
|
+
notes: [
|
|
157
|
+
"Apply one or more ordered upsert/release deltas in one backend transaction; exactly one concurrency intent is required.",
|
|
158
|
+
"Every --upsert-resource must be immediately followed by its own --mode; --release-resource takes exactly one value.",
|
|
159
|
+
"Target grammar: optional first positional <session> is an alias for --session <id>; do not supply both.",
|
|
160
|
+
],
|
|
161
|
+
},
|
|
162
|
+
{
|
|
163
|
+
name: "session transition",
|
|
164
|
+
summary: "Atomically transition one exact resource claim mode",
|
|
165
|
+
usage: `${CLI_NAME} session transition [<session>|--session <id>] [--repository <id>] ` +
|
|
166
|
+
`--resource <path-or-glob> --mode <read|write|exclusive-write> ` +
|
|
167
|
+
`(--if-generation <non-negative-safe-int> | --force)`,
|
|
168
|
+
options: [
|
|
169
|
+
option("--resource", "Exact repository-relative resource to acquire or change", {
|
|
170
|
+
value: "<path-or-glob>",
|
|
171
|
+
required: true,
|
|
172
|
+
}),
|
|
173
|
+
option("--mode", "Target mode for the immediately preceding --resource", {
|
|
174
|
+
value: "<read|write|exclusive-write>",
|
|
175
|
+
required: true,
|
|
176
|
+
}),
|
|
177
|
+
option("--if-generation", "Expected claim-set generation for CAS; mutually exclusive with --force", {
|
|
178
|
+
value: "<non-negative-safe-int>",
|
|
179
|
+
}),
|
|
180
|
+
option("--force", "Explicitly permit unconditional atomic mutation; mutually exclusive with --if-generation"),
|
|
181
|
+
option("--session", "Target active session; omitted resolves the current owner", { value: "<id>" }),
|
|
182
|
+
option("--repository", "Expected repository identity", { value: "<id>" }),
|
|
183
|
+
],
|
|
184
|
+
notes: [
|
|
185
|
+
"Exactly one resource/mode pair is projected as one atomic upsert delta; same mode is an idempotent no-op.",
|
|
186
|
+
"Use exactly one concurrency intent: --if-generation <non-negative-safe-int> or explicit --force.",
|
|
187
|
+
"The target grammar accepts an optional first positional <session> or --session <id>, but not both.",
|
|
188
|
+
],
|
|
189
|
+
},
|
|
129
190
|
{
|
|
130
191
|
name: "session claims",
|
|
131
192
|
summary: "List canonical resource claims",
|
|
@@ -169,16 +230,76 @@ const HELP_COMMANDS = [
|
|
|
169
230
|
value: "<read|write|exclusive-write>",
|
|
170
231
|
required: true,
|
|
171
232
|
}),
|
|
233
|
+
option("--if-generation", "Expected claim-set generation for CAS; mutually exclusive with --force", {
|
|
234
|
+
value: "<non-negative-integer>",
|
|
235
|
+
}),
|
|
236
|
+
option("--force", "Explicitly permit unconditional complete replacement; mutually exclusive with --if-generation"),
|
|
172
237
|
option("--session", "Target active session; omitted resolves the current owner", { value: "<id>" }),
|
|
173
238
|
option("--repository", "Expected repository identity", { value: "<id>" }),
|
|
174
239
|
],
|
|
175
240
|
notes: [
|
|
176
|
-
"The desired claim set
|
|
177
|
-
"
|
|
241
|
+
"The desired claim set is a full replacement performed atomically in one updateClaims() transaction; " +
|
|
242
|
+
"use exactly one concurrency intent: --if-generation <non-negative-integer> for claim-set generation CAS, " +
|
|
243
|
+
"or explicit --force for unconditional replacement. On any invalid, conflicting, or stale claim the prior set is left unchanged.",
|
|
178
244
|
"Each --resource must be immediately followed by its own --mode; pairing is positional adjacency, not flag order.",
|
|
179
245
|
"Target grammar matches session update: optional first positional <session-id> is an alias for --session <id>; do not supply both.",
|
|
180
246
|
],
|
|
181
247
|
},
|
|
248
|
+
{
|
|
249
|
+
name: "resource mutate",
|
|
250
|
+
summary: "Atomically apply exact-resource claim additions, changes, and releases (alias)",
|
|
251
|
+
usage: `${CLI_NAME} resource mutate [<session>|--session <id>] [--repository <id>] ` +
|
|
252
|
+
`(--upsert-resource <path-or-glob> --mode <read|write|exclusive-write> | --release-resource <path-or-glob>)+ ` +
|
|
253
|
+
`(--if-generation <non-negative-safe-int> | --force)`,
|
|
254
|
+
options: [
|
|
255
|
+
option("--upsert-resource", "Exact repository-relative resource to add or change; each occurrence must be immediately followed by --mode", { value: "<path-or-glob>" }),
|
|
256
|
+
option("--mode", "Mode for the immediately preceding --upsert-resource", {
|
|
257
|
+
value: "<read|write|exclusive-write>",
|
|
258
|
+
}),
|
|
259
|
+
option("--release-resource", "Exact repository-relative resource to release; repeatable", {
|
|
260
|
+
value: "<path-or-glob>",
|
|
261
|
+
}),
|
|
262
|
+
option("--if-generation", "Expected claim-set generation for CAS; mutually exclusive with --force", {
|
|
263
|
+
value: "<non-negative-safe-int>",
|
|
264
|
+
}),
|
|
265
|
+
option("--force", "Explicitly permit unconditional atomic mutation; mutually exclusive with --if-generation"),
|
|
266
|
+
option("--session", "Target active session; omitted resolves the current owner", { value: "<id>" }),
|
|
267
|
+
option("--repository", "Expected repository identity", { value: "<id>" }),
|
|
268
|
+
],
|
|
269
|
+
notes: [
|
|
270
|
+
"Apply one or more ordered upsert/release deltas in one backend transaction; exactly one concurrency intent is required.",
|
|
271
|
+
"Every --upsert-resource must be immediately followed by its own --mode; --release-resource takes exactly one value.",
|
|
272
|
+
"Target grammar matches session mutate: optional first positional <session> is an alias for --session <id>; do not supply both.",
|
|
273
|
+
],
|
|
274
|
+
},
|
|
275
|
+
{
|
|
276
|
+
name: "resource transition",
|
|
277
|
+
summary: "Atomically transition one exact resource claim mode (alias)",
|
|
278
|
+
usage: `${CLI_NAME} resource transition [<session>|--session <id>] [--repository <id>] ` +
|
|
279
|
+
`--resource <path-or-glob> --mode <read|write|exclusive-write> ` +
|
|
280
|
+
`(--if-generation <non-negative-safe-int> | --force)`,
|
|
281
|
+
options: [
|
|
282
|
+
option("--resource", "Exact repository-relative resource to acquire or change", {
|
|
283
|
+
value: "<path-or-glob>",
|
|
284
|
+
required: true,
|
|
285
|
+
}),
|
|
286
|
+
option("--mode", "Target mode for the immediately preceding --resource", {
|
|
287
|
+
value: "<read|write|exclusive-write>",
|
|
288
|
+
required: true,
|
|
289
|
+
}),
|
|
290
|
+
option("--if-generation", "Expected claim-set generation for CAS; mutually exclusive with --force", {
|
|
291
|
+
value: "<non-negative-safe-int>",
|
|
292
|
+
}),
|
|
293
|
+
option("--force", "Explicitly permit unconditional atomic mutation; mutually exclusive with --if-generation"),
|
|
294
|
+
option("--session", "Target active session; omitted resolves the current owner", { value: "<id>" }),
|
|
295
|
+
option("--repository", "Expected repository identity", { value: "<id>" }),
|
|
296
|
+
],
|
|
297
|
+
notes: [
|
|
298
|
+
"Exactly one resource/mode pair is projected as one atomic upsert delta; same mode is an idempotent no-op.",
|
|
299
|
+
"Use exactly one concurrency intent: --if-generation <non-negative-safe-int> or explicit --force.",
|
|
300
|
+
"The target grammar accepts an optional first positional <session> or --session <id>, but not both.",
|
|
301
|
+
],
|
|
302
|
+
},
|
|
182
303
|
{
|
|
183
304
|
name: "resource list",
|
|
184
305
|
summary: "List canonical resource claims (alias)",
|
|
@@ -203,14 +324,20 @@ const HELP_COMMANDS = [
|
|
|
203
324
|
{
|
|
204
325
|
name: "session close",
|
|
205
326
|
summary: "Close the current or selected session",
|
|
206
|
-
usage: `${CLI_NAME} session close [<session-id>|--session <id>] [--integrated-revision <rev>]`,
|
|
327
|
+
usage: `${CLI_NAME} session close [<session-id>|--session <id>] [--integrated-revision <rev>] [--fetch-remote <name> --fetch-branch <branch>]`,
|
|
207
328
|
options: [
|
|
208
329
|
option("--session", "Select a session instead of the current worktree owner", { value: "<id>" }),
|
|
209
330
|
option("--integrated-revision", "Externally evidenced revision proving non-ancestry (squash/rebase) integration; independently re-verified via exact Git tree-object equivalence, never trusted blindly", { value: "<rev>" }),
|
|
331
|
+
option("--fetch-remote", "Explicitly fetch one remote integration branch into a disposable proof ref", {
|
|
332
|
+
value: "<name>",
|
|
333
|
+
}),
|
|
334
|
+
option("--fetch-branch", "Integration branch to fetch; requires --fetch-remote", { value: "<branch>" }),
|
|
210
335
|
],
|
|
211
336
|
notes: [
|
|
212
337
|
"Ordinary ancestry-based close remains the cheap/default path and requires no flags.",
|
|
213
|
-
"
|
|
338
|
+
"Network access is never implicit; --fetch-remote and --fetch-branch are accepted only with a full lowercase --integrated-revision SHA.",
|
|
339
|
+
"Fetch updates only a disposable internal proof ref with no tags, FETCH_HEAD, tracking-ref, or local integration-branch changes; remote tip races fail closed.",
|
|
340
|
+
"Nawabari never calls provider APIs such as GitHub; only explicit --fetch-remote/--fetch-branch options connect to the configured Git remote.",
|
|
214
341
|
"Target grammar: optional first positional <session-id> is an alias for --session <id>; do not supply both.",
|
|
215
342
|
],
|
|
216
343
|
},
|
|
@@ -392,7 +519,18 @@ function helpPayload(spec) {
|
|
|
392
519
|
session_targeting: {
|
|
393
520
|
canonical: "--session <id>",
|
|
394
521
|
positional_alias: "<session-id> as the first argument after a session-scoped subcommand",
|
|
395
|
-
commands: [
|
|
522
|
+
commands: [
|
|
523
|
+
"show",
|
|
524
|
+
"inspect",
|
|
525
|
+
"claim",
|
|
526
|
+
"claims",
|
|
527
|
+
"release",
|
|
528
|
+
"update",
|
|
529
|
+
"mutate",
|
|
530
|
+
"transition",
|
|
531
|
+
"close",
|
|
532
|
+
"discard",
|
|
533
|
+
],
|
|
396
534
|
ambiguity: "supplying both positional and --session is rejected",
|
|
397
535
|
discard_requires_explicit_target: true,
|
|
398
536
|
},
|
|
@@ -532,6 +670,8 @@ function parseOptions(arguments_, allowed) {
|
|
|
532
670
|
max_bytes: null,
|
|
533
671
|
max_hunks: null,
|
|
534
672
|
integrated_revision: null,
|
|
673
|
+
fetch_remote: null,
|
|
674
|
+
fetch_branch: null,
|
|
535
675
|
};
|
|
536
676
|
let dryRun = false;
|
|
537
677
|
for (let index = 0; index < arguments_.length; index += 1) {
|
|
@@ -619,6 +759,10 @@ function parseOptions(arguments_, allowed) {
|
|
|
619
759
|
options.max_hunks = value;
|
|
620
760
|
else if (name === "--integrated-revision")
|
|
621
761
|
options.integrated_revision = value;
|
|
762
|
+
else if (name === "--fetch-remote")
|
|
763
|
+
options.fetch_remote = value;
|
|
764
|
+
else if (name === "--fetch-branch")
|
|
765
|
+
options.fetch_branch = value;
|
|
622
766
|
}
|
|
623
767
|
if (options.apply && dryRun) {
|
|
624
768
|
return failure(usageError("INVALID_ARGUMENT", "--apply and --dry-run cannot be used together."));
|
|
@@ -656,6 +800,213 @@ function parseTargetedOptions(arguments_, allowed, required = false) {
|
|
|
656
800
|
}
|
|
657
801
|
return parsed;
|
|
658
802
|
}
|
|
803
|
+
const CLAIM_CONCURRENCY_OPTION_NAMES = new Set(["--if-generation", "--force"]);
|
|
804
|
+
function isClaimConcurrencyOption(name) {
|
|
805
|
+
return CLAIM_CONCURRENCY_OPTION_NAMES.has(name);
|
|
806
|
+
}
|
|
807
|
+
/** Shared #176 CAS/force option vocabulary and value consumption. */
|
|
808
|
+
function consumeClaimConcurrencyOption(name, inlineValue, nextValue, state) {
|
|
809
|
+
if (name === "--force") {
|
|
810
|
+
if (inlineValue !== null) {
|
|
811
|
+
return failure(usageError("INVALID_ARGUMENT", "--force does not accept a value.", { option: name }));
|
|
812
|
+
}
|
|
813
|
+
if (state.force) {
|
|
814
|
+
return failure(usageError("INVALID_ARGUMENT", "--force may be supplied only once.", { option: name }));
|
|
815
|
+
}
|
|
816
|
+
return { ok: true, value: { state: { ...state, force: true }, consumed_next_value: false } };
|
|
817
|
+
}
|
|
818
|
+
const value = inlineValue ?? nextValue;
|
|
819
|
+
const negativeGeneration = /^-\d+$/u.test(value ?? "");
|
|
820
|
+
if (value === undefined || value === "" || (inlineValue === null && value.startsWith("-") && !negativeGeneration)) {
|
|
821
|
+
return failure(usageError("MISSING_ARGUMENT", `${name} requires a value.`, { option: name }));
|
|
822
|
+
}
|
|
823
|
+
if (state.expected_claim_set_generation !== null) {
|
|
824
|
+
return failure(usageError("INVALID_ARGUMENT", "--if-generation may be supplied only once.", { option: name }));
|
|
825
|
+
}
|
|
826
|
+
if (!/^\d+$/u.test(value)) {
|
|
827
|
+
return failure(usageError("INVALID_ARGUMENT", "--if-generation requires a non-negative safe integer.", {
|
|
828
|
+
option: name,
|
|
829
|
+
value,
|
|
830
|
+
}));
|
|
831
|
+
}
|
|
832
|
+
const parsedGeneration = Number(value);
|
|
833
|
+
if (!Number.isSafeInteger(parsedGeneration)) {
|
|
834
|
+
return failure(usageError("INVALID_ARGUMENT", "--if-generation requires a non-negative safe integer.", {
|
|
835
|
+
option: name,
|
|
836
|
+
value,
|
|
837
|
+
}));
|
|
838
|
+
}
|
|
839
|
+
return {
|
|
840
|
+
ok: true,
|
|
841
|
+
value: {
|
|
842
|
+
state: { ...state, expected_claim_set_generation: parsedGeneration },
|
|
843
|
+
consumed_next_value: inlineValue === null,
|
|
844
|
+
},
|
|
845
|
+
};
|
|
846
|
+
}
|
|
847
|
+
function finalizeClaimConcurrency(state, requireConcurrencyIntent) {
|
|
848
|
+
if (!requireConcurrencyIntent && (state.force || state.expected_claim_set_generation !== null)) {
|
|
849
|
+
return failure(usageError("INVALID_ARGUMENT", "session claim does not accept --if-generation or --force.", {
|
|
850
|
+
options: ["--if-generation", "--force"],
|
|
851
|
+
}));
|
|
852
|
+
}
|
|
853
|
+
if (requireConcurrencyIntent && state.force && state.expected_claim_set_generation !== null) {
|
|
854
|
+
return failure(usageError("INVALID_ARGUMENT", "Specify exactly one of --if-generation or --force, not both.", {
|
|
855
|
+
options: ["--if-generation", "--force"],
|
|
856
|
+
}));
|
|
857
|
+
}
|
|
858
|
+
if (requireConcurrencyIntent && !state.force && state.expected_claim_set_generation === null) {
|
|
859
|
+
return failure(usageError("MISSING_ARGUMENT", "Exactly one of --if-generation or --force is required.", {
|
|
860
|
+
options: ["--if-generation", "--force"],
|
|
861
|
+
}));
|
|
862
|
+
}
|
|
863
|
+
return { ok: true, value: state };
|
|
864
|
+
}
|
|
865
|
+
/**
|
|
866
|
+
* Parse the public atomic delta grammar without interpreting resource identity.
|
|
867
|
+
* Resource canonicalization, duplicate/conflict authority, and all mutation
|
|
868
|
+
* semantics remain in the domain/backend primitive.
|
|
869
|
+
*/
|
|
870
|
+
function parseClaimDeltaMutation(arguments_) {
|
|
871
|
+
const positional = splitPositionalSessionTarget(arguments_);
|
|
872
|
+
if (!positional.ok)
|
|
873
|
+
return positional;
|
|
874
|
+
let sessionId = positional.value.sessionId;
|
|
875
|
+
let repository = null;
|
|
876
|
+
const deltas = [];
|
|
877
|
+
let pendingUpsertResource = null;
|
|
878
|
+
let concurrencyState = {
|
|
879
|
+
expected_claim_set_generation: null,
|
|
880
|
+
force: false,
|
|
881
|
+
};
|
|
882
|
+
const recognized = new Set([
|
|
883
|
+
"--session",
|
|
884
|
+
"--repository",
|
|
885
|
+
"--upsert-resource",
|
|
886
|
+
"--mode",
|
|
887
|
+
"--release-resource",
|
|
888
|
+
...CLAIM_CONCURRENCY_OPTION_NAMES,
|
|
889
|
+
]);
|
|
890
|
+
const rejectPending = (name) => failure(usageError("INVALID_ARGUMENT", `${name} cannot appear between --upsert-resource and its --mode; only --mode is permitted immediately after --upsert-resource.`, { option: name }));
|
|
891
|
+
const targetArguments = positional.value.arguments;
|
|
892
|
+
for (let index = 0; index < targetArguments.length; index += 1) {
|
|
893
|
+
const { name, inlineValue } = optionParts(targetArguments[index]);
|
|
894
|
+
if (!recognized.has(name)) {
|
|
895
|
+
if (pendingUpsertResource !== null)
|
|
896
|
+
return rejectPending(name);
|
|
897
|
+
return failure(usageError("INVALID_ARGUMENT", `Unknown option: ${name}.`, { option: name }));
|
|
898
|
+
}
|
|
899
|
+
if (pendingUpsertResource !== null && name !== "--mode")
|
|
900
|
+
return rejectPending(name);
|
|
901
|
+
if (isClaimConcurrencyOption(name)) {
|
|
902
|
+
const consumed = consumeClaimConcurrencyOption(name, inlineValue, targetArguments[index + 1], concurrencyState);
|
|
903
|
+
if (!consumed.ok)
|
|
904
|
+
return consumed;
|
|
905
|
+
concurrencyState = consumed.value.state;
|
|
906
|
+
if (consumed.value.consumed_next_value)
|
|
907
|
+
index += 1;
|
|
908
|
+
continue;
|
|
909
|
+
}
|
|
910
|
+
const value = inlineValue ?? targetArguments[index + 1];
|
|
911
|
+
if (value === undefined || value === "" || (inlineValue === null && value.startsWith("-"))) {
|
|
912
|
+
return failure(usageError("MISSING_ARGUMENT", `${name} requires a value.`, { option: name }));
|
|
913
|
+
}
|
|
914
|
+
if (inlineValue === null)
|
|
915
|
+
index += 1;
|
|
916
|
+
if (name === "--session") {
|
|
917
|
+
if (sessionId !== null) {
|
|
918
|
+
return failure(usageError("INVALID_ARGUMENT", "Specify a session target either positionally or with --session, not both.", {
|
|
919
|
+
option: "--session",
|
|
920
|
+
}));
|
|
921
|
+
}
|
|
922
|
+
sessionId = value;
|
|
923
|
+
}
|
|
924
|
+
else if (name === "--repository") {
|
|
925
|
+
repository = value;
|
|
926
|
+
}
|
|
927
|
+
else if (name === "--upsert-resource") {
|
|
928
|
+
pendingUpsertResource = value;
|
|
929
|
+
}
|
|
930
|
+
else if (name === "--release-resource") {
|
|
931
|
+
deltas.push({ kind: "release", resource: value });
|
|
932
|
+
}
|
|
933
|
+
else {
|
|
934
|
+
if (pendingUpsertResource === null) {
|
|
935
|
+
return failure(usageError("INVALID_ARGUMENT", "--mode must be immediately preceded by its own --upsert-resource.", {
|
|
936
|
+
option: "--mode",
|
|
937
|
+
}));
|
|
938
|
+
}
|
|
939
|
+
if (!isResourceClaimMode(value)) {
|
|
940
|
+
return failure(usageError("INVALID_ARGUMENT", "--mode requires read, write, or exclusive-write.", {
|
|
941
|
+
option: "--mode",
|
|
942
|
+
value,
|
|
943
|
+
}));
|
|
944
|
+
}
|
|
945
|
+
deltas.push({
|
|
946
|
+
kind: "upsert",
|
|
947
|
+
resource: pendingUpsertResource,
|
|
948
|
+
mode: value,
|
|
949
|
+
});
|
|
950
|
+
pendingUpsertResource = null;
|
|
951
|
+
}
|
|
952
|
+
}
|
|
953
|
+
if (pendingUpsertResource !== null) {
|
|
954
|
+
return failure(usageError("MISSING_ARGUMENT", "--upsert-resource must be immediately followed by --mode.", {
|
|
955
|
+
option: "--mode",
|
|
956
|
+
}));
|
|
957
|
+
}
|
|
958
|
+
if (deltas.length === 0) {
|
|
959
|
+
return failure(usageError("MISSING_ARGUMENT", "At least one --upsert-resource or --release-resource is required.", {
|
|
960
|
+
options: ["--upsert-resource", "--release-resource"],
|
|
961
|
+
}));
|
|
962
|
+
}
|
|
963
|
+
const concurrency = finalizeClaimConcurrency(concurrencyState, true);
|
|
964
|
+
if (!concurrency.ok)
|
|
965
|
+
return concurrency;
|
|
966
|
+
return {
|
|
967
|
+
ok: true,
|
|
968
|
+
value: {
|
|
969
|
+
session_id: sessionId,
|
|
970
|
+
repository,
|
|
971
|
+
deltas,
|
|
972
|
+
expected_claim_set_generation: concurrency.value.expected_claim_set_generation,
|
|
973
|
+
force: concurrency.value.force,
|
|
974
|
+
},
|
|
975
|
+
};
|
|
976
|
+
}
|
|
977
|
+
/**
|
|
978
|
+
* Parse the one-resource transition convenience grammar. The underlying
|
|
979
|
+
* mutation remains the canonical multi-delta primitive; this parser only
|
|
980
|
+
* constrains its public projection to exactly one upsert pair.
|
|
981
|
+
*/
|
|
982
|
+
function parseClaimTransition(arguments_) {
|
|
983
|
+
const parsed = parseClaimReplacementPairs(arguments_);
|
|
984
|
+
if (!parsed.ok)
|
|
985
|
+
return parsed;
|
|
986
|
+
if (parsed.value.pairs.length !== 1) {
|
|
987
|
+
return failure(usageError("INVALID_ARGUMENT", "transition accepts exactly one --resource/--mode pair.", {
|
|
988
|
+
pair_count: parsed.value.pairs.length,
|
|
989
|
+
}));
|
|
990
|
+
}
|
|
991
|
+
const pair = parsed.value.pairs[0];
|
|
992
|
+
if (pair === undefined || !isResourceClaimMode(pair.mode)) {
|
|
993
|
+
return failure(usageError("INVALID_ARGUMENT", "--mode requires read, write, or exclusive-write.", {
|
|
994
|
+
option: "--mode",
|
|
995
|
+
value: pair?.mode ?? null,
|
|
996
|
+
}));
|
|
997
|
+
}
|
|
998
|
+
return {
|
|
999
|
+
ok: true,
|
|
1000
|
+
value: {
|
|
1001
|
+
session_id: parsed.value.session_id,
|
|
1002
|
+
repository: parsed.value.repository,
|
|
1003
|
+
resource: pair.resource,
|
|
1004
|
+
mode: pair.mode,
|
|
1005
|
+
expected_claim_set_generation: parsed.value.expected_claim_set_generation,
|
|
1006
|
+
force: parsed.value.force,
|
|
1007
|
+
},
|
|
1008
|
+
};
|
|
1009
|
+
}
|
|
659
1010
|
/**
|
|
660
1011
|
* `update` accepts a complete desired claim set as repeated
|
|
661
1012
|
* `--resource <path> --mode <mode>` pairs. Pairing is by strict local
|
|
@@ -664,7 +1015,7 @@ function parseTargetedOptions(arguments_, allowed, required = false) {
|
|
|
664
1015
|
* parallel-array position, so argv order can never associate a resource
|
|
665
1016
|
* with the wrong mode.
|
|
666
1017
|
*/
|
|
667
|
-
function parseClaimReplacementPairs(arguments_) {
|
|
1018
|
+
function parseClaimReplacementPairs(arguments_, requireConcurrencyIntent = true) {
|
|
668
1019
|
const positional = splitPositionalSessionTarget(arguments_);
|
|
669
1020
|
if (!positional.ok)
|
|
670
1021
|
return positional;
|
|
@@ -672,16 +1023,34 @@ function parseClaimReplacementPairs(arguments_) {
|
|
|
672
1023
|
let repository = null;
|
|
673
1024
|
const pairs = [];
|
|
674
1025
|
let pendingResource = null;
|
|
1026
|
+
let concurrencyState = {
|
|
1027
|
+
expected_claim_set_generation: null,
|
|
1028
|
+
force: false,
|
|
1029
|
+
};
|
|
675
1030
|
sessionId = positional.value.sessionId;
|
|
676
1031
|
const targetArguments = positional.value.arguments;
|
|
677
1032
|
for (let index = 0; index < targetArguments.length; index += 1) {
|
|
678
1033
|
const { name, inlineValue } = optionParts(targetArguments[index]);
|
|
679
|
-
|
|
1034
|
+
const isConcurrencyOption = isClaimConcurrencyOption(name);
|
|
1035
|
+
if (name !== "--session" &&
|
|
1036
|
+
name !== "--repository" &&
|
|
1037
|
+
name !== "--resource" &&
|
|
1038
|
+
name !== "--mode" &&
|
|
1039
|
+
!(requireConcurrencyIntent && isConcurrencyOption)) {
|
|
680
1040
|
return failure(usageError("INVALID_ARGUMENT", `Unknown option: ${name}.`, { option: name }));
|
|
681
1041
|
}
|
|
682
1042
|
if (pendingResource !== null && name !== "--mode") {
|
|
683
1043
|
return failure(usageError("INVALID_ARGUMENT", `${name} cannot appear between --resource and its --mode; only --mode is permitted immediately after --resource.`, { option: name }));
|
|
684
1044
|
}
|
|
1045
|
+
if (requireConcurrencyIntent && isConcurrencyOption) {
|
|
1046
|
+
const consumed = consumeClaimConcurrencyOption(name, inlineValue, targetArguments[index + 1], concurrencyState);
|
|
1047
|
+
if (!consumed.ok)
|
|
1048
|
+
return consumed;
|
|
1049
|
+
concurrencyState = consumed.value.state;
|
|
1050
|
+
if (consumed.value.consumed_next_value)
|
|
1051
|
+
index += 1;
|
|
1052
|
+
continue;
|
|
1053
|
+
}
|
|
685
1054
|
const value = inlineValue ?? targetArguments[index + 1];
|
|
686
1055
|
if (value === undefined || value === "" || (inlineValue === null && value.startsWith("-"))) {
|
|
687
1056
|
return failure(usageError("MISSING_ARGUMENT", `${name} requires a value.`, { option: name }));
|
|
@@ -714,7 +1083,19 @@ function parseClaimReplacementPairs(arguments_) {
|
|
|
714
1083
|
if (pairs.length === 0) {
|
|
715
1084
|
return failure(usageError("MISSING_ARGUMENT", "--resource requires a value.", { option: "--resource" }));
|
|
716
1085
|
}
|
|
717
|
-
|
|
1086
|
+
const concurrency = finalizeClaimConcurrency(concurrencyState, requireConcurrencyIntent);
|
|
1087
|
+
if (!concurrency.ok)
|
|
1088
|
+
return concurrency;
|
|
1089
|
+
return {
|
|
1090
|
+
ok: true,
|
|
1091
|
+
value: {
|
|
1092
|
+
session_id: sessionId,
|
|
1093
|
+
repository,
|
|
1094
|
+
pairs,
|
|
1095
|
+
expected_claim_set_generation: concurrency.value.expected_claim_set_generation,
|
|
1096
|
+
force: concurrency.value.force,
|
|
1097
|
+
},
|
|
1098
|
+
};
|
|
718
1099
|
}
|
|
719
1100
|
/**
|
|
720
1101
|
* `claim` accepts exactly one `--resource`/`--mode` pair. Reuses the same
|
|
@@ -723,7 +1104,7 @@ function parseClaimReplacementPairs(arguments_) {
|
|
|
723
1104
|
* first pair (last-wins).
|
|
724
1105
|
*/
|
|
725
1106
|
function parseSingleClaimPair(arguments_) {
|
|
726
|
-
const parsed = parseClaimReplacementPairs(arguments_);
|
|
1107
|
+
const parsed = parseClaimReplacementPairs(arguments_, false);
|
|
727
1108
|
if (!parsed.ok)
|
|
728
1109
|
return parsed;
|
|
729
1110
|
if (parsed.value.pairs.length > 1) {
|
|
@@ -794,6 +1175,40 @@ function sessionContext(cwd) {
|
|
|
794
1175
|
async function resolveSelectedSession(backend, context, sessionId) {
|
|
795
1176
|
return sessionId === null ? backend.resolveCurrentSession(context) : backend.getSession(context, sessionId);
|
|
796
1177
|
}
|
|
1178
|
+
async function executeClaimDeltaMutation(arguments_, backend, context, operation) {
|
|
1179
|
+
const parsed = parseClaimDeltaMutation(arguments_);
|
|
1180
|
+
if (!parsed.ok)
|
|
1181
|
+
return parsed;
|
|
1182
|
+
if (backend.applyClaimDeltas === undefined)
|
|
1183
|
+
return claimCapabilityUnavailable(operation);
|
|
1184
|
+
const concurrency = parsed.value.force
|
|
1185
|
+
? { force: true }
|
|
1186
|
+
: { expected_claim_set_generation: parsed.value.expected_claim_set_generation };
|
|
1187
|
+
const result = await backend.applyClaimDeltas(context, {
|
|
1188
|
+
session_id: parsed.value.session_id,
|
|
1189
|
+
repository: parsed.value.repository,
|
|
1190
|
+
deltas: parsed.value.deltas,
|
|
1191
|
+
...concurrency,
|
|
1192
|
+
});
|
|
1193
|
+
return result.ok ? { ok: true, value: result.value } : result;
|
|
1194
|
+
}
|
|
1195
|
+
async function executeClaimTransition(arguments_, backend, context, operation) {
|
|
1196
|
+
const parsed = parseClaimTransition(arguments_);
|
|
1197
|
+
if (!parsed.ok)
|
|
1198
|
+
return parsed;
|
|
1199
|
+
if (backend.applyClaimDeltas === undefined)
|
|
1200
|
+
return claimCapabilityUnavailable(operation);
|
|
1201
|
+
const concurrency = parsed.value.force
|
|
1202
|
+
? { force: true }
|
|
1203
|
+
: { expected_claim_set_generation: parsed.value.expected_claim_set_generation };
|
|
1204
|
+
const result = await backend.applyClaimDeltas(context, {
|
|
1205
|
+
session_id: parsed.value.session_id,
|
|
1206
|
+
repository: parsed.value.repository,
|
|
1207
|
+
deltas: [{ kind: "upsert", resource: parsed.value.resource, mode: parsed.value.mode }],
|
|
1208
|
+
...concurrency,
|
|
1209
|
+
});
|
|
1210
|
+
return result.ok ? { ok: true, value: result.value } : result;
|
|
1211
|
+
}
|
|
797
1212
|
async function executeCommand(commandArguments, dependencies) {
|
|
798
1213
|
const [command, subcommand, ...rest] = commandArguments;
|
|
799
1214
|
const context = sessionContext(dependencies.cwd);
|
|
@@ -820,6 +1235,9 @@ async function executeCommand(commandArguments, dependencies) {
|
|
|
820
1235
|
return parsed;
|
|
821
1236
|
if (dependencies.backend.updateClaims === undefined)
|
|
822
1237
|
return claimCapabilityUnavailable(subcommand);
|
|
1238
|
+
const concurrency = parsed.value.force
|
|
1239
|
+
? { force: true }
|
|
1240
|
+
: { expected_claim_set_generation: parsed.value.expected_claim_set_generation };
|
|
823
1241
|
const result = await dependencies.backend.updateClaims(context, {
|
|
824
1242
|
session_id: parsed.value.session_id,
|
|
825
1243
|
repository: parsed.value.repository,
|
|
@@ -827,9 +1245,16 @@ async function executeCommand(commandArguments, dependencies) {
|
|
|
827
1245
|
resource: pair.resource,
|
|
828
1246
|
mode: pair.mode,
|
|
829
1247
|
})),
|
|
1248
|
+
...concurrency,
|
|
830
1249
|
});
|
|
831
1250
|
return result.ok ? { ok: true, value: result.value } : result;
|
|
832
1251
|
}
|
|
1252
|
+
if (subcommand === "mutate") {
|
|
1253
|
+
return executeClaimDeltaMutation(rest, dependencies.backend, context, "session mutate");
|
|
1254
|
+
}
|
|
1255
|
+
if (subcommand === "transition") {
|
|
1256
|
+
return executeClaimTransition(rest, dependencies.backend, context, "session transition");
|
|
1257
|
+
}
|
|
833
1258
|
if (subcommand === "claims") {
|
|
834
1259
|
const parsed = parseTargetedOptions(rest, new Set(["--session"]));
|
|
835
1260
|
if (!parsed.ok)
|
|
@@ -848,6 +1273,7 @@ async function executeCommand(commandArguments, dependencies) {
|
|
|
848
1273
|
const result = await dependencies.backend.releaseClaims(context, {
|
|
849
1274
|
session_id: parsed.value.session_id,
|
|
850
1275
|
claim_ids: parsed.value.claim_id === null ? null : [parsed.value.claim_id],
|
|
1276
|
+
force: true,
|
|
851
1277
|
});
|
|
852
1278
|
return result.ok ? { ok: true, value: result.value } : result;
|
|
853
1279
|
}
|
|
@@ -922,13 +1348,27 @@ async function executeCommand(commandArguments, dependencies) {
|
|
|
922
1348
|
return selected;
|
|
923
1349
|
return { ok: true, value: { session_id: selected.value.session_id } };
|
|
924
1350
|
}
|
|
925
|
-
const parsed = parseTargetedOptions(rest, new Set(subcommand === "close"
|
|
1351
|
+
const parsed = parseTargetedOptions(rest, new Set(subcommand === "close"
|
|
1352
|
+
? ["--session", "--integrated-revision", "--fetch-remote", "--fetch-branch"]
|
|
1353
|
+
: ["--session"]));
|
|
926
1354
|
if (!parsed.ok)
|
|
927
1355
|
return parsed;
|
|
928
1356
|
if (subcommand === "close") {
|
|
1357
|
+
const hasFetchRemote = parsed.value.fetch_remote !== null;
|
|
1358
|
+
const hasFetchBranch = parsed.value.fetch_branch !== null;
|
|
1359
|
+
if (hasFetchRemote !== hasFetchBranch) {
|
|
1360
|
+
return failure(usageError("INVALID_ARGUMENT", "--fetch-remote and --fetch-branch must be supplied together for an explicit integration fetch."));
|
|
1361
|
+
}
|
|
1362
|
+
if ((hasFetchRemote || hasFetchBranch) && parsed.value.integrated_revision === null) {
|
|
1363
|
+
return failure(usageError("MISSING_ARGUMENT", "Explicit integration fetch requires --integrated-revision <full-sha>.", {
|
|
1364
|
+
option: "--integrated-revision",
|
|
1365
|
+
}));
|
|
1366
|
+
}
|
|
929
1367
|
const closeOptions = {
|
|
930
1368
|
session_id: parsed.value.session_id,
|
|
931
1369
|
integrated_revision: parsed.value.integrated_revision,
|
|
1370
|
+
fetch_remote: parsed.value.fetch_remote,
|
|
1371
|
+
fetch_branch: parsed.value.fetch_branch,
|
|
932
1372
|
};
|
|
933
1373
|
const selected = await dependencies.backend.closeSession(context, closeOptions);
|
|
934
1374
|
return selected.ok ? { ok: true, value: selected.value } : selected;
|
|
@@ -969,6 +1409,9 @@ async function executeCommand(commandArguments, dependencies) {
|
|
|
969
1409
|
return parsed;
|
|
970
1410
|
if (dependencies.backend.updateClaims === undefined)
|
|
971
1411
|
return claimCapabilityUnavailable(resourceSubcommand);
|
|
1412
|
+
const concurrency = parsed.value.force
|
|
1413
|
+
? { force: true }
|
|
1414
|
+
: { expected_claim_set_generation: parsed.value.expected_claim_set_generation };
|
|
972
1415
|
const result = await dependencies.backend.updateClaims(context, {
|
|
973
1416
|
session_id: parsed.value.session_id,
|
|
974
1417
|
repository: parsed.value.repository,
|
|
@@ -976,9 +1419,16 @@ async function executeCommand(commandArguments, dependencies) {
|
|
|
976
1419
|
resource: pair.resource,
|
|
977
1420
|
mode: pair.mode,
|
|
978
1421
|
})),
|
|
1422
|
+
...concurrency,
|
|
979
1423
|
});
|
|
980
1424
|
return result.ok ? { ok: true, value: result.value } : result;
|
|
981
1425
|
}
|
|
1426
|
+
if (resourceSubcommand === "mutate") {
|
|
1427
|
+
return executeClaimDeltaMutation(rest, dependencies.backend, context, "resource mutate");
|
|
1428
|
+
}
|
|
1429
|
+
if (resourceSubcommand === "transition") {
|
|
1430
|
+
return executeClaimTransition(rest, dependencies.backend, context, "resource transition");
|
|
1431
|
+
}
|
|
982
1432
|
if (resourceSubcommand === "list" || resourceSubcommand === "claims") {
|
|
983
1433
|
const parsed = parseTargetedOptions(rest, new Set(["--session"]));
|
|
984
1434
|
if (!parsed.ok)
|
|
@@ -997,6 +1447,7 @@ async function executeCommand(commandArguments, dependencies) {
|
|
|
997
1447
|
const result = await dependencies.backend.releaseClaims(context, {
|
|
998
1448
|
session_id: parsed.value.session_id,
|
|
999
1449
|
claim_ids: parsed.value.claim_id === null ? null : [parsed.value.claim_id],
|
|
1450
|
+
force: true,
|
|
1000
1451
|
});
|
|
1001
1452
|
return result.ok ? { ok: true, value: result.value } : result;
|
|
1002
1453
|
}
|