@kisev/agentomatic 11.0.0-dev.57.gb877c35a2446 → 11.0.0-dev.59.g0506803c61e5
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.js +58 -9
- package/dist/config-setup.d.ts +10 -3
- package/dist/config-setup.js +187 -81
- package/dist/doctor.js +25 -0
- package/dist/installer.d.ts +6 -8
- package/dist/installer.js +2 -2
- package/dist/reconcile.js +1 -1
- package/dist/terminal-wizard.d.ts +5 -3
- package/dist/terminal-wizard.js +40 -213
- package/package.json +4 -3
package/dist/cli.js
CHANGED
|
@@ -8,7 +8,7 @@ import { apply, defaultSelection, InstallerError, normalizeSelection, preview, S
|
|
|
8
8
|
import { LifecycleError } from "./lifecycle.js";
|
|
9
9
|
import { skillsInstallerSpec } from "./package-metadata.js";
|
|
10
10
|
import { applyReconcile, previewReconcile } from "./reconcile.js";
|
|
11
|
-
import { promptText, selectOption, selectOptions } from "./terminal-wizard.js";
|
|
11
|
+
import { confirmQuestion, promptText, selectOption, selectOptions } from "./terminal-wizard.js";
|
|
12
12
|
function parseOptions(values) {
|
|
13
13
|
const options = {
|
|
14
14
|
scope: "project",
|
|
@@ -41,6 +41,16 @@ function parseOptions(values) {
|
|
|
41
41
|
throw new InstallerError("invalid_input", "--yes may be supplied once");
|
|
42
42
|
options.yes = true;
|
|
43
43
|
}
|
|
44
|
+
else if (value === "--core") {
|
|
45
|
+
if (options.core === true || options.core === false)
|
|
46
|
+
throw new InstallerError("invalid_input", "Use only one core integration option");
|
|
47
|
+
options.core = true;
|
|
48
|
+
}
|
|
49
|
+
else if (value === "--no-core") {
|
|
50
|
+
if (options.core === true || options.core === false)
|
|
51
|
+
throw new InstallerError("invalid_input", "Use only one core integration option");
|
|
52
|
+
options.core = false;
|
|
53
|
+
}
|
|
44
54
|
else if (value === "--provider") {
|
|
45
55
|
options.provider = values[++index];
|
|
46
56
|
if (!options.provider)
|
|
@@ -162,6 +172,7 @@ function rootHelp() {
|
|
|
162
172
|
["--skill-commands <list|none>", "Select adapters for installed portable skills."],
|
|
163
173
|
["--agents <list|none>", "Select fixed agents."],
|
|
164
174
|
["--plugins <list|none>", "Select optional plugin wrappers; rtk is the default."],
|
|
175
|
+
["--core / --no-core", "Toggle plugin entry wiring and npm dependency provisioning."],
|
|
165
176
|
["--targets <list|none>", "Select config targets for the config command."],
|
|
166
177
|
["--fragments <list|none>", "Select config fragments for the config command."],
|
|
167
178
|
]),
|
|
@@ -268,13 +279,16 @@ function contextualHelp(arguments_) {
|
|
|
268
279
|
["--skill-commands <list|none>", "Select installed-skill adapters."],
|
|
269
280
|
["--agents <list|none>", "Select fixed agents."],
|
|
270
281
|
["--plugins <list|none>", "Select optional plugin wrappers; rtk is preselected."],
|
|
282
|
+
["--core", "Wire the plugin entry and pin the npm dependency in one run."],
|
|
283
|
+
["--no-core", "Skip plugin entry wiring and npm dependency provisioning."],
|
|
271
284
|
["--json", "Emit stable machine-readable output."],
|
|
272
285
|
["--help", "Show this command help and exit."],
|
|
273
286
|
], [
|
|
274
|
-
"Without selection flags, a TTY opens command, agent, and
|
|
287
|
+
"Without selection flags, a TTY opens command, agent, plugin, and core selectors.",
|
|
275
288
|
"Use Up/Down to move, Space to toggle, A/N for all/none, and Enter to confirm.",
|
|
276
289
|
"Outside a TTY, provide command selection, --agents, and --plugins.",
|
|
277
|
-
"
|
|
290
|
+
"Confirmed core integration wires the core-plugin fragment in the same run;",
|
|
291
|
+
"asset writes never touch opencode.json directly.",
|
|
278
292
|
"Portable skills are installed separately; restart OpenCode after asset changes.",
|
|
279
293
|
], [
|
|
280
294
|
shellCommand(["install", "--global", "--dry-run"]),
|
|
@@ -566,10 +580,14 @@ async function interactiveInstallerSelection() {
|
|
|
566
580
|
const commands = await group("Skill command adapters", SKILL_COMMANDS, SKILL_COMMANDS);
|
|
567
581
|
const agents = await group("Fixed agents", defaults.agents, defaults.agents);
|
|
568
582
|
const plugins = await group("Selectable plugins", SELECTABLE_PLUGINS, defaults.plugins);
|
|
583
|
+
const core = await selectOption("Core integration: wire the plugin entry and pin the npm dependency?", ["Yes", "No"], process.stdin, process.stderr);
|
|
584
|
+
if (core === null)
|
|
585
|
+
throw new InstallerError("cancelled", "Wizard cancelled");
|
|
569
586
|
return normalizeSelection({
|
|
570
587
|
commands,
|
|
571
588
|
agents: agents,
|
|
572
589
|
plugins: plugins,
|
|
590
|
+
core_activation: core === 0,
|
|
573
591
|
});
|
|
574
592
|
}
|
|
575
593
|
function installerSelection(options) {
|
|
@@ -577,6 +595,7 @@ function installerSelection(options) {
|
|
|
577
595
|
commands: options.commands,
|
|
578
596
|
agents: options.agents,
|
|
579
597
|
plugins: options.plugins,
|
|
598
|
+
...(options.core === undefined ? {} : { core_activation: options.core }),
|
|
580
599
|
});
|
|
581
600
|
}
|
|
582
601
|
function configSelectionFromOptions(options) {
|
|
@@ -641,8 +660,8 @@ function requireApplyMode(options) {
|
|
|
641
660
|
throw new InstallerError("invalid_input", "Applying outside a terminal requires --yes; use --dry-run to preview");
|
|
642
661
|
}
|
|
643
662
|
async function confirmSummary() {
|
|
644
|
-
const choice = await
|
|
645
|
-
if (choice !==
|
|
663
|
+
const choice = await confirmQuestion("Apply these changes?");
|
|
664
|
+
if (choice !== true)
|
|
646
665
|
throw new InstallerError("cancelled", "Cancelled before apply");
|
|
647
666
|
}
|
|
648
667
|
function applyHint(command) {
|
|
@@ -961,12 +980,36 @@ async function run(arguments_) {
|
|
|
961
980
|
? installerSelection(options)
|
|
962
981
|
: await interactiveInstallerSelection()
|
|
963
982
|
: undefined;
|
|
983
|
+
const coreSelection = action === "install" && selection?.core_activation
|
|
984
|
+
? normalizeConfigSelection(options.scope, {
|
|
985
|
+
targets: ["opencode"],
|
|
986
|
+
fragments: ["core-plugin"],
|
|
987
|
+
})
|
|
988
|
+
: undefined;
|
|
989
|
+
const applyCoreIntegration = async () => {
|
|
990
|
+
if (!coreSelection)
|
|
991
|
+
return null;
|
|
992
|
+
try {
|
|
993
|
+
return await applyConfigSetup(coreSelection, options.scope);
|
|
994
|
+
}
|
|
995
|
+
catch (error) {
|
|
996
|
+
const known = error instanceof LifecycleError
|
|
997
|
+
? error
|
|
998
|
+
: new InstallerError("internal_error", error instanceof Error ? error.message : String(error));
|
|
999
|
+
process.stderr.write(`Assets applied, but core integration failed [${known.code}]: ${terminalSafe(known.message)}\nRetry with: ${shellCommand(["config", ...scopeArguments(options.scope), "--targets", "opencode", "--fragments", "core-plugin"])}\n`);
|
|
1000
|
+
process.exitCode = 2;
|
|
1001
|
+
return null;
|
|
1002
|
+
}
|
|
1003
|
+
};
|
|
964
1004
|
requireApplyMode(options);
|
|
965
1005
|
if (options.dryRun) {
|
|
966
1006
|
const plan = await preview(action, options.scope, process.cwd(), undefined, selection, !options.noDependency);
|
|
1007
|
+
const corePlan = coreSelection
|
|
1008
|
+
? await previewConfigSetup(coreSelection, options.scope)
|
|
1009
|
+
: undefined;
|
|
967
1010
|
if (options.json)
|
|
968
|
-
process.stdout.write(`${JSON.stringify({ status: "ok", applied: false, requires_restart: plan.requires_restart, plan }, null, 2)}\n`);
|
|
969
|
-
else
|
|
1011
|
+
process.stdout.write(`${JSON.stringify({ status: "ok", applied: false, requires_restart: plan.requires_restart, plan, ...(corePlan ? { core_integration: corePlan } : {}) }, null, 2)}\n`);
|
|
1012
|
+
else {
|
|
970
1013
|
process.stdout.write(renderPlan(plan, {
|
|
971
1014
|
applied: false,
|
|
972
1015
|
applyHint: applyHint([
|
|
@@ -976,6 +1019,9 @@ async function run(arguments_) {
|
|
|
976
1019
|
...(options.noDependency ? ["--no-dependency"] : []),
|
|
977
1020
|
]),
|
|
978
1021
|
}));
|
|
1022
|
+
if (corePlan)
|
|
1023
|
+
process.stdout.write(renderConfigSetup(corePlan, { applied: false }));
|
|
1024
|
+
}
|
|
979
1025
|
}
|
|
980
1026
|
else {
|
|
981
1027
|
if (!options.yes) {
|
|
@@ -984,17 +1030,20 @@ async function run(arguments_) {
|
|
|
984
1030
|
await confirmSummary();
|
|
985
1031
|
}
|
|
986
1032
|
const plan = await apply(action, options.scope, process.cwd(), undefined, {}, selection, !options.noDependency);
|
|
1033
|
+
const corePlan = await applyCoreIntegration();
|
|
987
1034
|
if (options.json)
|
|
988
|
-
process.stdout.write(`${JSON.stringify({ status: "ok", applied: true, requires_restart: plan.requires_restart, plan }, null, 2)}\n`);
|
|
1035
|
+
process.stdout.write(`${JSON.stringify({ status: "ok", applied: true, requires_restart: plan.requires_restart, plan, ...(corePlan ? { core_integration: corePlan } : {}) }, null, 2)}\n`);
|
|
989
1036
|
else
|
|
990
1037
|
process.stdout.write(renderPlan(plan, {
|
|
991
1038
|
applied: true,
|
|
992
|
-
...(action === "install"
|
|
1039
|
+
...(action === "install" && !corePlan
|
|
993
1040
|
? {
|
|
994
1041
|
hint: `Connect the package into user configs: ${shellCommand(["config", ...scopeArguments(options.scope), "--dry-run"])}`,
|
|
995
1042
|
}
|
|
996
1043
|
: {}),
|
|
997
1044
|
}));
|
|
1045
|
+
if (corePlan && !options.json)
|
|
1046
|
+
process.stdout.write(renderConfigSetup(corePlan, { applied: true }));
|
|
998
1047
|
}
|
|
999
1048
|
return;
|
|
1000
1049
|
}
|
package/dist/config-setup.d.ts
CHANGED
|
@@ -2,32 +2,39 @@ import { type DependencyRunner } from "./self-install.js";
|
|
|
2
2
|
import { LifecycleError, type Scope } from "./lifecycle.js";
|
|
3
3
|
export declare class ConfigSetupError extends LifecycleError {
|
|
4
4
|
}
|
|
5
|
-
export declare const CONFIG_TARGETS: readonly ["opencode", "
|
|
5
|
+
export declare const CONFIG_TARGETS: readonly ["opencode", "kilo", "mimo"];
|
|
6
6
|
export type ConfigTargetName = (typeof CONFIG_TARGETS)[number];
|
|
7
|
+
export type TargetFileKind = "main" | "tui";
|
|
7
8
|
export declare const CONFIG_FRAGMENTS: readonly [{
|
|
8
9
|
readonly name: "core-plugin";
|
|
9
10
|
readonly description: "Register @kisev/agentomatic in the plugin array (OpenCode only)";
|
|
10
11
|
readonly targets: readonly ["opencode"];
|
|
12
|
+
readonly file: "main";
|
|
11
13
|
}, {
|
|
12
14
|
readonly name: "skills-state-permissions";
|
|
13
15
|
readonly description: "Allow the standard skills XDG state paths without per-run prompts";
|
|
14
16
|
readonly targets: readonly ["opencode", "kilo", "mimo"];
|
|
17
|
+
readonly file: "main";
|
|
15
18
|
}, {
|
|
16
19
|
readonly name: "lsp-preset";
|
|
17
20
|
readonly description: "Add LSP servers from the shared catalog (OpenCode only)";
|
|
18
21
|
readonly targets: readonly ["opencode"];
|
|
22
|
+
readonly file: "main";
|
|
19
23
|
}, {
|
|
20
24
|
readonly name: "secrets-guard";
|
|
21
25
|
readonly description: "Deny reads and edits of common secret files";
|
|
22
26
|
readonly targets: readonly ["opencode", "kilo", "mimo"];
|
|
27
|
+
readonly file: "main";
|
|
23
28
|
}, {
|
|
24
29
|
readonly name: "kilo-display";
|
|
25
30
|
readonly description: "Expand Kilo reasoning, terminal, edit, and tool blocks";
|
|
26
31
|
readonly targets: readonly ["kilo"];
|
|
32
|
+
readonly file: "main";
|
|
27
33
|
}, {
|
|
28
34
|
readonly name: "tui-schema";
|
|
29
|
-
readonly description: "
|
|
30
|
-
readonly targets: readonly ["
|
|
35
|
+
readonly description: "Unify each agent TUI file: schema, theme, stacked diffs, and leader keybinds";
|
|
36
|
+
readonly targets: readonly ["opencode", "kilo", "mimo"];
|
|
37
|
+
readonly file: "tui";
|
|
31
38
|
}];
|
|
32
39
|
export type FragmentName = (typeof CONFIG_FRAGMENTS)[number]["name"];
|
|
33
40
|
export type ConfigSetupSelection = {
|
package/dist/config-setup.js
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { readFileSync } from "node:fs";
|
|
2
|
+
import { lstat } from "node:fs/promises";
|
|
2
3
|
import { homedir } from "node:os";
|
|
3
4
|
import { dirname, join, resolve } from "node:path";
|
|
4
5
|
import { fileURLToPath } from "node:url";
|
|
6
|
+
import { archiveMutations } from "./installer.js";
|
|
5
7
|
import { requirePackageVersion } from "./package-metadata.js";
|
|
6
8
|
import { ensureDependency, planDependency } from "./self-install.js";
|
|
7
9
|
import { applyTransaction, deploymentRoot, LifecycleError, lifecycleRoot, readRegular, recoverTransaction, sha256, withLifecycleLock, } from "./lifecycle.js";
|
|
@@ -35,40 +37,68 @@ const SECRET_PATHS = [
|
|
|
35
37
|
];
|
|
36
38
|
export class ConfigSetupError extends LifecycleError {
|
|
37
39
|
}
|
|
38
|
-
export const CONFIG_TARGETS = ["opencode", "
|
|
40
|
+
export const CONFIG_TARGETS = ["opencode", "kilo", "mimo"];
|
|
39
41
|
export const CONFIG_FRAGMENTS = [
|
|
40
42
|
{
|
|
41
43
|
name: "core-plugin",
|
|
42
44
|
description: "Register @kisev/agentomatic in the plugin array (OpenCode only)",
|
|
43
45
|
targets: ["opencode"],
|
|
46
|
+
file: "main",
|
|
44
47
|
},
|
|
45
48
|
{
|
|
46
49
|
name: "skills-state-permissions",
|
|
47
50
|
description: "Allow the standard skills XDG state paths without per-run prompts",
|
|
48
51
|
targets: ["opencode", "kilo", "mimo"],
|
|
52
|
+
file: "main",
|
|
49
53
|
},
|
|
50
54
|
{
|
|
51
55
|
name: "lsp-preset",
|
|
52
56
|
description: "Add LSP servers from the shared catalog (OpenCode only)",
|
|
53
57
|
targets: ["opencode"],
|
|
58
|
+
file: "main",
|
|
54
59
|
},
|
|
55
60
|
{
|
|
56
61
|
name: "secrets-guard",
|
|
57
62
|
description: "Deny reads and edits of common secret files",
|
|
58
63
|
targets: ["opencode", "kilo", "mimo"],
|
|
64
|
+
file: "main",
|
|
59
65
|
},
|
|
60
66
|
{
|
|
61
67
|
name: "kilo-display",
|
|
62
68
|
description: "Expand Kilo reasoning, terminal, edit, and tool blocks",
|
|
63
69
|
targets: ["kilo"],
|
|
70
|
+
file: "main",
|
|
64
71
|
},
|
|
65
72
|
{
|
|
66
73
|
name: "tui-schema",
|
|
67
|
-
description: "
|
|
68
|
-
targets: ["
|
|
74
|
+
description: "Unify each agent TUI file: schema, theme, stacked diffs, and leader keybinds",
|
|
75
|
+
targets: ["opencode", "kilo", "mimo"],
|
|
76
|
+
file: "tui",
|
|
69
77
|
},
|
|
70
78
|
];
|
|
71
79
|
const FRAGMENT_NAMES = CONFIG_FRAGMENTS.map((fragment) => fragment.name);
|
|
80
|
+
const UNIFIED_TUI_KEYBINDS = {
|
|
81
|
+
command_list: "alt+p",
|
|
82
|
+
app_exit: "ctrl+d,<leader>q,<leader>й",
|
|
83
|
+
messages_first: "ctrl+g",
|
|
84
|
+
messages_last: "ctrl+alt+g",
|
|
85
|
+
editor_open: "<leader>e,<leader>у",
|
|
86
|
+
theme_list: "<leader>t,<leader>е",
|
|
87
|
+
sidebar_toggle: "<leader>b,<leader>и",
|
|
88
|
+
status_view: "<leader>s,<leader>ы",
|
|
89
|
+
session_export: "<leader>x,<leader>ч",
|
|
90
|
+
session_new: "<leader>n,<leader>т",
|
|
91
|
+
session_list: "<leader>l,<leader>д",
|
|
92
|
+
session_timeline: "<leader>g,<leader>п",
|
|
93
|
+
session_compact: "<leader>c,<leader>с",
|
|
94
|
+
model_list: "<leader>m,<leader>ь",
|
|
95
|
+
agent_list: "<leader>a,<leader>ф",
|
|
96
|
+
messages_copy: "<leader>y,<leader>н",
|
|
97
|
+
messages_undo: "<leader>u,<leader>г",
|
|
98
|
+
messages_redo: "<leader>r,<leader>к",
|
|
99
|
+
messages_toggle_conceal: "<leader>h,<leader>р",
|
|
100
|
+
tips_toggle: "<leader>h,<leader>р",
|
|
101
|
+
};
|
|
72
102
|
const LSP_SERVER_COMMANDS = {
|
|
73
103
|
python: ["basedpyright-langserver", "--stdio"],
|
|
74
104
|
typescript: ["typescript-language-server", "--stdio"],
|
|
@@ -151,20 +181,39 @@ function fragmentEdits(fragment, target, current) {
|
|
|
151
181
|
{ kind: "set-if-absent", path: ["mcp_tool_display"], value: "expanded" },
|
|
152
182
|
];
|
|
153
183
|
}
|
|
184
|
+
const tuiSchemas = {
|
|
185
|
+
opencode: "https://opencode.ai/tui.json",
|
|
186
|
+
kilo: undefined,
|
|
187
|
+
mimo: "https://mimo.xiaomi.com/mimocode/tui.json",
|
|
188
|
+
};
|
|
154
189
|
return [
|
|
155
|
-
|
|
190
|
+
...(tuiSchemas[target]
|
|
191
|
+
? [{ kind: "set-if-absent", path: ["$schema"], value: tuiSchemas[target] }]
|
|
192
|
+
: []),
|
|
193
|
+
{ kind: "set-if-absent", path: ["theme"], value: "ayu" },
|
|
156
194
|
{ kind: "set-if-absent", path: ["diff_style"], value: "stacked" },
|
|
195
|
+
...Object.entries(UNIFIED_TUI_KEYBINDS).map(([key, value]) => ({
|
|
196
|
+
kind: "set-if-absent",
|
|
197
|
+
path: ["keybinds", key],
|
|
198
|
+
value,
|
|
199
|
+
})),
|
|
157
200
|
];
|
|
158
201
|
}
|
|
159
202
|
function applicable(fragment, target) {
|
|
160
203
|
const targets = CONFIG_FRAGMENTS.find((item) => item.name === fragment)?.targets ?? [];
|
|
161
204
|
return targets.includes(target);
|
|
162
205
|
}
|
|
206
|
+
function fragmentFileKind(fragment) {
|
|
207
|
+
return CONFIG_FRAGMENTS.find((item) => item.name === fragment)?.file ?? "main";
|
|
208
|
+
}
|
|
163
209
|
export function normalizeConfigSelection(scope, value = {}) {
|
|
164
210
|
const requestedTargets = value.targets ?? ["opencode"];
|
|
165
211
|
const targets = CONFIG_TARGETS.filter((target) => requestedTargets.includes(target));
|
|
166
|
-
if (targets.length !== requestedTargets.length)
|
|
212
|
+
if (targets.length !== requestedTargets.length) {
|
|
213
|
+
if (requestedTargets.includes("tui"))
|
|
214
|
+
throw new ConfigSetupError("invalid_selection", "The tui target moved into opencode, kilo, and mimo as the tui-schema fragment");
|
|
167
215
|
throw new ConfigSetupError("invalid_selection", "Unknown config target selection");
|
|
216
|
+
}
|
|
168
217
|
if (scope === "project" && (targets.length !== 1 || targets[0] !== "opencode"))
|
|
169
218
|
throw new ConfigSetupError("invalid_selection", "Project scope supports only the opencode target");
|
|
170
219
|
const requestedFragments = value.fragments ?? [];
|
|
@@ -177,8 +226,6 @@ export async function defaultConfigSelection(scope, cwd = process.cwd(), home =
|
|
|
177
226
|
const exists = async (path) => Boolean(await readRegular(path).catch(() => undefined));
|
|
178
227
|
const targets = ["opencode"];
|
|
179
228
|
if (scope === "global") {
|
|
180
|
-
if (await exists(join(home, ".config", "opencode", "tui.json")))
|
|
181
|
-
targets.push("tui");
|
|
182
229
|
if (await exists(join(home, ".config", "kilo", "kilo.json")))
|
|
183
230
|
targets.push("kilo");
|
|
184
231
|
if (await exists(join(home, ".config", "kilo", "kilo.jsonc")))
|
|
@@ -193,9 +240,41 @@ export async function defaultConfigSelection(scope, cwd = process.cwd(), home =
|
|
|
193
240
|
fragments: [...FRAGMENT_NAMES],
|
|
194
241
|
});
|
|
195
242
|
}
|
|
196
|
-
async function resolveTargetFile(target, scope, cwd, home) {
|
|
243
|
+
async function resolveTargetFile(target, scope, cwd, home, kind = "main") {
|
|
197
244
|
const existing = async (root, path) => Boolean(await readRegular(join(root, path)).catch(() => undefined));
|
|
198
245
|
const globalRoot = deploymentRoot("global", cwd, home);
|
|
246
|
+
if (kind === "tui") {
|
|
247
|
+
if (target === "kilo") {
|
|
248
|
+
const kiloRoot = join(home, ".config", "kilo");
|
|
249
|
+
for (const path of ["tui.jsonc", "tui.json"])
|
|
250
|
+
if (await existing(kiloRoot, path))
|
|
251
|
+
return { target, root: kiloRoot, path, absolute: join(kiloRoot, path), exists: true };
|
|
252
|
+
return {
|
|
253
|
+
target,
|
|
254
|
+
root: kiloRoot,
|
|
255
|
+
path: "tui.jsonc",
|
|
256
|
+
absolute: join(kiloRoot, "tui.jsonc"),
|
|
257
|
+
exists: false,
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
if (target === "mimo") {
|
|
261
|
+
const mimoRoot = join(home, ".config", "mimocode");
|
|
262
|
+
return {
|
|
263
|
+
target,
|
|
264
|
+
root: mimoRoot,
|
|
265
|
+
path: "tui.json",
|
|
266
|
+
absolute: join(mimoRoot, "tui.json"),
|
|
267
|
+
exists: await existing(mimoRoot, "tui.json"),
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
return {
|
|
271
|
+
target,
|
|
272
|
+
root: globalRoot,
|
|
273
|
+
path: "tui.json",
|
|
274
|
+
absolute: join(globalRoot, "tui.json"),
|
|
275
|
+
exists: await existing(globalRoot, "tui.json"),
|
|
276
|
+
};
|
|
277
|
+
}
|
|
199
278
|
if (target === "opencode") {
|
|
200
279
|
if (scope === "global") {
|
|
201
280
|
for (const path of ["opencode.json", "opencode.jsonc"])
|
|
@@ -226,14 +305,6 @@ async function resolveTargetFile(target, scope, cwd, home) {
|
|
|
226
305
|
exists: false,
|
|
227
306
|
};
|
|
228
307
|
}
|
|
229
|
-
if (target === "tui")
|
|
230
|
-
return {
|
|
231
|
-
target,
|
|
232
|
-
root: globalRoot,
|
|
233
|
-
path: "tui.json",
|
|
234
|
-
absolute: join(globalRoot, "tui.json"),
|
|
235
|
-
exists: await existing(globalRoot, "tui.json"),
|
|
236
|
-
};
|
|
237
308
|
if (target === "kilo") {
|
|
238
309
|
const root = join(home, ".config", "kilo");
|
|
239
310
|
for (const path of ["kilo.jsonc", "kilo.json"])
|
|
@@ -260,86 +331,101 @@ async function build(selection, scope, cwd = process.cwd(), home = homedir()) {
|
|
|
260
331
|
const allowedRoots = new Set([root]);
|
|
261
332
|
const files = [];
|
|
262
333
|
for (const target of CONFIG_TARGETS.filter((name) => selection.targets.includes(name))) {
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
const reason = error instanceof LifecycleError ? error.message : "target is unreadable";
|
|
271
|
-
for (const fragment of selection.fragments.filter((item) => applicable(item, target)))
|
|
272
|
-
operations.push({ target, path: "unknown", fragment, operation: "conflict", reason });
|
|
273
|
-
continue;
|
|
274
|
-
}
|
|
275
|
-
if (file.exists && current === undefined) {
|
|
276
|
-
for (const fragment of selection.fragments.filter((item) => applicable(item, target)))
|
|
277
|
-
operations.push({
|
|
278
|
-
target,
|
|
279
|
-
path: file.absolute,
|
|
280
|
-
fragment,
|
|
281
|
-
operation: "conflict",
|
|
282
|
-
reason: "target is unreadable",
|
|
283
|
-
});
|
|
284
|
-
continue;
|
|
285
|
-
}
|
|
286
|
-
files.push(file);
|
|
287
|
-
allowedRoots.add(file.root);
|
|
288
|
-
let text = current ? current.toString("utf8") : "{}\n";
|
|
289
|
-
let changed = false;
|
|
290
|
-
for (const fragment of selection.fragments) {
|
|
291
|
-
if (!applicable(fragment, target))
|
|
292
|
-
continue;
|
|
293
|
-
let parsed = undefined;
|
|
334
|
+
const kinds = ["main"];
|
|
335
|
+
const wantsTui = selection.fragments.some((fragment) => applicable(fragment, target) && fragmentFileKind(fragment) === "tui");
|
|
336
|
+
if (wantsTui && scope === "global")
|
|
337
|
+
kinds.push("tui");
|
|
338
|
+
for (const kind of kinds) {
|
|
339
|
+
let file;
|
|
340
|
+
let current;
|
|
294
341
|
try {
|
|
295
|
-
|
|
342
|
+
file = await resolveTargetFile(target, scope, cwd, home, kind);
|
|
343
|
+
current = file.exists ? await readRegular(file.absolute) : undefined;
|
|
296
344
|
}
|
|
297
|
-
catch {
|
|
298
|
-
|
|
345
|
+
catch (error) {
|
|
346
|
+
const reason = error instanceof LifecycleError ? error.message : "target is unreadable";
|
|
347
|
+
for (const fragment of selection.fragments.filter((item) => applicable(item, target) && fragmentFileKind(item) === kind))
|
|
348
|
+
operations.push({ target, path: "unknown", fragment, operation: "conflict", reason });
|
|
349
|
+
continue;
|
|
299
350
|
}
|
|
300
|
-
|
|
301
|
-
const
|
|
302
|
-
if (applied.changed) {
|
|
303
|
-
text = applied.text;
|
|
304
|
-
changed = true;
|
|
351
|
+
if (file.exists && current === undefined) {
|
|
352
|
+
for (const fragment of selection.fragments.filter((item) => applicable(item, target) && fragmentFileKind(item) === kind))
|
|
305
353
|
operations.push({
|
|
306
354
|
target,
|
|
307
355
|
path: file.absolute,
|
|
308
356
|
fragment,
|
|
309
|
-
operation:
|
|
357
|
+
operation: "conflict",
|
|
358
|
+
reason: "target is unreadable",
|
|
310
359
|
});
|
|
360
|
+
continue;
|
|
361
|
+
}
|
|
362
|
+
files.push(file);
|
|
363
|
+
allowedRoots.add(file.root);
|
|
364
|
+
let text = current ? current.toString("utf8") : "{}\n";
|
|
365
|
+
let changed = false;
|
|
366
|
+
for (const fragment of selection.fragments) {
|
|
367
|
+
if (!applicable(fragment, target) || fragmentFileKind(fragment) !== kind)
|
|
368
|
+
continue;
|
|
369
|
+
let parsed = undefined;
|
|
370
|
+
try {
|
|
371
|
+
parsed = parseJsonc(text);
|
|
311
372
|
}
|
|
312
|
-
|
|
313
|
-
|
|
373
|
+
catch {
|
|
374
|
+
parsed = undefined;
|
|
375
|
+
}
|
|
376
|
+
try {
|
|
377
|
+
const applied = applyJsoncEdits(text, fragmentEdits(fragment, target, parsed));
|
|
378
|
+
if (applied.changed) {
|
|
379
|
+
text = applied.text;
|
|
380
|
+
changed = true;
|
|
381
|
+
operations.push({
|
|
382
|
+
target,
|
|
383
|
+
path: file.absolute,
|
|
384
|
+
fragment,
|
|
385
|
+
operation: file.exists ? "update" : "create",
|
|
386
|
+
});
|
|
387
|
+
}
|
|
388
|
+
else {
|
|
389
|
+
operations.push({ target, path: file.absolute, fragment, operation: "unchanged" });
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
catch (error) {
|
|
393
|
+
operations.push({
|
|
394
|
+
target,
|
|
395
|
+
path: file.absolute,
|
|
396
|
+
fragment,
|
|
397
|
+
operation: "conflict",
|
|
398
|
+
reason: error instanceof LifecycleError || error instanceof Error
|
|
399
|
+
? error.message
|
|
400
|
+
: String(error),
|
|
401
|
+
});
|
|
314
402
|
}
|
|
315
403
|
}
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
operation: "
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
404
|
+
if (changed) {
|
|
405
|
+
const content = Buffer.from(text.endsWith("\n") ? text : `${text}\n`);
|
|
406
|
+
mutations.push({
|
|
407
|
+
...(file.root === root ? {} : { root: file.root }),
|
|
408
|
+
path: file.path,
|
|
409
|
+
operation: "write",
|
|
410
|
+
content,
|
|
411
|
+
mode: 0o644,
|
|
412
|
+
expected: current ? { sha256: sha256(current) } : { absent: true },
|
|
325
413
|
});
|
|
326
414
|
}
|
|
327
415
|
}
|
|
328
|
-
if (changed) {
|
|
329
|
-
const content = Buffer.from(text.endsWith("\n") ? text : `${text}\n`);
|
|
330
|
-
mutations.push({
|
|
331
|
-
...(file.root === root ? {} : { root: file.root }),
|
|
332
|
-
path: file.path,
|
|
333
|
-
operation: "write",
|
|
334
|
-
content,
|
|
335
|
-
mode: 0o644,
|
|
336
|
-
expected: current ? { sha256: sha256(current) } : { absent: true },
|
|
337
|
-
});
|
|
338
|
-
}
|
|
339
416
|
}
|
|
340
417
|
const skipped = selection.fragments
|
|
341
|
-
.filter((fragment) =>
|
|
342
|
-
|
|
418
|
+
.filter((fragment) => {
|
|
419
|
+
if (fragmentFileKind(fragment) === "tui" && scope === "project")
|
|
420
|
+
return true;
|
|
421
|
+
return !selection.targets.some((target) => applicable(fragment, target));
|
|
422
|
+
})
|
|
423
|
+
.map((fragment) => ({
|
|
424
|
+
fragment,
|
|
425
|
+
reason: fragmentFileKind(fragment) === "tui" && scope === "project"
|
|
426
|
+
? "TUI files are global-only"
|
|
427
|
+
: "no selected target supports this fragment",
|
|
428
|
+
}));
|
|
343
429
|
const sorted = operations.sort((left, right) => CONFIG_TARGETS.indexOf(left.target) - CONFIG_TARGETS.indexOf(right.target) ||
|
|
344
430
|
FRAGMENT_NAMES.indexOf(left.fragment) -
|
|
345
431
|
FRAGMENT_NAMES.indexOf(right.fragment) ||
|
|
@@ -414,7 +500,27 @@ export async function applyConfigSetup(selection, scope, cwd = process.cwd(), ho
|
|
|
414
500
|
}
|
|
415
501
|
throw new ConfigSetupError("invalid_state", "Config setup plan has no applicable changes");
|
|
416
502
|
}
|
|
417
|
-
|
|
503
|
+
const backups = [];
|
|
504
|
+
for (const mutation of built.mutations) {
|
|
505
|
+
if (mutation.operation !== "write")
|
|
506
|
+
continue;
|
|
507
|
+
const target = join(mutation.root ?? root, mutation.path);
|
|
508
|
+
const current = await readRegular(target).catch(() => undefined);
|
|
509
|
+
if (!current || current.equals(mutation.content))
|
|
510
|
+
continue;
|
|
511
|
+
backups.push({
|
|
512
|
+
path: target,
|
|
513
|
+
record: {
|
|
514
|
+
sha256: sha256(current),
|
|
515
|
+
mode: (await lstat(target)).mode & 0o777,
|
|
516
|
+
},
|
|
517
|
+
content: current,
|
|
518
|
+
reason: "pre-apply config backup",
|
|
519
|
+
kind: "config-backup",
|
|
520
|
+
});
|
|
521
|
+
}
|
|
522
|
+
const backupMutations = await archiveMutations(backups, scope, cwd, home, requirePackageVersion(), new Date().toISOString());
|
|
523
|
+
await applyTransaction(root, stateRoot, [...built.mutations, ...backupMutations], {
|
|
418
524
|
validateFinal: async () => {
|
|
419
525
|
for (const mutation of built.mutations) {
|
|
420
526
|
if (mutation.operation !== "write")
|
package/dist/doctor.js
CHANGED
|
@@ -505,6 +505,31 @@ export async function collectDoctorFacts(scope, cwd = process.cwd(), home = home
|
|
|
505
505
|
status: dependency.status,
|
|
506
506
|
}, ["Run config with the core-plugin fragment to provision the persistent npm dependency."]));
|
|
507
507
|
}
|
|
508
|
+
const archiveIndexPath = join(archiveRoot(scope, project, homeRoot), "index.json");
|
|
509
|
+
const archiveIndexRaw = await regular(archiveIndexPath);
|
|
510
|
+
let backupEntries = [];
|
|
511
|
+
if (archiveIndexRaw) {
|
|
512
|
+
try {
|
|
513
|
+
const parsed = JSON.parse(archiveIndexRaw.raw.toString("utf8"));
|
|
514
|
+
if (Array.isArray(parsed.entries))
|
|
515
|
+
backupEntries = parsed.entries.filter((item) => Boolean(item &&
|
|
516
|
+
typeof item === "object" &&
|
|
517
|
+
item.kind === "config-backup"));
|
|
518
|
+
}
|
|
519
|
+
catch {
|
|
520
|
+
partial.push("config.backups");
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
const latestBackup = backupEntries
|
|
524
|
+
.map((item) => String(item.timestamp ?? ""))
|
|
525
|
+
.sort()
|
|
526
|
+
.at(-1);
|
|
527
|
+
checks.push(check("config.backups", archiveIndexRaw ? "pass" : "incomplete", "User configuration snapshots are tracked in the archive", {
|
|
528
|
+
archive_index: archiveIndexPath,
|
|
529
|
+
config_backups: backupEntries.length,
|
|
530
|
+
latest_backup_at: latestBackup ?? "never",
|
|
531
|
+
latest_paths: backupEntries.slice(-3).map((item) => String(item.original_path ?? "")),
|
|
532
|
+
}, ["Configuration edits archive the previous content before every apply."]));
|
|
508
533
|
const rtkWrapper = await regular(join(deployment, "plugins", "rtk.js"));
|
|
509
534
|
const rtkDeployed = rtkWrapper.status === "present";
|
|
510
535
|
let rtkStats = undefined;
|
package/dist/installer.d.ts
CHANGED
|
@@ -31,17 +31,15 @@ export type Plan = {
|
|
|
31
31
|
};
|
|
32
32
|
requires_restart: boolean;
|
|
33
33
|
};
|
|
34
|
-
type ManifestFile = {
|
|
35
|
-
sha256: string;
|
|
36
|
-
mode: number;
|
|
37
|
-
kind: "command" | "agent" | "plugin" | "state";
|
|
38
|
-
};
|
|
39
34
|
export type ArchiveCandidate = {
|
|
40
35
|
path: string;
|
|
41
|
-
record:
|
|
36
|
+
record: {
|
|
37
|
+
sha256: string;
|
|
38
|
+
mode: number;
|
|
39
|
+
};
|
|
42
40
|
content: Buffer;
|
|
43
41
|
reason: string;
|
|
44
|
-
kind: "command" | "agent" | "plugin" | "state";
|
|
42
|
+
kind: "command" | "agent" | "plugin" | "state" | "config-backup";
|
|
45
43
|
};
|
|
46
44
|
export declare const SELECTABLE_PLUGINS: SelectablePlugin[];
|
|
47
45
|
export declare const SKILL_COMMANDS: string[];
|
|
@@ -49,7 +47,7 @@ export declare function defaultSelection(): InstallerSelection;
|
|
|
49
47
|
export declare function normalizeSelection(value?: Partial<InstallerSelection>): InstallerSelection;
|
|
50
48
|
export declare class InstallerError extends LifecycleError {
|
|
51
49
|
}
|
|
52
|
-
export declare function archiveMutations(candidates: readonly ArchiveCandidate[], scope: Scope, cwd: string, home: string, sourceVersion: string): Promise<FileMutation[]>;
|
|
50
|
+
export declare function archiveMutations(candidates: readonly ArchiveCandidate[], scope: Scope, cwd: string, home: string, sourceVersion: string, timestamp?: string): Promise<FileMutation[]>;
|
|
53
51
|
export declare function preview(action: Action, scope: Scope, cwd?: string, home?: string, selection?: Partial<InstallerSelection>, provisionDependency?: boolean): Promise<Plan>;
|
|
54
52
|
export declare function apply(action: Action, scope: Scope, cwd?: string, home?: string, options?: TransactionOptions & {
|
|
55
53
|
dependencyRunner?: DependencyRunner;
|
package/dist/installer.js
CHANGED
|
@@ -184,7 +184,7 @@ async function currentManifest(root) {
|
|
|
184
184
|
}
|
|
185
185
|
return {};
|
|
186
186
|
}
|
|
187
|
-
export async function archiveMutations(candidates, scope, cwd, home, sourceVersion) {
|
|
187
|
+
export async function archiveMutations(candidates, scope, cwd, home, sourceVersion, timestamp = new Date(0).toISOString()) {
|
|
188
188
|
if (!candidates.length)
|
|
189
189
|
return [];
|
|
190
190
|
const root = archiveRoot(scope, cwd, home);
|
|
@@ -226,7 +226,7 @@ export async function archiveMutations(candidates, scope, cwd, home, sourceVersi
|
|
|
226
226
|
const known = new Set(entries.map((entry) => `${String(entry.original_path)}:${String(entry.digest)}`));
|
|
227
227
|
const mutations = [];
|
|
228
228
|
const queuedObjects = new Set();
|
|
229
|
-
const now =
|
|
229
|
+
const now = timestamp;
|
|
230
230
|
for (const candidate of candidates) {
|
|
231
231
|
const digestValue = sha256(candidate.content);
|
|
232
232
|
const key = `${candidate.path}:${digestValue}`;
|
package/dist/reconcile.js
CHANGED
|
@@ -223,7 +223,7 @@ async function build(scope, cwd = process.cwd(), home = homedir()) {
|
|
|
223
223
|
: "state";
|
|
224
224
|
archiveCandidates.push({
|
|
225
225
|
path: relativePath(root, target),
|
|
226
|
-
record: { sha256: record.sha256, mode: 0o644
|
|
226
|
+
record: { sha256: record.sha256, mode: 0o644 },
|
|
227
227
|
content: value.content,
|
|
228
228
|
reason: "retired inventory asset",
|
|
229
229
|
kind,
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
-
|
|
2
|
-
export declare function
|
|
3
|
-
export declare function
|
|
1
|
+
import type { Readable, Writable } from "node:stream";
|
|
2
|
+
export declare function selectOption(label: string, options: readonly string[], stdin?: Readable, stderr?: Writable, initial?: number): Promise<number | null>;
|
|
3
|
+
export declare function selectOptions(label: string, options: readonly string[], initialSelected?: readonly string[], stdin?: Readable, stderr?: Writable): Promise<string[] | null>;
|
|
4
|
+
export declare function promptText(label: string, stdin?: Readable, stderr?: Writable): Promise<string | null>;
|
|
5
|
+
export declare function confirmQuestion(label: string, stdin?: Readable, stderr?: Writable): Promise<boolean | null>;
|
package/dist/terminal-wizard.js
CHANGED
|
@@ -1,36 +1,11 @@
|
|
|
1
|
+
import * as clack from "@clack/prompts";
|
|
1
2
|
import { InstallerError } from "./installer.js";
|
|
2
|
-
function
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
return { name: "return", sequence: seq };
|
|
6
|
-
if (seq === " ")
|
|
7
|
-
return { name: "space", sequence: seq };
|
|
8
|
-
if (seq === "a" || seq === "A")
|
|
9
|
-
return { name: "all", sequence: seq };
|
|
10
|
-
if (seq === "n" || seq === "N")
|
|
11
|
-
return { name: "none", sequence: seq };
|
|
12
|
-
if (seq === "\x03")
|
|
13
|
-
return { name: "ctrl+c", sequence: seq };
|
|
14
|
-
if (seq === "\x04")
|
|
15
|
-
return { name: "ctrl+d", sequence: seq };
|
|
16
|
-
if (seq === "\x1b" || seq === "\x1b\x1b" || seq === "\x1b\x1b\x1b")
|
|
17
|
-
return { name: "escape", sequence: seq };
|
|
18
|
-
if (seq === "\x1b[A" || seq === "\x1bOA" || seq === "\x1b[OA")
|
|
19
|
-
return { name: "up", sequence: seq };
|
|
20
|
-
if (seq === "\x1b[B" || seq === "\x1bOB" || seq === "\x1b[OB")
|
|
21
|
-
return { name: "down", sequence: seq };
|
|
22
|
-
if (seq === "\x1b[C" || seq === "\x1bOC" || seq === "\x1b[OC")
|
|
23
|
-
return { name: "right", sequence: seq };
|
|
24
|
-
if (seq === "\x1b[D" || seq === "\x1bOD" || seq === "\x1b[OD")
|
|
25
|
-
return { name: "left", sequence: seq };
|
|
26
|
-
return { name: "unknown", sequence: seq };
|
|
3
|
+
function interactiveStreams(stdin, stderr) {
|
|
4
|
+
if (!isTTY(stdin) || !isTTY(stderr))
|
|
5
|
+
throw new InstallerError("terminal_required", "This operation requires an interactive terminal");
|
|
27
6
|
}
|
|
28
|
-
function
|
|
29
|
-
|
|
30
|
-
return remaining.slice(0, 4);
|
|
31
|
-
if (remaining.startsWith("\x1b[") || remaining.startsWith("\x1bO"))
|
|
32
|
-
return remaining.slice(0, 3);
|
|
33
|
-
return remaining[0];
|
|
7
|
+
function isTTY(stream) {
|
|
8
|
+
return Boolean(stream.isTTY);
|
|
34
9
|
}
|
|
35
10
|
function validateOptions(options) {
|
|
36
11
|
if (options.length === 0)
|
|
@@ -38,200 +13,52 @@ function validateOptions(options) {
|
|
|
38
13
|
if (new Set(options).size !== options.length)
|
|
39
14
|
throw new InstallerError("invalid_input", "Interactive selector options must be unique");
|
|
40
15
|
}
|
|
41
|
-
function clearLines(stderr, count) {
|
|
42
|
-
for (let i = 0; i < count; i++) {
|
|
43
|
-
stderr.write("\x1b[A\x1b[K");
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
16
|
export async function selectOption(label, options, stdin = process.stdin, stderr = process.stderr, initial = 0) {
|
|
47
|
-
|
|
48
|
-
throw new InstallerError("terminal_required", "This operation requires an interactive terminal");
|
|
49
|
-
}
|
|
17
|
+
interactiveStreams(stdin, stderr);
|
|
50
18
|
validateOptions(options);
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
const cursor = i === selected ? "> " : " ";
|
|
58
|
-
stderr.write(`${cursor}${options[i]}\n`);
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
render();
|
|
62
|
-
return new Promise((resolve) => {
|
|
63
|
-
stdin.setRawMode(true);
|
|
64
|
-
stdin.resume();
|
|
65
|
-
function cleanup() {
|
|
66
|
-
stdin.setRawMode(false);
|
|
67
|
-
stdin.pause();
|
|
68
|
-
stdin.removeListener("data", onData);
|
|
69
|
-
}
|
|
70
|
-
function onData(data) {
|
|
71
|
-
let remaining = data.toString();
|
|
72
|
-
while (remaining) {
|
|
73
|
-
const sequence = nextSequence(remaining);
|
|
74
|
-
remaining = remaining.slice(sequence.length);
|
|
75
|
-
const key = parseKey(Buffer.from(sequence));
|
|
76
|
-
if (key.name === "ctrl+c" || key.name === "ctrl+d" || key.name === "escape") {
|
|
77
|
-
cleanup();
|
|
78
|
-
clearLines(stderr, lineCount);
|
|
79
|
-
resolve(null);
|
|
80
|
-
return;
|
|
81
|
-
}
|
|
82
|
-
if (key.name === "return") {
|
|
83
|
-
cleanup();
|
|
84
|
-
clearLines(stderr, lineCount);
|
|
85
|
-
resolve(selected);
|
|
86
|
-
return;
|
|
87
|
-
}
|
|
88
|
-
if (key.name === "up") {
|
|
89
|
-
clearLines(stderr, lineCount);
|
|
90
|
-
selected = (selected - 1 + options.length) % options.length;
|
|
91
|
-
render();
|
|
92
|
-
}
|
|
93
|
-
else if (key.name === "down") {
|
|
94
|
-
clearLines(stderr, lineCount);
|
|
95
|
-
selected = (selected + 1) % options.length;
|
|
96
|
-
render();
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
stdin.on("data", onData);
|
|
19
|
+
const value = await clack.select({
|
|
20
|
+
message: label,
|
|
21
|
+
options: options.map((option, index) => ({ value: index, label: option })),
|
|
22
|
+
initialValue: Math.max(0, Math.min(initial, options.length - 1)),
|
|
23
|
+
input: stdin,
|
|
24
|
+
output: stderr,
|
|
101
25
|
});
|
|
26
|
+
if (clack.isCancel(value))
|
|
27
|
+
return null;
|
|
28
|
+
return value;
|
|
102
29
|
}
|
|
103
30
|
export async function selectOptions(label, options, initialSelected = [], stdin = process.stdin, stderr = process.stderr) {
|
|
104
|
-
|
|
105
|
-
throw new InstallerError("terminal_required", "This operation requires an interactive terminal");
|
|
106
|
-
}
|
|
31
|
+
interactiveStreams(stdin, stderr);
|
|
107
32
|
validateOptions(options);
|
|
108
33
|
const allowed = new Set(options);
|
|
109
34
|
for (const value of initialSelected) {
|
|
110
35
|
if (!allowed.has(value))
|
|
111
36
|
throw new InstallerError("invalid_input", `Unknown initial selector option: ${value}`);
|
|
112
37
|
}
|
|
113
|
-
const
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
const cursor = index === active ? "> " : " ";
|
|
121
|
-
const marker = selected.has(options[index]) ? "[x]" : "[ ]";
|
|
122
|
-
stderr.write(`${cursor}${marker} ${options[index]}\n`);
|
|
123
|
-
}
|
|
124
|
-
stderr.write(` Selected ${selected.size}/${options.length}\n`);
|
|
125
|
-
}
|
|
126
|
-
render();
|
|
127
|
-
return new Promise((resolve) => {
|
|
128
|
-
stdin.setRawMode(true);
|
|
129
|
-
stdin.resume();
|
|
130
|
-
function cleanup() {
|
|
131
|
-
stdin.setRawMode(false);
|
|
132
|
-
stdin.pause();
|
|
133
|
-
stdin.removeListener("data", onData);
|
|
134
|
-
}
|
|
135
|
-
function finish(value) {
|
|
136
|
-
cleanup();
|
|
137
|
-
clearLines(stderr, lineCount);
|
|
138
|
-
resolve(value);
|
|
139
|
-
}
|
|
140
|
-
function redraw(update) {
|
|
141
|
-
clearLines(stderr, lineCount);
|
|
142
|
-
update();
|
|
143
|
-
render();
|
|
144
|
-
}
|
|
145
|
-
function onData(data) {
|
|
146
|
-
let remaining = data.toString();
|
|
147
|
-
while (remaining) {
|
|
148
|
-
const sequence = nextSequence(remaining);
|
|
149
|
-
remaining = remaining.slice(sequence.length);
|
|
150
|
-
const key = parseKey(Buffer.from(sequence));
|
|
151
|
-
if (key.name === "ctrl+c" || key.name === "ctrl+d" || key.name === "escape") {
|
|
152
|
-
finish(null);
|
|
153
|
-
return;
|
|
154
|
-
}
|
|
155
|
-
if (key.name === "return") {
|
|
156
|
-
finish(options.filter((option) => selected.has(option)));
|
|
157
|
-
return;
|
|
158
|
-
}
|
|
159
|
-
if (key.name === "up") {
|
|
160
|
-
redraw(() => {
|
|
161
|
-
active = (active - 1 + options.length) % options.length;
|
|
162
|
-
});
|
|
163
|
-
}
|
|
164
|
-
else if (key.name === "down") {
|
|
165
|
-
redraw(() => {
|
|
166
|
-
active = (active + 1) % options.length;
|
|
167
|
-
});
|
|
168
|
-
}
|
|
169
|
-
else if (key.name === "space") {
|
|
170
|
-
redraw(() => {
|
|
171
|
-
const option = options[active];
|
|
172
|
-
if (selected.has(option))
|
|
173
|
-
selected.delete(option);
|
|
174
|
-
else
|
|
175
|
-
selected.add(option);
|
|
176
|
-
});
|
|
177
|
-
}
|
|
178
|
-
else if (key.name === "all") {
|
|
179
|
-
redraw(() => {
|
|
180
|
-
for (const option of options)
|
|
181
|
-
selected.add(option);
|
|
182
|
-
});
|
|
183
|
-
}
|
|
184
|
-
else if (key.name === "none") {
|
|
185
|
-
redraw(() => selected.clear());
|
|
186
|
-
}
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
|
-
stdin.on("data", onData);
|
|
38
|
+
const value = await clack.multiselect({
|
|
39
|
+
message: label,
|
|
40
|
+
options: options.map((option) => ({ value: option, label: option })),
|
|
41
|
+
initialValues: [...initialSelected],
|
|
42
|
+
required: false,
|
|
43
|
+
input: stdin,
|
|
44
|
+
output: stderr,
|
|
190
45
|
});
|
|
46
|
+
if (clack.isCancel(value))
|
|
47
|
+
return null;
|
|
48
|
+
return value;
|
|
191
49
|
}
|
|
192
50
|
export async function promptText(label, stdin = process.stdin, stderr = process.stderr) {
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
stdin.removeListener("data", onData);
|
|
207
|
-
}
|
|
208
|
-
function onData(data) {
|
|
209
|
-
for (const seq of data.toString()) {
|
|
210
|
-
if (seq === "\r" || seq === "\n") {
|
|
211
|
-
cleanup();
|
|
212
|
-
stderr.write("\n");
|
|
213
|
-
resolve(buffer.trim() || null);
|
|
214
|
-
return;
|
|
215
|
-
}
|
|
216
|
-
if (seq === "\x03" || seq === "\x04" || seq === "\x1b") {
|
|
217
|
-
cleanup();
|
|
218
|
-
stderr.write("\n");
|
|
219
|
-
resolve(null);
|
|
220
|
-
return;
|
|
221
|
-
}
|
|
222
|
-
if (seq === "\x7f" || seq === "\b") {
|
|
223
|
-
if (buffer.length > 0) {
|
|
224
|
-
buffer = buffer.slice(0, -1);
|
|
225
|
-
stderr.write("\x1b[D \x1b[D");
|
|
226
|
-
}
|
|
227
|
-
continue;
|
|
228
|
-
}
|
|
229
|
-
if (seq >= " ") {
|
|
230
|
-
buffer += seq;
|
|
231
|
-
stderr.write(seq);
|
|
232
|
-
}
|
|
233
|
-
}
|
|
234
|
-
}
|
|
235
|
-
stdin.on("data", onData);
|
|
236
|
-
});
|
|
51
|
+
interactiveStreams(stdin, stderr);
|
|
52
|
+
const value = await clack.text({ message: label, input: stdin, output: stderr });
|
|
53
|
+
if (clack.isCancel(value))
|
|
54
|
+
return null;
|
|
55
|
+
const trimmed = value.trim();
|
|
56
|
+
return trimmed.length ? trimmed : null;
|
|
57
|
+
}
|
|
58
|
+
export async function confirmQuestion(label, stdin = process.stdin, stderr = process.stderr) {
|
|
59
|
+
interactiveStreams(stdin, stderr);
|
|
60
|
+
const value = await clack.confirm({ message: label, input: stdin, output: stderr });
|
|
61
|
+
if (clack.isCancel(value))
|
|
62
|
+
return null;
|
|
63
|
+
return value;
|
|
237
64
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kisev/agentomatic",
|
|
3
|
-
"version": "11.0.0-dev.
|
|
3
|
+
"version": "11.0.0-dev.59.g0506803c61e5",
|
|
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": {
|
|
@@ -49,8 +49,9 @@
|
|
|
49
49
|
"pack:check": "node test/pack-allowlist.mjs"
|
|
50
50
|
},
|
|
51
51
|
"dependencies": {
|
|
52
|
-
"@
|
|
53
|
-
"@kisev/
|
|
52
|
+
"@clack/prompts": "^1.8.1",
|
|
53
|
+
"@kisev/memomatic": "1.0.0-dev.59.g0506803c61e5",
|
|
54
|
+
"@kisev/safe-fs": "1.0.0-dev.59.g0506803c61e5"
|
|
54
55
|
},
|
|
55
56
|
"devDependencies": {
|
|
56
57
|
"@opencode-ai/plugin": "1.18.29",
|