@kisev/agentomatic 11.0.2 → 12.0.0-dev.113.g1bd7385234c6
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 +32 -16
- package/README.ru.md +33 -16
- package/dist/agent-profiles.d.ts +8 -2
- package/dist/agent-profiles.js +131 -82
- package/dist/assets/agents/architect.md +4 -4
- package/dist/assets/agents/critic.md +10 -11
- package/dist/assets/agents/manager.md +11 -12
- package/dist/assets/agents/mapper.md +4 -4
- package/dist/assets/agents/review.md +11 -12
- package/dist/assets/agents/worker.md +29 -32
- package/dist/assets/commands/asd-ste100.md +10 -0
- package/dist/assets/commands/code-simplify.md +12 -0
- package/dist/assets/commands/eli5.md +10 -0
- package/dist/assets/commands/handoff.md +10 -0
- package/dist/assets/commands/rtk-stats.md +2 -2
- package/dist/assets/commands/skill-doctor.md +10 -0
- package/dist/assets/migration-inventory.json +17 -6
- package/dist/assets/plugins/code-simplify.js +3 -0
- package/dist/assets/plugins/rtk.js +1 -1
- package/dist/assets/plugins/rules-injector.js +1 -1
- package/dist/assets/plugins/zed-bell.js +1 -1
- package/dist/catalog.d.ts +2 -2
- package/dist/catalog.js +9 -3
- package/dist/cli-output.js +6 -6
- package/dist/cli.d.ts +1 -1
- package/dist/cli.js +1 -1257
- package/dist/command-cli.d.ts +1 -0
- package/dist/command-cli.js +1004 -0
- package/dist/config-setup.d.ts +14 -8
- package/dist/config-setup.js +197 -64
- package/dist/contracts.js +26 -0
- package/dist/doctor.d.ts +0 -1
- package/dist/doctor.js +42 -55
- package/dist/index.d.ts +13 -118
- package/dist/index.js +118 -125
- package/dist/installer.d.ts +25 -2
- package/dist/installer.js +68 -23
- package/dist/jsonc.d.ts +18 -2
- package/dist/jsonc.js +109 -5
- package/dist/opencode-config.d.ts +11 -0
- package/dist/opencode-config.js +237 -0
- package/dist/plugins/code-simplify.d.ts +25 -0
- package/dist/plugins/code-simplify.js +49 -0
- package/dist/plugins/events.d.ts +20 -0
- package/dist/plugins/events.js +48 -0
- package/dist/plugins/rtk.d.ts +9 -12
- package/dist/plugins/rtk.js +20 -5
- package/dist/plugins/rules-injector.d.ts +21 -11
- package/dist/plugins/rules-injector.js +43 -31
- package/dist/plugins/zed-bell.d.ts +8 -5
- package/dist/plugins/zed-bell.js +10 -2
- package/dist/reconcile.d.ts +4 -1
- package/dist/reconcile.js +12 -16
- package/dist/registry.d.ts +1 -0
- package/dist/registry.js +21 -5
- package/dist/routing.d.ts +5 -3
- package/dist/routing.js +57 -22
- package/dist/self-install.d.ts +7 -0
- package/dist/self-install.js +52 -1
- package/dist/terminal-wizard.d.ts +1 -1
- package/dist/terminal-wizard.js +2 -2
- package/package.json +12 -7
- package/dist/assets/commands/skill-improve.md +0 -10
- package/dist/assets/commands/stopit.md +0 -10
|
@@ -0,0 +1,1004 @@
|
|
|
1
|
+
import { Command, Option, common, applyConfig, configFileSync, reporter } from "./generated/cli.js";
|
|
2
|
+
import { applyAgentProfileChange, previewAgentProfileChange, listAgentProfiles, availableModels, availableModelVariants, validateAgentName, validateModel, validateVariant, FIXED_AGENT_ROLES, } from "./agent-profiles.js";
|
|
3
|
+
import { apply, preview, installedSelection, defaultSelection, normalizeSelection, InstallerError, SKILL_COMMANDS, SELECTABLE_PLUGINS, } from "./installer.js";
|
|
4
|
+
import { applyConfigSetup, previewConfigSetup, recoverConfigSetup, normalizeConfigSelection, defaultConfigSelection, CONFIG_TARGETS, CONFIG_FRAGMENTS, inspectIntegration, } from "./config-setup.js";
|
|
5
|
+
import { collectDoctorFacts, doctorExitCode } from "./doctor.js";
|
|
6
|
+
import { applyReconcile, previewReconcile } from "./reconcile.js";
|
|
7
|
+
import { CATALOG } from "./catalog.js";
|
|
8
|
+
import { skillsInstallerSpec } from "./package-metadata.js";
|
|
9
|
+
import { LifecycleError } from "./lifecycle.js";
|
|
10
|
+
import { previewDependencyRemoval, removeDependency, planDependency } from "./self-install.js";
|
|
11
|
+
import { selectOption, selectOptions, promptText, confirmQuestion } from "./terminal-wizard.js";
|
|
12
|
+
import { renderPlan, renderInventory, renderDoctor, renderConfigSetup, renderReconcile, terminalSafe, shellCommand, scopeArguments, } from "./cli-output.js";
|
|
13
|
+
let diagnostics;
|
|
14
|
+
let machineOutput = false;
|
|
15
|
+
let diagnosticJSON = false;
|
|
16
|
+
const commands = {
|
|
17
|
+
install: {
|
|
18
|
+
description: "Install components, connect integration, and optionally configure models and critics.",
|
|
19
|
+
options: ["commands", "agents", "plugins", "core", "dependency", "targets", "fragments"],
|
|
20
|
+
},
|
|
21
|
+
configure: {
|
|
22
|
+
description: "Choose components, agent models, critics, or application integration.",
|
|
23
|
+
options: [],
|
|
24
|
+
},
|
|
25
|
+
"configure components": {
|
|
26
|
+
description: "Change the installed component set without resetting models.",
|
|
27
|
+
options: ["commands", "agents", "plugins", "core", "dependency", "targets", "fragments"],
|
|
28
|
+
},
|
|
29
|
+
"configure agent": {
|
|
30
|
+
description: "Choose a model and variant, or supply --model provider/model directly.",
|
|
31
|
+
options: ["provider", "model", "variant", "clearVariant"],
|
|
32
|
+
name: true,
|
|
33
|
+
},
|
|
34
|
+
"configure critics": {
|
|
35
|
+
description: "Add, configure, or remove additional critics interactively.",
|
|
36
|
+
options: [],
|
|
37
|
+
},
|
|
38
|
+
"configure integration": {
|
|
39
|
+
description: "Connect or disconnect the plugin and merge application presets.",
|
|
40
|
+
options: ["targets", "fragments", "dependency"],
|
|
41
|
+
},
|
|
42
|
+
status: {
|
|
43
|
+
description: "Show installed components, connection, dependency, models, and critic pool without changes.",
|
|
44
|
+
options: [],
|
|
45
|
+
},
|
|
46
|
+
doctor: {
|
|
47
|
+
description: "Diagnose versions, ownership, drift, configuration, tools, and LSP without repair.",
|
|
48
|
+
options: [],
|
|
49
|
+
},
|
|
50
|
+
uninstall: {
|
|
51
|
+
description: "Remove owned components; disconnect the plugin by default; retain models and npm dependency unless selected.",
|
|
52
|
+
options: ["disconnect", "removeDependency"],
|
|
53
|
+
},
|
|
54
|
+
agent: { description: "List agents or manage additional critics.", options: [] },
|
|
55
|
+
"agent list": {
|
|
56
|
+
description: "Show installed and saved profiles, models, ownership, collisions, and drift.",
|
|
57
|
+
options: [],
|
|
58
|
+
},
|
|
59
|
+
"agent add-critic": {
|
|
60
|
+
description: "Add critic-<name> interactively or with an explicit model.",
|
|
61
|
+
options: ["provider", "model", "variant"],
|
|
62
|
+
name: true,
|
|
63
|
+
},
|
|
64
|
+
"agent remove": {
|
|
65
|
+
description: "Remove an additional critic; fixed roles cannot be removed here.",
|
|
66
|
+
options: [],
|
|
67
|
+
name: true,
|
|
68
|
+
},
|
|
69
|
+
maintenance: {
|
|
70
|
+
description: "Clean up retired files, repair selected components, or recover an interrupted transaction.",
|
|
71
|
+
options: [],
|
|
72
|
+
},
|
|
73
|
+
"maintenance cleanup": {
|
|
74
|
+
description: "Archive exact-owned retired files; preserve modified and user-owned files.",
|
|
75
|
+
options: [],
|
|
76
|
+
},
|
|
77
|
+
"maintenance repair": {
|
|
78
|
+
description: "Restore only the saved selected components; never overwrite modified managed files.",
|
|
79
|
+
options: ["dependency"],
|
|
80
|
+
},
|
|
81
|
+
"maintenance recover": {
|
|
82
|
+
description: "Preview and confirm journal-bound recovery, then request a fresh plan.",
|
|
83
|
+
options: [],
|
|
84
|
+
},
|
|
85
|
+
catalog: {
|
|
86
|
+
description: "Print the running package's capability catalog as JSON, not installed state.",
|
|
87
|
+
options: [],
|
|
88
|
+
},
|
|
89
|
+
};
|
|
90
|
+
const mutationOptions = new Set(["dryRun", "yes"]);
|
|
91
|
+
const observations = new Set(["status", "doctor", "agent list", "catalog"]);
|
|
92
|
+
function fail(code, message) {
|
|
93
|
+
throw new InstallerError(code, message);
|
|
94
|
+
}
|
|
95
|
+
function value(result) {
|
|
96
|
+
return result === null ? fail("cancelled", "Wizard cancelled; no changes applied") : result;
|
|
97
|
+
}
|
|
98
|
+
function tty() {
|
|
99
|
+
return Boolean(process.stdin.isTTY && process.stderr.isTTY);
|
|
100
|
+
}
|
|
101
|
+
function json(value) {
|
|
102
|
+
process.stdout.write(`${JSON.stringify(value, null, 2)}\n`);
|
|
103
|
+
}
|
|
104
|
+
function applyHint(args) {
|
|
105
|
+
return `${shellCommand(args)} (confirm interactively; add --yes outside a terminal)`;
|
|
106
|
+
}
|
|
107
|
+
function help(topic = "") {
|
|
108
|
+
if (topic && !commands[topic])
|
|
109
|
+
fail("invalid_input", `Unknown command: ${topic}`);
|
|
110
|
+
const visible = topic
|
|
111
|
+
? Object.entries(commands).filter(([key]) => key === topic || key.startsWith(`${topic} `))
|
|
112
|
+
: Object.entries(commands).filter(([key]) => !key.includes(" "));
|
|
113
|
+
return [
|
|
114
|
+
`agentomatic ${CATALOG.version}`,
|
|
115
|
+
"",
|
|
116
|
+
topic
|
|
117
|
+
? commands[topic].description
|
|
118
|
+
: "Install, configure, inspect, and remove application integration. Portable skills use the separate skills CLI.",
|
|
119
|
+
...(!topic
|
|
120
|
+
? [
|
|
121
|
+
`Install skills separately: npx --yes ${skillsInstallerSpec()} add https://kisev.github.io/skills --agent opencode --copy`,
|
|
122
|
+
]
|
|
123
|
+
: []),
|
|
124
|
+
"",
|
|
125
|
+
`Usage: agentomatic${topic ? ` ${topic}${commands[topic].name ? " [name]" : ""}` : " <command>"} [options]`,
|
|
126
|
+
"",
|
|
127
|
+
"Commands:",
|
|
128
|
+
...visible.map(([key, entry]) => ` ${key.padEnd(26)} ${entry.description}`),
|
|
129
|
+
"",
|
|
130
|
+
"Options:",
|
|
131
|
+
"Common: --global (otherwise project), --json, --help, --version",
|
|
132
|
+
...(observations.has(topic) || topic === "catalog"
|
|
133
|
+
? []
|
|
134
|
+
: [
|
|
135
|
+
"Mutations: --dry-run previews without writes; --yes confirms a fresh plan without a final prompt.",
|
|
136
|
+
]),
|
|
137
|
+
...(topic && commands[topic].options.length
|
|
138
|
+
? [
|
|
139
|
+
`Options: ${commands[topic].options.map((option) => ({ clearVariant: "--clear-variant", removeDependency: "--remove-dependency", dependency: "--no-dependency", core: "--core / --no-core", disconnect: "--disconnect / --no-disconnect" })[option] ?? `--${option} <value>`).join(", ")}`,
|
|
140
|
+
]
|
|
141
|
+
: []),
|
|
142
|
+
...(!topic || ["install", "configure components"].includes(topic)
|
|
143
|
+
? [
|
|
144
|
+
"Non-TTY component selection: --commands <list|none> --agents <list|none> --plugins <list|none>",
|
|
145
|
+
"Saved selection is reused on repeat runs. --no-core disconnects the plugin; npm removal is an explicit uninstall choice.",
|
|
146
|
+
]
|
|
147
|
+
: []),
|
|
148
|
+
...(topic === "configure integration"
|
|
149
|
+
? [
|
|
150
|
+
"Outside a terminal supply both --targets and --fragments. Lists are comma-separated; none selects an empty list.",
|
|
151
|
+
"Targets: opencode, kilo, mimo (project scope: opencode only).",
|
|
152
|
+
`Fragments: ${CONFIG_FRAGMENTS.map((fragment) => fragment.name).join(", ")}.`,
|
|
153
|
+
]
|
|
154
|
+
: []),
|
|
155
|
+
"Behavior:",
|
|
156
|
+
"Project scope uses .opencode under the current directory; run from the project root. Global scope uses ~/.config/opencode.",
|
|
157
|
+
"Use status to inspect the installation, doctor to diagnose problems, and maintenance for explicit recovery.",
|
|
158
|
+
"",
|
|
159
|
+
"Examples:",
|
|
160
|
+
` ${shellCommand([...(topic ? topic.split(" ") : ["install"]), ...(commands[topic]?.name ? [topic === "agent remove" ? "critic-security" : "critic"] : []), ...(observations.has(topic) || ["agent", "maintenance"].includes(topic) ? [] : ["--dry-run"])])}`,
|
|
161
|
+
"Documentation: https://github.com/kisev/skills/blob/main/docs/how-to/opencode-integration.md",
|
|
162
|
+
"",
|
|
163
|
+
].join("\n");
|
|
164
|
+
}
|
|
165
|
+
function parse(topic, args) {
|
|
166
|
+
const parser = common(new Command()
|
|
167
|
+
.exitOverride()
|
|
168
|
+
.helpOption(false)
|
|
169
|
+
.configureOutput({ writeErr: () => { } }), "AGENTOMATIC");
|
|
170
|
+
if (commands[topic].name)
|
|
171
|
+
parser.argument("[name]");
|
|
172
|
+
const flags = [
|
|
173
|
+
"--global",
|
|
174
|
+
"--dry-run",
|
|
175
|
+
"--yes",
|
|
176
|
+
"--core",
|
|
177
|
+
"--no-core",
|
|
178
|
+
"--no-dependency",
|
|
179
|
+
"--commands <value>",
|
|
180
|
+
"--agents <value>",
|
|
181
|
+
"--plugins <value>",
|
|
182
|
+
"--targets <value>",
|
|
183
|
+
"--fragments <value>",
|
|
184
|
+
"--model <value>",
|
|
185
|
+
"--provider <value>",
|
|
186
|
+
"--variant <value>",
|
|
187
|
+
"--clear-variant",
|
|
188
|
+
"--disconnect",
|
|
189
|
+
"--no-disconnect",
|
|
190
|
+
"--remove-dependency",
|
|
191
|
+
];
|
|
192
|
+
for (const flag of flags) {
|
|
193
|
+
const option = new Option(flag, flag);
|
|
194
|
+
if (flag.includes("<value>") || ["--global", "--dry-run"].includes(flag))
|
|
195
|
+
option.env(`AGENTOMATIC_${option
|
|
196
|
+
.attributeName()
|
|
197
|
+
.replace(/[A-Z]/g, (letter) => `_${letter}`)
|
|
198
|
+
.toUpperCase()}`);
|
|
199
|
+
parser.addOption(option);
|
|
200
|
+
}
|
|
201
|
+
try {
|
|
202
|
+
for (const flag of args.filter((arg) => arg.startsWith("--")))
|
|
203
|
+
if (args.filter((arg) => arg === flag).length > 1)
|
|
204
|
+
throw new Error(`${flag} may be supplied once`);
|
|
205
|
+
for (const [left, right] of [
|
|
206
|
+
["--core", "--no-core"],
|
|
207
|
+
["--disconnect", "--no-disconnect"],
|
|
208
|
+
["--variant", "--clear-variant"],
|
|
209
|
+
])
|
|
210
|
+
if (args.includes(left) && args.includes(right))
|
|
211
|
+
throw new Error(`Choose either ${left} or ${right}`);
|
|
212
|
+
parser.parse(args, { from: "user" });
|
|
213
|
+
applyConfig(parser, configFileSync(parser.opts().config));
|
|
214
|
+
const parsed = parser.opts();
|
|
215
|
+
machineOutput = parsed.json === true;
|
|
216
|
+
diagnosticJSON = parsed.logFormat === "json";
|
|
217
|
+
diagnostics = reporter({ ...parsed, progress: parsed.progress ?? "never" });
|
|
218
|
+
for (const flag of flags) {
|
|
219
|
+
const key = new Option(flag).attributeName();
|
|
220
|
+
const source = parser.getOptionValueSource(key);
|
|
221
|
+
if (source &&
|
|
222
|
+
source !== "default" &&
|
|
223
|
+
![
|
|
224
|
+
"global",
|
|
225
|
+
...commands[topic].options,
|
|
226
|
+
...(!observations.has(topic) ? mutationOptions : []),
|
|
227
|
+
].includes(key))
|
|
228
|
+
throw new Error(`${topic} does not accept ${flag.split(" ")[0]}`);
|
|
229
|
+
}
|
|
230
|
+
if (topic === "catalog" && parsed.global)
|
|
231
|
+
throw new Error("catalog accepts no scope: it describes the running package");
|
|
232
|
+
const list = (input) => input === undefined
|
|
233
|
+
? undefined
|
|
234
|
+
: Array.isArray(input)
|
|
235
|
+
? input.map(String)
|
|
236
|
+
: String(input) === "none"
|
|
237
|
+
? []
|
|
238
|
+
: String(input)
|
|
239
|
+
.split(",")
|
|
240
|
+
.map((item) => item.trim())
|
|
241
|
+
.filter(Boolean);
|
|
242
|
+
const specified = (key) => Boolean(parser.getOptionValueSource(key) && parser.getOptionValueSource(key) !== "default");
|
|
243
|
+
return {
|
|
244
|
+
scope: parsed.global ? "global" : "project",
|
|
245
|
+
dryRun: parsed.dryRun === true,
|
|
246
|
+
yes: parsed.yes === true,
|
|
247
|
+
json: parsed.json === true,
|
|
248
|
+
name: parser.args[0],
|
|
249
|
+
commands: list(parsed.commands),
|
|
250
|
+
agents: list(parsed.agents),
|
|
251
|
+
plugins: list(parsed.plugins),
|
|
252
|
+
targets: list(parsed.targets),
|
|
253
|
+
fragments: list(parsed.fragments),
|
|
254
|
+
core: specified("core") ? parsed.core : undefined,
|
|
255
|
+
noDependency: parsed.dependency === false,
|
|
256
|
+
model: parsed.model,
|
|
257
|
+
provider: parsed.provider,
|
|
258
|
+
variant: parsed.clearVariant ? null : parsed.variant,
|
|
259
|
+
disconnect: specified("disconnect") ? parsed.disconnect : undefined,
|
|
260
|
+
removeDependency: parsed.removeDependency === true,
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
catch (error) {
|
|
264
|
+
fail("invalid_input", error instanceof Error ? error.message : String(error));
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
async function consent(options, hasChanges) {
|
|
268
|
+
if (!options.dryRun && !options.yes && hasChanges)
|
|
269
|
+
if (value(await confirmQuestion("Apply the displayed changes?")) !== true)
|
|
270
|
+
fail("cancelled", "No changes applied");
|
|
271
|
+
}
|
|
272
|
+
function requireApply(options) {
|
|
273
|
+
if (!options.dryRun && !options.yes && !tty())
|
|
274
|
+
fail("confirmation_required", "Use --dry-run to inspect or --yes to apply outside a terminal");
|
|
275
|
+
}
|
|
276
|
+
function conflicts(operations) {
|
|
277
|
+
const blocked = operations.filter((operation) => operation.operation === "conflict");
|
|
278
|
+
if (blocked.length)
|
|
279
|
+
fail("conflict", `Plan blocked; preserved paths: ${blocked.map((operation) => operation.path).join(", ")}. Inspect doctor; restore recorded bytes or resolve ownership before retrying.`);
|
|
280
|
+
}
|
|
281
|
+
async function integrationSelection(options, presetsOnly = false) {
|
|
282
|
+
if (options.targets !== undefined || options.fragments !== undefined) {
|
|
283
|
+
if (options.targets === undefined || options.fragments === undefined)
|
|
284
|
+
fail("invalid_input", "Supply both --targets and --fragments");
|
|
285
|
+
return normalizeConfigSelection(options.scope, {
|
|
286
|
+
targets: options.targets,
|
|
287
|
+
fragments: options.fragments,
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
const defaults = await defaultConfigSelection(options.scope);
|
|
291
|
+
const targets = value(await selectOptions("Applications to configure", options.scope === "project" ? ["opencode"] : CONFIG_TARGETS, defaults.targets));
|
|
292
|
+
if (!targets.length)
|
|
293
|
+
return { targets, fragments: [] };
|
|
294
|
+
const connection = [];
|
|
295
|
+
if (!presetsOnly && targets.includes("opencode")) {
|
|
296
|
+
const choice = value(await selectOption("OpenCode plugin connection", [
|
|
297
|
+
"Keep current connection",
|
|
298
|
+
"Connect and pin this package version",
|
|
299
|
+
"Disconnect agentomatic",
|
|
300
|
+
]));
|
|
301
|
+
if (choice === 1)
|
|
302
|
+
connection.push("core-plugin");
|
|
303
|
+
if (choice === 2)
|
|
304
|
+
connection.push("core-disable");
|
|
305
|
+
}
|
|
306
|
+
const available = CONFIG_FRAGMENTS.filter((fragment) => !["core-plugin", "core-disable"].includes(fragment.name) &&
|
|
307
|
+
fragment.targets.some((target) => targets.includes(target))).map((fragment) => fragment.name);
|
|
308
|
+
const fragments = value(await selectOptions("Application presets", available, defaults.fragments.filter((fragment) => available.includes(fragment))));
|
|
309
|
+
return normalizeConfigSelection(options.scope, {
|
|
310
|
+
targets,
|
|
311
|
+
fragments: [...connection, ...fragments],
|
|
312
|
+
});
|
|
313
|
+
}
|
|
314
|
+
async function modelSelection(options, name, current) {
|
|
315
|
+
validateAgentName(name);
|
|
316
|
+
if (options.model) {
|
|
317
|
+
const model = validateModel(options.model.includes("/") ? options.model : `${options.provider ?? ""}/${options.model}`);
|
|
318
|
+
return { action: "model-set", name, model, variant: validateVariant(options.variant) ?? null };
|
|
319
|
+
}
|
|
320
|
+
if (options.provider)
|
|
321
|
+
fail("invalid_input", "--provider requires --model");
|
|
322
|
+
if (options.variant !== undefined) {
|
|
323
|
+
if (!current?.model)
|
|
324
|
+
fail("invalid_input", "Changing only variant requires a saved model");
|
|
325
|
+
return {
|
|
326
|
+
action: "model-set",
|
|
327
|
+
name,
|
|
328
|
+
model: current.model,
|
|
329
|
+
variant: validateVariant(options.variant) ?? null,
|
|
330
|
+
};
|
|
331
|
+
}
|
|
332
|
+
const choices = current?.model
|
|
333
|
+
? [
|
|
334
|
+
`Keep current (${current.model}${current.variant ? ` / ${current.variant}` : ""})`,
|
|
335
|
+
"Change model",
|
|
336
|
+
...(current.variant ? ["Clear variant"] : []),
|
|
337
|
+
]
|
|
338
|
+
: ["Change model"];
|
|
339
|
+
const choice = choices[value(await selectOption(`Agent: ${name}`, choices))];
|
|
340
|
+
if (choice.startsWith("Keep current"))
|
|
341
|
+
return { action: "model-set", name, model: current.model, variant: current.variant ?? null };
|
|
342
|
+
if (choice === "Clear variant")
|
|
343
|
+
return { action: "model-set", name, model: current.model, variant: null };
|
|
344
|
+
try {
|
|
345
|
+
const models = await availableModels();
|
|
346
|
+
const providers = [...new Set(models.map((model) => model.split("/", 1)[0]))].sort();
|
|
347
|
+
const provider = providers[value(await selectOption("Provider", providers))];
|
|
348
|
+
const candidates = models.filter((model) => model.startsWith(`${provider}/`));
|
|
349
|
+
const model = candidates[value(await selectOption("Model", candidates))];
|
|
350
|
+
const variants = await availableModelVariants(model);
|
|
351
|
+
const variant = variants.length
|
|
352
|
+
? [undefined, ...variants][value(await selectOption("Variant", ["(none)", ...variants]))]
|
|
353
|
+
: undefined;
|
|
354
|
+
return { action: "model-set", name, model, variant: variant ?? null };
|
|
355
|
+
}
|
|
356
|
+
catch (error) {
|
|
357
|
+
if (!(error instanceof LifecycleError && error.code === "catalog_unavailable"))
|
|
358
|
+
throw error;
|
|
359
|
+
process.stderr.write("Model catalog unavailable; enter an explicit model instead of a catalog pick.\n");
|
|
360
|
+
const model = validateModel(value(await promptText("Model (provider/model)")));
|
|
361
|
+
const answer = await promptText(current?.variant ? "Variant (empty keeps current)" : "Variant (optional)");
|
|
362
|
+
const variant = (answer ? validateVariant(answer) : current?.variant) ?? null;
|
|
363
|
+
return { action: "model-set", name, model, variant };
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
async function profileDraft(options, names) {
|
|
367
|
+
const inventory = await listAgentProfiles(options.scope);
|
|
368
|
+
const records = new Map(inventory.profiles
|
|
369
|
+
.filter((profile) => profile.ownership !== "user-owned")
|
|
370
|
+
.map((profile) => [profile.name, { ...profile }]));
|
|
371
|
+
const changes = [];
|
|
372
|
+
while (true) {
|
|
373
|
+
const actions = ["Configure model", "Add critic", "Remove additional critic", "Done"];
|
|
374
|
+
const action = actions[value(await selectOption("Agent models and critics (changes are staged until confirmation)", actions, process.stdin, process.stderr, 3))];
|
|
375
|
+
if (action === "Done")
|
|
376
|
+
return changes;
|
|
377
|
+
if (action === "Add critic") {
|
|
378
|
+
const suffix = value(await promptText("Critic name (critic-<suffix>)"));
|
|
379
|
+
const name = validateAgentName(suffix.startsWith("critic-") ? suffix : `critic-${suffix}`);
|
|
380
|
+
if (records.has(name))
|
|
381
|
+
fail("profile_exists", `Profile already exists: ${name}`);
|
|
382
|
+
const request = { ...(await modelSelection(options, name)), action: "critic-add" };
|
|
383
|
+
changes.push(request);
|
|
384
|
+
names.push(name);
|
|
385
|
+
records.set(name, {
|
|
386
|
+
name,
|
|
387
|
+
ownership: "managed",
|
|
388
|
+
state: "current",
|
|
389
|
+
model: request.model,
|
|
390
|
+
variant: request.variant ?? undefined,
|
|
391
|
+
});
|
|
392
|
+
}
|
|
393
|
+
else {
|
|
394
|
+
const candidates = action === "Remove additional critic"
|
|
395
|
+
? names.filter((name) => name.startsWith("critic-"))
|
|
396
|
+
: names;
|
|
397
|
+
if (!candidates.length) {
|
|
398
|
+
process.stderr.write("No eligible profiles; choose another action.\n");
|
|
399
|
+
continue;
|
|
400
|
+
}
|
|
401
|
+
const name = candidates[value(await selectOption("Agent", candidates))];
|
|
402
|
+
if (action === "Remove additional critic") {
|
|
403
|
+
changes.push({ action: "critic-remove", name });
|
|
404
|
+
names = names.filter((candidate) => candidate !== name);
|
|
405
|
+
records.delete(name);
|
|
406
|
+
}
|
|
407
|
+
else {
|
|
408
|
+
const request = await modelSelection(options, name, records.get(name));
|
|
409
|
+
changes.push(request);
|
|
410
|
+
records.set(name, {
|
|
411
|
+
...records.get(name),
|
|
412
|
+
model: request.model,
|
|
413
|
+
variant: request.variant ?? undefined,
|
|
414
|
+
});
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
async function componentSelection(options) {
|
|
420
|
+
const supplied = [options.commands, options.agents, options.plugins].some((selection) => selection !== undefined);
|
|
421
|
+
if (supplied && (!options.commands || !options.agents || !options.plugins))
|
|
422
|
+
fail("invalid_input", "Supply --commands, --agents, and --plugins together (none selects an empty set)");
|
|
423
|
+
const overrides = supplied
|
|
424
|
+
? {
|
|
425
|
+
commands: options.commands,
|
|
426
|
+
agents: options.agents,
|
|
427
|
+
plugins: options.plugins,
|
|
428
|
+
}
|
|
429
|
+
: {};
|
|
430
|
+
const saved = await installedSelection(options.scope, undefined, undefined, overrides);
|
|
431
|
+
const defaults = saved ?? defaultSelection();
|
|
432
|
+
if (supplied) {
|
|
433
|
+
return normalizeSelection({
|
|
434
|
+
...overrides,
|
|
435
|
+
core_activation: options.core ?? defaults.core_activation,
|
|
436
|
+
});
|
|
437
|
+
}
|
|
438
|
+
if (!tty()) {
|
|
439
|
+
if (!saved)
|
|
440
|
+
fail("terminal_required", "First install outside a terminal requires --commands, --agents, and --plugins");
|
|
441
|
+
return { ...saved, core_activation: options.core ?? saved.core_activation };
|
|
442
|
+
}
|
|
443
|
+
process.stderr.write(`Portable skills are installed separately through the skills CLI (npx --yes ${skillsInstallerSpec()} add https://kisev.github.io/skills --agent opencode --copy). Selecting an adapter does not install its skill.\n`);
|
|
444
|
+
const commands = value(await selectOptions("Skill command adapters", SKILL_COMMANDS, defaults.commands));
|
|
445
|
+
const agents = value(await selectOptions("Fixed agents", FIXED_AGENT_ROLES, defaults.agents));
|
|
446
|
+
const plugins = value(await selectOptions("Optional plugins", SELECTABLE_PLUGINS, defaults.plugins));
|
|
447
|
+
const core_activation = options.core ??
|
|
448
|
+
value(await confirmQuestion("Connect the OpenCode plugin and pin its npm dependency?", process.stdin, process.stderr, defaults.core_activation));
|
|
449
|
+
return normalizeSelection({ commands, agents, plugins, core_activation });
|
|
450
|
+
}
|
|
451
|
+
async function deploy(options, repair = false) {
|
|
452
|
+
const selection = repair
|
|
453
|
+
? await installedSelection(options.scope)
|
|
454
|
+
: await componentSelection(options);
|
|
455
|
+
if (!selection)
|
|
456
|
+
fail("not_installed", "No saved installation to repair; use install");
|
|
457
|
+
requireApply(options);
|
|
458
|
+
let integration = {
|
|
459
|
+
targets: ["opencode"],
|
|
460
|
+
fragments: [selection.core_activation ? "core-plugin" : "core-disable"],
|
|
461
|
+
};
|
|
462
|
+
if (!repair &&
|
|
463
|
+
(options.targets !== undefined ||
|
|
464
|
+
options.fragments !== undefined ||
|
|
465
|
+
(tty() && value(await confirmQuestion("Configure application presets as well?"))))) {
|
|
466
|
+
integration = await integrationSelection(options, true);
|
|
467
|
+
const chosenCore = integration.fragments.includes("core-plugin")
|
|
468
|
+
? true
|
|
469
|
+
: integration.fragments.includes("core-disable")
|
|
470
|
+
? false
|
|
471
|
+
: undefined;
|
|
472
|
+
if (chosenCore !== undefined) {
|
|
473
|
+
if (options.core !== undefined && options.core !== chosenCore)
|
|
474
|
+
fail("invalid_input", "Core flags conflict with the selected connection fragment");
|
|
475
|
+
selection.core_activation = chosenCore;
|
|
476
|
+
}
|
|
477
|
+
integration = {
|
|
478
|
+
targets: [...new Set([...integration.targets, "opencode"])],
|
|
479
|
+
fragments: [
|
|
480
|
+
...new Set([
|
|
481
|
+
...integration.fragments.filter((fragment) => !["core-plugin", "core-disable"].includes(fragment)),
|
|
482
|
+
selection.core_activation ? "core-plugin" : "core-disable",
|
|
483
|
+
]),
|
|
484
|
+
],
|
|
485
|
+
};
|
|
486
|
+
}
|
|
487
|
+
let changes = [];
|
|
488
|
+
if (!repair &&
|
|
489
|
+
tty() &&
|
|
490
|
+
value(await confirmQuestion("Configure agent models or additional critics now?"))) {
|
|
491
|
+
const inventory = await listAgentProfiles(options.scope);
|
|
492
|
+
changes = await profileDraft(options, [
|
|
493
|
+
...selection.agents,
|
|
494
|
+
...inventory.profiles
|
|
495
|
+
.filter((profile) => profile.name.startsWith("critic-") && profile.ownership !== "user-owned")
|
|
496
|
+
.map((profile) => profile.name),
|
|
497
|
+
]);
|
|
498
|
+
}
|
|
499
|
+
const plan = await preview("install", options.scope, undefined, undefined, selection, !options.noDependency, changes);
|
|
500
|
+
const config = await previewConfigSetup(integration, options.scope, undefined, undefined, false, false);
|
|
501
|
+
const report = {
|
|
502
|
+
status: "ok",
|
|
503
|
+
command: repair ? "maintenance repair" : "install",
|
|
504
|
+
applied: false,
|
|
505
|
+
plan,
|
|
506
|
+
integration: config,
|
|
507
|
+
profile_changes: changes,
|
|
508
|
+
stages: ["owned-components-and-profiles", "npm-dependency", "application-config"],
|
|
509
|
+
atomic_across_stages: false,
|
|
510
|
+
};
|
|
511
|
+
if (!options.json) {
|
|
512
|
+
if (repair)
|
|
513
|
+
process.stdout.write("Repair the saved installation; component selection and model choices are retained.\n");
|
|
514
|
+
const args = [
|
|
515
|
+
repair ? "maintenance" : "install",
|
|
516
|
+
...(repair ? ["repair"] : []),
|
|
517
|
+
...scopeArguments(options.scope),
|
|
518
|
+
...(!repair
|
|
519
|
+
? [
|
|
520
|
+
"--commands",
|
|
521
|
+
selection.commands.join(",") || "none",
|
|
522
|
+
"--agents",
|
|
523
|
+
selection.agents.join(",") || "none",
|
|
524
|
+
"--plugins",
|
|
525
|
+
selection.plugins.join(",") || "none",
|
|
526
|
+
selection.core_activation ? "--core" : "--no-core",
|
|
527
|
+
]
|
|
528
|
+
: []),
|
|
529
|
+
...(options.noDependency ? ["--no-dependency"] : []),
|
|
530
|
+
...(!repair
|
|
531
|
+
? [
|
|
532
|
+
"--targets",
|
|
533
|
+
integration.targets.join(","),
|
|
534
|
+
"--fragments",
|
|
535
|
+
integration.fragments
|
|
536
|
+
.filter((fragment) => !["core-plugin", "core-disable"].includes(fragment))
|
|
537
|
+
.join(",") || "none",
|
|
538
|
+
]
|
|
539
|
+
: []),
|
|
540
|
+
];
|
|
541
|
+
process.stdout.write(renderPlan(plan, {
|
|
542
|
+
applied: false,
|
|
543
|
+
applyHint: changes.length ? undefined : applyHint(args),
|
|
544
|
+
}) + renderConfigSetup(config, { applied: false }));
|
|
545
|
+
if (changes.length)
|
|
546
|
+
process.stdout.write(`Staged profiles: ${terminalSafe(JSON.stringify(changes))}\n`);
|
|
547
|
+
process.stdout.write("Components, npm, and application configuration are separate stages; a later failure does not roll back completed stages.\n");
|
|
548
|
+
}
|
|
549
|
+
if (options.dryRun) {
|
|
550
|
+
if (options.json)
|
|
551
|
+
json(report);
|
|
552
|
+
return;
|
|
553
|
+
}
|
|
554
|
+
conflicts(plan.operations);
|
|
555
|
+
conflicts(config.operations);
|
|
556
|
+
await consent(options, [...plan.operations, ...config.operations].some((operation) => operation.operation !== "unchanged") || Boolean(plan.dependency && plan.dependency.status !== "satisfied"));
|
|
557
|
+
const completed = [];
|
|
558
|
+
try {
|
|
559
|
+
const applied = await apply("install", options.scope, undefined, undefined, { expectedDigest: plan.digest }, selection, !options.noDependency, changes);
|
|
560
|
+
completed.push("owned-components-and-profiles");
|
|
561
|
+
if (applied.dependency?.status !== "manual" && applied.dependency)
|
|
562
|
+
completed.push("npm-dependency");
|
|
563
|
+
const connected = await applyConfigSetup(integration, options.scope, undefined, undefined, {
|
|
564
|
+
provisionDependency: false,
|
|
565
|
+
receipt: config.receipt,
|
|
566
|
+
syncSelection: false,
|
|
567
|
+
});
|
|
568
|
+
completed.push("application-config");
|
|
569
|
+
const result = {
|
|
570
|
+
...report,
|
|
571
|
+
applied: true,
|
|
572
|
+
plan: applied,
|
|
573
|
+
integration: connected,
|
|
574
|
+
completed_stages: completed,
|
|
575
|
+
npm_dependency: applied.dependency?.applied ?? "skipped",
|
|
576
|
+
...(applied.dependency?.status === "manual"
|
|
577
|
+
? {
|
|
578
|
+
pending_stages: ["npm-dependency"],
|
|
579
|
+
next_step: "Create an owning npm project, then repeat install; the preview reports the dependency directory.",
|
|
580
|
+
}
|
|
581
|
+
: {}),
|
|
582
|
+
requires_restart: applied.requires_restart || connected.requires_restart,
|
|
583
|
+
};
|
|
584
|
+
if (options.json)
|
|
585
|
+
json(result);
|
|
586
|
+
else
|
|
587
|
+
process.stdout.write(renderPlan(applied, { applied: true }) + renderConfigSetup(connected, { applied: true }));
|
|
588
|
+
}
|
|
589
|
+
catch (error) {
|
|
590
|
+
if (error instanceof LifecycleError &&
|
|
591
|
+
error.code === "npm_dependency_failed" &&
|
|
592
|
+
!completed.length)
|
|
593
|
+
completed.push("owned-components-and-profiles");
|
|
594
|
+
partial(options, error, completed, `Run status, then ${repair ? "maintenance repair" : "install"} with the same selected components; use configure integration for configuration-only continuation.`, completed.includes("owned-components-and-profiles") && plan.requires_restart);
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
function partial(options, error, completed, next, requiresRestart = false) {
|
|
598
|
+
const code = error instanceof LifecycleError ? error.code : "internal_error";
|
|
599
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
600
|
+
if (options.json) {
|
|
601
|
+
json({
|
|
602
|
+
status: completed.length ? "partial" : "error",
|
|
603
|
+
applied: completed.length > 0,
|
|
604
|
+
completed_stages: completed,
|
|
605
|
+
requires_restart: requiresRestart,
|
|
606
|
+
error: { code, message },
|
|
607
|
+
next_step: next,
|
|
608
|
+
});
|
|
609
|
+
process.exitCode = 2;
|
|
610
|
+
throw new ReportedError();
|
|
611
|
+
}
|
|
612
|
+
fail(code, `${message}\nCompleted: ${completed.join(", ") || "none"}. Restart required: ${requiresRestart ? "yes" : "no"}. ${next}`);
|
|
613
|
+
}
|
|
614
|
+
class ReportedError extends Error {
|
|
615
|
+
}
|
|
616
|
+
async function configureIntegration(options) {
|
|
617
|
+
requireApply(options);
|
|
618
|
+
const selection = await integrationSelection(options);
|
|
619
|
+
const plan = await previewConfigSetup(selection, options.scope, undefined, undefined, !options.noDependency);
|
|
620
|
+
if (!options.json)
|
|
621
|
+
process.stdout.write(renderConfigSetup(plan, {
|
|
622
|
+
applied: false,
|
|
623
|
+
applyHint: applyHint([
|
|
624
|
+
"configure",
|
|
625
|
+
"integration",
|
|
626
|
+
...scopeArguments(options.scope),
|
|
627
|
+
"--targets",
|
|
628
|
+
selection.targets.join(",") || "none",
|
|
629
|
+
"--fragments",
|
|
630
|
+
selection.fragments.join(",") || "none",
|
|
631
|
+
...(options.noDependency ? ["--no-dependency"] : []),
|
|
632
|
+
]),
|
|
633
|
+
}));
|
|
634
|
+
if (options.dryRun) {
|
|
635
|
+
if (options.json)
|
|
636
|
+
json({ status: "ok", applied: false, plan });
|
|
637
|
+
return;
|
|
638
|
+
}
|
|
639
|
+
conflicts(plan.operations);
|
|
640
|
+
await consent(options, plan.confirmable || Boolean(plan.dependency && plan.dependency.status !== "satisfied"));
|
|
641
|
+
try {
|
|
642
|
+
const result = await applyConfigSetup(selection, options.scope, undefined, undefined, {
|
|
643
|
+
provisionDependency: !options.noDependency,
|
|
644
|
+
receipt: plan.receipt,
|
|
645
|
+
});
|
|
646
|
+
if (options.json)
|
|
647
|
+
json({
|
|
648
|
+
status: "ok",
|
|
649
|
+
applied: true,
|
|
650
|
+
plan: result,
|
|
651
|
+
requires_restart: result.requires_restart,
|
|
652
|
+
});
|
|
653
|
+
else
|
|
654
|
+
process.stdout.write(renderConfigSetup(result, { applied: true }));
|
|
655
|
+
}
|
|
656
|
+
catch (error) {
|
|
657
|
+
partial(options, error, error instanceof LifecycleError && error.code === "npm_dependency_failed"
|
|
658
|
+
? ["application-config"]
|
|
659
|
+
: [], "Inspect status and retry configure integration with the same selection.", error instanceof LifecycleError &&
|
|
660
|
+
error.code === "npm_dependency_failed" &&
|
|
661
|
+
plan.requires_restart);
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
async function profileChange(options, request) {
|
|
665
|
+
requireApply(options);
|
|
666
|
+
const plan = await previewAgentProfileChange(request, options.scope);
|
|
667
|
+
if (!options.json) {
|
|
668
|
+
const args = request.changes
|
|
669
|
+
? ["configure", "critics"]
|
|
670
|
+
: request.action === "critic-remove"
|
|
671
|
+
? ["agent", "remove", request.name]
|
|
672
|
+
: [
|
|
673
|
+
request.action === "critic-add" ? "agent" : "configure",
|
|
674
|
+
request.action === "critic-add" ? "add-critic" : "agent",
|
|
675
|
+
request.name,
|
|
676
|
+
"--model",
|
|
677
|
+
request.model,
|
|
678
|
+
...(request.variant
|
|
679
|
+
? ["--variant", request.variant]
|
|
680
|
+
: request.action === "model-set"
|
|
681
|
+
? ["--clear-variant"]
|
|
682
|
+
: []),
|
|
683
|
+
];
|
|
684
|
+
process.stdout.write(renderPlan(plan, {
|
|
685
|
+
applied: false,
|
|
686
|
+
applyHint: applyHint([...args, ...scopeArguments(options.scope)]),
|
|
687
|
+
}));
|
|
688
|
+
}
|
|
689
|
+
if (options.dryRun) {
|
|
690
|
+
if (options.json)
|
|
691
|
+
json({ status: "ok", applied: false, plan });
|
|
692
|
+
return;
|
|
693
|
+
}
|
|
694
|
+
conflicts(plan.operations);
|
|
695
|
+
await consent(options, plan.operations.some((operation) => operation.operation !== "unchanged"));
|
|
696
|
+
const result = await applyAgentProfileChange(request, options.scope, undefined, undefined, {
|
|
697
|
+
expectedDigest: plan.digest,
|
|
698
|
+
});
|
|
699
|
+
if (options.json)
|
|
700
|
+
json(result);
|
|
701
|
+
else
|
|
702
|
+
process.stdout.write(renderPlan(result.plan, { applied: true }));
|
|
703
|
+
}
|
|
704
|
+
async function uninstall(options) {
|
|
705
|
+
requireApply(options);
|
|
706
|
+
const disconnect = options.disconnect ??
|
|
707
|
+
(tty()
|
|
708
|
+
? value(await confirmQuestion("Disconnect the agentomatic plugin as well? (other presets stay)"))
|
|
709
|
+
: true);
|
|
710
|
+
const remove = options.removeDependency ||
|
|
711
|
+
(tty() &&
|
|
712
|
+
value(await selectOption("npm dependency", ["Keep (default)", "Remove agentomatic dependency"])) === 1);
|
|
713
|
+
const plan = await preview("uninstall", options.scope);
|
|
714
|
+
const selection = { targets: ["opencode"], fragments: ["core-disable"] };
|
|
715
|
+
const config = disconnect
|
|
716
|
+
? await previewConfigSetup(selection, options.scope, undefined, undefined, false, false)
|
|
717
|
+
: undefined;
|
|
718
|
+
const dependency = remove ? await previewDependencyRemoval(options.scope) : undefined;
|
|
719
|
+
const report = {
|
|
720
|
+
status: "ok",
|
|
721
|
+
applied: false,
|
|
722
|
+
plan,
|
|
723
|
+
integration: config,
|
|
724
|
+
dependency_removal: dependency,
|
|
725
|
+
models: "retained",
|
|
726
|
+
portable_skills: "untouched",
|
|
727
|
+
atomic_across_stages: false,
|
|
728
|
+
};
|
|
729
|
+
if (!options.json) {
|
|
730
|
+
process.stdout.write(renderPlan(plan, {
|
|
731
|
+
applied: false,
|
|
732
|
+
applyHint: applyHint([
|
|
733
|
+
"uninstall",
|
|
734
|
+
...scopeArguments(options.scope),
|
|
735
|
+
...(disconnect ? [] : ["--no-disconnect"]),
|
|
736
|
+
...(remove ? ["--remove-dependency"] : []),
|
|
737
|
+
]),
|
|
738
|
+
}) +
|
|
739
|
+
(config ? renderConfigSetup(config, { applied: false }) : "Plugin connection: retained\n"));
|
|
740
|
+
process.stdout.write(`npm dependency: ${remove ? `remove ${dependency?.names.join(", ") || "none present"}` : "retained"}\nSaved models: retained\nPortable skills and other presets: untouched\nSeparate stages; completed changes are retained on a later failure.\n`);
|
|
741
|
+
}
|
|
742
|
+
if (options.dryRun) {
|
|
743
|
+
if (options.json)
|
|
744
|
+
json(report);
|
|
745
|
+
return;
|
|
746
|
+
}
|
|
747
|
+
conflicts(plan.operations);
|
|
748
|
+
if (config)
|
|
749
|
+
conflicts(config.operations);
|
|
750
|
+
await consent(options, plan.operations.some((operation) => operation.operation !== "unchanged") ||
|
|
751
|
+
Boolean(config?.confirmable) ||
|
|
752
|
+
Boolean(dependency?.names.length));
|
|
753
|
+
const completed = [];
|
|
754
|
+
try {
|
|
755
|
+
const removed = await apply("uninstall", options.scope, undefined, undefined, {
|
|
756
|
+
expectedDigest: plan.digest,
|
|
757
|
+
});
|
|
758
|
+
completed.push("owned-components");
|
|
759
|
+
const disconnected = config
|
|
760
|
+
? await applyConfigSetup(selection, options.scope, undefined, undefined, {
|
|
761
|
+
provisionDependency: false,
|
|
762
|
+
receipt: config.receipt,
|
|
763
|
+
syncSelection: false,
|
|
764
|
+
})
|
|
765
|
+
: undefined;
|
|
766
|
+
if (config)
|
|
767
|
+
completed.push("plugin-disconnection");
|
|
768
|
+
if (dependency) {
|
|
769
|
+
await removeDependency(dependency);
|
|
770
|
+
completed.push("npm-removal");
|
|
771
|
+
}
|
|
772
|
+
if (options.json)
|
|
773
|
+
json({
|
|
774
|
+
...report,
|
|
775
|
+
applied: true,
|
|
776
|
+
plan: removed,
|
|
777
|
+
integration: disconnected,
|
|
778
|
+
completed_stages: completed,
|
|
779
|
+
requires_restart: removed.requires_restart || Boolean(disconnected?.requires_restart),
|
|
780
|
+
});
|
|
781
|
+
else
|
|
782
|
+
process.stdout.write("Uninstall complete. Saved models retained; restart OpenCode if files changed.\n");
|
|
783
|
+
}
|
|
784
|
+
catch (error) {
|
|
785
|
+
partial(options, error, completed, "Inspect status and retry uninstall with the same disconnect and dependency choices.", (completed.includes("owned-components") && plan.requires_restart) ||
|
|
786
|
+
(completed.includes("plugin-disconnection") && Boolean(config?.requires_restart)));
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
async function run(args) {
|
|
790
|
+
if (args.includes("--version") || args.includes("-V")) {
|
|
791
|
+
if (args.some((arg) => !["--version", "-V", "--json"].includes(arg)))
|
|
792
|
+
fail("invalid_input", "--version accepts only --json");
|
|
793
|
+
if (args.includes("--json"))
|
|
794
|
+
json({ status: "ok", version: CATALOG.version });
|
|
795
|
+
else
|
|
796
|
+
process.stdout.write(`${CATALOG.version}\n`);
|
|
797
|
+
return;
|
|
798
|
+
}
|
|
799
|
+
if (args[0] === "help")
|
|
800
|
+
args = [...args.slice(1), "--help"];
|
|
801
|
+
let topic = args.shift() ?? "";
|
|
802
|
+
if (["configure", "agent", "maintenance"].includes(topic) && args[0] && !args[0].startsWith("-"))
|
|
803
|
+
topic += ` ${args.shift()}`;
|
|
804
|
+
if (["--help", "-h", ""].includes(topic)) {
|
|
805
|
+
if (args.some((arg) => !["--help", "-h"].includes(arg)))
|
|
806
|
+
fail("invalid_input", "Unexpected root help argument");
|
|
807
|
+
process.stdout.write(help());
|
|
808
|
+
return;
|
|
809
|
+
}
|
|
810
|
+
if (args.includes("--help") || args.includes("-h")) {
|
|
811
|
+
if (!commands[topic])
|
|
812
|
+
fail("invalid_input", `Unknown command: ${topic}`);
|
|
813
|
+
parse(topic, args.filter((arg) => !["--help", "-h"].includes(arg)));
|
|
814
|
+
process.stdout.write(help(topic));
|
|
815
|
+
return;
|
|
816
|
+
}
|
|
817
|
+
if (!commands[topic]) {
|
|
818
|
+
const match = [...new Set(Object.keys(commands).map((key) => key.split(" ", 1)[0]))].find((root) => root.startsWith(topic) || topic.startsWith(root));
|
|
819
|
+
fail("invalid_input", `Unknown command: ${topic}.${match ? ` Did you mean '${match}'?` : ""} Use install, configure, status, doctor, uninstall, agent, maintenance, or catalog.`);
|
|
820
|
+
}
|
|
821
|
+
const options = parse(topic, args);
|
|
822
|
+
if (topic === "configure") {
|
|
823
|
+
requireApply(options);
|
|
824
|
+
const routes = [
|
|
825
|
+
"configure components",
|
|
826
|
+
"configure agent",
|
|
827
|
+
"configure critics",
|
|
828
|
+
"configure integration",
|
|
829
|
+
];
|
|
830
|
+
const selected = value(await selectOption("What would you like to configure?", [
|
|
831
|
+
"Installed components",
|
|
832
|
+
"Agent models",
|
|
833
|
+
"Additional critics",
|
|
834
|
+
"Application connection and presets",
|
|
835
|
+
]));
|
|
836
|
+
topic = routes[selected];
|
|
837
|
+
}
|
|
838
|
+
if (topic === "install" || topic === "configure components" || topic === "maintenance repair") {
|
|
839
|
+
await deploy(options, topic === "maintenance repair");
|
|
840
|
+
return;
|
|
841
|
+
}
|
|
842
|
+
if (topic === "configure integration") {
|
|
843
|
+
await configureIntegration(options);
|
|
844
|
+
return;
|
|
845
|
+
}
|
|
846
|
+
if (topic === "uninstall") {
|
|
847
|
+
await uninstall(options);
|
|
848
|
+
return;
|
|
849
|
+
}
|
|
850
|
+
if (topic === "catalog") {
|
|
851
|
+
json({ schema_version: 1, status: "ok", ...CATALOG });
|
|
852
|
+
return;
|
|
853
|
+
}
|
|
854
|
+
if (topic === "status" || topic === "agent list") {
|
|
855
|
+
const inventory = await listAgentProfiles(options.scope);
|
|
856
|
+
if (topic === "agent list") {
|
|
857
|
+
if (options.json)
|
|
858
|
+
json({ status: "ok", inventory });
|
|
859
|
+
else
|
|
860
|
+
process.stdout.write(renderInventory(inventory));
|
|
861
|
+
return;
|
|
862
|
+
}
|
|
863
|
+
const selection = await installedSelection(options.scope);
|
|
864
|
+
const connection = await inspectIntegration(options.scope);
|
|
865
|
+
const dependency = planDependency(options.scope);
|
|
866
|
+
const result = {
|
|
867
|
+
status: connection.problem ? "incomplete" : "ok",
|
|
868
|
+
scope: options.scope,
|
|
869
|
+
installed: Boolean(selection),
|
|
870
|
+
selection,
|
|
871
|
+
connection,
|
|
872
|
+
dependency,
|
|
873
|
+
inventory,
|
|
874
|
+
};
|
|
875
|
+
if (options.json)
|
|
876
|
+
json(result);
|
|
877
|
+
else
|
|
878
|
+
process.stdout.write(`Installation: ${selection ? "present" : "not installed"}\nCommands: ${selection?.commands.join(", ") || "none"}\nPlugins: ${selection?.plugins.join(", ") || "none"}\nPlugin connection: ${connection.connected ? "active" : "not confirmed (use doctor)"}\nnpm dependency: ${dependency.status}\n` +
|
|
879
|
+
renderInventory(inventory));
|
|
880
|
+
return;
|
|
881
|
+
}
|
|
882
|
+
if (topic === "doctor") {
|
|
883
|
+
const report = await collectDoctorFacts(options.scope);
|
|
884
|
+
if (options.json)
|
|
885
|
+
json(report);
|
|
886
|
+
else
|
|
887
|
+
process.stdout.write(renderDoctor(report));
|
|
888
|
+
process.exitCode = doctorExitCode(report);
|
|
889
|
+
return;
|
|
890
|
+
}
|
|
891
|
+
if (topic === "configure agent" || topic === "agent add-critic") {
|
|
892
|
+
const inventory = await listAgentProfiles(options.scope);
|
|
893
|
+
let name = options.name;
|
|
894
|
+
if (!name) {
|
|
895
|
+
if (topic === "agent add-critic")
|
|
896
|
+
name = value(await promptText("Critic name (critic-<suffix>)"));
|
|
897
|
+
else {
|
|
898
|
+
const names = inventory.profiles
|
|
899
|
+
.filter((profile) => profile.ownership !== "user-owned")
|
|
900
|
+
.map((profile) => profile.name);
|
|
901
|
+
name = names[value(await selectOption("Agent", names))];
|
|
902
|
+
}
|
|
903
|
+
}
|
|
904
|
+
if (topic === "agent add-critic" && !name.startsWith("critic-"))
|
|
905
|
+
name = `critic-${name}`;
|
|
906
|
+
const request = await modelSelection(options, name, inventory.profiles.find((profile) => profile.name === name));
|
|
907
|
+
await profileChange(options, {
|
|
908
|
+
...request,
|
|
909
|
+
action: topic === "agent add-critic" ? "critic-add" : "model-set",
|
|
910
|
+
});
|
|
911
|
+
return;
|
|
912
|
+
}
|
|
913
|
+
if (topic === "agent remove") {
|
|
914
|
+
if (!options.name)
|
|
915
|
+
fail("invalid_input", "agent remove requires an additional critic name");
|
|
916
|
+
await profileChange(options, { action: "critic-remove", name: options.name });
|
|
917
|
+
return;
|
|
918
|
+
}
|
|
919
|
+
if (topic === "configure critics") {
|
|
920
|
+
requireApply(options);
|
|
921
|
+
const inventory = await listAgentProfiles(options.scope);
|
|
922
|
+
const changes = await profileDraft(options, inventory.profiles
|
|
923
|
+
.filter((profile) => (profile.name === "critic" || profile.name.startsWith("critic-")) &&
|
|
924
|
+
profile.ownership !== "user-owned")
|
|
925
|
+
.map((profile) => profile.name));
|
|
926
|
+
if (!changes.length) {
|
|
927
|
+
if (options.json)
|
|
928
|
+
json({ status: "ok", applied: false });
|
|
929
|
+
else
|
|
930
|
+
process.stdout.write("No profile changes selected.\n");
|
|
931
|
+
return;
|
|
932
|
+
}
|
|
933
|
+
await profileChange(options, { action: "model-set", changes });
|
|
934
|
+
return;
|
|
935
|
+
}
|
|
936
|
+
if (topic === "maintenance recover") {
|
|
937
|
+
requireApply(options);
|
|
938
|
+
const plan = await recoverConfigSetup(options.scope, true);
|
|
939
|
+
if (!options.json)
|
|
940
|
+
process.stdout.write(`Recovery paths:\n${plan.paths.map((path) => ` ${terminalSafe(path)}`).join("\n")}\n`);
|
|
941
|
+
if (options.dryRun) {
|
|
942
|
+
if (options.json)
|
|
943
|
+
json({ status: "ok", applied: false, plan });
|
|
944
|
+
return;
|
|
945
|
+
}
|
|
946
|
+
await consent(options, plan.paths.length > 0);
|
|
947
|
+
const result = await recoverConfigSetup(options.scope, false, undefined, undefined, plan.digest);
|
|
948
|
+
if (options.json)
|
|
949
|
+
json({ status: "ok", ...result, next_step: "Request a fresh preview" });
|
|
950
|
+
else
|
|
951
|
+
process.stdout.write("Recovery complete; request a fresh preview.\n");
|
|
952
|
+
return;
|
|
953
|
+
}
|
|
954
|
+
if (topic === "maintenance cleanup") {
|
|
955
|
+
requireApply(options);
|
|
956
|
+
const plan = await previewReconcile(options.scope);
|
|
957
|
+
if (!options.json)
|
|
958
|
+
process.stdout.write(renderReconcile(plan, {
|
|
959
|
+
applied: false,
|
|
960
|
+
applyHint: applyHint(["maintenance", "cleanup", ...scopeArguments(options.scope)]),
|
|
961
|
+
}));
|
|
962
|
+
if (options.dryRun) {
|
|
963
|
+
if (options.json)
|
|
964
|
+
json({ status: "ok", applied: false, plan });
|
|
965
|
+
return;
|
|
966
|
+
}
|
|
967
|
+
if (plan.conflicts.length || plan.modified_managed.length)
|
|
968
|
+
fail("conflict", "Cleanup blocked by modified or conflicting files; inspect the preview paths and restore recorded bytes before retrying");
|
|
969
|
+
if (!plan.confirmable) {
|
|
970
|
+
if (options.json)
|
|
971
|
+
json({ status: "ok", applied: false, plan });
|
|
972
|
+
return;
|
|
973
|
+
}
|
|
974
|
+
await consent(options, true);
|
|
975
|
+
const result = await applyReconcile(options.scope, undefined, undefined, {
|
|
976
|
+
expectedDigest: plan.digest,
|
|
977
|
+
});
|
|
978
|
+
if (options.json)
|
|
979
|
+
json(result);
|
|
980
|
+
else
|
|
981
|
+
process.stdout.write(renderReconcile(result.plan, { applied: true }));
|
|
982
|
+
return;
|
|
983
|
+
}
|
|
984
|
+
process.stdout.write(help(topic));
|
|
985
|
+
}
|
|
986
|
+
try {
|
|
987
|
+
await run(process.argv.slice(2));
|
|
988
|
+
}
|
|
989
|
+
catch (error) {
|
|
990
|
+
if (!(error instanceof ReportedError)) {
|
|
991
|
+
const code = error instanceof LifecycleError ? error.code : "internal_error";
|
|
992
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
993
|
+
if (machineOutput || process.argv.includes("--json"))
|
|
994
|
+
json({ status: "error", error: { code, message } });
|
|
995
|
+
else if (diagnosticJSON)
|
|
996
|
+
process.stderr.write(`${JSON.stringify({ level: "error", phase: "agentomatic.error", code, message })}\n`);
|
|
997
|
+
else
|
|
998
|
+
process.stderr.write(`Error [${code}]: ${terminalSafe(message)}\n`);
|
|
999
|
+
}
|
|
1000
|
+
process.exitCode = 2;
|
|
1001
|
+
}
|
|
1002
|
+
finally {
|
|
1003
|
+
diagnostics?.close();
|
|
1004
|
+
}
|