@zq-silk/yui 0.0.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.
Files changed (95) hide show
  1. package/ARCHITECTURE.md +141 -0
  2. package/LICENSE +21 -0
  3. package/README.md +211 -0
  4. package/dist/agent/adapterCatalog.js +10 -0
  5. package/dist/agent/agent.js +89 -0
  6. package/dist/agent/agentRegistry.js +10 -0
  7. package/dist/agent/argumentPolicy.js +80 -0
  8. package/dist/brief/taskBrief.js +37 -0
  9. package/dist/cli/commandCatalog.js +647 -0
  10. package/dist/cli/completion.js +111 -0
  11. package/dist/cli/completionWizard.js +143 -0
  12. package/dist/cli/dynamicCompletion.js +48 -0
  13. package/dist/cli/helpRenderer.js +32 -0
  14. package/dist/cli/interactionCandidates.js +139 -0
  15. package/dist/cli/interactionPolicy.js +389 -0
  16. package/dist/cli/interactiveSelection.js +185 -0
  17. package/dist/cli/invocationRouter.js +51 -0
  18. package/dist/cli/roleOptionCatalog.js +67 -0
  19. package/dist/cli/roleWizard.js +546 -0
  20. package/dist/cli/selectionPorts.js +1 -0
  21. package/dist/cli/updateCommand.js +22 -0
  22. package/dist/cli.js +402 -0
  23. package/dist/commands/agentCommands.js +196 -0
  24. package/dist/commands/globalRoleCommands.js +367 -0
  25. package/dist/commands/jobCommands.js +100 -0
  26. package/dist/commands/operatorCommands.js +38 -0
  27. package/dist/commands/repositoryCommands.js +86 -0
  28. package/dist/commands/roleConfiguration.js +201 -0
  29. package/dist/commands/taskCommands.js +1344 -0
  30. package/dist/commands/taskContextCommand.js +215 -0
  31. package/dist/commands/taskInputCommands.js +423 -0
  32. package/dist/commands/taskRoleRuntimeStatus.js +152 -0
  33. package/dist/completion/completionInstaller.js +168 -0
  34. package/dist/completion/completionPort.js +1 -0
  35. package/dist/completion/completionState.js +137 -0
  36. package/dist/completion/completionWizard.js +125 -0
  37. package/dist/completion/fileCompletionManager.js +51 -0
  38. package/dist/config/yuiConfig.js +17 -0
  39. package/dist/context/dispatchContext.js +74 -0
  40. package/dist/controller/clientRuntime.js +215 -0
  41. package/dist/controller/controller.js +158 -0
  42. package/dist/controller/controllerMain.js +37 -0
  43. package/dist/controller/fileSchedulerStoreAdapter.js +322 -0
  44. package/dist/controller/runtime.js +31 -0
  45. package/dist/controller/sessionNotify.js +136 -0
  46. package/dist/core/controllerClient.js +127 -0
  47. package/dist/core/controllerServer.js +269 -0
  48. package/dist/core/protocol.js +169 -0
  49. package/dist/decision/decision.js +42 -0
  50. package/dist/doctor/doctor.js +229 -0
  51. package/dist/errors/cliError.js +38 -0
  52. package/dist/event/taskEvent.js +44 -0
  53. package/dist/executor/agentAdapter.js +338 -0
  54. package/dist/executor/agentExecutor.js +144 -0
  55. package/dist/executor/executorRegistry.js +101 -0
  56. package/dist/executor/fileRoleLaunchPlanner.js +156 -0
  57. package/dist/executor/launchPlan.js +16 -0
  58. package/dist/input/inputRequest.js +326 -0
  59. package/dist/message/message.js +69 -0
  60. package/dist/milestone/milestone.js +27 -0
  61. package/dist/operator/operatorContext.js +66 -0
  62. package/dist/output/rolePresentation.js +82 -0
  63. package/dist/output/table.js +77 -0
  64. package/dist/output/terminal.js +198 -0
  65. package/dist/repository/gitWorkspace.js +210 -0
  66. package/dist/repository/repository.js +55 -0
  67. package/dist/repository/taskWorkspacePreparer.js +256 -0
  68. package/dist/role/role.js +246 -0
  69. package/dist/role/systemRoles.js +20 -0
  70. package/dist/run/agentRun.js +102 -0
  71. package/dist/scheduler/activeRoleRunDelivery.js +94 -0
  72. package/dist/scheduler/archivedTaskRuntime.js +12 -0
  73. package/dist/scheduler/leaderFailure.js +18 -0
  74. package/dist/scheduler/leaderWakeupProcessor.js +143 -0
  75. package/dist/scheduler/operatorInputNotificationProcessor.js +85 -0
  76. package/dist/scheduler/operatorNotification.js +17 -0
  77. package/dist/scheduler/pendingWakeup.js +33 -0
  78. package/dist/scheduler/ports.js +1 -0
  79. package/dist/scheduler/roleRunLiveness.js +41 -0
  80. package/dist/scheduler/wakeupQueue.js +13 -0
  81. package/dist/setup/setupCommand.js +317 -0
  82. package/dist/storage/durableFile.js +38 -0
  83. package/dist/storage/storageSchema.js +259 -0
  84. package/dist/storage/taskStore.js +1032 -0
  85. package/dist/task/task.js +216 -0
  86. package/dist/tmux/commandExecutor.js +69 -0
  87. package/dist/tmux/terminalHandoff.js +17 -0
  88. package/dist/tmux/tmuxManager.js +408 -0
  89. package/dist/workItem/workItem.js +45 -0
  90. package/dist/worktree/roleWorkspace.js +62 -0
  91. package/i18n/README.zh-CN.md +205 -0
  92. package/package.json +47 -0
  93. package/skills/yui-leader/SKILL.md +72 -0
  94. package/skills/yui-operator/SKILL.md +57 -0
  95. package/skills/yui-worker/SKILL.md +31 -0
@@ -0,0 +1,389 @@
1
+ const taskTarget = (command, argumentIndex = 2, statuses) => ({
2
+ commandPath: ["task", command],
3
+ selectors: [{
4
+ argumentIndex,
5
+ entity: "task",
6
+ provider: "tasks",
7
+ actionTarget: true,
8
+ ...(statuses === undefined ? {} : { statuses })
9
+ }]
10
+ });
11
+ export const INTERACTION_POLICIES = Object.freeze([
12
+ ...["show", "update", "remove"].map((command) => ({
13
+ commandPath: ["agent", command],
14
+ selectors: [{
15
+ argumentIndex: 2,
16
+ entity: "agent",
17
+ provider: "configured-agents",
18
+ actionTarget: true
19
+ }],
20
+ ...(command === "remove"
21
+ ? { confirmation: { action: "Remove Agent", targetArgumentIndex: 2 } }
22
+ : {})
23
+ })),
24
+ {
25
+ commandPath: ["role", "add"],
26
+ selectors: [{
27
+ option: "--agent",
28
+ requiredOption: true,
29
+ entity: "agent",
30
+ provider: "configured-agents",
31
+ actionTarget: false
32
+ }],
33
+ trailingOptions: {
34
+ "--agent": "value", "--workspace": "value", "--description": "value",
35
+ "--responsibility": "value", "--constraint": "value", "--expected-output": "value",
36
+ "--system-prompt": "value", "--skill": "value", "--model": "value",
37
+ "--effort": "value", "--sandbox": "value", "--approval": "value",
38
+ "--permission-mode": "value", "--search": "value"
39
+ }
40
+ },
41
+ ...["show", "update", "remove", "enter"].map((command) => ({
42
+ commandPath: ["role", command],
43
+ selectors: [{
44
+ argumentIndex: 2,
45
+ entity: "global-role",
46
+ provider: "global-roles",
47
+ actionTarget: true
48
+ }],
49
+ ...(command === "remove"
50
+ ? { confirmation: { action: "Remove Role", targetArgumentIndex: 2 } }
51
+ : {})
52
+ })),
53
+ {
54
+ commandPath: ["role", "bind"],
55
+ selectors: [
56
+ { argumentIndex: 2, entity: "global-role", provider: "global-roles", actionTarget: true },
57
+ { argumentIndex: 3, entity: "agent", provider: "configured-agents", actionTarget: false }
58
+ ]
59
+ },
60
+ ...["record", "replace"].map((command) => ({
61
+ commandPath: ["role", "session", command],
62
+ selectors: [{
63
+ argumentIndex: 3,
64
+ entity: "global-role",
65
+ provider: "global-roles",
66
+ actionTarget: true
67
+ }],
68
+ trailingOptions: command === "replace"
69
+ ? { "--native-id": "value", "--reason": "value" }
70
+ : { "--native-id": "value" }
71
+ })),
72
+ taskTarget("show"),
73
+ taskTarget("context"),
74
+ {
75
+ ...taskTarget("update"),
76
+ trailingOptions: {
77
+ "--title": "value",
78
+ "--description": "value",
79
+ "--priority": "value",
80
+ "--tags": "value",
81
+ "--due-at": "value",
82
+ "--clear-description": "flag",
83
+ "--clear-priority": "flag",
84
+ "--clear-tags": "flag",
85
+ "--clear-due-at": "flag"
86
+ }
87
+ },
88
+ taskTarget("activate", 2, ["draft"]),
89
+ {
90
+ ...taskTarget("complete", 2, ["active"]),
91
+ trailingOptions: { "--summary": "value" }
92
+ },
93
+ taskTarget("reopen", 2, ["completed"]),
94
+ {
95
+ ...taskTarget("archive"),
96
+ confirmation: { action: "Archive task", targetArgumentIndex: 2 }
97
+ },
98
+ taskTarget("reconcile"),
99
+ {
100
+ commandPath: ["operator", "submit"],
101
+ selectors: [{
102
+ option: "--task",
103
+ entity: "task",
104
+ provider: "tasks",
105
+ actionTarget: false
106
+ }],
107
+ trailingOptions: { "--task": "value" }
108
+ },
109
+ {
110
+ commandPath: ["task", "create"],
111
+ selectors: [{
112
+ option: "--repository",
113
+ entity: "repository",
114
+ provider: "repositories",
115
+ actionTarget: false
116
+ }],
117
+ trailingOptions: { "--repository": "value", "--base": "value" }
118
+ },
119
+ ...["send", "list"].map((command) => ({
120
+ commandPath: ["task", "message", command],
121
+ selectors: [{
122
+ argumentIndex: 3,
123
+ entity: "task",
124
+ provider: "tasks",
125
+ actionTarget: true
126
+ }]
127
+ })),
128
+ {
129
+ commandPath: ["task", "input", "request"],
130
+ selectors: [{
131
+ argumentIndex: 3,
132
+ entity: "task",
133
+ provider: "tasks",
134
+ actionTarget: true,
135
+ statuses: ["active"]
136
+ }],
137
+ trailingOptions: {
138
+ "--question": "value",
139
+ "--choice": "value",
140
+ "--blocks": "value",
141
+ "--recommend": "value",
142
+ "--timeout-seconds": "value"
143
+ }
144
+ },
145
+ {
146
+ commandPath: ["task", "input", "show"],
147
+ selectors: [
148
+ { argumentIndex: 3, entity: "input-request", provider: "input-requests", actionTarget: true },
149
+ { option: "--task", entity: "task", provider: "tasks", actionTarget: false }
150
+ ],
151
+ trailingOptions: { "--task": "value" }
152
+ },
153
+ {
154
+ commandPath: ["task", "input", "answer"],
155
+ selectors: [
156
+ {
157
+ argumentIndex: 3,
158
+ entity: "input-request",
159
+ provider: "input-requests",
160
+ actionTarget: true,
161
+ statuses: ["open"]
162
+ },
163
+ { option: "--task", entity: "task", provider: "tasks", actionTarget: false }
164
+ ],
165
+ trailingOptions: { "--task": "value", "--choice": "value", "--text": "value" }
166
+ },
167
+ {
168
+ commandPath: ["task", "input", "cancel"],
169
+ selectors: [
170
+ {
171
+ argumentIndex: 3,
172
+ entity: "task",
173
+ provider: "tasks",
174
+ actionTarget: true,
175
+ statuses: ["active"]
176
+ },
177
+ {
178
+ argumentIndex: 4,
179
+ entity: "input-request",
180
+ provider: "input-requests",
181
+ dependsOn: 3,
182
+ actionTarget: true,
183
+ statuses: ["open"]
184
+ }
185
+ ],
186
+ trailingOptions: { "--reason": "value" }
187
+ },
188
+ {
189
+ commandPath: ["task", "role", "add"],
190
+ selectors: [
191
+ { argumentIndex: 3, entity: "task", provider: "tasks", actionTarget: true },
192
+ { option: "--agent", entity: "agent", provider: "configured-agents", actionTarget: false }
193
+ ],
194
+ trailingOptions: {
195
+ "--agent": "value", "--description": "value", "--responsibility": "value",
196
+ "--constraint": "value", "--expected-output": "value", "--system-prompt": "value",
197
+ "--skill": "value", "--model": "value", "--effort": "value",
198
+ "--sandbox": "value", "--approval": "value", "--permission-mode": "value",
199
+ "--search": "value"
200
+ }
201
+ },
202
+ {
203
+ commandPath: ["task", "role", "list"],
204
+ selectors: [{ argumentIndex: 3, entity: "task", provider: "tasks", actionTarget: true }]
205
+ },
206
+ ...["show", "status", "update", "remove"].map((command) => ({
207
+ commandPath: ["task", "role", command],
208
+ selectors: [
209
+ { argumentIndex: 3, entity: "task", provider: "tasks", actionTarget: true },
210
+ {
211
+ argumentIndex: 4,
212
+ entity: "task-role",
213
+ provider: "task-roles",
214
+ dependsOn: 3,
215
+ actionTarget: true
216
+ }
217
+ ],
218
+ ...(command === "remove"
219
+ ? { confirmation: { action: "Remove Task Role", targetArgumentIndex: 4 } }
220
+ : {})
221
+ })),
222
+ {
223
+ commandPath: ["task", "role", "bind"],
224
+ selectors: [
225
+ { argumentIndex: 3, entity: "task", provider: "tasks", actionTarget: true },
226
+ {
227
+ argumentIndex: 4,
228
+ entity: "task-role",
229
+ provider: "task-roles",
230
+ dependsOn: 3,
231
+ actionTarget: true
232
+ },
233
+ { argumentIndex: 5, entity: "agent", provider: "configured-agents", actionTarget: false }
234
+ ]
235
+ },
236
+ {
237
+ commandPath: ["task", "role", "enter"],
238
+ selectors: [
239
+ { argumentIndex: 3, entity: "task", provider: "tasks", actionTarget: true },
240
+ {
241
+ argumentIndex: 4,
242
+ entity: "task-role",
243
+ provider: "task-roles",
244
+ dependsOn: 3,
245
+ actionTarget: true
246
+ }
247
+ ]
248
+ },
249
+ {
250
+ commandPath: ["task", "work", "create"],
251
+ selectors: [
252
+ { argumentIndex: 3, entity: "task", provider: "tasks", actionTarget: true },
253
+ {
254
+ option: "--role",
255
+ entity: "task-role",
256
+ provider: "task-roles",
257
+ dependsOn: 3,
258
+ actionTarget: false
259
+ }
260
+ ],
261
+ trailingOptions: { "--role": "value" }
262
+ },
263
+ {
264
+ commandPath: ["task", "work", "list"],
265
+ selectors: [{ argumentIndex: 3, entity: "task", provider: "tasks", actionTarget: true }]
266
+ },
267
+ {
268
+ commandPath: ["task", "work", "update"],
269
+ selectors: [{ argumentIndex: 3, entity: "work-item", provider: "work-items", actionTarget: true }],
270
+ trailingOptions: { "--summary": "value" }
271
+ },
272
+ {
273
+ commandPath: ["task", "work", "dispatch"],
274
+ selectors: [{ argumentIndex: 3, entity: "work-item", provider: "work-items", actionTarget: true }],
275
+ trailingOptions: { "--input": "value" }
276
+ },
277
+ {
278
+ commandPath: ["task", "run", "list"],
279
+ selectors: [{ argumentIndex: 3, entity: "work-item", provider: "work-items", actionTarget: true }]
280
+ },
281
+ ...["retry", "yield"].map((command) => ({
282
+ commandPath: ["task", "run", command],
283
+ selectors: [{ argumentIndex: 3, entity: "run", provider: "runs", actionTarget: true }],
284
+ ...(command === "yield" ? { trailingOptions: { "--summary": "value" } } : {})
285
+ })),
286
+ {
287
+ commandPath: ["task", "enter"],
288
+ selectors: [{ argumentIndex: 2, entity: "task", provider: "tasks", actionTarget: true }]
289
+ },
290
+ {
291
+ commandPath: ["jobs", "retry"],
292
+ selectors: [{ argumentIndex: 2, entity: "job", provider: "jobs", actionTarget: true }]
293
+ },
294
+ ...[
295
+ ["brief", "show"],
296
+ ["brief", "update"],
297
+ ["decision", "record"],
298
+ ["decision", "list"],
299
+ ["decision", "show"],
300
+ ["decision", "supersede"],
301
+ ["milestone", "add"],
302
+ ["milestone", "list"],
303
+ ["milestone", "show"],
304
+ ["event", "list"],
305
+ ["event", "show"]
306
+ ].map(([group, command]) => {
307
+ const trailingOptions = {};
308
+ if (group === "brief" && command === "update") {
309
+ Object.assign(trailingOptions, { "--objective": "value", "--boundary": "value", "--focus": "value", "--leader-summary": "value" });
310
+ }
311
+ else if (group === "decision" && command === "record") {
312
+ Object.assign(trailingOptions, { "--title": "value", "--rationale": "value" });
313
+ }
314
+ else if (group === "decision" && command === "list") {
315
+ Object.assign(trailingOptions, { "--status": "value" });
316
+ }
317
+ else if (group === "decision" && command === "supersede") {
318
+ Object.assign(trailingOptions, { "--reason": "value" });
319
+ }
320
+ else if (group === "milestone" && command === "add") {
321
+ Object.assign(trailingOptions, { "--title": "value", "--summary": "value" });
322
+ }
323
+ const mutatesKnowledge = (group === "brief" && command === "update")
324
+ || (group === "decision" && (command === "record" || command === "supersede"))
325
+ || (group === "milestone" && command === "add");
326
+ const selectors = [{
327
+ argumentIndex: 3,
328
+ entity: "task",
329
+ provider: "tasks",
330
+ actionTarget: true,
331
+ ...(mutatesKnowledge ? { statuses: ["draft", "active"] } : {})
332
+ }];
333
+ if (group === "decision" && (command === "show" || command === "supersede")) {
334
+ selectors.push({
335
+ argumentIndex: 4,
336
+ entity: "decision",
337
+ provider: "task-decisions",
338
+ dependsOn: 3,
339
+ actionTarget: true,
340
+ ...(command === "supersede" ? { statuses: ["active"] } : {})
341
+ });
342
+ }
343
+ else if (group === "milestone" && command === "show") {
344
+ selectors.push({
345
+ argumentIndex: 4,
346
+ entity: "milestone",
347
+ provider: "task-milestones",
348
+ dependsOn: 3,
349
+ actionTarget: true
350
+ });
351
+ }
352
+ else if (group === "event" && command === "show") {
353
+ selectors.push({
354
+ argumentIndex: 4,
355
+ entity: "event",
356
+ provider: "task-events",
357
+ dependsOn: 3,
358
+ actionTarget: true
359
+ });
360
+ }
361
+ return {
362
+ commandPath: ["task", group, command],
363
+ selectors,
364
+ ...(Object.keys(trailingOptions).length > 0 ? { trailingOptions } : {})
365
+ };
366
+ })
367
+ ]);
368
+ export function findInteractionPolicy(node) {
369
+ if (node === undefined)
370
+ return undefined;
371
+ const path = node.path.slice(1);
372
+ return INTERACTION_POLICIES.find((policy) => policy.commandPath.length === path.length
373
+ && policy.commandPath.every((segment, index) => segment === path[index]));
374
+ }
375
+ export function validateInteractionPolicies(policies = INTERACTION_POLICIES) {
376
+ const paths = new Set();
377
+ for (const policy of policies) {
378
+ const path = policy.commandPath.join(" ");
379
+ if (paths.has(path))
380
+ throw new Error(`Duplicate interaction policy: ${path}`);
381
+ paths.add(path);
382
+ for (const selector of policy.selectors) {
383
+ if ((selector.argumentIndex === undefined) === (selector.option === undefined)) {
384
+ throw new Error(`Interaction selector must declare exactly one slot: ${path}`);
385
+ }
386
+ }
387
+ }
388
+ }
389
+ validateInteractionPolicies();
@@ -0,0 +1,185 @@
1
+ import { renderTable } from "../output/table.js";
2
+ import { getSelectionCandidates } from "./interactionCandidates.js";
3
+ import { findInteractionPolicy } from "./interactionPolicy.js";
4
+ export async function resolveInteractiveArguments(args, node, ports, io) {
5
+ const resolved = [...args];
6
+ if (!io.interactive || !allowsInteractiveSelection(args, io.json)) {
7
+ return { kind: "unchanged", args: resolved };
8
+ }
9
+ const policy = findInteractionPolicy(node);
10
+ if (hasUnknownOption(resolved, node) || !interactionPrerequisitesReady(resolved, node, policy)) {
11
+ return { kind: "unchanged", args: resolved };
12
+ }
13
+ let changed = false;
14
+ let selectedActionTarget = false;
15
+ if (policy !== undefined) {
16
+ for (const selector of policy.selectors) {
17
+ const slot = missingSlot(resolved, selector, node);
18
+ if (slot === undefined)
19
+ continue;
20
+ const candidates = await getSelectionCandidates(selector, ports, resolved);
21
+ if (candidates === null)
22
+ continue;
23
+ const selected = await selectCandidate(candidates, io);
24
+ if (selected === undefined)
25
+ return { kind: "cancelled", args: resolved };
26
+ if (slot.kind === "argument") {
27
+ resolved.splice(slot.index, 0, selected.value);
28
+ }
29
+ else if (slot.optionPresent) {
30
+ resolved.splice(slot.index + 1, 0, selected.value);
31
+ }
32
+ else {
33
+ resolved.push(slot.option, selected.value);
34
+ }
35
+ changed = true;
36
+ selectedActionTarget ||= selector.actionTarget;
37
+ }
38
+ }
39
+ const enumResult = await resolveCatalogEnums(resolved, node, io);
40
+ if (enumResult === "cancelled")
41
+ return { kind: "cancelled", args: resolved };
42
+ changed ||= enumResult === "resolved";
43
+ if (selectedActionTarget && policy?.confirmation !== undefined) {
44
+ const target = resolved[policy.confirmation.targetArgumentIndex] ?? "";
45
+ const answer = (await io.question(`${policy.confirmation.action} ${target}? [y/N]: `))?.trim().toLowerCase();
46
+ if (answer !== "y" && answer !== "yes") {
47
+ return { kind: "cancelled", args: resolved };
48
+ }
49
+ }
50
+ return changed
51
+ ? { kind: "resolved", args: resolved }
52
+ : { kind: "unchanged", args: resolved };
53
+ }
54
+ export function allowsInteractiveSelection(args, globalJson) {
55
+ return !globalJson && !args.includes("--json")
56
+ && !(args[0] === "completion" && args[1] === "candidates");
57
+ }
58
+ function missingSlot(args, selector, node) {
59
+ if (selector.argumentIndex !== undefined) {
60
+ const value = args[selector.argumentIndex];
61
+ return value === undefined || value.startsWith("--")
62
+ ? { kind: "argument", index: selector.argumentIndex }
63
+ : undefined;
64
+ }
65
+ const option = selector.option;
66
+ if (option === undefined)
67
+ return undefined;
68
+ const index = args.indexOf(option);
69
+ if (index < 0) {
70
+ return selector.requiredOption === true
71
+ ? { kind: "option", option, index: args.length, optionPresent: false }
72
+ : undefined;
73
+ }
74
+ const value = args[index + 1];
75
+ if (value !== undefined && !value.startsWith("--"))
76
+ return undefined;
77
+ if (!node.options.includes(option))
78
+ return undefined;
79
+ return { kind: "option", option, index, optionPresent: true };
80
+ }
81
+ function hasUnknownOption(args, node) {
82
+ const known = new Set([...node.options, "--json"]);
83
+ return args.some((argument) => argument.startsWith("-") && !known.has(argument));
84
+ }
85
+ function interactionPrerequisitesReady(args, node, policy) {
86
+ const usage = node.usage[0] ?? "";
87
+ const required = usage.replaceAll(/\[[^\]]*\]/g, "").trim().split(/\s+/);
88
+ const tail = required.slice(node.path.length);
89
+ let position = 0;
90
+ for (let index = 0; index < tail.length; index += 1) {
91
+ const token = tail[index] ?? "";
92
+ if (token.startsWith("--")) {
93
+ const optionIndex = args.indexOf(token);
94
+ const next = tail[index + 1];
95
+ if (optionIndex < 0)
96
+ return false;
97
+ if (next?.startsWith("<") === true) {
98
+ const value = args[optionIndex + 1];
99
+ if (value === undefined || value.startsWith("--"))
100
+ return false;
101
+ index += 1;
102
+ }
103
+ continue;
104
+ }
105
+ if (!token.startsWith("<") || !token.endsWith(">"))
106
+ continue;
107
+ const absoluteIndex = node.path.length - 1 + position;
108
+ const value = args[absoluteIndex];
109
+ const selectable = policy?.selectors.some((selector) => selector.argumentIndex === absoluteIndex) === true
110
+ || Object.hasOwn(node.argumentValues, position);
111
+ if ((value === undefined || value.startsWith("--")) && !selectable)
112
+ return false;
113
+ position += 1;
114
+ }
115
+ for (const option of node.options) {
116
+ const optionIndex = args.indexOf(option);
117
+ if (optionIndex < 0)
118
+ continue;
119
+ const value = args[optionIndex + 1];
120
+ const selectable = policy?.selectors.some((selector) => selector.option === option) === true
121
+ || Object.hasOwn(node.optionValues, option);
122
+ if ((value === undefined || value.startsWith("--")) && !selectable)
123
+ return false;
124
+ }
125
+ return true;
126
+ }
127
+ async function resolveCatalogEnums(args, node, io) {
128
+ let changed = false;
129
+ const argumentBase = node.path.length - 1;
130
+ for (const [relativeText, choices] of Object.entries(node.argumentValues)) {
131
+ const index = argumentBase + Number(relativeText);
132
+ const value = args[index];
133
+ if (value !== undefined && !value.startsWith("--"))
134
+ continue;
135
+ const selected = await selectValues(`Select value`, choices, io);
136
+ if (selected === undefined)
137
+ return "cancelled";
138
+ args.splice(index, 0, selected);
139
+ changed = true;
140
+ }
141
+ for (const [option, choices] of Object.entries(node.optionValues)) {
142
+ const index = args.indexOf(option);
143
+ if (index < 0)
144
+ continue;
145
+ const value = args[index + 1];
146
+ if (value !== undefined && !value.startsWith("--"))
147
+ continue;
148
+ const selected = await selectValues(`Select ${option.slice(2)}`, choices, io);
149
+ if (selected === undefined)
150
+ return "cancelled";
151
+ args.splice(index + 1, 0, selected);
152
+ changed = true;
153
+ }
154
+ return changed ? "resolved" : "unchanged";
155
+ }
156
+ async function selectValues(title, values, io) {
157
+ return (await selectCandidate({
158
+ entityLabel: "value",
159
+ title,
160
+ columns: [{ header: "Value", minWidth: 5, maxWidth: 32 }],
161
+ candidates: values.map((value) => ({ value, cells: [value] })),
162
+ emptyMessage: "No values are available.",
163
+ overflowHint: "Pass the value explicitly."
164
+ }, io))?.value;
165
+ }
166
+ async function selectCandidate(set, io) {
167
+ if (set.candidates.length === 0) {
168
+ io.write(`○ ${set.emptyMessage}\n`);
169
+ return undefined;
170
+ }
171
+ io.write(`${renderTable(set.title, [{ header: "#", minWidth: 1, maxWidth: 4 }, ...set.columns], set.candidates.map((candidate, index) => [String(index + 1), ...candidate.cells]), io.width)}\n\n`);
172
+ const answer = (await io.question(`Choose ${set.entityLabel} [1-${set.candidates.length}/value, q]: `))?.trim();
173
+ if (answer === undefined || answer.toLowerCase() === "q" || answer.toLowerCase() === "quit") {
174
+ return undefined;
175
+ }
176
+ if (answer.length === 0) {
177
+ return set.candidates.find((candidate) => candidate.value === set.defaultValue)
178
+ ?? set.candidates[0];
179
+ }
180
+ const numeric = Number(answer);
181
+ if (Number.isSafeInteger(numeric) && numeric >= 1 && numeric <= set.candidates.length) {
182
+ return set.candidates[numeric - 1];
183
+ }
184
+ return set.candidates.find((candidate) => candidate.value === answer);
185
+ }
@@ -0,0 +1,51 @@
1
+ import { findChild, ROOT_COMMAND } from "./commandCatalog.js";
2
+ export function routeInvocation(args) {
3
+ if (args.length === 0)
4
+ return { kind: "execute", node: ROOT_COMMAND };
5
+ if (args[0] === "help")
6
+ return resolveHelpPath(args.slice(1));
7
+ return resolveExecutionPath(args);
8
+ }
9
+ function resolveHelpPath(path) {
10
+ if (path.length === 0)
11
+ return { kind: "help", node: ROOT_COMMAND };
12
+ let node = ROOT_COMMAND;
13
+ let nearestGroup = ROOT_COMMAND;
14
+ for (const segment of path) {
15
+ const child = findChild(node, segment);
16
+ if (child === undefined || child.hidden) {
17
+ return { kind: "path-error", typedPath: path.join(" "), helpNode: nearestGroup };
18
+ }
19
+ node = child;
20
+ if (node.kind !== "leaf")
21
+ nearestGroup = node;
22
+ }
23
+ return { kind: "help", node };
24
+ }
25
+ function resolveExecutionPath(args) {
26
+ let node = ROOT_COMMAND;
27
+ let nearestGroup = ROOT_COMMAND;
28
+ let index = 0;
29
+ while (index < args.length) {
30
+ if (node.kind === "leaf")
31
+ break;
32
+ const child = findChild(node, args[index] ?? "");
33
+ if (child === undefined) {
34
+ if (node.kind === "hybrid" && node.acceptsArguments)
35
+ break;
36
+ return {
37
+ kind: "path-error",
38
+ typedPath: args.slice(0, index + 1).join(" "),
39
+ helpNode: nearestGroup
40
+ };
41
+ }
42
+ node = child;
43
+ index += 1;
44
+ if (node.kind !== "leaf")
45
+ nearestGroup = node;
46
+ }
47
+ if (node.kind === "group" && index === args.length) {
48
+ return { kind: "incomplete", typedPath: args.join(" "), helpNode: node };
49
+ }
50
+ return { kind: "execute", node };
51
+ }