@kisev/agentomatic 12.0.1-dev.130.g78ea8685c8d4 → 12.0.1-dev.132.g6d50bbe22d9e
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/dist/cli-output.d.ts +6 -2
- package/dist/cli-output.js +87 -35
- package/dist/command-cli.js +260 -67
- package/dist/terminal-wizard.d.ts +12 -2
- package/dist/terminal-wizard.js +84 -11
- package/package.json +3 -3
package/dist/cli-output.d.ts
CHANGED
|
@@ -6,18 +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;
|
|
20
|
-
export declare function renderFixedRoles(inventory: AgentInventory): string;
|
|
23
|
+
export declare function renderCriticsTable(inventory: AgentInventory, color?: boolean): string;
|
|
24
|
+
export declare function renderFixedRoles(inventory: AgentInventory, color?: boolean): string;
|
|
21
25
|
export declare function renderInventory(inventory: AgentInventory): string;
|
|
22
26
|
export declare function shellCommand(arguments_: readonly string[]): string;
|
|
23
27
|
export declare function scopeArguments(scope: Scope): [] | ["--global"];
|
package/dist/cli-output.js
CHANGED
|
@@ -34,6 +34,36 @@ export function terminalSafe(value) {
|
|
|
34
34
|
})
|
|
35
35
|
.join("");
|
|
36
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
|
+
};
|
|
37
67
|
function groupFor(path) {
|
|
38
68
|
if (path.startsWith("agents/"))
|
|
39
69
|
return "Agents";
|
|
@@ -53,10 +83,13 @@ function actionName(action) {
|
|
|
53
83
|
reconcile: "Repair selected agents",
|
|
54
84
|
}[action];
|
|
55
85
|
}
|
|
56
|
-
function operationSummary(operations) {
|
|
86
|
+
function operationSummary(operations, color) {
|
|
57
87
|
return OPERATIONS.map((operation) => {
|
|
58
88
|
const count = operations.filter((item) => item.operation === operation).length;
|
|
59
|
-
|
|
89
|
+
if (!count)
|
|
90
|
+
return undefined;
|
|
91
|
+
const spec = OPERATION_STYLES[operation];
|
|
92
|
+
return paint(color, spec.code, `${spec.symbol} ${operation} ${count}`);
|
|
60
93
|
})
|
|
61
94
|
.filter(Boolean)
|
|
62
95
|
.join(", ");
|
|
@@ -67,7 +100,7 @@ function shortPath(path, group) {
|
|
|
67
100
|
const value = path.slice(path.indexOf("/") + 1);
|
|
68
101
|
return terminalSafe(value.endsWith(".md") || value.endsWith(".js") ? value.slice(0, -3) : value);
|
|
69
102
|
}
|
|
70
|
-
function detailLines(operations) {
|
|
103
|
+
function detailLines(operations, color) {
|
|
71
104
|
const lines = [];
|
|
72
105
|
for (const group of GROUPS) {
|
|
73
106
|
const grouped = operations.filter((item) => groupFor(item.path) === group && item.operation !== "unchanged");
|
|
@@ -79,7 +112,8 @@ function detailLines(operations) {
|
|
|
79
112
|
: shortPath(item.path, group));
|
|
80
113
|
const visible = operation === "conflict" ? values : values.slice(0, 8);
|
|
81
114
|
const rest = values.length - visible.length;
|
|
82
|
-
|
|
115
|
+
const spec = OPERATION_STYLES[operation];
|
|
116
|
+
lines.push(` ${paint(color, spec.code, spec.symbol)} ${group}/${operation}: ${visible.join(", ")}${rest ? ` (+${rest} more)` : ""}`);
|
|
83
117
|
}
|
|
84
118
|
}
|
|
85
119
|
return lines;
|
|
@@ -91,9 +125,10 @@ function migrationSummary(operations) {
|
|
|
91
125
|
: undefined;
|
|
92
126
|
}
|
|
93
127
|
export function renderPlan(plan, options) {
|
|
128
|
+
const color = options.color ?? colorEnabled();
|
|
94
129
|
const version = "package_version" in plan ? ` @kisev/agentomatic ${plan.package_version}` : "";
|
|
95
130
|
const lines = [
|
|
96
|
-
`${actionName(plan.action)}${version} (${plan.scope})
|
|
131
|
+
paint(color, "1", `${actionName(plan.action)}${version} (${plan.scope})`),
|
|
97
132
|
`Target: ${terminalSafe(plan.root)}`,
|
|
98
133
|
...("selection" in plan
|
|
99
134
|
? [
|
|
@@ -102,72 +137,88 @@ export function renderPlan(plan, options) {
|
|
|
102
137
|
]
|
|
103
138
|
: []),
|
|
104
139
|
"",
|
|
105
|
-
options.applied ? "Applied changes:" : "Planned changes:",
|
|
140
|
+
paint(color, "1", options.applied ? "Applied changes:" : "Planned changes:"),
|
|
106
141
|
];
|
|
107
142
|
for (const group of GROUPS) {
|
|
108
143
|
const operations = plan.operations.filter((item) => groupFor(item.path) === group);
|
|
109
144
|
if (operations.length)
|
|
110
|
-
lines.push(` ${group}: ${operationSummary(operations)}`);
|
|
145
|
+
lines.push(` ${paint(color, "1", group)}: ${operationSummary(operations, color)}`);
|
|
111
146
|
}
|
|
112
147
|
const migration = migrationSummary(plan.operations);
|
|
113
148
|
if (migration)
|
|
114
149
|
lines.push(migration);
|
|
115
|
-
const details = detailLines(plan.operations);
|
|
150
|
+
const details = detailLines(plan.operations, color);
|
|
116
151
|
if (details.length)
|
|
117
|
-
lines.push("", "Details:", ...details);
|
|
152
|
+
lines.push("", paint(color, "1", "Details:"), ...details);
|
|
118
153
|
const conflicts = plan.operations.filter((item) => item.operation === "conflict").length;
|
|
119
|
-
lines.push("", `Conflicts: ${conflicts
|
|
154
|
+
lines.push("", `Conflicts: ${conflicts ? paint(color, "31", `${MARKS.bad} ${conflicts}`) : paint(color, "32", `${MARKS.ok} none`)}`);
|
|
120
155
|
const restartNote = "restart the host session so the deployed roles and settings load";
|
|
121
156
|
lines.push(plan.requires_restart
|
|
122
|
-
? `${options.applied ? "Restart required" : "Restart after apply"}: yes — ${restartNote}`
|
|
123
|
-
: `${options.applied ? "Restart required" : "Restart after apply"}: no`);
|
|
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`)}`);
|
|
124
159
|
if (!options.applied && options.applyHint)
|
|
125
|
-
lines.push("", "Apply (repeat non-interactively):", ` ${options.applyHint}`);
|
|
160
|
+
lines.push("", paint(color, "1", "Apply (repeat non-interactively):"), ` ${options.applyHint}`);
|
|
126
161
|
if (options.applied && options.hint)
|
|
127
|
-
lines.push("", "Next:", ` ${options.hint}`);
|
|
162
|
+
lines.push("", paint(color, "1", "Next:"), ` ${options.hint}`);
|
|
128
163
|
return `${lines.join("\n")}\n`;
|
|
129
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
|
+
}
|
|
130
174
|
export function renderConfigSetup(plan, options) {
|
|
175
|
+
const color = options.color ?? colorEnabled();
|
|
131
176
|
const lines = [
|
|
132
|
-
`Integration configuration @kisev/agentomatic ${plan.package_version} (${plan.scope})
|
|
177
|
+
paint(color, "1", `Integration configuration @kisev/agentomatic ${plan.package_version} (${plan.scope})`),
|
|
133
178
|
`Root: ${terminalSafe(plan.root)}`,
|
|
134
179
|
"",
|
|
135
180
|
];
|
|
136
181
|
if (plan.targets.length)
|
|
137
|
-
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"})`)}`));
|
|
138
183
|
else
|
|
139
184
|
lines.push("Targets: none");
|
|
140
185
|
const fragments = plan.operations.filter((item) => item.fragment !== "file");
|
|
141
186
|
if (plan.dependency)
|
|
142
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.");
|
|
143
188
|
if (fragments.length) {
|
|
144
|
-
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
|
+
}));
|
|
145
196
|
}
|
|
146
197
|
if (plan.skipped_fragments.length)
|
|
147
|
-
lines.push("", "Skipped fragments:", ...plan.skipped_fragments.map((item) => ` ${item.fragment}: ${terminalSafe(item.reason)}`));
|
|
148
|
-
const conflicts = plan.operations.filter((item) => item.operation === "conflict");
|
|
149
|
-
lines.push("", `Conflicts: ${conflicts.
|
|
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`)}`);
|
|
150
201
|
lines.push(plan.requires_restart
|
|
151
|
-
? `${options.applied ? "Restart required" : "Restart after apply"}: yes — restart the host session so the merged presets load`
|
|
152
|
-
: `${options.applied ? "Restart required" : "Restart after apply"}: no`);
|
|
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`)}`);
|
|
153
204
|
if (!options.applied) {
|
|
154
205
|
if (!plan.confirmable)
|
|
155
|
-
lines.push("", "No configuration changes are required.");
|
|
206
|
+
lines.push("", paint(color, "32", "No configuration changes are required."));
|
|
156
207
|
else if (options.applyHint)
|
|
157
|
-
lines.push("", "Apply (repeat non-interactively):", ` ${options.applyHint}`);
|
|
208
|
+
lines.push("", paint(color, "1", "Apply (repeat non-interactively):"), ` ${options.applyHint}`);
|
|
158
209
|
}
|
|
159
210
|
if (options.applied && options.hint)
|
|
160
|
-
lines.push("", "Next:", ` ${options.hint}`);
|
|
211
|
+
lines.push("", paint(color, "1", "Next:"), ` ${options.hint}`);
|
|
161
212
|
return `${lines.join("\n")}\n`;
|
|
162
213
|
}
|
|
163
|
-
function table(rows) {
|
|
214
|
+
function table(rows, color = false) {
|
|
164
215
|
const widths = rows[0].map((_, column) => Math.max(...rows.map((row) => row[column]?.length ?? 0)));
|
|
165
|
-
return rows.map((row) => row
|
|
216
|
+
return rows.map((row, index) => paint(color && index === 0, "1", row
|
|
166
217
|
.map((value, column) => value.padEnd(widths[column]))
|
|
167
218
|
.join(" ")
|
|
168
|
-
.trimEnd());
|
|
219
|
+
.trimEnd()));
|
|
169
220
|
}
|
|
170
|
-
export function renderCriticsTable(inventory) {
|
|
221
|
+
export function renderCriticsTable(inventory, color = colorEnabled()) {
|
|
171
222
|
// The panel keeps the package-owned fixed critic (`critic`) apart from the
|
|
172
223
|
// additional `critic-*` pool entries and points at the canonical overview.
|
|
173
224
|
const critics = inventory.profiles.filter((profile) => profile.ownership !== "user-owned" &&
|
|
@@ -178,31 +229,32 @@ export function renderCriticsTable(inventory) {
|
|
|
178
229
|
const rows = [
|
|
179
230
|
["NAME", "MODEL", "VARIANT", "PROVIDER"],
|
|
180
231
|
...critics.map((profile) => [
|
|
181
|
-
terminalSafe(profile.name)
|
|
232
|
+
`${profile.name === "critic" ? MARKS.item : MARKS.change} ${terminalSafe(profile.name)}`,
|
|
182
233
|
terminalSafe(profile.model ?? "default (host)"),
|
|
183
234
|
terminalSafe(profile.variant ?? "-"),
|
|
184
235
|
terminalSafe(profile.model?.split("/", 1)[0] ?? "-"),
|
|
185
236
|
]),
|
|
186
237
|
];
|
|
187
|
-
|
|
238
|
+
const head = paint(color, "1", "Critics:");
|
|
239
|
+
return (`${head}\n${table(rows, color).join("\n")}\n` +
|
|
188
240
|
`Pool: additional critics are optional specialist profiles (` +
|
|
189
241
|
`${pool.length ? pool.map((profile) => terminalSafe(profile.name)).join(", ") : "none"}); ` +
|
|
190
242
|
"the fixed critic always deploys with the package.\n");
|
|
191
243
|
}
|
|
192
|
-
export function renderFixedRoles(inventory) {
|
|
244
|
+
export function renderFixedRoles(inventory, color = colorEnabled()) {
|
|
193
245
|
const roles = inventory.profiles.filter((profile) => profile.ownership !== "user-owned" &&
|
|
194
246
|
FIXED_AGENT_ROLES.includes(profile.name));
|
|
195
247
|
const rows = [
|
|
196
248
|
["NAME", "MODEL", "VARIANT"],
|
|
197
249
|
...roles.map((profile) => [
|
|
198
|
-
terminalSafe(profile.name)
|
|
250
|
+
`${MARKS.item} ${terminalSafe(profile.name)}`,
|
|
199
251
|
terminalSafe(profile.model ?? "default (host)"),
|
|
200
252
|
terminalSafe(profile.variant ?? "-"),
|
|
201
253
|
]),
|
|
202
254
|
];
|
|
203
255
|
return [
|
|
204
|
-
"Fixed roles (all six belong to the package; models and critic composition: agentomatic configure agent):",
|
|
205
|
-
...table(rows),
|
|
256
|
+
paint(color, "1", "Fixed roles (all six belong to the package; models and critic composition: agentomatic configure agent):"),
|
|
257
|
+
...table(rows, color),
|
|
206
258
|
"",
|
|
207
259
|
].join("\n");
|
|
208
260
|
}
|
package/dist/command-cli.js
CHANGED
|
@@ -9,8 +9,8 @@ import { CATALOG } from "./catalog.js";
|
|
|
9
9
|
import { skillsInstallerSpec } from "./package-metadata.js";
|
|
10
10
|
import { LifecycleError } from "./lifecycle.js";
|
|
11
11
|
import { previewDependencyRemoval, removeDependency, planDependency } from "./self-install.js";
|
|
12
|
-
import { selectOption, selectOptions, promptText, confirmQuestion } from "./terminal-wizard.js";
|
|
13
|
-
import { renderPlan, renderInventory, renderCriticsTable, renderFixedRoles, 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";
|
|
14
14
|
let diagnostics;
|
|
15
15
|
let machineOutput = false;
|
|
16
16
|
let diagnosticJSON = false;
|
|
@@ -304,7 +304,21 @@ function conflicts(operations) {
|
|
|
304
304
|
if (blocked.length)
|
|
305
305
|
fail("conflict", `Plan blocked; preserved paths: ${blocked.map((operation) => operation.path).join(", ")}. Inspect doctor; restore recorded bytes or resolve ownership before retrying.`);
|
|
306
306
|
}
|
|
307
|
-
|
|
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) {
|
|
308
322
|
if (options.targets !== undefined || options.fragments !== undefined) {
|
|
309
323
|
if (options.targets === undefined || options.fragments === undefined)
|
|
310
324
|
fail("invalid_input", "Supply both --targets and --fragments");
|
|
@@ -314,30 +328,60 @@ async function integrationSelection(options, presetsOnly = false) {
|
|
|
314
328
|
});
|
|
315
329
|
}
|
|
316
330
|
const defaults = await defaultConfigSelection(options.scope);
|
|
317
|
-
const
|
|
318
|
-
|
|
319
|
-
|
|
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: [] };
|
|
320
374
|
const connection = [];
|
|
321
|
-
if (
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
"Disconnect agentomatic",
|
|
326
|
-
]));
|
|
327
|
-
if (choice === 1)
|
|
328
|
-
connection.push("core-plugin");
|
|
329
|
-
if (choice === 2)
|
|
330
|
-
connection.push("core-disable");
|
|
331
|
-
}
|
|
332
|
-
const available = CONFIG_FRAGMENTS.filter((fragment) => !["core-plugin", "core-disable"].includes(fragment.name) &&
|
|
333
|
-
fragment.targets.some((target) => targets.includes(target))).map((fragment) => fragment.name);
|
|
334
|
-
const fragments = value(await selectOptions("Application presets to merge", available, defaults.fragments.filter((fragment) => available.includes(fragment)), available.map((name) => CONFIG_FRAGMENTS.find((fragment) => fragment.name === name)?.description)));
|
|
375
|
+
if (state.connection === 1)
|
|
376
|
+
connection.push("core-plugin");
|
|
377
|
+
if (state.connection === 2)
|
|
378
|
+
connection.push("core-disable");
|
|
335
379
|
return normalizeConfigSelection(options.scope, {
|
|
336
|
-
targets,
|
|
337
|
-
fragments: [...connection, ...fragments],
|
|
380
|
+
targets: state.targets,
|
|
381
|
+
fragments: [...connection, ...applicablePresets(state.fragments, state.targets)],
|
|
338
382
|
});
|
|
339
383
|
}
|
|
340
|
-
async function modelSelection(options, name, current) {
|
|
384
|
+
async function modelSelection(options, name, current, allowBack = false) {
|
|
341
385
|
validateAgentName(name);
|
|
342
386
|
if (options.model) {
|
|
343
387
|
const model = validateModel(options.model.includes("/") ? options.model : `${options.provider ?? ""}/${options.model}`);
|
|
@@ -362,42 +406,102 @@ async function modelSelection(options, name, current) {
|
|
|
362
406
|
...(current.variant ? ["Clear variant"] : []),
|
|
363
407
|
]
|
|
364
408
|
: ["Change model"];
|
|
365
|
-
const
|
|
366
|
-
|
|
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"))
|
|
367
489
|
return { action: "model-set", name, model: current.model, variant: current.variant ?? null };
|
|
368
|
-
if (choice === "Clear variant")
|
|
490
|
+
if (state.choice === "Clear variant")
|
|
369
491
|
return { action: "model-set", name, model: current.model, variant: null };
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
const variants = await availableModelVariants(model);
|
|
377
|
-
let variant = null;
|
|
378
|
-
if (variants === null) {
|
|
379
|
-
// The variants metadata is unavailable: say so honestly and accept a
|
|
380
|
-
// manual variant instead of degrading the whole model selection.
|
|
381
|
-
process.stderr.write("Model variants are unknown (variants metadata unavailable); enter one manually or leave it empty.\n");
|
|
382
|
-
const answer = await promptText(current?.variant ? "Variant (empty keeps current)" : "Variant (optional)");
|
|
383
|
-
variant = (answer ? validateVariant(answer) : current?.variant) ?? null;
|
|
384
|
-
}
|
|
385
|
-
else if (variants.length) {
|
|
386
|
-
const labels = variants.map((item) => item === current?.variant ? `${item} (default)` : item);
|
|
387
|
-
const choice = value(await selectOption("Variant", ["(none)", ...labels], undefined, undefined, current?.variant ? variants.indexOf(current.variant) + 1 : 0));
|
|
388
|
-
variant = choice === 0 ? null : (variants[choice - 1] ?? null);
|
|
389
|
-
}
|
|
390
|
-
return { action: "model-set", name, model, variant };
|
|
391
|
-
}
|
|
392
|
-
catch (error) {
|
|
393
|
-
if (!(error instanceof LifecycleError && error.code === "catalog_unavailable"))
|
|
394
|
-
throw error;
|
|
395
|
-
process.stderr.write("Start opencode in another terminal to browse models, or enter provider/model manually\n");
|
|
396
|
-
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");
|
|
397
498
|
const answer = await promptText(current?.variant ? "Variant (empty keeps current)" : "Variant (optional)");
|
|
398
|
-
|
|
399
|
-
return { action: "model-set", name, model, variant };
|
|
499
|
+
variant = (answer ? validateVariant(answer) : current?.variant) ?? null;
|
|
400
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 };
|
|
401
505
|
}
|
|
402
506
|
async function profileDraft(options, names) {
|
|
403
507
|
const inventory = await listAgentProfiles(options.scope);
|
|
@@ -407,15 +511,20 @@ async function profileDraft(options, names) {
|
|
|
407
511
|
const changes = [];
|
|
408
512
|
while (true) {
|
|
409
513
|
const actions = ["Configure model", "Add critic", "Remove additional critic", "Done"];
|
|
410
|
-
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];
|
|
411
518
|
if (action === "Done")
|
|
412
519
|
return changes;
|
|
413
520
|
if (action === "Add critic") {
|
|
414
521
|
const name = await promptCriticName();
|
|
415
522
|
if (records.has(name))
|
|
416
523
|
fail("profile_exists", `Profile already exists: ${name}`);
|
|
417
|
-
const request =
|
|
418
|
-
|
|
524
|
+
const request = await modelSelection(options, name, undefined, true);
|
|
525
|
+
if (isBack(request))
|
|
526
|
+
continue;
|
|
527
|
+
changes.push({ ...request, action: "critic-add" });
|
|
419
528
|
names.push(name);
|
|
420
529
|
records.set(name, {
|
|
421
530
|
name,
|
|
@@ -433,14 +542,19 @@ async function profileDraft(options, names) {
|
|
|
433
542
|
process.stderr.write("No eligible profiles; choose another action.\n");
|
|
434
543
|
continue;
|
|
435
544
|
}
|
|
436
|
-
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)];
|
|
437
549
|
if (action === "Remove additional critic") {
|
|
438
550
|
changes.push({ action: "critic-remove", name });
|
|
439
551
|
names = names.filter((candidate) => candidate !== name);
|
|
440
552
|
records.delete(name);
|
|
441
553
|
}
|
|
442
554
|
else {
|
|
443
|
-
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;
|
|
444
558
|
changes.push(request);
|
|
445
559
|
records.set(name, {
|
|
446
560
|
...records.get(name),
|
|
@@ -483,11 +597,41 @@ async function componentSelection(options) {
|
|
|
483
597
|
return { ...saved, agents, core_activation: options.core ?? saved.core_activation };
|
|
484
598
|
}
|
|
485
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`);
|
|
486
|
-
const
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
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
|
+
});
|
|
491
635
|
}
|
|
492
636
|
async function deploy(options, repair = false) {
|
|
493
637
|
const selection = repair
|
|
@@ -659,9 +803,50 @@ function partial(options, error, completed, next, requiresRestart = false) {
|
|
|
659
803
|
}
|
|
660
804
|
class ReportedError extends Error {
|
|
661
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
|
+
}
|
|
662
814
|
async function configureIntegration(options) {
|
|
663
815
|
requireApply(options);
|
|
664
|
-
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
|
+
}
|
|
665
850
|
const plan = await previewConfigSetup(selection, options.scope, undefined, undefined, !options.noDependency);
|
|
666
851
|
if (!options.json)
|
|
667
852
|
process.stdout.write(renderConfigSetup(plan, {
|
|
@@ -697,8 +882,10 @@ async function configureIntegration(options) {
|
|
|
697
882
|
requires_restart: result.requires_restart,
|
|
698
883
|
});
|
|
699
884
|
else {
|
|
700
|
-
process.stdout.write(renderConfigSetup(result, { applied: true }));
|
|
701
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");
|
|
702
889
|
}
|
|
703
890
|
}
|
|
704
891
|
catch (error) {
|
|
@@ -880,6 +1067,8 @@ async function run(args) {
|
|
|
880
1067
|
"Agent models and critics",
|
|
881
1068
|
"Application connection and presets",
|
|
882
1069
|
]));
|
|
1070
|
+
if (isBack(selected))
|
|
1071
|
+
fail("cancelled", "Wizard cancelled; no changes applied");
|
|
883
1072
|
topic = routes[selected];
|
|
884
1073
|
}
|
|
885
1074
|
if (topic === "install" || topic === "configure components" || topic === "maintenance repair") {
|
|
@@ -965,10 +1154,14 @@ async function run(args) {
|
|
|
965
1154
|
// configure agent on a fresh critic-<name> adds the critic: one command
|
|
966
1155
|
// covers model-set for existing profiles and critic-add for new critics.
|
|
967
1156
|
const request = await modelSelection(options, name, undefined);
|
|
1157
|
+
if (isBack(request))
|
|
1158
|
+
fail("cancelled", "Wizard cancelled; no changes applied");
|
|
968
1159
|
await profileChange(options, { ...request, action: "critic-add" });
|
|
969
1160
|
return;
|
|
970
1161
|
}
|
|
971
1162
|
const request = await modelSelection(options, name, existing);
|
|
1163
|
+
if (isBack(request))
|
|
1164
|
+
fail("cancelled", "Wizard cancelled; no changes applied");
|
|
972
1165
|
await profileChange(options, { ...request, action: "model-set" });
|
|
973
1166
|
return;
|
|
974
1167
|
}
|
|
@@ -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,24 +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
|
|
31
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.
|
|
32
99
|
const MULTISELECT_CONTROLS = " (move: up/down; toggle: space; all/none: a; enter confirms)";
|
|
33
|
-
export async function selectOptions(label, options, initialSelected = [],
|
|
100
|
+
export async function selectOptions(label, options, initialSelected = [], descriptions, stdin = process.stdin, stderr = process.stderr, back = false) {
|
|
34
101
|
interactiveStreams(stdin, stderr);
|
|
35
102
|
validateOptions(options);
|
|
36
103
|
const allowed = new Set(options);
|
|
@@ -38,13 +105,16 @@ export async function selectOptions(label, options, initialSelected = [], hints,
|
|
|
38
105
|
if (!allowed.has(value))
|
|
39
106
|
throw new InstallerError("invalid_input", `Unknown initial selector option: ${value}`);
|
|
40
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());
|
|
41
115
|
const value = await clack.multiselect({
|
|
42
116
|
message: `${label}${MULTISELECT_CONTROLS}`,
|
|
43
|
-
options:
|
|
44
|
-
value: option,
|
|
45
|
-
label: option,
|
|
46
|
-
...(hints?.[index] ? { hint: hints[index] } : {}),
|
|
47
|
-
})),
|
|
117
|
+
options: entries,
|
|
48
118
|
initialValues: [...initialSelected],
|
|
49
119
|
required: false,
|
|
50
120
|
input: stdin,
|
|
@@ -52,7 +122,10 @@ export async function selectOptions(label, options, initialSelected = [], hints,
|
|
|
52
122
|
});
|
|
53
123
|
if (clack.isCancel(value))
|
|
54
124
|
return null;
|
|
55
|
-
|
|
125
|
+
const selected = value;
|
|
126
|
+
if (selected.includes(BACK_VALUE))
|
|
127
|
+
return BACK;
|
|
128
|
+
return selected;
|
|
56
129
|
}
|
|
57
130
|
export async function promptText(label, stdin = process.stdin, stderr = process.stderr) {
|
|
58
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.132.g6d50bbe22d9e",
|
|
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,12 +50,12 @@
|
|
|
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.132.g6d50bbe22d9e",
|
|
54
54
|
"commander": "15.0.0",
|
|
55
55
|
"zod": "4.1.8"
|
|
56
56
|
},
|
|
57
57
|
"devDependencies": {
|
|
58
|
-
"@opencode/plugin": "2.0.
|
|
58
|
+
"@opencode/plugin": "2.0.25",
|
|
59
59
|
"@types/node": "22.15.30",
|
|
60
60
|
"typescript": "7.0.2"
|
|
61
61
|
},
|