@kisev/agentomatic 12.0.1-dev.129.g99196bf396c1 → 12.0.1-dev.131.gfdcd7e5c228f
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 +3 -2
- package/README.ru.md +3 -1
- package/dist/cli-output.d.ts +7 -2
- package/dist/cli-output.js +114 -36
- package/dist/command-cli.js +295 -93
- package/dist/config-setup.d.ts +1 -0
- package/dist/config-setup.js +7 -0
- package/dist/installer.d.ts +1 -0
- package/dist/installer.js +8 -0
- package/dist/terminal-wizard.d.ts +12 -2
- package/dist/terminal-wizard.js +88 -8
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -110,8 +110,9 @@ npx agentomatic configure integration --global --dry-run
|
|
|
110
110
|
}
|
|
111
111
|
```
|
|
112
112
|
|
|
113
|
-
Restart OpenCode after activation or asset changes. A confirmed `install`
|
|
114
|
-
|
|
113
|
+
Restart OpenCode after activation or asset changes. A confirmed `install`
|
|
114
|
+
deploys the six fixed agents only as one complete package; agent models and
|
|
115
|
+
critics live in the confirmed `configure agent` command.
|
|
115
116
|
`configure` provides one settings menu; `status` shows the installed set and models.
|
|
116
117
|
`uninstall` proposes plugin disconnection, retains models, and removes the npm
|
|
117
118
|
dependency only with an explicit selection (`--remove-dependency`). Other
|
package/README.ru.md
CHANGED
|
@@ -114,7 +114,9 @@ npx agentomatic configure integration --global --dry-run
|
|
|
114
114
|
```
|
|
115
115
|
|
|
116
116
|
Перезапустите OpenCode после активации или изменения файлов. Подтверждённый
|
|
117
|
-
`install` настраивает core и
|
|
117
|
+
`install` настраивает core и разворачивает шесть фиксированных агентов только
|
|
118
|
+
целиком пакетом; модели агентов и критиков меняет подтверждённая команда
|
|
119
|
+
`configure agent`.
|
|
118
120
|
`configure` открывает единое меню настроек, `status` показывает состав и модели.
|
|
119
121
|
`uninstall` предлагает отключение плагина, сохраняет модели и удаляет npm-зависимость
|
|
120
122
|
только при явном выборе (`--remove-dependency`). Другие фрагменты применяет
|
package/dist/cli-output.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type AgentInventory, type AgentProfilePlan } from "./agent-profiles.js";
|
|
2
2
|
import type { ConfigSetupPlan } from "./config-setup.js";
|
|
3
3
|
import type { Plan as InstallerPlan } from "./installer.js";
|
|
4
4
|
import type { ReconcilePlan } from "./reconcile.js";
|
|
@@ -6,17 +6,22 @@ import type { DoctorReport } from "./doctor.js";
|
|
|
6
6
|
import type { Scope } from "./lifecycle.js";
|
|
7
7
|
type DisplayPlan = InstallerPlan | AgentProfilePlan;
|
|
8
8
|
export declare function terminalSafe(value: string): string;
|
|
9
|
+
export declare function colorEnabled(env?: NodeJS.ProcessEnv, isTTY?: boolean | undefined): boolean;
|
|
9
10
|
export declare function renderPlan(plan: DisplayPlan, options: {
|
|
10
11
|
applied: boolean;
|
|
11
12
|
applyHint?: string;
|
|
12
13
|
hint?: string;
|
|
14
|
+
color?: boolean;
|
|
13
15
|
}): string;
|
|
16
|
+
export declare function renderConfigNoop(plan: ConfigSetupPlan, color?: boolean): string;
|
|
14
17
|
export declare function renderConfigSetup(plan: ConfigSetupPlan, options: {
|
|
15
18
|
applied: boolean;
|
|
16
19
|
applyHint?: string;
|
|
17
20
|
hint?: string;
|
|
21
|
+
color?: boolean;
|
|
18
22
|
}): string;
|
|
19
|
-
export declare function renderCriticsTable(inventory: AgentInventory): string;
|
|
23
|
+
export declare function renderCriticsTable(inventory: AgentInventory, color?: boolean): string;
|
|
24
|
+
export declare function renderFixedRoles(inventory: AgentInventory, color?: boolean): string;
|
|
20
25
|
export declare function renderInventory(inventory: AgentInventory): string;
|
|
21
26
|
export declare function shellCommand(arguments_: readonly string[]): string;
|
|
22
27
|
export declare function scopeArguments(scope: Scope): [] | ["--global"];
|
package/dist/cli-output.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { FIXED_AGENT_ROLES } from "./agent-profiles.js";
|
|
1
2
|
import { CATALOG } from "./catalog.js";
|
|
2
3
|
const GROUPS = ["Agents", "Commands", "Plugins", "State"];
|
|
3
4
|
const OPERATIONS = [
|
|
@@ -33,6 +34,36 @@ export function terminalSafe(value) {
|
|
|
33
34
|
})
|
|
34
35
|
.join("");
|
|
35
36
|
}
|
|
37
|
+
// ANSI styling is opt-in per call so tests can force either form; the CLI uses
|
|
38
|
+
// the environment default, which disables color on non-TTY output and NO_COLOR.
|
|
39
|
+
export function colorEnabled(env = process.env, isTTY = process.stdout.isTTY) {
|
|
40
|
+
if (env.NO_COLOR !== undefined && env.NO_COLOR !== "")
|
|
41
|
+
return false;
|
|
42
|
+
if (env.FORCE_COLOR !== undefined && env.FORCE_COLOR !== "")
|
|
43
|
+
return env.FORCE_COLOR !== "0";
|
|
44
|
+
return Boolean(isTTY);
|
|
45
|
+
}
|
|
46
|
+
function paint(color, code, text) {
|
|
47
|
+
return color && code ? `\u001b[${code}m${text}\u001b[0m` : text;
|
|
48
|
+
}
|
|
49
|
+
// One glyph per operation keeps the summary readable even with color off; the
|
|
50
|
+
// matching ANSI code is applied only when color is enabled.
|
|
51
|
+
const OPERATION_STYLES = {
|
|
52
|
+
create: { symbol: "▸", code: "32" },
|
|
53
|
+
update: { symbol: "●", code: "36" },
|
|
54
|
+
remove: { symbol: "■", code: "31" },
|
|
55
|
+
"archive-pending": { symbol: "■", code: "33" },
|
|
56
|
+
conflict: { symbol: "✗", code: "31" },
|
|
57
|
+
missing: { symbol: "✗", code: "33" },
|
|
58
|
+
unchanged: { symbol: "◻", code: "2" },
|
|
59
|
+
};
|
|
60
|
+
const MARKS = {
|
|
61
|
+
ok: "✓",
|
|
62
|
+
bad: "✗",
|
|
63
|
+
change: "▸",
|
|
64
|
+
item: "●",
|
|
65
|
+
pending: "◻",
|
|
66
|
+
};
|
|
36
67
|
function groupFor(path) {
|
|
37
68
|
if (path.startsWith("agents/"))
|
|
38
69
|
return "Agents";
|
|
@@ -52,10 +83,13 @@ function actionName(action) {
|
|
|
52
83
|
reconcile: "Repair selected agents",
|
|
53
84
|
}[action];
|
|
54
85
|
}
|
|
55
|
-
function operationSummary(operations) {
|
|
86
|
+
function operationSummary(operations, color) {
|
|
56
87
|
return OPERATIONS.map((operation) => {
|
|
57
88
|
const count = operations.filter((item) => item.operation === operation).length;
|
|
58
|
-
|
|
89
|
+
if (!count)
|
|
90
|
+
return undefined;
|
|
91
|
+
const spec = OPERATION_STYLES[operation];
|
|
92
|
+
return paint(color, spec.code, `${spec.symbol} ${operation} ${count}`);
|
|
59
93
|
})
|
|
60
94
|
.filter(Boolean)
|
|
61
95
|
.join(", ");
|
|
@@ -66,7 +100,7 @@ function shortPath(path, group) {
|
|
|
66
100
|
const value = path.slice(path.indexOf("/") + 1);
|
|
67
101
|
return terminalSafe(value.endsWith(".md") || value.endsWith(".js") ? value.slice(0, -3) : value);
|
|
68
102
|
}
|
|
69
|
-
function detailLines(operations) {
|
|
103
|
+
function detailLines(operations, color) {
|
|
70
104
|
const lines = [];
|
|
71
105
|
for (const group of GROUPS) {
|
|
72
106
|
const grouped = operations.filter((item) => groupFor(item.path) === group && item.operation !== "unchanged");
|
|
@@ -78,7 +112,8 @@ function detailLines(operations) {
|
|
|
78
112
|
: shortPath(item.path, group));
|
|
79
113
|
const visible = operation === "conflict" ? values : values.slice(0, 8);
|
|
80
114
|
const rest = values.length - visible.length;
|
|
81
|
-
|
|
115
|
+
const spec = OPERATION_STYLES[operation];
|
|
116
|
+
lines.push(` ${paint(color, spec.code, spec.symbol)} ${group}/${operation}: ${visible.join(", ")}${rest ? ` (+${rest} more)` : ""}`);
|
|
82
117
|
}
|
|
83
118
|
}
|
|
84
119
|
return lines;
|
|
@@ -90,9 +125,10 @@ function migrationSummary(operations) {
|
|
|
90
125
|
: undefined;
|
|
91
126
|
}
|
|
92
127
|
export function renderPlan(plan, options) {
|
|
128
|
+
const color = options.color ?? colorEnabled();
|
|
93
129
|
const version = "package_version" in plan ? ` @kisev/agentomatic ${plan.package_version}` : "";
|
|
94
130
|
const lines = [
|
|
95
|
-
`${actionName(plan.action)}${version} (${plan.scope})
|
|
131
|
+
paint(color, "1", `${actionName(plan.action)}${version} (${plan.scope})`),
|
|
96
132
|
`Target: ${terminalSafe(plan.root)}`,
|
|
97
133
|
...("selection" in plan
|
|
98
134
|
? [
|
|
@@ -101,84 +137,126 @@ export function renderPlan(plan, options) {
|
|
|
101
137
|
]
|
|
102
138
|
: []),
|
|
103
139
|
"",
|
|
104
|
-
options.applied ? "Applied changes:" : "Planned changes:",
|
|
140
|
+
paint(color, "1", options.applied ? "Applied changes:" : "Planned changes:"),
|
|
105
141
|
];
|
|
106
142
|
for (const group of GROUPS) {
|
|
107
143
|
const operations = plan.operations.filter((item) => groupFor(item.path) === group);
|
|
108
144
|
if (operations.length)
|
|
109
|
-
lines.push(` ${group}: ${operationSummary(operations)}`);
|
|
145
|
+
lines.push(` ${paint(color, "1", group)}: ${operationSummary(operations, color)}`);
|
|
110
146
|
}
|
|
111
147
|
const migration = migrationSummary(plan.operations);
|
|
112
148
|
if (migration)
|
|
113
149
|
lines.push(migration);
|
|
114
|
-
const details = detailLines(plan.operations);
|
|
150
|
+
const details = detailLines(plan.operations, color);
|
|
115
151
|
if (details.length)
|
|
116
|
-
lines.push("", "Details:", ...details);
|
|
152
|
+
lines.push("", paint(color, "1", "Details:"), ...details);
|
|
117
153
|
const conflicts = plan.operations.filter((item) => item.operation === "conflict").length;
|
|
118
|
-
lines.push("", `Conflicts: ${conflicts
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
:
|
|
154
|
+
lines.push("", `Conflicts: ${conflicts ? paint(color, "31", `${MARKS.bad} ${conflicts}`) : paint(color, "32", `${MARKS.ok} none`)}`);
|
|
155
|
+
const restartNote = "restart the host session so the deployed roles and settings load";
|
|
156
|
+
lines.push(plan.requires_restart
|
|
157
|
+
? `${options.applied ? "Restart required" : "Restart after apply"}: ${paint(color, "33", `${MARKS.ok} yes`)} — ${restartNote}`
|
|
158
|
+
: `${options.applied ? "Restart required" : "Restart after apply"}: ${paint(color, "2", `${MARKS.pending} no`)}`);
|
|
122
159
|
if (!options.applied && options.applyHint)
|
|
123
|
-
lines.push("", "Apply:", ` ${options.applyHint}`);
|
|
160
|
+
lines.push("", paint(color, "1", "Apply (repeat non-interactively):"), ` ${options.applyHint}`);
|
|
124
161
|
if (options.applied && options.hint)
|
|
125
|
-
lines.push("", "Next:", ` ${options.hint}`);
|
|
162
|
+
lines.push("", paint(color, "1", "Next:"), ` ${options.hint}`);
|
|
126
163
|
return `${lines.join("\n")}\n`;
|
|
127
164
|
}
|
|
165
|
+
export function renderConfigNoop(plan, color = colorEnabled()) {
|
|
166
|
+
return [
|
|
167
|
+
paint(color, "1", `Integration configuration @kisev/agentomatic ${plan.package_version} (${plan.scope})`),
|
|
168
|
+
`Root: ${terminalSafe(plan.root)}`,
|
|
169
|
+
"",
|
|
170
|
+
paint(color, "32", `${MARKS.ok} No configuration changes are required.`),
|
|
171
|
+
"",
|
|
172
|
+
].join("\n");
|
|
173
|
+
}
|
|
128
174
|
export function renderConfigSetup(plan, options) {
|
|
175
|
+
const color = options.color ?? colorEnabled();
|
|
129
176
|
const lines = [
|
|
130
|
-
`Integration configuration @kisev/agentomatic ${plan.package_version} (${plan.scope})
|
|
177
|
+
paint(color, "1", `Integration configuration @kisev/agentomatic ${plan.package_version} (${plan.scope})`),
|
|
131
178
|
`Root: ${terminalSafe(plan.root)}`,
|
|
132
179
|
"",
|
|
133
180
|
];
|
|
134
181
|
if (plan.targets.length)
|
|
135
|
-
lines.push("Targets:", ...plan.targets.map((item) => ` ${item.target}: ${terminalSafe(item.path)} (${item.exists ? "existing" : "new"})`));
|
|
182
|
+
lines.push(paint(color, "1", "Targets:"), ...plan.targets.map((item) => ` ${item.target}: ${terminalSafe(item.path)} ${paint(color, "2", `(${item.exists ? "existing" : "new"})`)}`));
|
|
136
183
|
else
|
|
137
184
|
lines.push("Targets: none");
|
|
138
185
|
const fragments = plan.operations.filter((item) => item.fragment !== "file");
|
|
139
186
|
if (plan.dependency)
|
|
140
187
|
lines.push("", `Dependency: ${plan.dependency.status} ${plan.dependency.name}@${plan.dependency.version}`, " Provisioning may access npm and update package.json, package-lock.json, and node_modules.");
|
|
141
188
|
if (fragments.length) {
|
|
142
|
-
lines.push("", options.applied ? "Applied fragments:" : "Planned fragments:", ...fragments.map((item) =>
|
|
189
|
+
lines.push("", paint(color, "1", options.applied ? "Applied fragments:" : "Planned fragments:"), ...fragments.map((item) => {
|
|
190
|
+
const spec = OPERATION_STYLES[item.operation];
|
|
191
|
+
const badge = spec
|
|
192
|
+
? `${paint(color, spec.code, spec.symbol)} `
|
|
193
|
+
: `${paint(color, "33", MARKS.change)} `;
|
|
194
|
+
return ` ${badge}${item.target}/${item.fragment}: ${item.operation}${item.reason ? ` (${terminalSafe(item.reason)})` : ""}`;
|
|
195
|
+
}));
|
|
143
196
|
}
|
|
144
197
|
if (plan.skipped_fragments.length)
|
|
145
|
-
lines.push("", "Skipped fragments:", ...plan.skipped_fragments.map((item) => ` ${item.fragment}: ${terminalSafe(item.reason)}`));
|
|
146
|
-
const conflicts = plan.operations.filter((item) => item.operation === "conflict");
|
|
147
|
-
lines.push("", `Conflicts: ${conflicts.
|
|
148
|
-
lines.push(
|
|
149
|
-
?
|
|
150
|
-
:
|
|
198
|
+
lines.push("", paint(color, "1", "Skipped fragments:"), ...plan.skipped_fragments.map((item) => ` ${item.fragment}: ${terminalSafe(item.reason)}`));
|
|
199
|
+
const conflicts = plan.operations.filter((item) => item.operation === "conflict").length;
|
|
200
|
+
lines.push("", `Conflicts: ${conflicts ? paint(color, "31", `${MARKS.bad} ${conflicts}`) : paint(color, "32", `${MARKS.ok} none`)}`);
|
|
201
|
+
lines.push(plan.requires_restart
|
|
202
|
+
? `${options.applied ? "Restart required" : "Restart after apply"}: ${paint(color, "33", `${MARKS.ok} yes`)} — restart the host session so the merged presets load`
|
|
203
|
+
: `${options.applied ? "Restart required" : "Restart after apply"}: ${paint(color, "2", `${MARKS.pending} no`)}`);
|
|
151
204
|
if (!options.applied) {
|
|
152
205
|
if (!plan.confirmable)
|
|
153
|
-
lines.push("", "No configuration changes are required.");
|
|
206
|
+
lines.push("", paint(color, "32", "No configuration changes are required."));
|
|
154
207
|
else if (options.applyHint)
|
|
155
|
-
lines.push("", "Apply:", ` ${options.applyHint}`);
|
|
208
|
+
lines.push("", paint(color, "1", "Apply (repeat non-interactively):"), ` ${options.applyHint}`);
|
|
156
209
|
}
|
|
157
210
|
if (options.applied && options.hint)
|
|
158
|
-
lines.push("", "Next:", ` ${options.hint}`);
|
|
211
|
+
lines.push("", paint(color, "1", "Next:"), ` ${options.hint}`);
|
|
159
212
|
return `${lines.join("\n")}\n`;
|
|
160
213
|
}
|
|
161
|
-
function table(rows) {
|
|
214
|
+
function table(rows, color = false) {
|
|
162
215
|
const widths = rows[0].map((_, column) => Math.max(...rows.map((row) => row[column]?.length ?? 0)));
|
|
163
|
-
return rows.map((row) => row
|
|
216
|
+
return rows.map((row, index) => paint(color && index === 0, "1", row
|
|
164
217
|
.map((value, column) => value.padEnd(widths[column]))
|
|
165
218
|
.join(" ")
|
|
166
|
-
.trimEnd());
|
|
219
|
+
.trimEnd()));
|
|
167
220
|
}
|
|
168
|
-
export function renderCriticsTable(inventory) {
|
|
169
|
-
|
|
221
|
+
export function renderCriticsTable(inventory, color = colorEnabled()) {
|
|
222
|
+
// The panel keeps the package-owned fixed critic (`critic`) apart from the
|
|
223
|
+
// additional `critic-*` pool entries and points at the canonical overview.
|
|
224
|
+
const critics = inventory.profiles.filter((profile) => profile.ownership !== "user-owned" &&
|
|
225
|
+
(profile.name === "critic" || profile.name.startsWith("critic-")));
|
|
170
226
|
if (!critics.length)
|
|
171
227
|
return "Critics:\n No critics\n";
|
|
228
|
+
const pool = critics.filter((profile) => profile.name !== "critic");
|
|
172
229
|
const rows = [
|
|
173
230
|
["NAME", "MODEL", "VARIANT", "PROVIDER"],
|
|
174
231
|
...critics.map((profile) => [
|
|
175
|
-
terminalSafe(profile.name)
|
|
176
|
-
terminalSafe(profile.model ?? "default"),
|
|
232
|
+
`${profile.name === "critic" ? MARKS.item : MARKS.change} ${terminalSafe(profile.name)}`,
|
|
233
|
+
terminalSafe(profile.model ?? "default (host)"),
|
|
177
234
|
terminalSafe(profile.variant ?? "-"),
|
|
178
235
|
terminalSafe(profile.model?.split("/", 1)[0] ?? "-"),
|
|
179
236
|
]),
|
|
180
237
|
];
|
|
181
|
-
|
|
238
|
+
const head = paint(color, "1", "Critics:");
|
|
239
|
+
return (`${head}\n${table(rows, color).join("\n")}\n` +
|
|
240
|
+
`Pool: additional critics are optional specialist profiles (` +
|
|
241
|
+
`${pool.length ? pool.map((profile) => terminalSafe(profile.name)).join(", ") : "none"}); ` +
|
|
242
|
+
"the fixed critic always deploys with the package.\n");
|
|
243
|
+
}
|
|
244
|
+
export function renderFixedRoles(inventory, color = colorEnabled()) {
|
|
245
|
+
const roles = inventory.profiles.filter((profile) => profile.ownership !== "user-owned" &&
|
|
246
|
+
FIXED_AGENT_ROLES.includes(profile.name));
|
|
247
|
+
const rows = [
|
|
248
|
+
["NAME", "MODEL", "VARIANT"],
|
|
249
|
+
...roles.map((profile) => [
|
|
250
|
+
`${MARKS.item} ${terminalSafe(profile.name)}`,
|
|
251
|
+
terminalSafe(profile.model ?? "default (host)"),
|
|
252
|
+
terminalSafe(profile.variant ?? "-"),
|
|
253
|
+
]),
|
|
254
|
+
];
|
|
255
|
+
return [
|
|
256
|
+
paint(color, "1", "Fixed roles (all six belong to the package; models and critic composition: agentomatic configure agent):"),
|
|
257
|
+
...table(rows, color),
|
|
258
|
+
"",
|
|
259
|
+
].join("\n");
|
|
182
260
|
}
|
|
183
261
|
export function renderInventory(inventory) {
|
|
184
262
|
const rows = [
|
|
@@ -254,7 +332,7 @@ export function renderReconcile(plan, options) {
|
|
|
254
332
|
lines.push("", "No cleanup changes are required.");
|
|
255
333
|
}
|
|
256
334
|
else if (options.applyHint) {
|
|
257
|
-
lines.push("", "Apply:", ` ${options.applyHint}`);
|
|
335
|
+
lines.push("", "Apply (repeat non-interactively):", ` ${options.applyHint}`);
|
|
258
336
|
}
|
|
259
337
|
}
|
|
260
338
|
return `${lines.join("\n")}\n`;
|
package/dist/command-cli.js
CHANGED
|
@@ -1,22 +1,23 @@
|
|
|
1
1
|
import { Command, Option, common, applyConfig, configFileSync, reporter } from "./generated/cli.js";
|
|
2
2
|
import { applyAgentProfileChange, previewAgentProfileChange, listAgentProfiles, availableModels, availableModelVariants, validateAgentName, validateModel, validateVariant, suggestCriticName, 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";
|
|
3
|
+
import { apply, preview, installedSelection, defaultSelection, normalizeSelection, InstallerError, SKILL_COMMANDS, SELECTABLE_PLUGINS, PLUGIN_DESCRIPTIONS, } from "./installer.js";
|
|
4
|
+
import { applyConfigSetup, previewConfigSetup, recoverConfigSetup, normalizeConfigSelection, defaultConfigSelection, CONFIG_TARGETS, CONFIG_FRAGMENTS, TARGET_DESCRIPTIONS, inspectIntegration, } from "./config-setup.js";
|
|
5
|
+
import { COMMAND_REGISTRY } from "./registry.js";
|
|
5
6
|
import { collectDoctorFacts, doctorExitCode } from "./doctor.js";
|
|
6
7
|
import { applyReconcile, previewReconcile } from "./reconcile.js";
|
|
7
8
|
import { CATALOG } from "./catalog.js";
|
|
8
9
|
import { skillsInstallerSpec } from "./package-metadata.js";
|
|
9
10
|
import { LifecycleError } from "./lifecycle.js";
|
|
10
11
|
import { previewDependencyRemoval, removeDependency, planDependency } from "./self-install.js";
|
|
11
|
-
import { selectOption, selectOptions, promptText, confirmQuestion } from "./terminal-wizard.js";
|
|
12
|
-
import { renderPlan, renderInventory, renderCriticsTable, renderDoctor, renderConfigSetup, renderReconcile, terminalSafe, shellCommand, scopeArguments, } from "./cli-output.js";
|
|
12
|
+
import { selectOption, selectOptions, promptText, confirmQuestion, runWizard, isBack, } from "./terminal-wizard.js";
|
|
13
|
+
import { renderPlan, renderInventory, renderCriticsTable, renderFixedRoles, renderDoctor, renderConfigSetup, renderConfigNoop, renderReconcile, terminalSafe, shellCommand, scopeArguments, } from "./cli-output.js";
|
|
13
14
|
let diagnostics;
|
|
14
15
|
let machineOutput = false;
|
|
15
16
|
let diagnosticJSON = false;
|
|
16
17
|
const commands = {
|
|
17
18
|
install: {
|
|
18
|
-
description: "
|
|
19
|
-
options: ["commands", "
|
|
19
|
+
description: "Deploy the fixed agent package with chosen command adapters, plugins, and harness integration; agent models stay with configure agent.",
|
|
20
|
+
options: ["commands", "plugins", "core", "dependency", "targets", "fragments"],
|
|
20
21
|
},
|
|
21
22
|
configure: {
|
|
22
23
|
description: "Choose components, agent models, critics, or application integration.",
|
|
@@ -24,7 +25,7 @@ const commands = {
|
|
|
24
25
|
},
|
|
25
26
|
"configure components": {
|
|
26
27
|
description: "Change the installed component set without resetting models.",
|
|
27
|
-
options: ["commands", "
|
|
28
|
+
options: ["commands", "plugins", "core", "dependency", "targets", "fragments"],
|
|
28
29
|
},
|
|
29
30
|
"configure agent": {
|
|
30
31
|
description: "Open a staged editor for every agent, or set one model with a name and --model provider/model.",
|
|
@@ -165,7 +166,8 @@ function help(topic = "") {
|
|
|
165
166
|
: []),
|
|
166
167
|
...(!topic || ["install", "configure components"].includes(topic)
|
|
167
168
|
? [
|
|
168
|
-
"
|
|
169
|
+
"Fixed agents deploy only as the complete six-role package; no install flag or prompt changes agent composition.",
|
|
170
|
+
"Non-TTY component selection: --commands <list|none> --plugins <list|none>",
|
|
169
171
|
"Saved selection is reused on repeat runs. --no-core disconnects the plugin; npm removal is an explicit uninstall choice.",
|
|
170
172
|
]
|
|
171
173
|
: []),
|
|
@@ -302,7 +304,21 @@ function conflicts(operations) {
|
|
|
302
304
|
if (blocked.length)
|
|
303
305
|
fail("conflict", `Plan blocked; preserved paths: ${blocked.map((operation) => operation.path).join(", ")}. Inspect doctor; restore recorded bytes or resolve ownership before retrying.`);
|
|
304
306
|
}
|
|
305
|
-
|
|
307
|
+
const CONNECTION_CHOICES = [
|
|
308
|
+
"Keep current connection",
|
|
309
|
+
"Connect and pin this package version",
|
|
310
|
+
"Disconnect agentomatic",
|
|
311
|
+
];
|
|
312
|
+
function presetFragments(targets, filter) {
|
|
313
|
+
return CONFIG_FRAGMENTS.filter((fragment) => !["core-plugin", "core-disable"].includes(fragment.name) &&
|
|
314
|
+
fragment.targets.some((target) => targets.includes(target)) &&
|
|
315
|
+
(!filter || filter.has(fragment.name))).map((fragment) => fragment.name);
|
|
316
|
+
}
|
|
317
|
+
function applicablePresets(fragments, targets) {
|
|
318
|
+
return fragments.filter((name) => !["core-plugin", "core-disable"].includes(name) &&
|
|
319
|
+
CONFIG_FRAGMENTS.find((fragment) => fragment.name === name)?.targets.some((target) => targets.includes(target)));
|
|
320
|
+
}
|
|
321
|
+
async function integrationSelection(options, presetsOnly = false, filter) {
|
|
306
322
|
if (options.targets !== undefined || options.fragments !== undefined) {
|
|
307
323
|
if (options.targets === undefined || options.fragments === undefined)
|
|
308
324
|
fail("invalid_input", "Supply both --targets and --fragments");
|
|
@@ -312,30 +328,60 @@ async function integrationSelection(options, presetsOnly = false) {
|
|
|
312
328
|
});
|
|
313
329
|
}
|
|
314
330
|
const defaults = await defaultConfigSelection(options.scope);
|
|
315
|
-
const
|
|
316
|
-
|
|
317
|
-
|
|
331
|
+
const scopedTargets = options.scope === "project" ? ["opencode"] : [...CONFIG_TARGETS];
|
|
332
|
+
const targetChoices = filter?.targets
|
|
333
|
+
? scopedTargets.filter((target) => filter.targets.includes(target))
|
|
334
|
+
: scopedTargets;
|
|
335
|
+
const fragmentFilter = filter?.fragments ? new Set(filter.fragments) : undefined;
|
|
336
|
+
const steps = [
|
|
337
|
+
{
|
|
338
|
+
run: async (state, back) => {
|
|
339
|
+
const result = await selectOptions("Agent harnesses to configure", targetChoices, state.targets.filter((target) => targetChoices.includes(target)), targetChoices.map((target) => TARGET_DESCRIPTIONS[target]), process.stdin, process.stderr, back);
|
|
340
|
+
if (isBack(result))
|
|
341
|
+
return result;
|
|
342
|
+
return { ...state, targets: value(result) };
|
|
343
|
+
},
|
|
344
|
+
},
|
|
345
|
+
{
|
|
346
|
+
enabled: (state) => !presetsOnly && state.targets.includes("opencode") && (filter?.connection ?? true),
|
|
347
|
+
run: async (state, back) => {
|
|
348
|
+
const result = await selectOption("OpenCode plugin connection", CONNECTION_CHOICES, process.stdin, process.stderr, state.connection, undefined, back);
|
|
349
|
+
if (isBack(result))
|
|
350
|
+
return result;
|
|
351
|
+
return { ...state, connection: value(result) };
|
|
352
|
+
},
|
|
353
|
+
},
|
|
354
|
+
{
|
|
355
|
+
enabled: (state) => presetFragments(state.targets, fragmentFilter).length > 0,
|
|
356
|
+
run: async (state, back) => {
|
|
357
|
+
const available = presetFragments(state.targets, fragmentFilter);
|
|
358
|
+
const result = await selectOptions("Application presets to merge", available, state.fragments.filter((fragment) => available.includes(fragment)), available.map((name) => CONFIG_FRAGMENTS.find((fragment) => fragment.name === name)?.description), process.stdin, process.stderr, back);
|
|
359
|
+
if (isBack(result))
|
|
360
|
+
return result;
|
|
361
|
+
return { ...state, fragments: value(result) };
|
|
362
|
+
},
|
|
363
|
+
},
|
|
364
|
+
];
|
|
365
|
+
const state = await runWizard({
|
|
366
|
+
targets: filter?.targets ? [...filter.targets] : defaults.targets,
|
|
367
|
+
connection: 0,
|
|
368
|
+
fragments: filter?.fragments
|
|
369
|
+
? [...filter.fragments]
|
|
370
|
+
: applicablePresets(defaults.fragments, defaults.targets),
|
|
371
|
+
}, steps);
|
|
372
|
+
if (!state.targets.length)
|
|
373
|
+
return { targets: [], fragments: [] };
|
|
318
374
|
const connection = [];
|
|
319
|
-
if (
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
"Disconnect agentomatic",
|
|
324
|
-
]));
|
|
325
|
-
if (choice === 1)
|
|
326
|
-
connection.push("core-plugin");
|
|
327
|
-
if (choice === 2)
|
|
328
|
-
connection.push("core-disable");
|
|
329
|
-
}
|
|
330
|
-
const available = CONFIG_FRAGMENTS.filter((fragment) => !["core-plugin", "core-disable"].includes(fragment.name) &&
|
|
331
|
-
fragment.targets.some((target) => targets.includes(target))).map((fragment) => fragment.name);
|
|
332
|
-
const fragments = value(await selectOptions("Application presets", available, defaults.fragments.filter((fragment) => available.includes(fragment))));
|
|
375
|
+
if (state.connection === 1)
|
|
376
|
+
connection.push("core-plugin");
|
|
377
|
+
if (state.connection === 2)
|
|
378
|
+
connection.push("core-disable");
|
|
333
379
|
return normalizeConfigSelection(options.scope, {
|
|
334
|
-
targets,
|
|
335
|
-
fragments: [...connection, ...fragments],
|
|
380
|
+
targets: state.targets,
|
|
381
|
+
fragments: [...connection, ...applicablePresets(state.fragments, state.targets)],
|
|
336
382
|
});
|
|
337
383
|
}
|
|
338
|
-
async function modelSelection(options, name, current) {
|
|
384
|
+
async function modelSelection(options, name, current, allowBack = false) {
|
|
339
385
|
validateAgentName(name);
|
|
340
386
|
if (options.model) {
|
|
341
387
|
const model = validateModel(options.model.includes("/") ? options.model : `${options.provider ?? ""}/${options.model}`);
|
|
@@ -360,42 +406,102 @@ async function modelSelection(options, name, current) {
|
|
|
360
406
|
...(current.variant ? ["Clear variant"] : []),
|
|
361
407
|
]
|
|
362
408
|
: ["Change model"];
|
|
363
|
-
const
|
|
364
|
-
|
|
409
|
+
const changing = (state) => state.manual === null && state.choice === "Change model";
|
|
410
|
+
const candidates = (state) => state.models.filter((model) => model.startsWith(`${state.providers[state.provider]}/`));
|
|
411
|
+
const state = await runWizard({
|
|
412
|
+
choice: choices[0],
|
|
413
|
+
provider: 0,
|
|
414
|
+
model: 0,
|
|
415
|
+
variant: 0,
|
|
416
|
+
models: [],
|
|
417
|
+
providers: [],
|
|
418
|
+
variants: null,
|
|
419
|
+
manual: null,
|
|
420
|
+
}, [
|
|
421
|
+
{
|
|
422
|
+
run: async (current_, canBack) => {
|
|
423
|
+
const result = await selectOption(`Agent: ${name}`, choices, process.stdin, process.stderr, Math.max(0, choices.indexOf(current_.choice)), undefined, allowBack || canBack);
|
|
424
|
+
if (isBack(result))
|
|
425
|
+
return result;
|
|
426
|
+
return { ...current_, choice: choices[value(result)] };
|
|
427
|
+
},
|
|
428
|
+
},
|
|
429
|
+
{
|
|
430
|
+
// The catalog is fetched on the provider screen (after the model
|
|
431
|
+
// choice), so the catalog-unavailable fallback keeps its exact order.
|
|
432
|
+
enabled: changing,
|
|
433
|
+
run: async (current_, canBack) => {
|
|
434
|
+
const fallback = async () => {
|
|
435
|
+
process.stderr.write("Start opencode in another terminal to browse models, or enter provider/model manually\n");
|
|
436
|
+
const model = validateModel(value(await promptText("Model (provider/model)")));
|
|
437
|
+
const answer = await promptText(current?.variant ? "Variant (empty keeps current)" : "Variant (optional)");
|
|
438
|
+
const variant = (answer ? validateVariant(answer) : current?.variant) ?? null;
|
|
439
|
+
return { ...current_, manual: { model, variant } };
|
|
440
|
+
};
|
|
441
|
+
let providers = current_.providers;
|
|
442
|
+
let models = current_.models;
|
|
443
|
+
if (!models.length) {
|
|
444
|
+
try {
|
|
445
|
+
models = await availableModels();
|
|
446
|
+
}
|
|
447
|
+
catch (error) {
|
|
448
|
+
if (!(error instanceof LifecycleError && error.code === "catalog_unavailable"))
|
|
449
|
+
throw error;
|
|
450
|
+
return fallback();
|
|
451
|
+
}
|
|
452
|
+
providers = [...new Set(models.map((model) => model.split("/", 1)[0]))].sort();
|
|
453
|
+
}
|
|
454
|
+
const result = await selectOption("Provider", providers, process.stdin, process.stderr, Math.min(current_.provider, providers.length - 1), undefined, canBack);
|
|
455
|
+
if (isBack(result))
|
|
456
|
+
return result;
|
|
457
|
+
const provider = value(result);
|
|
458
|
+
return { ...current_, models, providers, provider, model: 0, variants: null };
|
|
459
|
+
},
|
|
460
|
+
},
|
|
461
|
+
{
|
|
462
|
+
enabled: (current_) => changing(current_) && current_.models.length > 0,
|
|
463
|
+
run: async (current_, canBack) => {
|
|
464
|
+
const available = candidates(current_);
|
|
465
|
+
const result = await selectOption("Model", available, process.stdin, process.stderr, Math.min(current_.model, available.length - 1), undefined, canBack);
|
|
466
|
+
if (isBack(result))
|
|
467
|
+
return result;
|
|
468
|
+
const model = value(result);
|
|
469
|
+
const variants = await availableModelVariants(available[model]);
|
|
470
|
+
const variant = variants && current?.variant ? Math.max(0, variants.indexOf(current.variant) + 1) : 0;
|
|
471
|
+
return { ...current_, model, variants, variant };
|
|
472
|
+
},
|
|
473
|
+
},
|
|
474
|
+
{
|
|
475
|
+
enabled: (current_) => changing(current_) && current_.variants !== null && current_.variants.length > 0,
|
|
476
|
+
run: async (current_, canBack) => {
|
|
477
|
+
const variants = current_.variants;
|
|
478
|
+
const labels = variants.map((item) => item === current?.variant ? `${item} (default)` : item);
|
|
479
|
+
const result = await selectOption("Variant", ["(none)", ...labels], process.stdin, process.stderr, Math.min(current_.variant, labels.length), undefined, canBack);
|
|
480
|
+
if (isBack(result))
|
|
481
|
+
return result;
|
|
482
|
+
return { ...current_, variant: value(result) };
|
|
483
|
+
},
|
|
484
|
+
},
|
|
485
|
+
]);
|
|
486
|
+
if (state.manual)
|
|
487
|
+
return { action: "model-set", name, ...state.manual };
|
|
488
|
+
if (state.choice.startsWith("Keep current"))
|
|
365
489
|
return { action: "model-set", name, model: current.model, variant: current.variant ?? null };
|
|
366
|
-
if (choice === "Clear variant")
|
|
490
|
+
if (state.choice === "Clear variant")
|
|
367
491
|
return { action: "model-set", name, model: current.model, variant: null };
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
const variants = await availableModelVariants(model);
|
|
375
|
-
let variant = null;
|
|
376
|
-
if (variants === null) {
|
|
377
|
-
// The variants metadata is unavailable: say so honestly and accept a
|
|
378
|
-
// manual variant instead of degrading the whole model selection.
|
|
379
|
-
process.stderr.write("Model variants are unknown (variants metadata unavailable); enter one manually or leave it empty.\n");
|
|
380
|
-
const answer = await promptText(current?.variant ? "Variant (empty keeps current)" : "Variant (optional)");
|
|
381
|
-
variant = (answer ? validateVariant(answer) : current?.variant) ?? null;
|
|
382
|
-
}
|
|
383
|
-
else if (variants.length) {
|
|
384
|
-
const labels = variants.map((item) => item === current?.variant ? `${item} (default)` : item);
|
|
385
|
-
const choice = value(await selectOption("Variant", ["(none)", ...labels], undefined, undefined, current?.variant ? variants.indexOf(current.variant) + 1 : 0));
|
|
386
|
-
variant = choice === 0 ? null : (variants[choice - 1] ?? null);
|
|
387
|
-
}
|
|
388
|
-
return { action: "model-set", name, model, variant };
|
|
389
|
-
}
|
|
390
|
-
catch (error) {
|
|
391
|
-
if (!(error instanceof LifecycleError && error.code === "catalog_unavailable"))
|
|
392
|
-
throw error;
|
|
393
|
-
process.stderr.write("Start opencode in another terminal to browse models, or enter provider/model manually\n");
|
|
394
|
-
const model = validateModel(value(await promptText("Model (provider/model)")));
|
|
492
|
+
const chosen = candidates(state)[state.model];
|
|
493
|
+
let variant = null;
|
|
494
|
+
if (state.variants === null) {
|
|
495
|
+
// The variants metadata is unavailable: say so honestly and accept a
|
|
496
|
+
// manual variant instead of degrading the whole model selection.
|
|
497
|
+
process.stderr.write("Model variants are unknown (variants metadata unavailable); enter one manually or leave it empty.\n");
|
|
395
498
|
const answer = await promptText(current?.variant ? "Variant (empty keeps current)" : "Variant (optional)");
|
|
396
|
-
|
|
397
|
-
return { action: "model-set", name, model, variant };
|
|
499
|
+
variant = (answer ? validateVariant(answer) : current?.variant) ?? null;
|
|
398
500
|
}
|
|
501
|
+
else if (state.variants.length) {
|
|
502
|
+
variant = state.variant === 0 ? null : (state.variants[state.variant - 1] ?? null);
|
|
503
|
+
}
|
|
504
|
+
return { action: "model-set", name, model: chosen, variant };
|
|
399
505
|
}
|
|
400
506
|
async function profileDraft(options, names) {
|
|
401
507
|
const inventory = await listAgentProfiles(options.scope);
|
|
@@ -405,15 +511,20 @@ async function profileDraft(options, names) {
|
|
|
405
511
|
const changes = [];
|
|
406
512
|
while (true) {
|
|
407
513
|
const actions = ["Configure model", "Add critic", "Remove additional critic", "Done"];
|
|
408
|
-
const
|
|
514
|
+
const actionIndex = value(await selectOption("Agent models and critics (changes are staged until confirmation)", actions, process.stdin, process.stderr, 3));
|
|
515
|
+
if (isBack(actionIndex))
|
|
516
|
+
fail("cancelled", "Wizard cancelled; no changes applied");
|
|
517
|
+
const action = actions[actionIndex];
|
|
409
518
|
if (action === "Done")
|
|
410
519
|
return changes;
|
|
411
520
|
if (action === "Add critic") {
|
|
412
521
|
const name = await promptCriticName();
|
|
413
522
|
if (records.has(name))
|
|
414
523
|
fail("profile_exists", `Profile already exists: ${name}`);
|
|
415
|
-
const request =
|
|
416
|
-
|
|
524
|
+
const request = await modelSelection(options, name, undefined, true);
|
|
525
|
+
if (isBack(request))
|
|
526
|
+
continue;
|
|
527
|
+
changes.push({ ...request, action: "critic-add" });
|
|
417
528
|
names.push(name);
|
|
418
529
|
records.set(name, {
|
|
419
530
|
name,
|
|
@@ -431,14 +542,19 @@ async function profileDraft(options, names) {
|
|
|
431
542
|
process.stderr.write("No eligible profiles; choose another action.\n");
|
|
432
543
|
continue;
|
|
433
544
|
}
|
|
434
|
-
const
|
|
545
|
+
const selected = await selectOption("Agent", candidates, process.stdin, process.stderr, 0, undefined, true);
|
|
546
|
+
if (isBack(selected))
|
|
547
|
+
continue;
|
|
548
|
+
const name = candidates[value(selected)];
|
|
435
549
|
if (action === "Remove additional critic") {
|
|
436
550
|
changes.push({ action: "critic-remove", name });
|
|
437
551
|
names = names.filter((candidate) => candidate !== name);
|
|
438
552
|
records.delete(name);
|
|
439
553
|
}
|
|
440
554
|
else {
|
|
441
|
-
const request = await modelSelection(options, name, records.get(name));
|
|
555
|
+
const request = await modelSelection(options, name, records.get(name), true);
|
|
556
|
+
if (isBack(request))
|
|
557
|
+
continue;
|
|
442
558
|
changes.push(request);
|
|
443
559
|
records.set(name, {
|
|
444
560
|
...records.get(name),
|
|
@@ -449,17 +565,24 @@ async function profileDraft(options, names) {
|
|
|
449
565
|
}
|
|
450
566
|
}
|
|
451
567
|
}
|
|
568
|
+
// Descriptions for the adapter multiselect come from the command registry
|
|
569
|
+
// (one-to-one catalog data), with an explicit note for commandless skills.
|
|
570
|
+
function commandHints() {
|
|
571
|
+
const descriptions = new Map(COMMAND_REGISTRY.filter((command) => "skill" in command).map((command) => [command.name, command.description.split(" Russian trigger:")[0]]));
|
|
572
|
+
return SKILL_COMMANDS.map((name) => descriptions.get(name) ?? `No adapter: ${name} loads through the native Skill tool`);
|
|
573
|
+
}
|
|
452
574
|
async function componentSelection(options) {
|
|
453
|
-
const
|
|
454
|
-
|
|
455
|
-
|
|
575
|
+
const agents = [...FIXED_AGENT_ROLES];
|
|
576
|
+
const supplied = [options.commands, options.plugins].some((selection) => selection !== undefined);
|
|
577
|
+
if (supplied && (!options.commands || !options.plugins))
|
|
578
|
+
fail("invalid_input", "Supply --commands and --plugins together (none selects an empty set)");
|
|
456
579
|
const overrides = supplied
|
|
457
580
|
? {
|
|
458
581
|
commands: options.commands,
|
|
459
|
-
agents
|
|
582
|
+
agents,
|
|
460
583
|
plugins: options.plugins,
|
|
461
584
|
}
|
|
462
|
-
: {};
|
|
585
|
+
: { agents };
|
|
463
586
|
const saved = await installedSelection(options.scope, undefined, undefined, overrides);
|
|
464
587
|
const defaults = saved ?? defaultSelection();
|
|
465
588
|
if (supplied) {
|
|
@@ -470,16 +593,45 @@ async function componentSelection(options) {
|
|
|
470
593
|
}
|
|
471
594
|
if (!tty()) {
|
|
472
595
|
if (!saved)
|
|
473
|
-
fail("terminal_required", "First install outside a terminal requires --commands
|
|
474
|
-
return { ...saved, core_activation: options.core ?? saved.core_activation };
|
|
596
|
+
fail("terminal_required", "First install outside a terminal requires --commands and --plugins");
|
|
597
|
+
return { ...saved, agents, core_activation: options.core ?? saved.core_activation };
|
|
475
598
|
}
|
|
476
|
-
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`);
|
|
477
|
-
const
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
599
|
+
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.\nAll six fixed agent roles deploy as one package; set their models or add critics with \`agentomatic configure agent\` afterwards.\n`);
|
|
600
|
+
const state = await runWizard({
|
|
601
|
+
commands: defaults.commands,
|
|
602
|
+
plugins: defaults.plugins,
|
|
603
|
+
core_activation: defaults.core_activation,
|
|
604
|
+
}, [
|
|
605
|
+
{
|
|
606
|
+
run: async (current, back) => {
|
|
607
|
+
const result = await selectOptions("Skill command adapters", SKILL_COMMANDS, current.commands, commandHints(), process.stdin, process.stderr, back);
|
|
608
|
+
if (isBack(result))
|
|
609
|
+
return result;
|
|
610
|
+
return { ...current, commands: value(result) };
|
|
611
|
+
},
|
|
612
|
+
},
|
|
613
|
+
{
|
|
614
|
+
run: async (current, back) => {
|
|
615
|
+
const result = await selectOptions("Optional plugins", SELECTABLE_PLUGINS, current.plugins, SELECTABLE_PLUGINS.map((plugin) => PLUGIN_DESCRIPTIONS[plugin]), process.stdin, process.stderr, back);
|
|
616
|
+
if (isBack(result))
|
|
617
|
+
return result;
|
|
618
|
+
return { ...current, plugins: value(result) };
|
|
619
|
+
},
|
|
620
|
+
},
|
|
621
|
+
{
|
|
622
|
+
run: async (current) => ({
|
|
623
|
+
...current,
|
|
624
|
+
core_activation: options.core ??
|
|
625
|
+
value(await confirmQuestion("Connect the OpenCode plugin and pin its npm dependency?", process.stdin, process.stderr, current.core_activation)),
|
|
626
|
+
}),
|
|
627
|
+
},
|
|
628
|
+
]);
|
|
629
|
+
return normalizeSelection({
|
|
630
|
+
commands: state.commands ?? [],
|
|
631
|
+
agents,
|
|
632
|
+
plugins: state.plugins,
|
|
633
|
+
core_activation: state.core_activation,
|
|
634
|
+
});
|
|
483
635
|
}
|
|
484
636
|
async function deploy(options, repair = false) {
|
|
485
637
|
const selection = repair
|
|
@@ -495,7 +647,8 @@ async function deploy(options, repair = false) {
|
|
|
495
647
|
if (!repair &&
|
|
496
648
|
(options.targets !== undefined ||
|
|
497
649
|
options.fragments !== undefined ||
|
|
498
|
-
(tty() &&
|
|
650
|
+
(tty() &&
|
|
651
|
+
value(await confirmQuestion("Configure presets for the selected agent harnesses as well? Presets merge permission rules, the secrets guard, and terminal settings into each harness config."))))) {
|
|
499
652
|
integration = await integrationSelection(options, true);
|
|
500
653
|
const chosenCore = integration.fragments.includes("core-plugin")
|
|
501
654
|
? true
|
|
@@ -543,8 +696,6 @@ async function deploy(options, repair = false) {
|
|
|
543
696
|
? [
|
|
544
697
|
"--commands",
|
|
545
698
|
selection.commands.join(",") || "none",
|
|
546
|
-
"--agents",
|
|
547
|
-
selection.agents.join(",") || "none",
|
|
548
699
|
"--plugins",
|
|
549
700
|
selection.plugins.join(",") || "none",
|
|
550
701
|
selection.core_activation ? "--core" : "--no-core",
|
|
@@ -569,7 +720,7 @@ async function deploy(options, repair = false) {
|
|
|
569
720
|
if (changes.length)
|
|
570
721
|
process.stdout.write(`Staged profiles: ${terminalSafe(JSON.stringify(changes))}\n`);
|
|
571
722
|
process.stdout.write("Components, npm, and application configuration are separate stages; a later failure does not roll back completed stages.\n");
|
|
572
|
-
await
|
|
723
|
+
await epilogue(options, true);
|
|
573
724
|
}
|
|
574
725
|
if (options.dryRun) {
|
|
575
726
|
if (options.json)
|
|
@@ -610,7 +761,7 @@ async function deploy(options, repair = false) {
|
|
|
610
761
|
json(result);
|
|
611
762
|
else {
|
|
612
763
|
process.stdout.write(renderPlan(applied, { applied: true }) + renderConfigSetup(connected, { applied: true }));
|
|
613
|
-
await
|
|
764
|
+
await epilogue(options, true);
|
|
614
765
|
}
|
|
615
766
|
}
|
|
616
767
|
catch (error) {
|
|
@@ -621,13 +772,15 @@ async function deploy(options, repair = false) {
|
|
|
621
772
|
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);
|
|
622
773
|
}
|
|
623
774
|
}
|
|
624
|
-
// The
|
|
625
|
-
//
|
|
626
|
-
// configure
|
|
627
|
-
async function
|
|
775
|
+
// The epilogue: install adds the fixed-role package overview first, then the
|
|
776
|
+
// critic panel, and every variant points at the canonical `agent list` view
|
|
777
|
+
// plus the configure agent mutation point.
|
|
778
|
+
async function epilogue(options, overview) {
|
|
628
779
|
if (options.json)
|
|
629
780
|
return;
|
|
630
781
|
const inventory = await listAgentProfiles(options.scope);
|
|
782
|
+
if (overview)
|
|
783
|
+
process.stdout.write(renderFixedRoles(inventory));
|
|
631
784
|
process.stdout.write(renderCriticsTable(inventory));
|
|
632
785
|
process.stdout.write("Inspect any time: agentomatic agent list | Manage critics: agentomatic configure agent\n");
|
|
633
786
|
}
|
|
@@ -650,9 +803,50 @@ function partial(options, error, completed, next, requiresRestart = false) {
|
|
|
650
803
|
}
|
|
651
804
|
class ReportedError extends Error {
|
|
652
805
|
}
|
|
806
|
+
function configDivergence(plan) {
|
|
807
|
+
const changed = plan.operations.filter((operation) => operation.operation === "create" || operation.operation === "update");
|
|
808
|
+
const conflicts = plan.operations.some((operation) => operation.operation === "conflict");
|
|
809
|
+
const targets = CONFIG_TARGETS.filter((target) => changed.some((operation) => operation.target === target));
|
|
810
|
+
const fragments = CONFIG_FRAGMENTS.map((fragment) => fragment.name).filter((name) => changed.some((operation) => operation.fragment === name));
|
|
811
|
+
const connection = changed.some((operation) => operation.fragment === "core-plugin" || operation.fragment === "core-disable");
|
|
812
|
+
return { targets, fragments, connection, conflicts, changes: changed.length };
|
|
813
|
+
}
|
|
653
814
|
async function configureIntegration(options) {
|
|
654
815
|
requireApply(options);
|
|
655
|
-
const
|
|
816
|
+
const supplied = options.targets !== undefined || options.fragments !== undefined;
|
|
817
|
+
let selection;
|
|
818
|
+
if (supplied) {
|
|
819
|
+
selection = await integrationSelection(options);
|
|
820
|
+
}
|
|
821
|
+
else {
|
|
822
|
+
if (!tty())
|
|
823
|
+
fail("terminal_required", "configure integration outside a terminal requires --targets and --fragments");
|
|
824
|
+
// Diff before the first screen: an already-satisfied state asks nothing.
|
|
825
|
+
const defaults = await defaultConfigSelection(options.scope);
|
|
826
|
+
const baseline = await previewConfigSetup(defaults, options.scope, undefined, undefined, false, false);
|
|
827
|
+
const divergence = configDivergence(baseline);
|
|
828
|
+
const dependency = !options.noDependency &&
|
|
829
|
+
defaults.targets.includes("opencode") &&
|
|
830
|
+
defaults.fragments.includes("core-plugin")
|
|
831
|
+
? planDependency(options.scope)
|
|
832
|
+
: undefined;
|
|
833
|
+
const settled = !dependency || dependency.status === "satisfied" || dependency.status === "manual";
|
|
834
|
+
if (divergence.changes === 0 && !divergence.conflicts && settled) {
|
|
835
|
+
if (options.json)
|
|
836
|
+
json({ status: "ok", applied: false, plan: baseline, noop: true });
|
|
837
|
+
else {
|
|
838
|
+
process.stdout.write(renderConfigNoop(baseline));
|
|
839
|
+
await epilogue(options, false);
|
|
840
|
+
process.stdout.write("Done: nothing to apply.\n");
|
|
841
|
+
}
|
|
842
|
+
return;
|
|
843
|
+
}
|
|
844
|
+
selection = await integrationSelection(options, false, {
|
|
845
|
+
targets: divergence.targets.length ? divergence.targets : undefined,
|
|
846
|
+
fragments: divergence.fragments,
|
|
847
|
+
connection: divergence.connection,
|
|
848
|
+
});
|
|
849
|
+
}
|
|
656
850
|
const plan = await previewConfigSetup(selection, options.scope, undefined, undefined, !options.noDependency);
|
|
657
851
|
if (!options.json)
|
|
658
852
|
process.stdout.write(renderConfigSetup(plan, {
|
|
@@ -688,8 +882,10 @@ async function configureIntegration(options) {
|
|
|
688
882
|
requires_restart: result.requires_restart,
|
|
689
883
|
});
|
|
690
884
|
else {
|
|
691
|
-
|
|
692
|
-
|
|
885
|
+
await epilogue(options, false);
|
|
886
|
+
process.stdout.write(result.requires_restart
|
|
887
|
+
? "Done: configuration applied; restart the host session.\n"
|
|
888
|
+
: "Done: configuration applied.\n");
|
|
693
889
|
}
|
|
694
890
|
}
|
|
695
891
|
catch (error) {
|
|
@@ -738,7 +934,7 @@ async function profileChange(options, request) {
|
|
|
738
934
|
else {
|
|
739
935
|
process.stdout.write(renderPlan(result.plan, { applied: true }));
|
|
740
936
|
// After a panel mutation the fresh table closes the loop.
|
|
741
|
-
await
|
|
937
|
+
await epilogue(options, false);
|
|
742
938
|
}
|
|
743
939
|
}
|
|
744
940
|
async function uninstall(options) {
|
|
@@ -871,6 +1067,8 @@ async function run(args) {
|
|
|
871
1067
|
"Agent models and critics",
|
|
872
1068
|
"Application connection and presets",
|
|
873
1069
|
]));
|
|
1070
|
+
if (isBack(selected))
|
|
1071
|
+
fail("cancelled", "Wizard cancelled; no changes applied");
|
|
874
1072
|
topic = routes[selected];
|
|
875
1073
|
}
|
|
876
1074
|
if (topic === "install" || topic === "configure components" || topic === "maintenance repair") {
|
|
@@ -956,10 +1154,14 @@ async function run(args) {
|
|
|
956
1154
|
// configure agent on a fresh critic-<name> adds the critic: one command
|
|
957
1155
|
// covers model-set for existing profiles and critic-add for new critics.
|
|
958
1156
|
const request = await modelSelection(options, name, undefined);
|
|
1157
|
+
if (isBack(request))
|
|
1158
|
+
fail("cancelled", "Wizard cancelled; no changes applied");
|
|
959
1159
|
await profileChange(options, { ...request, action: "critic-add" });
|
|
960
1160
|
return;
|
|
961
1161
|
}
|
|
962
1162
|
const request = await modelSelection(options, name, existing);
|
|
1163
|
+
if (isBack(request))
|
|
1164
|
+
fail("cancelled", "Wizard cancelled; no changes applied");
|
|
963
1165
|
await profileChange(options, { ...request, action: "model-set" });
|
|
964
1166
|
return;
|
|
965
1167
|
}
|
package/dist/config-setup.d.ts
CHANGED
|
@@ -5,6 +5,7 @@ export declare class ConfigSetupError extends LifecycleError {
|
|
|
5
5
|
export declare const CONFIG_TARGETS: readonly ["opencode", "kilo", "mimo"];
|
|
6
6
|
export type ConfigTargetName = (typeof CONFIG_TARGETS)[number];
|
|
7
7
|
export type TargetFileKind = "main" | "tui";
|
|
8
|
+
export declare const TARGET_DESCRIPTIONS: Record<ConfigTargetName, string>;
|
|
8
9
|
export declare const CONFIG_FRAGMENTS: readonly [{
|
|
9
10
|
readonly name: "core-disable";
|
|
10
11
|
readonly description: "Disconnect only the agentomatic plugin, preserving unrelated configuration";
|
package/dist/config-setup.js
CHANGED
|
@@ -50,6 +50,13 @@ const SECRET_PATHS = [
|
|
|
50
50
|
export class ConfigSetupError extends LifecycleError {
|
|
51
51
|
}
|
|
52
52
|
export const CONFIG_TARGETS = ["opencode", "kilo", "mimo"];
|
|
53
|
+
// One-line descriptions for the interactive harness prompt, kept next to the
|
|
54
|
+
// target list so the wizard never invents them ad hoc.
|
|
55
|
+
export const TARGET_DESCRIPTIONS = {
|
|
56
|
+
opencode: "OpenCode: config opencode.json(c) and terminal cli.json",
|
|
57
|
+
kilo: "Kilo Code: config kilo.json(c) and TUI settings",
|
|
58
|
+
mimo: "MiMo: config mimocode.json(c) and TUI settings",
|
|
59
|
+
};
|
|
53
60
|
export const CONFIG_FRAGMENTS = [
|
|
54
61
|
{
|
|
55
62
|
name: "core-disable",
|
package/dist/installer.d.ts
CHANGED
|
@@ -62,6 +62,7 @@ export type ArchiveCandidate = {
|
|
|
62
62
|
};
|
|
63
63
|
export declare const SELECTABLE_PLUGINS: SelectablePlugin[];
|
|
64
64
|
export declare const SKILL_COMMANDS: string[];
|
|
65
|
+
export declare const PLUGIN_DESCRIPTIONS: Record<SelectablePlugin, string>;
|
|
65
66
|
export declare function defaultSelection(): InstallerSelection;
|
|
66
67
|
export declare function savedSelection(manifest: Pick<Manifest, "commands" | "agents" | "plugins">): Partial<InstallerSelection>;
|
|
67
68
|
export declare function installedSelection(scope: Scope, cwd?: string, home?: string, overrides?: Partial<InstallerSelection>): Promise<InstallerSelection | null>;
|
package/dist/installer.js
CHANGED
|
@@ -28,6 +28,14 @@ function retiredAssetPaths() {
|
|
|
28
28
|
export const SELECTABLE_PLUGINS = [...CATALOG.plugins];
|
|
29
29
|
export const SKILL_COMMANDS = [...CATALOG.skills];
|
|
30
30
|
const PACKAGE_COMMANDS = [...CATALOG.package_commands];
|
|
31
|
+
// One-line descriptions for the interactive wrappers prompt, kept next to the
|
|
32
|
+
// selectable set so the wizard never invents them ad hoc.
|
|
33
|
+
export const PLUGIN_DESCRIPTIONS = {
|
|
34
|
+
"code-simplify": "Injects the code-simplify prevention ladder into agent sessions",
|
|
35
|
+
"rules-injector": "Injects AGENTS.md rules into the session on demand",
|
|
36
|
+
rtk: "Compresses verbose shell tool output through the RTK CLI",
|
|
37
|
+
"zed-bell": "Plays a terminal bell on session events (Zed integrated)",
|
|
38
|
+
};
|
|
31
39
|
export function defaultSelection() {
|
|
32
40
|
return {
|
|
33
41
|
commands: [...SKILL_COMMANDS].sort(),
|
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
import type { Readable, Writable } from "node:stream";
|
|
2
|
-
export declare
|
|
3
|
-
export
|
|
2
|
+
export declare const BACK: unique symbol;
|
|
3
|
+
export type BackSignal = typeof BACK;
|
|
4
|
+
export declare function isBack(value: unknown): value is BackSignal;
|
|
5
|
+
export type WizardStep<T> = {
|
|
6
|
+
enabled?: (state: T) => boolean;
|
|
7
|
+
run: (state: T, back: boolean) => Promise<T | BackSignal>;
|
|
8
|
+
};
|
|
9
|
+
export declare function runWizard<T>(initial: T, steps: ReadonlyArray<WizardStep<T>>): Promise<T>;
|
|
10
|
+
export declare function truncateLine(value: string, width: number): string;
|
|
11
|
+
export declare function optionLabel(name: string, description: string | undefined, columns: number): string;
|
|
12
|
+
export declare function selectOption(label: string, options: readonly string[], stdin?: Readable, stderr?: Writable, initial?: number, descriptions?: readonly (string | undefined)[], back?: boolean): Promise<number | null | BackSignal>;
|
|
13
|
+
export declare function selectOptions(label: string, options: readonly string[], initialSelected?: readonly string[], descriptions?: readonly (string | undefined)[], stdin?: Readable, stderr?: Writable, back?: boolean): Promise<string[] | null | BackSignal>;
|
|
4
14
|
export declare function promptText(label: string, stdin?: Readable, stderr?: Writable): Promise<string | null>;
|
|
5
15
|
export declare function confirmQuestion(label: string, stdin?: Readable, stderr?: Writable, initialValue?: boolean): Promise<boolean | null>;
|
package/dist/terminal-wizard.js
CHANGED
|
@@ -1,5 +1,41 @@
|
|
|
1
1
|
import * as clack from "@clack/prompts";
|
|
2
2
|
import { InstallerError } from "./installer.js";
|
|
3
|
+
// Sentinel returned when the user chooses the "Back" item; `runWizard` steps
|
|
4
|
+
// back to the previous enabled screen and restores the saved answer.
|
|
5
|
+
export const BACK = Symbol("wizard-back");
|
|
6
|
+
export function isBack(value) {
|
|
7
|
+
return value === BACK;
|
|
8
|
+
}
|
|
9
|
+
const BACK_VALUE = "$__agentomatic_back__";
|
|
10
|
+
const BACK_LABEL = "← Back";
|
|
11
|
+
export async function runWizard(initial, steps) {
|
|
12
|
+
const enabled = (index, state) => !steps[index].enabled || steps[index].enabled(state);
|
|
13
|
+
let state = initial;
|
|
14
|
+
let index = 0;
|
|
15
|
+
while (index < steps.length) {
|
|
16
|
+
if (!enabled(index, state)) {
|
|
17
|
+
index += 1;
|
|
18
|
+
continue;
|
|
19
|
+
}
|
|
20
|
+
let previous = index - 1;
|
|
21
|
+
while (previous >= 0 && !enabled(previous, state))
|
|
22
|
+
previous -= 1;
|
|
23
|
+
const result = await steps[index].run(state, previous >= 0);
|
|
24
|
+
if (result === BACK) {
|
|
25
|
+
if (previous < 0)
|
|
26
|
+
continue;
|
|
27
|
+
index = previous;
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
30
|
+
state = result;
|
|
31
|
+
index += 1;
|
|
32
|
+
}
|
|
33
|
+
return state;
|
|
34
|
+
}
|
|
35
|
+
const DEFAULT_COLUMNS = 80;
|
|
36
|
+
// clack renders a guide and a state marker ("│ ◻ ") before the label; keep the
|
|
37
|
+
// composed label inside the remaining terminal columns so it never wraps.
|
|
38
|
+
const LABEL_RESERVE = 8;
|
|
3
39
|
function interactiveStreams(stdin, stderr) {
|
|
4
40
|
if (!isTTY(stdin) || !isTTY(stderr))
|
|
5
41
|
throw new InstallerError("terminal_required", "This operation requires an interactive terminal");
|
|
@@ -13,21 +49,55 @@ function validateOptions(options) {
|
|
|
13
49
|
if (new Set(options).size !== options.length)
|
|
14
50
|
throw new InstallerError("invalid_input", "Interactive selector options must be unique");
|
|
15
51
|
}
|
|
16
|
-
|
|
52
|
+
function terminalColumns(stderr) {
|
|
53
|
+
const columns = stderr.columns;
|
|
54
|
+
return Number.isFinite(columns) && (columns ?? 0) > 0 ? columns : DEFAULT_COLUMNS;
|
|
55
|
+
}
|
|
56
|
+
// Long single-line values are cut to the terminal width with a trailing
|
|
57
|
+
// ellipsis; the description is never silently dropped for a normal terminal.
|
|
58
|
+
export function truncateLine(value, width) {
|
|
59
|
+
if (width <= 1)
|
|
60
|
+
return "…";
|
|
61
|
+
return [...value].length <= width ? value : `${[...value].slice(0, width - 1).join("")}…`;
|
|
62
|
+
}
|
|
63
|
+
// Every option label carries its data-driven description so the text is
|
|
64
|
+
// visible on every line, not only under the cursor (clack renders `hint` for
|
|
65
|
+
// the focused option only).
|
|
66
|
+
export function optionLabel(name, description, columns) {
|
|
67
|
+
const combined = description ? `${name} — ${description}` : name;
|
|
68
|
+
return truncateLine(combined, Math.max(12, columns - LABEL_RESERVE));
|
|
69
|
+
}
|
|
70
|
+
function backOption() {
|
|
71
|
+
return { value: BACK_VALUE, label: BACK_LABEL };
|
|
72
|
+
}
|
|
73
|
+
export async function selectOption(label, options, stdin = process.stdin, stderr = process.stderr, initial = 0, descriptions, back = false) {
|
|
17
74
|
interactiveStreams(stdin, stderr);
|
|
18
75
|
validateOptions(options);
|
|
76
|
+
const columns = terminalColumns(stderr);
|
|
77
|
+
const entries = options.map((option, index) => ({
|
|
78
|
+
value: String(index),
|
|
79
|
+
label: optionLabel(option, descriptions?.[index], columns),
|
|
80
|
+
}));
|
|
81
|
+
if (back)
|
|
82
|
+
entries.push(backOption());
|
|
19
83
|
const value = await clack.select({
|
|
20
84
|
message: label,
|
|
21
|
-
options:
|
|
22
|
-
initialValue: Math.max(0, Math.min(initial, options.length - 1)),
|
|
85
|
+
options: entries,
|
|
86
|
+
initialValue: String(Math.max(0, Math.min(initial, options.length - 1))),
|
|
23
87
|
input: stdin,
|
|
24
88
|
output: stderr,
|
|
25
89
|
});
|
|
26
90
|
if (clack.isCancel(value))
|
|
27
91
|
return null;
|
|
28
|
-
|
|
92
|
+
if (value === BACK_VALUE)
|
|
93
|
+
return BACK;
|
|
94
|
+
return Number(value);
|
|
29
95
|
}
|
|
30
|
-
|
|
96
|
+
// The multiselect contract: each option carries a data-driven description and
|
|
97
|
+
// the message states the all/none controls, so every group is self-explanatory.
|
|
98
|
+
// SIMPLIFY: Back is a normal option, so the all/none key also selects it -> upgrade to a dedicated Back key if clack exposes one.
|
|
99
|
+
const MULTISELECT_CONTROLS = " (move: up/down; toggle: space; all/none: a; enter confirms)";
|
|
100
|
+
export async function selectOptions(label, options, initialSelected = [], descriptions, stdin = process.stdin, stderr = process.stderr, back = false) {
|
|
31
101
|
interactiveStreams(stdin, stderr);
|
|
32
102
|
validateOptions(options);
|
|
33
103
|
const allowed = new Set(options);
|
|
@@ -35,9 +105,16 @@ export async function selectOptions(label, options, initialSelected = [], stdin
|
|
|
35
105
|
if (!allowed.has(value))
|
|
36
106
|
throw new InstallerError("invalid_input", `Unknown initial selector option: ${value}`);
|
|
37
107
|
}
|
|
108
|
+
const columns = terminalColumns(stderr);
|
|
109
|
+
const entries = options.map((option, index) => ({
|
|
110
|
+
value: option,
|
|
111
|
+
label: optionLabel(option, descriptions?.[index], columns),
|
|
112
|
+
}));
|
|
113
|
+
if (back)
|
|
114
|
+
entries.push(backOption());
|
|
38
115
|
const value = await clack.multiselect({
|
|
39
|
-
message: label
|
|
40
|
-
options:
|
|
116
|
+
message: `${label}${MULTISELECT_CONTROLS}`,
|
|
117
|
+
options: entries,
|
|
41
118
|
initialValues: [...initialSelected],
|
|
42
119
|
required: false,
|
|
43
120
|
input: stdin,
|
|
@@ -45,7 +122,10 @@ export async function selectOptions(label, options, initialSelected = [], stdin
|
|
|
45
122
|
});
|
|
46
123
|
if (clack.isCancel(value))
|
|
47
124
|
return null;
|
|
48
|
-
|
|
125
|
+
const selected = value;
|
|
126
|
+
if (selected.includes(BACK_VALUE))
|
|
127
|
+
return BACK;
|
|
128
|
+
return selected;
|
|
49
129
|
}
|
|
50
130
|
export async function promptText(label, stdin = process.stdin, stderr = process.stderr) {
|
|
51
131
|
interactiveStreams(stdin, stderr);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kisev/agentomatic",
|
|
3
|
-
"version": "12.0.1-dev.
|
|
3
|
+
"version": "12.0.1-dev.131.gfdcd7e5c228f",
|
|
4
4
|
"description": "OpenCode integration and opt-in installer for portable Agent Skills.",
|
|
5
5
|
"homepage": "https://github.com/kisev/skills#readme",
|
|
6
6
|
"bugs": {
|
|
@@ -50,7 +50,7 @@
|
|
|
50
50
|
},
|
|
51
51
|
"dependencies": {
|
|
52
52
|
"@clack/prompts": "^1.8.1",
|
|
53
|
-
"@kisev/safe-fs": "12.0.1-dev.
|
|
53
|
+
"@kisev/safe-fs": "12.0.1-dev.131.gfdcd7e5c228f",
|
|
54
54
|
"commander": "15.0.0",
|
|
55
55
|
"zod": "4.1.8"
|
|
56
56
|
},
|