@zq-silk/yui 0.7.1 → 0.8.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +42 -23
- package/dist/cli/commandCatalog.js +234 -122
- package/dist/cli/completion.js +3 -3
- package/dist/cli/helpRenderer.js +3 -0
- package/dist/cli/interactionPolicy.js +44 -23
- package/dist/cli/interactiveSelection.js +1 -1
- package/dist/cli/invocationRouter.js +1 -1
- package/dist/cli/roleWizard.js +8 -8
- package/dist/cli.js +116 -72
- package/dist/commands/agentCommands.js +5 -5
- package/dist/commands/configCommands.js +351 -104
- package/dist/commands/configOverview.js +60 -0
- package/dist/commands/deliveryGuardPreflight.js +2 -2
- package/dist/commands/globalRoleCommands.js +9 -9
- package/dist/commands/profileCommands.js +8 -8
- package/dist/commands/resourcesCommands.js +6 -5
- package/dist/commands/taskCommands.js +3 -6
- package/dist/commands/taskRoleRuntimeStatus.js +3 -1
- package/dist/commands/telemetryCommands.js +11 -6
- package/dist/config/configCatalog.js +42 -0
- package/dist/config/yuiConfig.js +80 -35
- package/dist/context/sessionBootstrapManifest.js +1 -1
- package/dist/controller/clientRuntime.js +0 -2
- package/dist/controller/controller.js +21 -9
- package/dist/controller/fileSchedulerStoreAdapter.js +20 -9
- package/dist/controller/runtime.js +32 -18
- package/dist/doctor/doctor.js +2 -2
- package/dist/resources/autoResourceGc.js +3 -1
- package/dist/review/reviewConfig.js +0 -2
- package/dist/run/providerRetry.js +29 -16
- package/dist/run/providerRetryConfig.js +5 -3
- package/dist/runtime/launchDiagnostics.js +1 -1
- package/dist/scheduler/roleRunStall.js +12 -9
- package/dist/setup/setupCommand.js +153 -492
- package/dist/storage/compatibleTaskStore.js +9 -5
- package/dist/storage/migration/productionRegistry.js +58 -0
- package/dist/storage/taskStore.js +21 -2
- package/dist/telemetry/sqliteTelemetryStore.js +9 -1
- package/dist/telemetry/telemetryConfig.js +1 -18
- package/dist/telemetry/telemetryStore.js +2 -2
- package/dist/telemetry/telemetryWiring.js +6 -5
- package/dist/web/webSnapshot.js +5 -3
- package/i18n/README.zh-CN.md +37 -32
- package/package.json +1 -1
- package/skills/yui-leader/SKILL.md +12 -5
- package/skills/yui-operator/SKILL.md +44 -6
- package/skills/yui-runtime/SKILL.md +1 -1
|
@@ -1,22 +1,14 @@
|
|
|
1
1
|
import { existsSync, mkdirSync, realpathSync } from "node:fs";
|
|
2
2
|
import { delimiter, dirname, isAbsolute, join, relative, resolve } from "node:path";
|
|
3
3
|
import { createInterface } from "node:readline/promises";
|
|
4
|
-
import { isDeepStrictEqual } from "node:util";
|
|
5
4
|
import { configuredAgentToDefinition, createConfiguredAgent } from "../agent/agent.js";
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
5
|
+
import { assertRoleRuntimeMutationAllowed } from "../commands/roleRuntimeGuard.js";
|
|
6
|
+
import { resolveTmuxBin } from "../config/yuiConfig.js";
|
|
8
7
|
import { usageError } from "../errors/cliError.js";
|
|
9
|
-
import { defaultRoleAgentConfig, resolveAgentAdapter } from "../executor/agentAdapter.js";
|
|
10
|
-
import { AgentConfigurationCatalogService } from "../executor/agentConfigurationCatalog.js";
|
|
11
|
-
import { defaultTableWidth, renderTable } from "../output/table.js";
|
|
12
|
-
import { resolveTimeZone } from "../output/timePresentation.js";
|
|
13
8
|
import { createGlobalRole, createRoleAgentBinding, updateGlobalRole } from "../role/role.js";
|
|
14
|
-
import { SYSTEM_LEADER_ROLE, SYSTEM_OPERATOR_ROLE
|
|
15
|
-
import {
|
|
16
|
-
import { builtinAgentProfileInputs, createAgentProfile } from "../profile/agentProfile.js";
|
|
17
|
-
import { assertRoleRuntimeMutationAllowed } from "../commands/roleRuntimeGuard.js";
|
|
9
|
+
import { SYSTEM_LEADER_ROLE, SYSTEM_OPERATOR_ROLE } from "../role/systemRoles.js";
|
|
10
|
+
import { initializeCompatibleTaskStore } from "../storage/compatibleTaskStore.js";
|
|
18
11
|
import { ensureYuiHome, resolveYuiHome } from "../storage/taskStore.js";
|
|
19
|
-
import { initializeCompatibleFileTaskStore } from "../storage/compatibleTaskStore.js";
|
|
20
12
|
const BUILTIN_AGENTS = Object.freeze([
|
|
21
13
|
Object.freeze({
|
|
22
14
|
id: "codex",
|
|
@@ -32,7 +24,7 @@ const BUILTIN_AGENTS = Object.freeze([
|
|
|
32
24
|
})
|
|
33
25
|
]);
|
|
34
26
|
export async function runSetupCommand(args, env, executor, io = {}) {
|
|
35
|
-
|
|
27
|
+
parseSetupOptions(args);
|
|
36
28
|
if (!shouldPrompt(io))
|
|
37
29
|
throw setupRequiresInteractiveError();
|
|
38
30
|
const readline = createInterface({
|
|
@@ -44,48 +36,32 @@ export async function runSetupCommand(args, env, executor, io = {}) {
|
|
|
44
36
|
const question = createSetupQuestion(readline, io);
|
|
45
37
|
const home = resolveYuiHome(env);
|
|
46
38
|
ensureYuiHome(home);
|
|
47
|
-
const store =
|
|
48
|
-
const
|
|
49
|
-
|
|
50
|
-
const
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
`Leader model: ${result.leaderConfig.model ?? "CLI default"}.`,
|
|
56
|
-
`Leader reasoning effort: ${result.leaderConfig.effort ?? "CLI default"}.`,
|
|
57
|
-
`Leader permission: ${result.leaderConfig.permission.strategy}.`,
|
|
58
|
-
`Operator model: ${result.operatorConfig.model ?? "CLI default"}.`,
|
|
59
|
-
`Operator reasoning effort: ${result.operatorConfig.effort ?? "CLI default"}.`,
|
|
60
|
-
`Operator permission: ${result.operatorConfig.permission.strategy}.`,
|
|
61
|
-
`Worker Agent: ${result.workerAgentId}.`,
|
|
62
|
-
`Worker configuration: ${result.workerReusesLeader
|
|
63
|
-
? "Reused Leader configuration"
|
|
64
|
-
: "Configured separately"}.`,
|
|
65
|
-
`Worker model: ${result.workerConfig.model ?? "CLI default"}.`,
|
|
66
|
-
`Worker reasoning effort: ${result.workerConfig.effort ?? "CLI default"}.`,
|
|
67
|
-
`Worker permission: ${result.workerConfig.permission.strategy}.`,
|
|
68
|
-
...(result.reviewerInitialized
|
|
69
|
-
? [
|
|
70
|
-
`Reviewer Agent: ${result.reviewerAgentId}.`,
|
|
71
|
-
`Reviewer model: ${result.reviewerConfig?.model ?? "CLI default"}.`,
|
|
72
|
-
`Reviewer reasoning effort: ${result.reviewerConfig?.effort ?? "CLI default"}.`,
|
|
73
|
-
`Reviewer permission: ${result.reviewerConfig?.permission.strategy}.`,
|
|
74
|
-
...(result.reviewPolicy === undefined
|
|
75
|
-
? ["Review policy: disabled."]
|
|
76
|
-
: [`Review policy: ${result.reviewPolicy.roleName} (${result.reviewPolicy.trigger}).`])
|
|
77
|
-
]
|
|
78
|
-
: []),
|
|
79
|
-
`Project workspace: ${result.workspace}.`,
|
|
80
|
-
`Time zone: ${resolveTimeZone(store.getConfig().timeZone)}.`
|
|
81
|
-
];
|
|
82
|
-
if (dependency === undefined || dependency === "tmux") {
|
|
83
|
-
lines.push(...await setupTmux(env, executor, question));
|
|
39
|
+
const store = initializeCompatibleTaskStore(home);
|
|
40
|
+
const tmuxBin = resolveTmuxBin(store.getConfig().tmuxBin);
|
|
41
|
+
assertTmuxAvailable(executor, tmuxBin);
|
|
42
|
+
const choices = availableAgentChoices(store, env);
|
|
43
|
+
if (choices.length === 0) {
|
|
44
|
+
throw usageError("No usable Agent configuration was found. Setup never overwrites an existing Agent id; "
|
|
45
|
+
+ "repair it with yui config agent update, add another usable Agent with yui config agent add, "
|
|
46
|
+
+ "or install Codex or Claude, then run yui setup again.");
|
|
84
47
|
}
|
|
85
|
-
const
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
48
|
+
const currentOperator = store.getGlobalRole(SYSTEM_OPERATOR_ROLE);
|
|
49
|
+
const selected = await selectOperatorAgent(choices, currentOperator, store.getConfig().defaultAgent, question, io);
|
|
50
|
+
const agent = selected.existing ?? createConfiguredAgent(selected.id, selected.adapterId, selected.command, [], [], new Date());
|
|
51
|
+
const workspace = resolveWorkspace(store.getConfig().defaultWorkspace ?? join(dirname(resolve(home)), "workspace"), home);
|
|
52
|
+
const roleStatus = saveMinimumConfiguration(store, agent, workspace, new Set(choices.map(({ id }) => id)));
|
|
53
|
+
return `${[
|
|
54
|
+
"Yui setup complete.",
|
|
55
|
+
`Yui home: ${home}.`,
|
|
56
|
+
`Operator Agent: ${agent.id}.`,
|
|
57
|
+
`Leader Agent: ${roleStatus.leaderAgentId}.`,
|
|
58
|
+
`Default workspace: ${workspace}.`,
|
|
59
|
+
`Operator configuration: ${roleStatus.operator}.`,
|
|
60
|
+
`Leader configuration: ${roleStatus.leader}.`,
|
|
61
|
+
`Tmux: ${tmuxBin}.`,
|
|
62
|
+
"Setup did not configure Review, Worker, Profiles, or shell completion.",
|
|
63
|
+
"Run `yui operator enter` to continue, or `yui config show` to inspect configuration."
|
|
64
|
+
].join("\n")}\n`;
|
|
89
65
|
}
|
|
90
66
|
finally {
|
|
91
67
|
readline.close();
|
|
@@ -96,371 +72,129 @@ export function validateSetupInvocation(args, io = {}) {
|
|
|
96
72
|
if (!shouldPrompt(io))
|
|
97
73
|
throw setupRequiresInteractiveError();
|
|
98
74
|
}
|
|
99
|
-
|
|
100
|
-
const
|
|
101
|
-
const
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
{ header: "Agent", minWidth: 6, maxWidth: 16 },
|
|
113
|
-
{ header: "Adapter", minWidth: 7, maxWidth: 10 },
|
|
114
|
-
{ header: "Command", minWidth: 7, maxWidth: 24 },
|
|
115
|
-
{ header: "Description", minWidth: 12, maxWidth: 42 }
|
|
116
|
-
], candidates.map((candidate, index) => [
|
|
117
|
-
String(index + 1),
|
|
118
|
-
candidate.id,
|
|
119
|
-
candidate.adapterId,
|
|
120
|
-
candidate.command,
|
|
121
|
-
candidate.description
|
|
122
|
-
]), tableWidth(io))}\n`);
|
|
123
|
-
const selected = parseAgentSetSelection(await question(`Choose Agents by number or name, comma-separated [all: ${candidates.map(({ id }) => id).join(", ")}]: `), candidates);
|
|
124
|
-
const now = new Date();
|
|
125
|
-
const prepared = selected.map((choice) => prepareAgent(store, choice, now));
|
|
126
|
-
const configuredIds = new Set(prepared.map(({ id }) => id));
|
|
127
|
-
const config = store.getConfig();
|
|
128
|
-
const defaultFallback = prepared.some(({ id }) => id === config.defaultAgent)
|
|
129
|
-
? config.defaultAgent
|
|
130
|
-
: prepared[0].id;
|
|
131
|
-
const defaultAgentId = parseSingleAgentSelection(await question(`Choose default Agent [${defaultFallback}]: `), prepared, defaultFallback);
|
|
132
|
-
const currentOperatorAgent = store.getGlobalRole(SYSTEM_OPERATOR_ROLE)?.activeAgentId;
|
|
133
|
-
const operatorFallback = configuredIds.has(currentOperatorAgent ?? "")
|
|
134
|
-
? currentOperatorAgent
|
|
135
|
-
: defaultAgentId;
|
|
136
|
-
const operatorAgentId = parseSingleAgentSelection(await question(`Choose Operator Agent [${operatorFallback}]: `), prepared, operatorFallback);
|
|
137
|
-
const existingReviewer = store.getGlobalRole(DEFAULT_REVIEWER_ROLE);
|
|
138
|
-
const reviewerInitialized = freshHome || existingReviewer !== null;
|
|
139
|
-
let reviewerAgentId;
|
|
140
|
-
let reviewerConfig;
|
|
141
|
-
if (reviewerInitialized) {
|
|
142
|
-
const reviewerFallback = configuredIds.has(existingReviewer?.activeAgentId ?? "")
|
|
143
|
-
? existingReviewer.activeAgentId
|
|
144
|
-
: defaultAgentId;
|
|
145
|
-
reviewerAgentId = parseSingleAgentSelection(await question(`Choose Reviewer Agent [${reviewerFallback}]: `), prepared, reviewerFallback);
|
|
146
|
-
const reviewerAgent = prepared.find(({ id }) => id === reviewerAgentId);
|
|
147
|
-
if (reviewerAgent === undefined) {
|
|
148
|
-
throw usageError("Selected Reviewer Agent is no longer available.");
|
|
149
|
-
}
|
|
150
|
-
reviewerConfig = await promptRoleAgentConfig("Reviewer", reviewerAgent, existingReviewer, home, selectionIo, catalogs, true);
|
|
151
|
-
}
|
|
152
|
-
const defaultAgent = prepared.find(({ id }) => id === defaultAgentId);
|
|
153
|
-
const operatorAgent = prepared.find(({ id }) => id === operatorAgentId);
|
|
154
|
-
if (defaultAgent === undefined || operatorAgent === undefined) {
|
|
155
|
-
throw usageError("Selected setup Agent is no longer available.");
|
|
156
|
-
}
|
|
157
|
-
const leaderConfig = await promptRoleAgentConfig("Leader", defaultAgent, store.getGlobalRole(SYSTEM_LEADER_ROLE), home, selectionIo, catalogs);
|
|
158
|
-
const operatorConfig = await promptRoleAgentConfig("Operator", operatorAgent, store.getGlobalRole(SYSTEM_OPERATOR_ROLE), home, selectionIo, catalogs);
|
|
159
|
-
const existingWorker = store.getGlobalRole(SYSTEM_WORKER_ROLE);
|
|
160
|
-
const workerModeFallback = workerConfigurationModeFallback(existingWorker, defaultAgentId, leaderConfig);
|
|
161
|
-
io.output?.write("\n\nWorker is the default Agent configuration copied into Task Roles such as "
|
|
162
|
-
+ "investigator and implementer. Each Task Role gets its own Session.\n");
|
|
163
|
-
const workerReusesLeader = await selectWorkerConfigurationMode(question, io, workerModeFallback) === "reuse-leader";
|
|
164
|
-
let workerAgentId = defaultAgentId;
|
|
165
|
-
let workerConfig = structuredClone(leaderConfig);
|
|
166
|
-
if (!workerReusesLeader) {
|
|
167
|
-
io.output?.write("\n\nConfigure Worker separately:\n");
|
|
168
|
-
const existingWorkerAgent = existingWorker?.activeAgentId;
|
|
169
|
-
const workerFallback = configuredIds.has(existingWorkerAgent ?? "")
|
|
170
|
-
? existingWorkerAgent
|
|
171
|
-
: defaultAgentId;
|
|
172
|
-
workerAgentId = parseSingleAgentSelection(await question(`Choose Worker Agent [${workerFallback}]: `), prepared, workerFallback);
|
|
173
|
-
const workerAgent = prepared.find(({ id }) => id === workerAgentId);
|
|
174
|
-
if (workerAgent === undefined) {
|
|
175
|
-
throw usageError("Selected Worker Agent is no longer available.");
|
|
176
|
-
}
|
|
177
|
-
workerConfig = await promptRoleAgentConfig("Worker", workerAgent, existingWorker, home, selectionIo, catalogs, true);
|
|
178
|
-
}
|
|
179
|
-
const suggestedWorkspace = config.defaultWorkspace?.trim()
|
|
180
|
-
|| join(dirname(resolve(home)), "workspace");
|
|
181
|
-
io.output?.write("\n\n");
|
|
182
|
-
const workspaceAnswer = (await question(`Project workspace for stable checkouts and managed worktrees [${suggestedWorkspace}]: `)).trim();
|
|
183
|
-
const workspace = resolveWorkspace(workspaceAnswer || suggestedWorkspace, home);
|
|
184
|
-
if (config.defaultWorkspace !== undefined
|
|
185
|
-
&& resolve(config.defaultWorkspace) !== workspace) {
|
|
186
|
-
throw usageError(`Project workspace is fixed after setup: ${resolve(config.defaultWorkspace)}.`);
|
|
187
|
-
}
|
|
188
|
-
store.transaction((tx) => {
|
|
189
|
-
for (const agent of prepared)
|
|
190
|
-
tx.saveConfiguredAgent(agent);
|
|
191
|
-
const latestDefaultAgent = requireSetupAgent(tx, defaultAgentId);
|
|
192
|
-
const latestOperatorAgent = requireSetupAgent(tx, operatorAgentId);
|
|
193
|
-
const latestWorkerAgent = requireSetupAgent(tx, workerAgentId);
|
|
194
|
-
const operatorRole = prepareSystemRole(tx, SYSTEM_OPERATOR_ROLE, latestOperatorAgent, workspace, now, operatorConfig);
|
|
195
|
-
const leaderRole = prepareSystemRole(tx, SYSTEM_LEADER_ROLE, latestDefaultAgent, workspace, now, leaderConfig);
|
|
196
|
-
const workerRole = prepareSystemRole(tx, SYSTEM_WORKER_ROLE, latestWorkerAgent, workspace, now, workerConfig);
|
|
197
|
-
const reviewerRole = reviewerInitialized
|
|
198
|
-
&& reviewerAgentId !== undefined
|
|
199
|
-
&& reviewerConfig !== undefined
|
|
200
|
-
? prepareSystemRole(tx, DEFAULT_REVIEWER_ROLE, requireSetupAgent(tx, reviewerAgentId), workspace, now, reviewerConfig, freshHome ? reviewerRoleProfile() : undefined)
|
|
201
|
-
: null;
|
|
202
|
-
const latest = tx.getConfig();
|
|
203
|
-
tx.saveConfig({
|
|
204
|
-
...latest,
|
|
205
|
-
defaultAgent: defaultAgentId,
|
|
206
|
-
defaultWorkspace: workspace,
|
|
207
|
-
timeZone: resolveTimeZone(latest.timeZone),
|
|
208
|
-
...(freshHome
|
|
209
|
-
? { review: { roleName: DEFAULT_REVIEWER_ROLE, trigger: "final" } }
|
|
210
|
-
: {})
|
|
75
|
+
function availableAgentChoices(store, env) {
|
|
76
|
+
const choices = new Map();
|
|
77
|
+
const configured = store.listConfiguredAgents();
|
|
78
|
+
const configuredIds = new Set(configured.map(({ id }) => id));
|
|
79
|
+
for (const agent of configured) {
|
|
80
|
+
if (!commandOnPath(agent.command, env))
|
|
81
|
+
continue;
|
|
82
|
+
choices.set(agent.id, {
|
|
83
|
+
id: agent.id,
|
|
84
|
+
adapterId: agent.adapterId,
|
|
85
|
+
command: agent.command,
|
|
86
|
+
description: "Existing Yui Agent",
|
|
87
|
+
existing: agent
|
|
211
88
|
});
|
|
212
|
-
if (operatorRole !== null)
|
|
213
|
-
savePreparedSystemRole(tx, operatorRole, now);
|
|
214
|
-
if (leaderRole !== null)
|
|
215
|
-
savePreparedSystemRole(tx, leaderRole, now);
|
|
216
|
-
if (workerRole !== null)
|
|
217
|
-
savePreparedSystemRole(tx, workerRole, now);
|
|
218
|
-
if (reviewerRole !== null)
|
|
219
|
-
savePreparedSystemRole(tx, reviewerRole, now);
|
|
220
|
-
seedBuiltinProfiles(tx, now);
|
|
221
|
-
});
|
|
222
|
-
return {
|
|
223
|
-
agentIds: prepared.map(({ id }) => id),
|
|
224
|
-
defaultAgentId,
|
|
225
|
-
operatorAgentId,
|
|
226
|
-
leaderConfig,
|
|
227
|
-
operatorConfig,
|
|
228
|
-
workerAgentId,
|
|
229
|
-
workerConfig,
|
|
230
|
-
workerReusesLeader,
|
|
231
|
-
reviewerInitialized,
|
|
232
|
-
...(reviewerAgentId === undefined ? {} : { reviewerAgentId }),
|
|
233
|
-
...(reviewerConfig === undefined ? {} : { reviewerConfig }),
|
|
234
|
-
...(freshHome
|
|
235
|
-
? { reviewPolicy: { roleName: DEFAULT_REVIEWER_ROLE, trigger: "final" } }
|
|
236
|
-
: initialConfig.review === undefined ? {} : { reviewPolicy: initialConfig.review }),
|
|
237
|
-
workspace
|
|
238
|
-
};
|
|
239
|
-
}
|
|
240
|
-
function reviewerRoleProfile() {
|
|
241
|
-
const reviewer = builtinAgentProfileInputs().find(({ id }) => id === "reviewer");
|
|
242
|
-
if (reviewer === undefined)
|
|
243
|
-
return {};
|
|
244
|
-
return {
|
|
245
|
-
...(reviewer.description === undefined ? {} : { description: reviewer.description }),
|
|
246
|
-
...(reviewer.instructions === undefined ? {} : { systemPrompt: reviewer.instructions }),
|
|
247
|
-
...(reviewer.skills === undefined ? {} : { skills: [...reviewer.skills] })
|
|
248
|
-
};
|
|
249
|
-
}
|
|
250
|
-
async function selectWorkerConfigurationMode(question, io, fallback) {
|
|
251
|
-
const choices = [
|
|
252
|
-
[
|
|
253
|
-
"1",
|
|
254
|
-
`Reuse Leader${fallback === "reuse-leader" ? " (default)" : ""}`,
|
|
255
|
-
"Copy the complete Leader Agent launch configuration; skip Worker-specific prompts"
|
|
256
|
-
],
|
|
257
|
-
[
|
|
258
|
-
"2",
|
|
259
|
-
`Configure separately${fallback === "configure-separately" ? " (default)" : ""}`,
|
|
260
|
-
"Choose the Worker Agent, model, effort, and permission independently"
|
|
261
|
-
]
|
|
262
|
-
];
|
|
263
|
-
const fallbackNumber = fallback === "reuse-leader" ? "1" : "2";
|
|
264
|
-
io.output?.write(`\n${renderTable("Choose Worker configuration", [
|
|
265
|
-
{ header: "#", minWidth: 1, maxWidth: 4 },
|
|
266
|
-
{ header: "Choice", minWidth: 16, maxWidth: 28 },
|
|
267
|
-
{ header: "Consequence", minWidth: 24, maxWidth: 64 }
|
|
268
|
-
], choices, tableWidth(io))}\n\n`);
|
|
269
|
-
return parseWorkerConfigurationMode(await question(`Choose Worker configuration [1-2; default ${fallbackNumber}]: `), fallback);
|
|
270
|
-
}
|
|
271
|
-
function workerConfigurationModeFallback(existing, leaderAgentId, leaderConfig) {
|
|
272
|
-
if (existing === null)
|
|
273
|
-
return "reuse-leader";
|
|
274
|
-
return existing.activeAgentId === leaderAgentId
|
|
275
|
-
&& isDeepStrictEqual(existing.agentBindings[leaderAgentId]?.config, leaderConfig)
|
|
276
|
-
? "reuse-leader"
|
|
277
|
-
: "configure-separately";
|
|
278
|
-
}
|
|
279
|
-
function parseWorkerConfigurationMode(answer, fallback) {
|
|
280
|
-
const value = answer.trim().toLowerCase();
|
|
281
|
-
if (value.length === 0)
|
|
282
|
-
return fallback;
|
|
283
|
-
if (["1", "reuse", "reuse leader", "leader"].includes(value))
|
|
284
|
-
return "reuse-leader";
|
|
285
|
-
if (["2", "configure", "configure separately", "separate"].includes(value)) {
|
|
286
|
-
return "configure-separately";
|
|
287
|
-
}
|
|
288
|
-
throw usageError("Choose reuse Leader or configure separately for Worker.");
|
|
289
|
-
}
|
|
290
|
-
function savePreparedSystemRole(store, role, now) {
|
|
291
|
-
const sessions = store.getGlobalRoleSessionSet(role.name);
|
|
292
|
-
const activeSession = sessions?.sessions[sessions.activeAgentId];
|
|
293
|
-
if (sessions === null
|
|
294
|
-
|| sessions.activeAgentId === role.activeAgentId
|
|
295
|
-
|| (activeSession !== undefined && activeSession.status !== "stopped")) {
|
|
296
|
-
store.saveGlobalRole(role);
|
|
297
|
-
return;
|
|
298
89
|
}
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
90
|
+
for (const builtin of BUILTIN_AGENTS) {
|
|
91
|
+
if (configuredIds.has(builtin.id) || !commandOnPath(builtin.command, env))
|
|
92
|
+
continue;
|
|
93
|
+
choices.set(builtin.id, builtin);
|
|
94
|
+
}
|
|
95
|
+
return [...choices.values()].sort((left, right) => left.id.localeCompare(right.id));
|
|
96
|
+
}
|
|
97
|
+
async function selectOperatorAgent(choices, currentOperator, defaultAgent, question, io) {
|
|
98
|
+
const current = choices.find(({ id }) => id === currentOperator?.activeAgentId);
|
|
99
|
+
if (current !== undefined)
|
|
100
|
+
return current;
|
|
101
|
+
const configuredDefault = choices.find(({ id }) => id === defaultAgent);
|
|
102
|
+
if (configuredDefault !== undefined)
|
|
103
|
+
return configuredDefault;
|
|
104
|
+
if (choices.length === 1)
|
|
105
|
+
return choices[0];
|
|
106
|
+
io.output?.write([
|
|
107
|
+
"Available Operator Agents:",
|
|
108
|
+
...choices.map((choice, index) => ` ${index + 1}. ${choice.id} (${choice.adapterId}) - ${choice.description}`),
|
|
109
|
+
""
|
|
110
|
+
].join("\n"));
|
|
111
|
+
const answer = (await question(`Choose Operator Agent [1: ${choices[0].id}]: `)).trim();
|
|
112
|
+
if (answer.length === 0)
|
|
113
|
+
return choices[0];
|
|
114
|
+
const numeric = Number(answer);
|
|
115
|
+
const selected = Number.isSafeInteger(numeric) && numeric >= 1 && numeric <= choices.length
|
|
116
|
+
? choices[numeric - 1]
|
|
117
|
+
: choices.find(({ id }) => id.toLowerCase() === answer.toLowerCase());
|
|
118
|
+
if (selected === undefined) {
|
|
119
|
+
throw usageError("Choose one available Agent by number or name.");
|
|
310
120
|
}
|
|
121
|
+
return selected;
|
|
311
122
|
}
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
if (selection.effort === undefined)
|
|
335
|
-
delete candidate.effort;
|
|
336
|
-
else
|
|
337
|
-
candidate.effort = selection.effort;
|
|
338
|
-
if (selectPermission) {
|
|
339
|
-
const permission = await selectAgentPermission(resolved, io, candidate.permission);
|
|
340
|
-
if (permission.kind === "cancelled") {
|
|
341
|
-
throw usageError(`${label} permission configuration was cancelled.`);
|
|
123
|
+
function saveMinimumConfiguration(store, agent, workspace, usableAgentIds) {
|
|
124
|
+
return store.transaction((tx) => {
|
|
125
|
+
tx.saveConfiguredAgent(agent);
|
|
126
|
+
const config = tx.getConfig();
|
|
127
|
+
tx.saveConfig({
|
|
128
|
+
...config,
|
|
129
|
+
defaultAgent: config.defaultAgent ?? agent.id,
|
|
130
|
+
defaultWorkspace: config.defaultWorkspace ?? workspace
|
|
131
|
+
});
|
|
132
|
+
const now = new Date();
|
|
133
|
+
const leader = tx.getGlobalRole(SYSTEM_LEADER_ROLE);
|
|
134
|
+
let leaderStatus = "preserved";
|
|
135
|
+
let leaderAgentId = leader?.activeAgentId;
|
|
136
|
+
if (leader === null) {
|
|
137
|
+
assertRoleRuntimeMutationAllowed(tx, {
|
|
138
|
+
scope: "global",
|
|
139
|
+
roleName: SYSTEM_LEADER_ROLE
|
|
140
|
+
}, "creation");
|
|
141
|
+
const definition = configuredAgentToDefinition(agent);
|
|
142
|
+
tx.saveGlobalRole(createGlobalRole(SYSTEM_LEADER_ROLE, [createRoleAgentBinding(definition)], agent.id, workspace, now));
|
|
143
|
+
leaderStatus = "created";
|
|
144
|
+
leaderAgentId = agent.id;
|
|
342
145
|
}
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
try {
|
|
346
|
-
return resolveAgentAdapter(agent.adapterId).canonicalizeConfig(candidate);
|
|
347
|
-
}
|
|
348
|
-
catch (error) {
|
|
349
|
-
throw usageError(error instanceof Error ? error.message : String(error));
|
|
350
|
-
}
|
|
351
|
-
}
|
|
352
|
-
function availableAgentChoices(store, env) {
|
|
353
|
-
const existing = new Map(store.listConfiguredAgents().map((agent) => [agent.id, agent]));
|
|
354
|
-
return BUILTIN_AGENTS.flatMap((builtin) => {
|
|
355
|
-
const configured = existing.get(builtin.id);
|
|
356
|
-
const command = configured?.command ?? builtin.command;
|
|
357
|
-
if (!commandOnPath(command, env))
|
|
358
|
-
return [];
|
|
359
|
-
return [{
|
|
360
|
-
...builtin,
|
|
361
|
-
adapterId: configured?.adapterId ?? builtin.adapterId,
|
|
362
|
-
command
|
|
363
|
-
}];
|
|
364
|
-
});
|
|
365
|
-
}
|
|
366
|
-
function setupSelectionIo(question, io) {
|
|
367
|
-
return {
|
|
368
|
-
interactive: true,
|
|
369
|
-
json: false,
|
|
370
|
-
width: tableWidth(io),
|
|
371
|
-
write: (value) => { io.output.write(value); },
|
|
372
|
-
question
|
|
373
|
-
};
|
|
374
|
-
}
|
|
375
|
-
function prepareAgent(store, choice, now) {
|
|
376
|
-
const existing = store.getConfiguredAgent(choice.id);
|
|
377
|
-
if (existing !== null
|
|
378
|
-
&& existing.adapterId === choice.adapterId
|
|
379
|
-
&& existing.command === choice.command) {
|
|
380
|
-
return existing;
|
|
381
|
-
}
|
|
382
|
-
const agent = createConfiguredAgent(choice.id, choice.adapterId, choice.command, existing?.baseArgs ?? [], existing?.environment ?? [], now);
|
|
383
|
-
return agent;
|
|
384
|
-
}
|
|
385
|
-
function requireSetupAgent(store, agentId) {
|
|
386
|
-
const agent = store.getConfiguredAgent(agentId);
|
|
387
|
-
if (agent === null)
|
|
388
|
-
throw usageError(`Configured Agent not found: ${agentId}.`);
|
|
389
|
-
return agent;
|
|
390
|
-
}
|
|
391
|
-
function prepareSystemRole(store, name, agent, workspace, now, config, profile) {
|
|
392
|
-
const existing = store.getGlobalRole(name);
|
|
393
|
-
if (existing !== null) {
|
|
394
|
-
const definition = configuredAgentToDefinition(agent);
|
|
395
|
-
const binding = createRoleAgentBinding(definition, config);
|
|
396
|
-
if (name === "operator" && !Object.hasOwn(existing.agentBindings, agent.id)) {
|
|
397
|
-
const sameAdapter = Object.values(existing.agentBindings).find((candidate) => candidate.adapterId === agent.adapterId);
|
|
398
|
-
if (sameAdapter !== undefined) {
|
|
399
|
-
throw usageError(`${name} already has a ${agent.adapterId} Agent: ${sameAdapter.agentId}. `
|
|
400
|
-
+ "Update that Agent's configuration, or activate another adapter and "
|
|
401
|
-
+ "unbind it before selecting this Agent.");
|
|
402
|
-
}
|
|
146
|
+
if (leaderAgentId === undefined) {
|
|
147
|
+
throw usageError("Leader has no active Agent. Repair it with yui config role update leader, then run yui setup again.");
|
|
403
148
|
}
|
|
404
|
-
if (
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
149
|
+
if (!usableAgentIds.has(leaderAgentId)) {
|
|
150
|
+
throw usageError(`Leader Agent ${leaderAgentId} is unavailable. Repair it with yui config agent update `
|
|
151
|
+
+ "or yui config role update leader, then run yui setup again.");
|
|
152
|
+
}
|
|
153
|
+
const operator = tx.getGlobalRole(SYSTEM_OPERATOR_ROLE);
|
|
154
|
+
if (operator !== null && operator.activeAgentId === agent.id) {
|
|
155
|
+
return {
|
|
156
|
+
operator: "preserved",
|
|
157
|
+
leader: leaderStatus,
|
|
158
|
+
leaderAgentId
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
const definition = configuredAgentToDefinition(agent);
|
|
162
|
+
const binding = createRoleAgentBinding(definition);
|
|
163
|
+
if (operator === null) {
|
|
164
|
+
assertRoleRuntimeMutationAllowed(tx, {
|
|
165
|
+
scope: "global",
|
|
166
|
+
roleName: SYSTEM_OPERATOR_ROLE
|
|
167
|
+
}, "creation");
|
|
168
|
+
tx.saveGlobalRole(createGlobalRole(SYSTEM_OPERATOR_ROLE, [binding], agent.id, workspace, now));
|
|
169
|
+
return {
|
|
170
|
+
operator: "created",
|
|
171
|
+
leader: leaderStatus,
|
|
172
|
+
leaderAgentId
|
|
173
|
+
};
|
|
408
174
|
}
|
|
409
|
-
assertRoleRuntimeMutationAllowed(
|
|
175
|
+
assertRoleRuntimeMutationAllowed(tx, {
|
|
410
176
|
scope: "global",
|
|
411
|
-
roleName:
|
|
412
|
-
}, "desired
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
...(profile === undefined ? {} : profile)
|
|
418
|
-
}, now);
|
|
419
|
-
}
|
|
420
|
-
assertRoleRuntimeMutationAllowed(store, {
|
|
421
|
-
scope: "global",
|
|
422
|
-
roleName: name
|
|
423
|
-
}, "creation");
|
|
424
|
-
const definition = configuredAgentToDefinition(agent);
|
|
425
|
-
return createGlobalRole(name, [createRoleAgentBinding(definition, config)], definition.id, workspace, now, profile);
|
|
426
|
-
}
|
|
427
|
-
function parseAgentSetSelection(answer, candidates) {
|
|
428
|
-
const value = answer.trim().toLowerCase();
|
|
429
|
-
if (value.length === 0 || value === "all")
|
|
430
|
-
return [...candidates];
|
|
431
|
-
const tokens = value.split(",").map((token) => token.trim()).filter(Boolean);
|
|
432
|
-
if (tokens.length === 0)
|
|
433
|
-
throw usageError("Select at least one Agent.");
|
|
434
|
-
const selected = [];
|
|
435
|
-
for (const token of tokens) {
|
|
436
|
-
const numeric = Number(token);
|
|
437
|
-
const choice = Number.isSafeInteger(numeric) && numeric >= 1 && numeric <= candidates.length
|
|
438
|
-
? candidates[numeric - 1]
|
|
439
|
-
: candidates.find(({ id }) => id.toLowerCase() === token);
|
|
440
|
-
if (choice === undefined) {
|
|
441
|
-
throw usageError("Choose available Agents by number or name, separated by commas.");
|
|
177
|
+
roleName: SYSTEM_OPERATOR_ROLE
|
|
178
|
+
}, "desired Agent binding update");
|
|
179
|
+
const duplicateAdapter = Object.values(operator.agentBindings).find((candidate) => candidate.adapterId === agent.adapterId && candidate.agentId !== agent.id);
|
|
180
|
+
if (duplicateAdapter !== undefined) {
|
|
181
|
+
throw usageError(`Operator already has a ${agent.adapterId} Agent: ${duplicateAdapter.agentId}. `
|
|
182
|
+
+ "Use yui config agent update or yui config role bind to repair the Operator configuration.");
|
|
442
183
|
}
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
const selected = Number.isSafeInteger(numeric) && numeric >= 1 && numeric <= agents.length
|
|
454
|
-
? agents[numeric - 1]
|
|
455
|
-
: agents.find(({ id }) => id.toLowerCase() === value);
|
|
456
|
-
if (selected === undefined) {
|
|
457
|
-
throw usageError("Choose one of the configured Agents by number or name.");
|
|
458
|
-
}
|
|
459
|
-
return selected.id;
|
|
184
|
+
tx.saveGlobalRole(updateGlobalRole(operator, {
|
|
185
|
+
activeAgentId: agent.id,
|
|
186
|
+
agentBindings: { ...operator.agentBindings, [agent.id]: binding }
|
|
187
|
+
}, now));
|
|
188
|
+
return {
|
|
189
|
+
operator: "updated",
|
|
190
|
+
leader: leaderStatus,
|
|
191
|
+
leaderAgentId
|
|
192
|
+
};
|
|
193
|
+
});
|
|
460
194
|
}
|
|
461
195
|
function resolveWorkspace(value, home) {
|
|
462
196
|
if (!isAbsolute(value))
|
|
463
|
-
throw usageError("
|
|
197
|
+
throw usageError("Default workspace must be an absolute path.");
|
|
464
198
|
const requested = resolve(value);
|
|
465
199
|
assertWorkspaceOutsideHome(requested, resolve(home));
|
|
466
200
|
mkdirSync(requested, { recursive: true, mode: 0o700 });
|
|
@@ -472,7 +206,7 @@ function resolveWorkspace(value, home) {
|
|
|
472
206
|
function assertWorkspaceOutsideHome(workspace, homeRoot) {
|
|
473
207
|
const fromHome = relative(homeRoot, workspace);
|
|
474
208
|
if (fromHome === "" || (!fromHome.startsWith("..") && !isAbsolute(fromHome))) {
|
|
475
|
-
throw usageError("
|
|
209
|
+
throw usageError("Default workspace must be outside YUI_HOME.");
|
|
476
210
|
}
|
|
477
211
|
}
|
|
478
212
|
function commandOnPath(command, env) {
|
|
@@ -484,6 +218,15 @@ function commandOnPath(command, env) {
|
|
|
484
218
|
: [""];
|
|
485
219
|
return entries.some((entry) => extensions.some((extension) => existsSync(join(entry, `${command}${extension}`))));
|
|
486
220
|
}
|
|
221
|
+
function assertTmuxAvailable(executor, tmuxBin) {
|
|
222
|
+
try {
|
|
223
|
+
executor.run(tmuxBin, ["-V"], { timeoutMs: 3_000 });
|
|
224
|
+
}
|
|
225
|
+
catch {
|
|
226
|
+
throw usageError(`Tmux is required to run the Operator, but ${tmuxBin} is unavailable. `
|
|
227
|
+
+ "Install tmux or set yui config tools set tmux-bin, then run yui setup again.");
|
|
228
|
+
}
|
|
229
|
+
}
|
|
487
230
|
function createSetupQuestion(readline, io) {
|
|
488
231
|
if (io.input.isTTY === true)
|
|
489
232
|
return (prompt) => readline.question(prompt);
|
|
@@ -502,89 +245,7 @@ function shouldPrompt(io) {
|
|
|
502
245
|
function setupRequiresInteractiveError() {
|
|
503
246
|
return usageError("Setup requires an interactive terminal.");
|
|
504
247
|
}
|
|
505
|
-
function tableWidth(io) {
|
|
506
|
-
return io.output?.columns === undefined
|
|
507
|
-
? defaultTableWidth()
|
|
508
|
-
: Math.max(46, Math.min(io.output.columns, 140));
|
|
509
|
-
}
|
|
510
|
-
function cliIdentity(env) {
|
|
511
|
-
return env.YUI_CLI_NAME === "yui-dev" ? "yui-dev" : "yui";
|
|
512
|
-
}
|
|
513
248
|
function parseSetupOptions(args) {
|
|
514
|
-
if (args.length
|
|
515
|
-
|
|
516
|
-
if (args.length === 1 && args[0] === "tmux")
|
|
517
|
-
return "tmux";
|
|
518
|
-
throw usageError("Setup usage: yui setup [tmux]");
|
|
519
|
-
}
|
|
520
|
-
async function setupTmux(env, executor, question) {
|
|
521
|
-
const command = "tmux";
|
|
522
|
-
if (hasExecutable(command, ["-V"], executor))
|
|
523
|
-
return ["Tmux already installed."];
|
|
524
|
-
const plan = detectTmuxInstallPlan(env, executor);
|
|
525
|
-
if (plan === null) {
|
|
526
|
-
return ["Tmux is not installed.", "Install tmux manually, then run yui doctor."];
|
|
527
|
-
}
|
|
528
|
-
const lines = [
|
|
529
|
-
"Tmux is not installed.",
|
|
530
|
-
`Install with ${plan.manager}:`,
|
|
531
|
-
...plan.steps.map((step) => ` ${step.command} ${step.args.join(" ")}`)
|
|
532
|
-
];
|
|
533
|
-
const answer = (await question("Install tmux now? [y/N]: ")).trim().toLowerCase();
|
|
534
|
-
if (answer !== "y" && answer !== "yes") {
|
|
535
|
-
return [...lines, "Skipped tmux installation.", "After installing tmux, run yui doctor."];
|
|
536
|
-
}
|
|
537
|
-
for (const step of plan.steps)
|
|
538
|
-
executor.run(step.command, [...step.args], { inheritStdio: true });
|
|
539
|
-
return hasExecutable(command, ["-V"], executor)
|
|
540
|
-
? [...lines, "Tmux installed."]
|
|
541
|
-
: [...lines, `Tmux install command completed, but ${command} is still unavailable.`, plan.manualHint];
|
|
542
|
-
}
|
|
543
|
-
function detectTmuxInstallPlan(env, executor) {
|
|
544
|
-
if (process.platform === "darwin" && commandExists("brew", env, executor)) {
|
|
545
|
-
return {
|
|
546
|
-
manager: "Homebrew",
|
|
547
|
-
steps: [{ command: "brew", args: ["install", "tmux"] }],
|
|
548
|
-
manualHint: "brew install tmux"
|
|
549
|
-
};
|
|
550
|
-
}
|
|
551
|
-
if (process.platform !== "linux")
|
|
552
|
-
return null;
|
|
553
|
-
for (const candidate of [
|
|
554
|
-
{ command: "apt-get", args: ["install", "-y", "tmux"], hint: "sudo apt-get install -y tmux" },
|
|
555
|
-
{ command: "dnf", args: ["install", "-y", "tmux"], hint: "sudo dnf install -y tmux" },
|
|
556
|
-
{ command: "pacman", args: ["-S", "--noconfirm", "tmux"], hint: "sudo pacman -S --noconfirm tmux" },
|
|
557
|
-
{ command: "apk", args: ["add", "tmux"], hint: "sudo apk add tmux" }
|
|
558
|
-
]) {
|
|
559
|
-
if (!commandExists(candidate.command, env, executor))
|
|
560
|
-
continue;
|
|
561
|
-
const step = withLinuxPrivilege(candidate.command, candidate.args, env, executor);
|
|
562
|
-
if (step !== null) {
|
|
563
|
-
return { manager: candidate.command, steps: [step], manualHint: candidate.hint };
|
|
564
|
-
}
|
|
565
|
-
}
|
|
566
|
-
return null;
|
|
567
|
-
}
|
|
568
|
-
function hasExecutable(command, args, executor) {
|
|
569
|
-
try {
|
|
570
|
-
executor.run(command, args);
|
|
571
|
-
return true;
|
|
572
|
-
}
|
|
573
|
-
catch {
|
|
574
|
-
return false;
|
|
575
|
-
}
|
|
576
|
-
}
|
|
577
|
-
function commandExists(command, env, executor) {
|
|
578
|
-
if (command.includes("/"))
|
|
579
|
-
return existsSync(command);
|
|
580
|
-
return (env.PATH ?? "").split(delimiter).filter(Boolean)
|
|
581
|
-
.some((entry) => existsSync(join(entry, command)))
|
|
582
|
-
|| hasExecutable(command, ["--version"], executor);
|
|
583
|
-
}
|
|
584
|
-
function withLinuxPrivilege(command, args, env, executor) {
|
|
585
|
-
if (process.getuid?.() === 0)
|
|
586
|
-
return { command, args };
|
|
587
|
-
if (!commandExists("sudo", env, executor))
|
|
588
|
-
return null;
|
|
589
|
-
return { command: "sudo", args: [command, ...args] };
|
|
249
|
+
if (args.length !== 0)
|
|
250
|
+
throw usageError("Setup usage: yui setup");
|
|
590
251
|
}
|