@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
package/dist/cli.js ADDED
@@ -0,0 +1,402 @@
1
+ #!/usr/bin/env node
2
+ import { readFileSync } from "node:fs";
3
+ import { createInterface } from "node:readline/promises";
4
+ import { renderCommandHelp } from "./cli/helpRenderer.js";
5
+ import { routeInvocation } from "./cli/invocationRouter.js";
6
+ import { renderCompletion } from "./cli/completion.js";
7
+ import { resolveCompletionCandidates } from "./cli/dynamicCompletion.js";
8
+ import { allowsInteractiveSelection, resolveInteractiveArguments } from "./cli/interactiveSelection.js";
9
+ import { runCompletionWizard } from "./cli/completionWizard.js";
10
+ import { resolveRoleWizardArguments } from "./cli/roleWizard.js";
11
+ import { runUpdateCommand } from "./cli/updateCommand.js";
12
+ import { runAgentCommand } from "./commands/agentCommands.js";
13
+ import { runGlobalRoleCommand } from "./commands/globalRoleCommands.js";
14
+ import { runJobCommand } from "./commands/jobCommands.js";
15
+ import { runOperatorCommand } from "./commands/operatorCommands.js";
16
+ import { runRepositoryCommand } from "./commands/repositoryCommands.js";
17
+ import { runTaskCommand } from "./commands/taskCommands.js";
18
+ import { FileCompletionManager, resolveCliIdentity } from "./completion/fileCompletionManager.js";
19
+ import { callFileTaskController, ensureFileTaskController, FileTaskWorkflowRuntime, restartFileTaskController } from "./controller/clientRuntime.js";
20
+ import { FileSchedulerStoreAdapter } from "./controller/fileSchedulerStoreAdapter.js";
21
+ import { runSessionNotifyCommand } from "./controller/sessionNotify.js";
22
+ import { runDoctorCommand } from "./doctor/doctor.js";
23
+ import { CliError, usageError } from "./errors/cliError.js";
24
+ import { FileRoleLaunchPlanner } from "./executor/fileRoleLaunchPlanner.js";
25
+ import { FileTaskWorkspacePreparer } from "./repository/taskWorkspacePreparer.js";
26
+ import { inspectStorageSchema, requireStorageSchema } from "./storage/storageSchema.js";
27
+ import { FileTaskStore, resolveYuiHome } from "./storage/taskStore.js";
28
+ import { runSetupCommand, validateSetupInvocation } from "./setup/setupCommand.js";
29
+ import { NodeCommandExecutor } from "./tmux/commandExecutor.js";
30
+ import { TmuxManager } from "./tmux/tmuxManager.js";
31
+ const VERSION = readPackageVersion();
32
+ const rawArgs = process.argv.slice(2);
33
+ const jsonOutput = rawArgs.includes("--json");
34
+ const args = normalizeAliases(jsonOutput ? rawArgs.filter((argument) => argument !== "--json") : rawArgs);
35
+ void main().catch((error) => {
36
+ if (error instanceof CliError) {
37
+ const rendered = jsonOutput
38
+ ? JSON.stringify({ ok: false, code: error.code, message: error.message, details: {} })
39
+ : `${error.code}: ${error.message}${error.helpText === undefined ? "" : `\n\n${error.helpText.trimEnd()}`}`;
40
+ process.stderr.write(`${rendered}\n`);
41
+ process.exitCode = error.exitCode;
42
+ return;
43
+ }
44
+ const message = error instanceof Error ? error.message : String(error);
45
+ process.stderr.write(`${jsonOutput
46
+ ? JSON.stringify({ ok: false, code: "RUNTIME_ERROR", message, details: {} })
47
+ : `RUNTIME_ERROR: ${message}`}\n`);
48
+ process.exitCode = 5;
49
+ });
50
+ export async function main() {
51
+ if (args.length === 0) {
52
+ emit(renderCommandHelp((await import("./cli/commandCatalog.js")).ROOT_COMMAND, VERSION));
53
+ return;
54
+ }
55
+ if (args[0] === "version" && args.length === 1) {
56
+ emit(VERSION, true);
57
+ return;
58
+ }
59
+ const invocation = routeInvocation(args);
60
+ if (invocation.kind === "help") {
61
+ emit(renderCommandHelp(invocation.node, VERSION), true);
62
+ return;
63
+ }
64
+ if (invocation.kind === "path-error") {
65
+ throw usageError(`Unknown command: ${invocation.typedPath}`, renderCommandHelp(invocation.helpNode, VERSION));
66
+ }
67
+ if (invocation.kind === "incomplete") {
68
+ throw usageError(`Command required after: ${invocation.typedPath}`, renderCommandHelp(invocation.helpNode, VERSION));
69
+ }
70
+ if (args[0] === "version")
71
+ throw usageError("Version usage: yui version");
72
+ if (args[0] === "update") {
73
+ if (jsonOutput)
74
+ throw usageError("Update does not support --json.");
75
+ if (args.length !== 1)
76
+ throw usageError("Update usage: yui update");
77
+ process.exitCode = runUpdateCommand();
78
+ return;
79
+ }
80
+ const home = resolveYuiHome(process.env);
81
+ if (args[0] === "completion") {
82
+ await completionCommand(home, invocation.node);
83
+ return;
84
+ }
85
+ if (args[0] === "setup") {
86
+ if (jsonOutput)
87
+ throw usageError("Setup does not support --json.");
88
+ const setupIo = {
89
+ input: process.stdin,
90
+ output: process.stdout,
91
+ forceInteractive: process.env.YUI_SETUP_INTERACTIVE === "1"
92
+ };
93
+ validateSetupInvocation(args.slice(1), setupIo);
94
+ emit(await runSetupCommand(args.slice(1), process.env, new NodeCommandExecutor(), setupIo));
95
+ return;
96
+ }
97
+ if (args[0] === "doctor") {
98
+ emit(runDoctorCommand(args.slice(1), process.env, new NodeCommandExecutor()));
99
+ return;
100
+ }
101
+ if (args[0] === "internal") {
102
+ if (args[1] !== "session-notify" || args.length !== 3) {
103
+ throw usageError("Internal session notify usage is invalid.");
104
+ }
105
+ await runSessionNotifyCommand(args[2], process.env);
106
+ return;
107
+ }
108
+ requireStorageSchema(home);
109
+ const store = new FileTaskStore(home);
110
+ const resolved = await resolveTerminalArguments(args, invocation.node, store);
111
+ if (resolved === null) {
112
+ emit("Cancelled.");
113
+ return;
114
+ }
115
+ if (resolved[0] === "controller") {
116
+ const method = resolved[1];
117
+ if ((method !== "status" && method !== "stop" && method !== "restart") || resolved.length !== 2) {
118
+ throw usageError("Controller usage: yui controller status|stop|restart.");
119
+ }
120
+ const controllerMethod = method;
121
+ const result = controllerMethod === "restart"
122
+ ? await restartFileTaskController(home, { environment: process.env })
123
+ : await callFileTaskController(home, `controller.${controllerMethod}`);
124
+ emit(renderControllerResult(controllerMethod, result));
125
+ return;
126
+ }
127
+ const executor = new NodeCommandExecutor();
128
+ const tmux = new TmuxManager(process.env.YUI_TMUX_BIN ?? "tmux", executor, { yuiHome: home, terminalInput: process.stdin });
129
+ const schedulerStore = new FileSchedulerStoreAdapter(store);
130
+ const planner = new FileRoleLaunchPlanner(home, store, { environment: process.env });
131
+ const workspacePreparer = new FileTaskWorkspacePreparer(home, store);
132
+ const runtime = new FileTaskWorkflowRuntime(home, store, schedulerStore, planner, tmux, workspacePreparer, {
133
+ environment: process.env,
134
+ onError: (error) => {
135
+ const message = error instanceof Error ? error.message : String(error);
136
+ process.stderr.write(`Controller runtime error: ${message}\n`);
137
+ }
138
+ });
139
+ if (resolved[0] === "agent") {
140
+ emit(runAgentCommand(resolved.slice(1), store));
141
+ return;
142
+ }
143
+ if (resolved[0] === "repository") {
144
+ emit(await runRepositoryCommand(resolved.slice(1), store));
145
+ return;
146
+ }
147
+ if (resolved[0] === "role") {
148
+ const roleOptions = {
149
+ yuiHome: home,
150
+ env: process.env
151
+ };
152
+ const result = runGlobalRoleCommand(resolved.slice(1), store, roleOptions);
153
+ if (typeof result === "string") {
154
+ emit(result);
155
+ return;
156
+ }
157
+ await ensureFileTaskController(home, { environment: process.env });
158
+ runtime.prepareGlobalRoleEnter(result.role.name);
159
+ tmux.attachRole("operator", result.role.name);
160
+ return;
161
+ }
162
+ if (resolved[0] === "operator") {
163
+ if (resolved[1] === "enter") {
164
+ if (resolved.length !== 2)
165
+ throw usageError("Operator enter usage: yui operator enter.");
166
+ await ensureFileTaskController(home, { environment: process.env });
167
+ runtime.prepareGlobalRoleEnter("operator");
168
+ tmux.attachRole("operator", "operator");
169
+ return;
170
+ }
171
+ const result = runOperatorCommand(resolved.slice(1), store, { runtime, environment: process.env });
172
+ if (result.kind !== "output")
173
+ throw new Error("Operator submit returned an invalid control result.");
174
+ emit(result.output);
175
+ return;
176
+ }
177
+ if (resolved[0] === "task") {
178
+ const enteringTask = (resolved[1] === "enter")
179
+ || (resolved[1] === "role" && resolved[2] === "enter");
180
+ if (enteringTask) {
181
+ await ensureFileTaskController(home, { environment: process.env });
182
+ const taskId = resolved[1] === "enter" ? resolved[2] : resolved[3];
183
+ const task = taskId === undefined ? null : store.getTask(taskId);
184
+ if (task?.status === "active" && task.repositoryId !== undefined) {
185
+ await workspacePreparer.prepareTaskWorkspace(task.id);
186
+ }
187
+ }
188
+ const result = runTaskCommand(resolved.slice(1), store, { runtime, environment: process.env });
189
+ if (result.kind === "output") {
190
+ emit(result.output, false, result.data);
191
+ return;
192
+ }
193
+ if (result.output !== undefined)
194
+ emit(result.output);
195
+ tmux.attachRole(result.taskId, result.roleName);
196
+ return;
197
+ }
198
+ if (resolved[0] === "jobs") {
199
+ emit(runJobCommand(resolved.slice(1), store, { runtime }));
200
+ return;
201
+ }
202
+ throw usageError(`Command is not connected to the restored FileTaskStore framework yet: ${resolved[0]}.`, renderCommandHelp(invocation.node, VERSION));
203
+ }
204
+ function renderControllerResult(method, value) {
205
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
206
+ return JSON.stringify(value);
207
+ }
208
+ const result = value;
209
+ if (method === "status") {
210
+ if (result.running !== true)
211
+ return "Controller is not running.";
212
+ return result.pid === undefined
213
+ ? "Controller is running."
214
+ : `Controller is running (PID ${String(result.pid)}).`;
215
+ }
216
+ if (method === "restart") {
217
+ const previousPid = Number.isSafeInteger(result.previousPid) ? String(result.previousPid) : undefined;
218
+ const pid = Number.isSafeInteger(result.pid) ? String(result.pid) : undefined;
219
+ if (previousPid !== undefined && pid !== undefined) {
220
+ return `Controller restarted (PID ${previousPid} -> ${pid}). tmux sessions were not stopped.`;
221
+ }
222
+ return pid === undefined
223
+ ? "Controller restarted. tmux sessions were not stopped."
224
+ : `Controller started (PID ${pid}). tmux sessions were not stopped.`;
225
+ }
226
+ return result.stopped === true
227
+ ? "Controller stopped."
228
+ : "Controller was already stopped.";
229
+ }
230
+ async function completionCommand(home, node) {
231
+ if (args[1] === "candidates") {
232
+ const separator = args.indexOf("--");
233
+ const prefix = args[2];
234
+ if (prefix === undefined || separator !== 3) {
235
+ throw usageError("Completion candidates usage: yui completion candidates <prefix> -- <words...>");
236
+ }
237
+ const candidates = await resolveCompletionCandidates({
238
+ current: prefix,
239
+ words: args.slice(separator + 1),
240
+ ports: completionSelectionPorts(home)
241
+ });
242
+ process.stdout.write(candidates.length === 0 ? "" : `${candidates.join("\n")}\n`);
243
+ return;
244
+ }
245
+ if (jsonOutput)
246
+ throw usageError("Completion configuration does not support --json.");
247
+ if (process.stdin.isTTY !== true || process.stdout.isTTY !== true) {
248
+ throw usageError("Completion configuration requires an interactive terminal.");
249
+ }
250
+ const shell = completionShell(args[1]);
251
+ if (args.length > (shell === undefined ? 1 : 2)) {
252
+ throw usageError("Completion usage: yui completion [bash|zsh|fish]");
253
+ }
254
+ requireStorageSchema(home);
255
+ const store = new FileTaskStore(home);
256
+ const ioHandle = terminalIo();
257
+ try {
258
+ const manager = new FileCompletionManager(store, process.env, resolveCliIdentity(process.env));
259
+ emit(await runCompletionWizard(manager, ioHandle.io, shell === undefined ? {} : { shell }));
260
+ }
261
+ finally {
262
+ ioHandle.close();
263
+ }
264
+ void node;
265
+ }
266
+ function completionShell(value) {
267
+ if (value === undefined)
268
+ return undefined;
269
+ if (value === "bash" || value === "zsh" || value === "fish")
270
+ return value;
271
+ throw usageError("Completion shell must be one of bash, zsh, fish.");
272
+ }
273
+ async function resolveTerminalArguments(commandArgs, node, store) {
274
+ const interactive = process.stdin.isTTY === true && process.stdout.isTTY === true;
275
+ if (!interactive || !allowsInteractiveSelection(commandArgs, jsonOutput)) {
276
+ return [...commandArgs];
277
+ }
278
+ const handle = terminalIo();
279
+ try {
280
+ const ports = selectionPorts(store);
281
+ // Global Role add owns its Agent choice so the configured default can be
282
+ // shown explicitly. Other commands first resolve missing positional
283
+ // targets through the generic selector, then enter the focused Role UI.
284
+ if ((commandArgs[0] === "role" && commandArgs[1] === "add")
285
+ || (commandArgs[0] === "task" && commandArgs[1] === "role" && commandArgs[2] === "add")) {
286
+ const wizard = await resolveRoleWizardArguments(commandArgs, ports, handle.io);
287
+ if (wizard.kind === "cancelled")
288
+ return null;
289
+ const selected = await resolveInteractiveArguments(wizard.args, node, ports, handle.io);
290
+ return selected.kind === "cancelled" ? null : selected.args;
291
+ }
292
+ const selected = await resolveInteractiveArguments(commandArgs, node, ports, handle.io);
293
+ if (selected.kind === "cancelled")
294
+ return null;
295
+ const wizard = await resolveRoleWizardArguments(selected.args, ports, handle.io);
296
+ return wizard.kind === "cancelled" ? null : wizard.args;
297
+ }
298
+ finally {
299
+ // The Agent process must be the only reader of stdin after Role enter.
300
+ handle.close();
301
+ }
302
+ }
303
+ function terminalIo() {
304
+ const readline = createInterface({ input: process.stdin, output: process.stdout, terminal: true });
305
+ return {
306
+ io: {
307
+ interactive: true,
308
+ json: jsonOutput,
309
+ width: process.stdout.columns ?? 100,
310
+ write: (value) => { process.stdout.write(value); },
311
+ question: async (prompt) => {
312
+ try {
313
+ return await readline.question(prompt);
314
+ }
315
+ catch (error) {
316
+ if (error instanceof Error && (error.name === "AbortError"
317
+ || ("code" in error && error.code === "ERR_USE_AFTER_CLOSE")))
318
+ return undefined;
319
+ throw error;
320
+ }
321
+ }
322
+ },
323
+ close: () => { readline.close(); }
324
+ };
325
+ }
326
+ function selectionPorts(store) {
327
+ return {
328
+ call: (method, params) => selectionCall(store, method, params)
329
+ };
330
+ }
331
+ function selectionCall(store, method, params) {
332
+ const reader = store;
333
+ switch (method) {
334
+ case "agent.list": return store.listConfiguredAgents();
335
+ case "config.get": return store.getConfig();
336
+ case "role.list": return store.listGlobalRoles();
337
+ case "role.show": return store.getGlobalRole(String(params.name ?? ""));
338
+ case "repository.list": return callOptional(reader, "listRepositories");
339
+ case "task.list": return callOptional(reader, "listTasks");
340
+ case "task.role.list": return callOptional(reader, "listRoles", [params.taskId]);
341
+ case "task.role.show": return callOptional(reader, "getRole", [params.taskId, params.roleName]);
342
+ case "task.work.list": return callOptional(reader, "listWorkItems", [params.taskId]);
343
+ case "task.input.list": {
344
+ const taskId = typeof params.taskId === "string" ? params.taskId : undefined;
345
+ const requests = taskId === undefined
346
+ ? store.listAllInputRequests()
347
+ : store.listInputRequests(taskId);
348
+ return params.all === true ? requests : requests.filter((request) => request.status === "open");
349
+ }
350
+ case "task.run.list": return callOptional(reader, "listAgentRuns", [params.workItemId]);
351
+ case "task.decision.list": return callOptional(reader, "listDecisions", [params.taskId]);
352
+ case "task.milestone.list": return callOptional(reader, "listMilestones", [params.taskId]);
353
+ case "task.event.list": return callOptional(reader, "listEvents", [params.taskId]);
354
+ case "jobs.list": return callOptional(reader, "listJobs");
355
+ default: return [];
356
+ }
357
+ }
358
+ function callOptional(reader, method, args = []) {
359
+ const operation = reader[method];
360
+ return operation === undefined ? [] : Reflect.apply(operation, reader, args);
361
+ }
362
+ function readableStore(home) {
363
+ requireStorageSchema(home);
364
+ return new FileTaskStore(home);
365
+ }
366
+ function completionSelectionPorts(home) {
367
+ if (inspectStorageSchema(home).status === "uninitialized") {
368
+ return { call: () => [] };
369
+ }
370
+ return selectionPorts(readableStore(home));
371
+ }
372
+ function emit(output, literal = false, data) {
373
+ const normalized = literal ? output.trimEnd() : output.trimEnd();
374
+ process.stdout.write(`${jsonOutput
375
+ ? JSON.stringify(data === undefined
376
+ ? { ok: true, output: normalized }
377
+ : { ok: true, data })
378
+ : normalized}\n`);
379
+ }
380
+ function readPackageVersion() {
381
+ try {
382
+ const value = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8"));
383
+ if (typeof value.version === "string" && value.version.length > 0)
384
+ return value.version;
385
+ }
386
+ catch {
387
+ // Keep help/version available if package metadata is damaged.
388
+ }
389
+ return "0.0.0";
390
+ }
391
+ export function cliIdentity(env) {
392
+ return env.YUI_CLI_NAME === "yui-dev" ? "yui-dev" : "yui";
393
+ }
394
+ function normalizeAliases(input) {
395
+ const normalized = [...input];
396
+ if (normalized.length === 1 && (normalized[0] === "-v" || normalized[0] === "--version")) {
397
+ return ["version"];
398
+ }
399
+ const help = normalized.findIndex((argument) => argument === "-h" || argument === "--help");
400
+ return help === normalized.length - 1 ? ["help", ...normalized.slice(0, help)] : normalized;
401
+ }
402
+ export { renderCompletion };
@@ -0,0 +1,196 @@
1
+ import { agentNotFound, usageError } from "../errors/cliError.js";
2
+ import { defaultTableWidth, renderTable } from "../output/table.js";
3
+ import { createConfiguredAgent } from "../agent/agent.js";
4
+ const SUPPORTED_ADAPTERS = Object.freeze(["codex", "claude"]);
5
+ export function runAgentCommand(args, store) {
6
+ const [command, ...rest] = args;
7
+ switch (command) {
8
+ case "add": return addAgent(rest, store);
9
+ case "list": return listAgents(rest, store);
10
+ case "show": return showAgent(rest, store);
11
+ case "update": return updateAgent(rest, store);
12
+ case "remove": return removeAgent(rest, store);
13
+ default:
14
+ throw usageError(command === undefined
15
+ ? "Agent command is required."
16
+ : `Unknown command: agent ${command}`);
17
+ }
18
+ }
19
+ function addAgent(args, store) {
20
+ const [rawId, ...tail] = args;
21
+ const id = agentId(rawId);
22
+ const parsed = parseAgentOptions(tail, "add");
23
+ const adapterId = parsed.one("--adapter") ?? (SUPPORTED_ADAPTERS.includes(id) ? id : undefined);
24
+ if (adapterId === undefined)
25
+ throw usageError("--adapter is required.");
26
+ assertAdapter(adapterId);
27
+ const command = parsed.one("--command")?.trim();
28
+ if (command === undefined || command.length === 0)
29
+ throw usageError("--command is required.");
30
+ let agent;
31
+ try {
32
+ agent = createConfiguredAgent(id, adapterId, command, parsed.many("--arg"), parsed.many("--env").map(parseEnvironmentBinding), new Date());
33
+ }
34
+ catch (error) {
35
+ throw usageError(error instanceof Error ? error.message : String(error));
36
+ }
37
+ const created = store.createConfiguredAgentIfAbsent(agent);
38
+ if (created === null) {
39
+ throw usageError(`Agent already exists: ${id}. Use yui agent update to change it.`);
40
+ }
41
+ return renderAgent(`Added agent ${id}`, created);
42
+ }
43
+ function listAgents(args, store) {
44
+ assertNoArguments(args, "Agent list usage: yui agent list");
45
+ const agents = store.listConfiguredAgents();
46
+ if (agents.length === 0)
47
+ return "No agents configured.\n";
48
+ return `${renderTable("Agents", [
49
+ { header: "Agent", minWidth: 5, maxWidth: 24 },
50
+ { header: "Adapter", minWidth: 7, maxWidth: 12 },
51
+ { header: "Command", minWidth: 7, maxWidth: 48 },
52
+ { header: "Environment", minWidth: 11, maxWidth: 32 }
53
+ ], agents.map((agent) => [
54
+ agent.id,
55
+ agent.adapterId,
56
+ [agent.command, ...agent.baseArgs].join(" "),
57
+ agent.environment.map((binding) => `${binding.target}<-${binding.sourceName}`).join(", ")
58
+ ]), defaultTableWidth())}\n`;
59
+ }
60
+ function showAgent(args, store) {
61
+ const [rawId, ...rest] = args;
62
+ const id = agentId(rawId);
63
+ assertNoArguments(rest, "Agent show usage: yui agent show <agent-id>");
64
+ const agent = store.getConfiguredAgent(id);
65
+ if (agent === null)
66
+ throw agentNotFound(id);
67
+ return renderAgent(`Agent: ${id}`, agent);
68
+ }
69
+ function updateAgent(args, store) {
70
+ const [rawId, ...tail] = args;
71
+ const id = agentId(rawId);
72
+ const parsed = parseAgentOptions(tail, "update");
73
+ if (parsed.seen.size === 0) {
74
+ throw usageError("Agent update requires at least one operational option.");
75
+ }
76
+ if (parsed.has("--arg") && parsed.has("--clear-args")) {
77
+ throw usageError("--arg and --clear-args cannot be used together.");
78
+ }
79
+ if (parsed.has("--env") && parsed.has("--clear-env")) {
80
+ throw usageError("--env and --clear-env cannot be used together.");
81
+ }
82
+ const adapterId = parsed.one("--adapter")?.trim();
83
+ if (adapterId !== undefined)
84
+ assertAdapter(adapterId);
85
+ const command = parsed.one("--command")?.trim();
86
+ if (command !== undefined && command.length === 0)
87
+ throw usageError("--command is required.");
88
+ const patch = {
89
+ ...(adapterId === undefined ? {} : { adapterId }),
90
+ ...(command === undefined ? {} : { command }),
91
+ ...(parsed.has("--arg")
92
+ ? { baseArgs: parsed.many("--arg") }
93
+ : parsed.has("--clear-args") ? { baseArgs: [] } : {}),
94
+ ...(parsed.has("--env")
95
+ ? { environment: parsed.many("--env").map(parseEnvironmentBinding) }
96
+ : parsed.has("--clear-env") ? { environment: [] } : {})
97
+ };
98
+ const result = store.updateConfiguredAgent(id, patch, new Date());
99
+ if (result === null)
100
+ throw agentNotFound(id);
101
+ return result.status === "unchanged"
102
+ ? `Agent ${id} unchanged\n`
103
+ : renderAgent(`Updated agent ${id}`, result.agent);
104
+ }
105
+ function removeAgent(args, store) {
106
+ const [rawId, ...rest] = args;
107
+ const id = agentId(rawId);
108
+ assertNoArguments(rest, "Agent remove usage: yui agent remove <agent-id>");
109
+ if (!store.removeConfiguredAgent(id))
110
+ throw agentNotFound(id);
111
+ return `Removed agent ${id}\n`;
112
+ }
113
+ function parseAgentOptions(args, mode) {
114
+ const valueOptions = new Map([
115
+ ["--adapter", { repeatable: false, allowOptionLikeValue: false }],
116
+ ["--command", { repeatable: false, allowOptionLikeValue: false }],
117
+ ["--arg", { repeatable: true, allowOptionLikeValue: true }],
118
+ ["--env", { repeatable: true, allowOptionLikeValue: false }]
119
+ ]);
120
+ const flags = mode === "update" ? new Set(["--clear-args", "--clear-env"]) : new Set();
121
+ const seen = new Set();
122
+ const values = new Map();
123
+ for (let index = 0; index < args.length; index += 1) {
124
+ const option = args[index];
125
+ if (flags.has(option)) {
126
+ if (seen.has(option))
127
+ throw usageError(`Option may only be specified once: ${option}`);
128
+ seen.add(option);
129
+ continue;
130
+ }
131
+ const spec = valueOptions.get(option);
132
+ if (spec === undefined) {
133
+ throw usageError(option.startsWith("--")
134
+ ? `Unsupported option: ${option}`
135
+ : `Unexpected argument: ${option}`);
136
+ }
137
+ if (!spec.repeatable && seen.has(option)) {
138
+ throw usageError(`Option may only be specified once: ${option}`);
139
+ }
140
+ const value = args[index + 1];
141
+ if (value === undefined || (!spec.allowOptionLikeValue && value.startsWith("--"))) {
142
+ throw usageError(`${option} is required.`);
143
+ }
144
+ seen.add(option);
145
+ values.set(option, [...(values.get(option) ?? []), value]);
146
+ index += 1;
147
+ }
148
+ return {
149
+ seen,
150
+ has: (option) => seen.has(option),
151
+ one: (option) => values.get(option)?.[0],
152
+ many: (option) => [...(values.get(option) ?? [])]
153
+ };
154
+ }
155
+ function parseEnvironmentBinding(value) {
156
+ const separator = value.indexOf("=");
157
+ if (separator <= 0 || separator === value.length - 1) {
158
+ throw usageError("--env must use TARGET=PROCESS_NAME.");
159
+ }
160
+ const target = value.slice(0, separator).trim();
161
+ const sourceName = value.slice(separator + 1).trim();
162
+ if (!environmentName(target) || !environmentName(sourceName)) {
163
+ throw usageError("--env must use valid environment names: TARGET=PROCESS_NAME.");
164
+ }
165
+ return { target, source: "process", sourceName, required: true };
166
+ }
167
+ function environmentName(value) {
168
+ return /^[A-Za-z_][A-Za-z0-9_]*$/.test(value);
169
+ }
170
+ function agentId(value) {
171
+ if (value === undefined || !/^[A-Za-z0-9_-]+$/.test(value.trim())) {
172
+ throw usageError(value === undefined || value.trim().length === 0
173
+ ? "Agent id is required."
174
+ : "Agent id may only contain letters, numbers, hyphens, and underscores.");
175
+ }
176
+ return value.trim();
177
+ }
178
+ function assertAdapter(value) {
179
+ if (!SUPPORTED_ADAPTERS.includes(value)) {
180
+ throw usageError(`Agent adapter is not supported: ${value}. Supported adapters: ${SUPPORTED_ADAPTERS.join(", ")}.`);
181
+ }
182
+ }
183
+ function assertNoArguments(args, message) {
184
+ if (args.length > 0)
185
+ throw usageError(`${message}. Unexpected argument: ${args[0]}`);
186
+ }
187
+ function renderAgent(title, agent) {
188
+ return [
189
+ title,
190
+ `Adapter: ${agent.adapterId}`,
191
+ `Executable: ${agent.command}`,
192
+ `Arguments: ${agent.baseArgs.join(" ")}`,
193
+ `Environment bindings: ${agent.environment.length}`,
194
+ ...agent.environment.map((binding) => ` ${binding.target} <- process:${binding.sourceName} (${binding.required ? "required" : "optional"})`)
195
+ ].join("\n").concat("\n");
196
+ }