@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.
Files changed (64) hide show
  1. package/README.md +32 -16
  2. package/README.ru.md +33 -16
  3. package/dist/agent-profiles.d.ts +8 -2
  4. package/dist/agent-profiles.js +131 -82
  5. package/dist/assets/agents/architect.md +4 -4
  6. package/dist/assets/agents/critic.md +10 -11
  7. package/dist/assets/agents/manager.md +11 -12
  8. package/dist/assets/agents/mapper.md +4 -4
  9. package/dist/assets/agents/review.md +11 -12
  10. package/dist/assets/agents/worker.md +29 -32
  11. package/dist/assets/commands/asd-ste100.md +10 -0
  12. package/dist/assets/commands/code-simplify.md +12 -0
  13. package/dist/assets/commands/eli5.md +10 -0
  14. package/dist/assets/commands/handoff.md +10 -0
  15. package/dist/assets/commands/rtk-stats.md +2 -2
  16. package/dist/assets/commands/skill-doctor.md +10 -0
  17. package/dist/assets/migration-inventory.json +17 -6
  18. package/dist/assets/plugins/code-simplify.js +3 -0
  19. package/dist/assets/plugins/rtk.js +1 -1
  20. package/dist/assets/plugins/rules-injector.js +1 -1
  21. package/dist/assets/plugins/zed-bell.js +1 -1
  22. package/dist/catalog.d.ts +2 -2
  23. package/dist/catalog.js +9 -3
  24. package/dist/cli-output.js +6 -6
  25. package/dist/cli.d.ts +1 -1
  26. package/dist/cli.js +1 -1257
  27. package/dist/command-cli.d.ts +1 -0
  28. package/dist/command-cli.js +1004 -0
  29. package/dist/config-setup.d.ts +14 -8
  30. package/dist/config-setup.js +197 -64
  31. package/dist/contracts.js +26 -0
  32. package/dist/doctor.d.ts +0 -1
  33. package/dist/doctor.js +42 -55
  34. package/dist/index.d.ts +13 -118
  35. package/dist/index.js +118 -125
  36. package/dist/installer.d.ts +25 -2
  37. package/dist/installer.js +68 -23
  38. package/dist/jsonc.d.ts +18 -2
  39. package/dist/jsonc.js +109 -5
  40. package/dist/opencode-config.d.ts +11 -0
  41. package/dist/opencode-config.js +237 -0
  42. package/dist/plugins/code-simplify.d.ts +25 -0
  43. package/dist/plugins/code-simplify.js +49 -0
  44. package/dist/plugins/events.d.ts +20 -0
  45. package/dist/plugins/events.js +48 -0
  46. package/dist/plugins/rtk.d.ts +9 -12
  47. package/dist/plugins/rtk.js +20 -5
  48. package/dist/plugins/rules-injector.d.ts +21 -11
  49. package/dist/plugins/rules-injector.js +43 -31
  50. package/dist/plugins/zed-bell.d.ts +8 -5
  51. package/dist/plugins/zed-bell.js +10 -2
  52. package/dist/reconcile.d.ts +4 -1
  53. package/dist/reconcile.js +12 -16
  54. package/dist/registry.d.ts +1 -0
  55. package/dist/registry.js +21 -5
  56. package/dist/routing.d.ts +5 -3
  57. package/dist/routing.js +57 -22
  58. package/dist/self-install.d.ts +7 -0
  59. package/dist/self-install.js +52 -1
  60. package/dist/terminal-wizard.d.ts +1 -1
  61. package/dist/terminal-wizard.js +2 -2
  62. package/package.json +12 -7
  63. package/dist/assets/commands/skill-improve.md +0 -10
  64. 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
+ }