@krtclcdy/tersio 1.0.4 → 2.0.0

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/tersio.js CHANGED
@@ -2,82 +2,82 @@
2
2
  // tersio.ts — Install Tersio (caveman/rtk/ponytail) add-ons on any OMP device.
3
3
  // Usage: node tersio.js [install|update|reinstall|doctor|uninstall|version|help] [options]
4
4
  // Requires: node/npm and omp CLI
5
- import { execFile } from "node:child_process";
6
- import { promises as fs } from "node:fs";
7
- import os from "node:os";
8
- import path from "node:path";
9
- import readline from "node:readline";
10
- import { fileURLToPath } from "node:url";
11
- import { promisify } from "node:util";
12
- import { createRequire } from "node:module";
13
- import { httpsGet, httpsDownload, sha256File, parseChecksum, readTextIfExists, } from "./extensions/lib/utils.js";
14
- const IS_WINDOWS = process.platform === "win32";
15
- const HOME = process.env.HOME || process.env.USERPROFILE || "";
16
- const PACKAGE_NAME = "@krtclcdy/tersio";
17
- const PACKAGE_BIN = "tersio";
18
- const { version: PACKAGE_VERSION } = createRequire(import.meta.url)("./package.json");
5
+ import { execFile } from 'node:child_process';
6
+ import { promises as fs } from 'node:fs';
7
+ import os from 'node:os';
8
+ import path from 'node:path';
9
+ import readline from 'node:readline';
10
+ import { fileURLToPath } from 'node:url';
11
+ import { promisify } from 'node:util';
12
+ import { createRequire } from 'node:module';
13
+ import { CAVEMAN_REMOTE_RULE, RTK_RELEASE_API, fetchJson, findFile, httpsGet, httpsDownload, sha256File, parseChecksum, readTextIfExists, rtkPlatformSpec, } from './extensions/lib/utils.js';
14
+ const IS_WINDOWS = process.platform === 'win32';
15
+ const RTK_BINARY_NAME = IS_WINDOWS ? 'rtk.exe' : 'rtk';
16
+ const HOME = process.env.HOME || process.env.USERPROFILE || '';
17
+ const OMP_AGENT_DIR = path.join(HOME, '.omp', 'agent');
18
+ const OMP_PLUGINS_DIR = path.join(HOME, '.omp', 'plugins');
19
+ const BUN_BIN_DIR = path.join(HOME, '.bun', 'bin');
20
+ const OMP_BIN = IS_WINDOWS ? 'omp.cmd' : 'omp';
21
+ const PACKAGE_NAME = '@krtclcdy/tersio';
22
+ const PACKAGE_BIN = 'tersio';
23
+ const { version: PACKAGE_VERSION } = createRequire(import.meta.url)('./package.json');
19
24
  // Session-start mode defaults for fresh installs.
20
- const COMBO_DEFAULTS = { off: true, medium: true, balanced: true, max: true };
21
- const CAVEMAN_DEFAULTS = { off: true, lite: true, full: true, ultra: true, wenyan: true };
25
+ const CAVEMAN_DEFAULTS = new Set(['off', 'lite', 'full', 'ultra', 'wenyan']);
26
+ const PONYTAIL_DEFAULTS = new Set(['off', 'lite', 'full', 'ultra', 'review']);
27
+ const RTK_DEFAULTS = new Set(['on', 'off']);
22
28
  // ponytail: Combo preset implies all three modes. Mirrors COMBO_LEVELS in
23
29
  // extensions/shared/session-state.ts (rtk as boolean here). Single source.
24
30
  const COMBO_PRESET_MODES = {
25
- off: { caveman: "off", rtk: false, ponytail: "off" },
26
- medium: { caveman: "lite", rtk: true, ponytail: "lite" },
27
- balanced: { caveman: "full", rtk: true, ponytail: "full" },
28
- max: { caveman: "ultra", rtk: true, ponytail: "ultra" },
31
+ off: { caveman: 'off', rtk: false, ponytail: 'off' },
32
+ medium: { caveman: 'lite', rtk: true, ponytail: 'lite' },
33
+ balanced: { caveman: 'full', rtk: true, ponytail: 'full' },
34
+ max: { caveman: 'ultra', rtk: true, ponytail: 'ultra' },
29
35
  };
36
+ const COMBO_DEFAULTS = new Set(Object.keys(COMBO_PRESET_MODES));
30
37
  function parseEnum(value, valid, flag) {
31
38
  if (value === undefined)
32
39
  return undefined;
33
40
  const v = value.trim().toLowerCase();
34
- if (!(v in valid)) {
35
- console.error(`[fail] Invalid ${flag}: ${value}. Valid: ${Object.keys(valid).join(", ")}`);
41
+ if (!valid.has(v)) {
42
+ console.error(`[fail] Invalid ${flag}: ${value}. Valid: ${[...valid].join(', ')}`);
36
43
  process.exit(1);
37
44
  }
38
45
  return v;
39
46
  }
40
47
  function flagValue(name) {
41
48
  const i = args.indexOf(name);
42
- if (i === -1)
43
- return undefined;
44
- return args[i + 1];
49
+ if (i !== -1)
50
+ return args[i + 1];
51
+ const prefix = `${name}=`;
52
+ return args.find((arg) => arg.startsWith(prefix))?.slice(prefix.length);
45
53
  }
46
54
  // --- CLI flags ---
47
- const COMMANDS = { install: true, update: true, reinstall: true, doctor: true, uninstall: true, version: true, help: true };
55
+ const COMMANDS = new Set(['install', 'update', 'reinstall', 'doctor', 'uninstall', 'version', 'help']);
48
56
  const args = process.argv.slice(2);
49
- const commandArg = args.find((arg, index) => !arg.startsWith("-") && args[index - 1] !== "--scope");
57
+ const commandArg = args.find((arg, index) => !arg.startsWith('-') && args[index - 1] !== '--scope');
50
58
  const command = commandArg?.toLowerCase() || null;
51
- const unknownCommand = command !== null && !(command in COMMANDS);
52
- const install = command === "install";
53
- const update = command === "update";
54
- const reinstall = command === "reinstall";
55
- const showVersion = command === "version" || args.includes("--version") || args.includes("-v");
56
- const showHelp = command === "help" || args.includes("--help") || args.includes("-h");
57
- const applyUpdate = args.includes("--apply-update");
58
- const dryRun = args.includes("--dry-run");
59
- const yes = args.includes("--yes") || args.includes("-y") || install || update || reinstall || applyUpdate;
60
- const verbose = args.includes("--verbose");
61
- const doctor = command === "doctor" || args.includes("--doctor");
62
- const uninstall = command === "uninstall" || args.includes("--uninstall");
63
- const removePonytail = args.includes("--remove-ponytail");
64
- const removeRtk = args.includes("--remove-rtk");
65
- const scopeFlag = (() => {
66
- const i = args.indexOf("--scope");
67
- if (i === -1)
68
- return null;
69
- return args[i + 1]?.toLowerCase() || null;
70
- })();
59
+ const unknownCommand = command !== null && !COMMANDS.has(command);
60
+ const install = command === 'install';
61
+ const update = command === 'update';
62
+ const reinstall = command === 'reinstall';
63
+ const showVersion = command === 'version' || args.includes('--version') || args.includes('-v');
64
+ const showHelp = command === 'help' || args.includes('--help') || args.includes('-h');
65
+ const applyUpdate = args.includes('--apply-update');
66
+ const dryRun = args.includes('--dry-run');
67
+ const yes = args.includes('--yes') || args.includes('-y') || install || update || reinstall || applyUpdate;
68
+ const verbose = args.includes('--verbose');
69
+ const doctor = command === 'doctor' || args.includes('--doctor');
70
+ const uninstall = command === 'uninstall' || args.includes('--uninstall');
71
+ const removePonytail = args.includes('--remove-ponytail');
72
+ const removeRtk = args.includes('--remove-rtk');
73
+ const scopeFlag = flagValue('--scope')?.toLowerCase() ?? null;
71
74
  // --- Profile selection (session-start mode defaults) ---
72
- const comboDefaultFlag = parseEnum(flagValue("--combo-default"), COMBO_DEFAULTS, "--combo-default");
73
- const cavemanDefaultFlag = parseEnum(flagValue("--caveman-default"), CAVEMAN_DEFAULTS, "--caveman-default");
74
- const rtkDefaultFlag = flagValue("--rtk-default")?.toLowerCase();
75
- if (rtkDefaultFlag !== undefined && rtkDefaultFlag !== "on" && rtkDefaultFlag !== "off") {
76
- console.error(`[fail] Invalid --rtk-default: ${rtkDefaultFlag}. Use: on, off`);
77
- process.exit(1);
78
- }
79
- const profileFlagsGiven = comboDefaultFlag !== undefined
80
- || cavemanDefaultFlag !== undefined || rtkDefaultFlag !== undefined;
75
+ const comboDefaultFlag = parseEnum(flagValue('--combo-default'), COMBO_DEFAULTS, '--combo-default');
76
+ const cavemanDefaultFlag = parseEnum(flagValue('--caveman-default'), CAVEMAN_DEFAULTS, '--caveman-default');
77
+ const ponytailDefaultFlag = parseEnum(flagValue('--ponytail-default'), PONYTAIL_DEFAULTS, '--ponytail-default');
78
+ const rtkDefaultFlag = parseEnum(flagValue('--rtk-default'), RTK_DEFAULTS, '--rtk-default');
79
+ const profileFlagsGiven = [comboDefaultFlag, cavemanDefaultFlag, rtkDefaultFlag, ponytailDefaultFlag]
80
+ .some((flag) => flag !== undefined);
81
81
  function printHelp() {
82
82
  console.log(`Usage: ${PACKAGE_BIN} [command] [options]
83
83
 
@@ -96,6 +96,7 @@ Options:
96
96
  --remove-rtk (uninstall: also remove the RTK binary)
97
97
  --combo-default off|medium|balanced|max (implies caveman, rtk, ponytail)
98
98
  --caveman-default off|lite|full|ultra|wenyan (override; default follows combo)
99
+ --ponytail-default off|lite|full|ultra|review (override; default follows combo)
99
100
  --rtk-default on|off (override; default follows combo)
100
101
  --yes, -y
101
102
  --dry-run
@@ -105,14 +106,12 @@ Options:
105
106
  }
106
107
  function debug(...a) {
107
108
  if (verbose)
108
- console.log(" [debug]", ...a);
109
+ console.log(' [debug]', ...a);
109
110
  }
110
111
  const RL = readline.createInterface({ input: process.stdin, output: process.stdout });
111
112
  let rlOpen = true;
112
113
  function ask(q) {
113
- const { promise, resolve } = Promise.withResolvers();
114
- RL.question(q, resolve);
115
- return promise;
114
+ return new Promise((resolve) => RL.question(q, resolve));
116
115
  }
117
116
  function closeRL() { if (rlOpen) {
118
117
  RL.close();
@@ -120,25 +119,22 @@ function closeRL() { if (rlOpen) {
120
119
  } }
121
120
  // Paths to extension source files (relative to this script)
122
121
  const SCRIPT_DIR = path.dirname(fileURLToPath(import.meta.url));
123
- const EXT_DIR = path.join(SCRIPT_DIR, "extensions");
124
- const SHARED_SESSION_STATE = path.join(EXT_DIR, "shared", "session-state.js");
125
- const CAVEMAN_INDEX = path.join(EXT_DIR, "caveman-session", "index.js");
126
- const RTK_SESSION_INDEX = path.join(EXT_DIR, "rtk-session", "index.js");
127
- const UPDATER_INDEX = path.join(EXT_DIR, "ai-addons-updater", "index.js");
128
- const COMBO_TOGGLE_INDEX = path.join(EXT_DIR, "combo-toggle", "index.js");
129
- const AMANAI_REWARD_INDEX = path.join(EXT_DIR, "amanai-reward", "index.js");
130
- const MODE_REINFORCEMENT_INDEX = path.join(EXT_DIR, "shared", "mode-reinforcement.js");
131
- const SHARED_TYPES = path.join(EXT_DIR, "shared", "types.js");
132
- const LIB_UTILS = path.join(EXT_DIR, "lib", "utils.js");
133
- const SHARED_PLUGIN_SETTINGS = path.join(EXT_DIR, "shared", "plugin-settings.js");
134
- const CAVEMAN_REMOTE_RULE = "https://raw.githubusercontent.com/JuliusBrussee/caveman/main/src/rules/caveman-activate.md";
135
- const RTK_RELEASE_API = "https://api.github.com/repos/rtk-ai/rtk/releases/latest";
122
+ const EXT_DIR = path.join(SCRIPT_DIR, 'extensions');
123
+ const SHARED_SESSION_STATE = path.join(EXT_DIR, 'shared', 'session-state.js');
124
+ const CAVEMAN_INDEX = path.join(EXT_DIR, 'caveman-session', 'index.js');
125
+ const RTK_SESSION_INDEX = path.join(EXT_DIR, 'rtk-session', 'index.js');
126
+ const UPDATER_INDEX = path.join(EXT_DIR, 'ai-addons-updater', 'index.js');
127
+ const COMBO_TOGGLE_INDEX = path.join(EXT_DIR, 'combo-toggle', 'index.js');
128
+ const MODE_REINFORCEMENT_INDEX = path.join(EXT_DIR, 'shared', 'mode-reinforcement.js');
129
+ const SHARED_TYPES = path.join(EXT_DIR, 'shared', 'types.js');
130
+ const LIB_UTILS = path.join(EXT_DIR, 'lib', 'utils.js');
131
+ const SHARED_PLUGIN_SETTINGS = path.join(EXT_DIR, 'shared', 'plugin-settings.js');
136
132
  // --- Helpers ---
137
133
  const execFileP = promisify(execFile);
138
134
  async function execP(cmd, args, opts = {}) {
139
135
  return execFileP(cmd, args, {
140
136
  timeout: opts.timeout || 120000,
141
- encoding: "utf8",
137
+ encoding: 'utf8',
142
138
  ...opts,
143
139
  });
144
140
  }
@@ -157,49 +153,61 @@ async function writeIfChanged(dest, content, options = {}) {
157
153
  await fs.copyFile(dest, `${dest}.bak`);
158
154
  debug(`${path.basename(dest)} → ${path.basename(dest)}.bak`);
159
155
  }
160
- await fs.writeFile(dest, content, "utf8");
156
+ await fs.writeFile(dest, content, 'utf8');
161
157
  console.log(` [write] ${dest}`);
162
158
  return true;
163
159
  }
160
+ // omp ships 'extensions: null'; appending list items under a null scalar breaks
161
+ // YAML parsing. Normalize null/~/[]/empty to a mapping key first.
162
+ const EXTENSIONS_KEY_RE = /^\s*extensions\s*:/i;
163
+ const EXTENSIONS_NULL_RE = /^\s*extensions\s*:\s*(?:\[\s*\]|null|~)?\s*$/i;
164
+ function normalizeExtensionsKey(lines) {
165
+ const idx = lines.findIndex((l) => EXTENSIONS_NULL_RE.test(l));
166
+ if (idx === -1)
167
+ return false;
168
+ lines[idx] = 'extensions:';
169
+ return true;
170
+ }
171
+ async function writeConfigLines(configPath, lines, logMsg) {
172
+ await fs.mkdir(path.dirname(configPath), { recursive: true });
173
+ await fs.copyFile(configPath, `${configPath}.bak`).catch(() => { });
174
+ await fs.writeFile(configPath, lines.join('\n'), 'utf8');
175
+ console.log(logMsg);
176
+ }
164
177
  async function ensureExtensionInConfig(configPath, extensionPath, label, options = {}) {
165
- const normalizedPath = extensionPath.replace(/\\/g, "/");
178
+ const normalizedPath = extensionPath.replace(/\\/g, '/');
166
179
  const line = ` - ${normalizedPath}`;
167
180
  const raw = await readTextIfExists(configPath);
168
- let lines = (raw || "").split("\n");
181
+ let lines = (raw || '').split('\n');
169
182
  if (lines.some((l) => l.includes(normalizedPath))) {
170
183
  debug(`${label} already in config.yml`);
171
184
  return false;
172
185
  }
173
- const extLineIdx = lines.findIndex((l) => /^\s*extensions\s*:/i.test(l));
186
+ const extLineIdx = lines.findIndex((l) => EXTENSIONS_KEY_RE.test(l));
174
187
  if (options.dryRun) {
175
188
  console.log(` [dry-run] would add ${label} to config.yml: ${normalizedPath}`);
176
189
  return true;
177
190
  }
178
- // ponytail: omp ships "extensions: null"; appending list items under a null scalar breaks YAML parsing. Normalize null/~/[]/empty to a mapping key first.
179
- const emptyArrayIdx = lines.findIndex((l) => /^\s*extensions\s*:\s*(?:\[\s*\]|null|~)?\s*$/i.test(l));
180
- if (emptyArrayIdx !== -1) {
181
- lines[emptyArrayIdx] = "extensions:";
182
- lines.splice(emptyArrayIdx + 1, 0, line, "");
191
+ if (normalizeExtensionsKey(lines)) {
192
+ lines.splice(extLineIdx + 1, 0, line, '');
183
193
  }
184
194
  else if (extLineIdx === -1) {
185
- lines.push("extensions:");
195
+ lines.push('extensions:');
186
196
  lines.push(line);
187
- lines.push("");
197
+ lines.push('');
188
198
  }
189
199
  else {
190
200
  lines.splice(extLineIdx + 1, 0, line);
191
201
  }
192
- await fs.mkdir(path.dirname(configPath), { recursive: true });
193
- await fs.writeFile(configPath, lines.join("\n"), "utf8");
194
- console.log(` [write] Added ${label} to config.yml`);
202
+ await writeConfigLines(configPath, lines, ` [write] Added ${label} to config.yml`);
195
203
  return true;
196
204
  }
197
205
  async function ensureExtensionAfterConfigEntry(configPath, extensionPath, afterPath, label, options = {}) {
198
- const normalizedPath = extensionPath.replace(/\\/g, "/");
199
- const normalizedAfterPath = afterPath.replace(/\\/g, "/");
206
+ const normalizedPath = extensionPath.replace(/\\/g, '/');
207
+ const normalizedAfterPath = afterPath.replace(/\\/g, '/');
200
208
  const line = ` - ${normalizedPath}`;
201
209
  const raw = await readTextIfExists(configPath);
202
- const lines = (raw || "").split("\n");
210
+ const lines = (raw || '').split('\n');
203
211
  const existingIndex = lines.findIndex((entry) => entry.includes(normalizedPath));
204
212
  const afterIndex = lines.findIndex((entry) => entry.includes(normalizedAfterPath));
205
213
  if (existingIndex !== -1 && afterIndex !== -1 && existingIndex === afterIndex + 1)
@@ -215,215 +223,180 @@ async function ensureExtensionAfterConfigEntry(configPath, extensionPath, afterP
215
223
  lines.splice(refreshedAfterIndex + 1, 0, line);
216
224
  }
217
225
  else {
218
- const extensionsIndex = lines.findIndex((entry) => /^\s*extensions\s*:/i.test(entry));
226
+ const extensionsIndex = lines.findIndex((entry) => EXTENSIONS_KEY_RE.test(entry));
219
227
  if (extensionsIndex === -1) {
220
- lines.push("extensions:", line, "");
228
+ lines.push('extensions:', line, '');
221
229
  }
222
230
  else {
223
- if (/^\s*extensions\s*:\s*(?:\[\s*\]|null|~)?\s*$/i.test(lines[extensionsIndex]))
224
- lines[extensionsIndex] = "extensions:";
231
+ normalizeExtensionsKey(lines);
225
232
  lines.splice(extensionsIndex + 1, 0, line);
226
233
  }
227
234
  }
228
- await fs.mkdir(path.dirname(configPath), { recursive: true });
229
- await fs.writeFile(configPath, lines.join("\n"), "utf8");
230
- console.log(` [write] Placed ${label} after Ponytail in config.yml`);
235
+ await writeConfigLines(configPath, lines, ` [write] Placed ${label} after Ponytail in config.yml`);
231
236
  return true;
232
237
  }
233
238
  function readPonytailConfig() {
234
239
  const dir = process.env.XDG_CONFIG_HOME
235
- ? path.join(process.env.XDG_CONFIG_HOME, "ponytail")
236
- : path.join(HOME, ".config", "ponytail");
237
- return { dir, path: path.join(dir, "config.json") };
240
+ ? path.join(process.env.XDG_CONFIG_HOME, 'ponytail')
241
+ : path.join(HOME, '.config', 'ponytail');
242
+ return { dir, path: path.join(dir, 'config.json') };
238
243
  }
239
- async function ensurePonytailDefaultOff(options = {}) {
240
- const config = readPonytailConfig();
241
- if (options.dryRun) {
242
- console.log(` [dry-run] would set Ponytail defaultMode=off in ${config.path}`);
243
- return;
244
- }
245
- let cfg = {};
246
- const existing = await readTextIfExists(config.path);
247
- if (existing) {
248
- try {
249
- cfg = JSON.parse(existing.replace(/^\uFEFF/, ""));
250
- if (!cfg || typeof cfg !== "object" || Array.isArray(cfg)) {
251
- cfg = {};
252
- }
253
- }
254
- catch {
255
- cfg = {};
256
- }
244
+ function parseJsonObject(text) {
245
+ if (!text)
246
+ return null;
247
+ try {
248
+ const parsed = JSON.parse(text.replace(/^\uFEFF/, ''));
249
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed))
250
+ return null;
251
+ return parsed;
257
252
  }
258
- if (cfg.defaultMode === "off") {
259
- debug("Ponytail defaultMode already off");
260
- return;
253
+ catch {
254
+ return null;
261
255
  }
262
- await fs.mkdir(config.dir, { recursive: true });
263
- cfg.defaultMode = "off";
264
- await fs.writeFile(config.path, JSON.stringify(cfg, null, 2) + "\n", "utf8");
265
- console.log(` [write] Set Ponytail defaultMode=off in ${config.path}`);
266
256
  }
267
- // Sets ~/.config/ponytail/config.json#hideStatus=true so the upstream ponytail
268
- // status bar doesn't render — the combo extension owns the bar instead.
269
- async function ensurePonytailHideStatus(options = {}) {
257
+ function parsePonytailConfig(existing) {
258
+ return parseJsonObject(existing) ?? {};
259
+ }
260
+ async function patchPonytailConfig(apply, dryRunNote, writeNote, options = {}) {
270
261
  const config = readPonytailConfig();
271
262
  if (options.dryRun) {
272
- console.log(` [dry-run] would set Ponytail hideStatus=true in ${config.path}`);
263
+ console.log(` [dry-run] ${dryRunNote} in ${config.path}`);
273
264
  return;
274
265
  }
275
- let cfg = {};
276
- const existing = await readTextIfExists(config.path);
277
- if (existing) {
278
- try {
279
- cfg = JSON.parse(existing.replace(/^\uFEFF/, ""));
280
- if (!cfg || typeof cfg !== "object" || Array.isArray(cfg))
281
- cfg = {};
282
- }
283
- catch {
284
- cfg = {};
285
- }
286
- }
287
- if (cfg.hideStatus === true) {
288
- debug("Ponytail hideStatus already true");
266
+ const cfg = parsePonytailConfig(await readTextIfExists(config.path));
267
+ if (!apply(cfg)) {
268
+ debug('Ponytail config already up to date');
289
269
  return;
290
270
  }
291
271
  await fs.mkdir(config.dir, { recursive: true });
292
- cfg.hideStatus = true;
293
- await fs.writeFile(config.path, JSON.stringify(cfg, null, 2) + "\n", "utf8");
294
- console.log(` [write] Set Ponytail hideStatus=true in ${config.path}`);
272
+ await fs.writeFile(config.path, JSON.stringify(cfg, null, 2) + '\n', 'utf8');
273
+ console.log(` [write] ${writeNote} in ${config.path}`);
274
+ }
275
+ async function ensurePonytailConfigValue(key, value, options = {}) {
276
+ await patchPonytailConfig((cfg) => {
277
+ if (cfg[key] === value)
278
+ return false;
279
+ cfg[key] = value;
280
+ return true;
281
+ }, `would set Ponytail ${key}=${value}`, `Set Ponytail ${key}=${value}`, options);
295
282
  }
283
+ // Read plugins/package.json tolerantly; corrupt or missing files start fresh
284
+ // with installer-managed defaults.
285
+ async function readPluginsPackage(pkgPath) {
286
+ const pkg = parseJsonObject(await readTextIfExists(pkgPath)) ?? {};
287
+ return { ...pkg, name: pkg.name || 'omp-plugins', private: true, dependencies: pkg.dependencies || {} };
288
+ }
289
+ const PONYTAIL_GITHUB_SPEC = 'github:DietrichGebert/ponytail';
290
+ const PONYTAIL_NPM_SPEC = '@dietrichgebert/ponytail@latest';
296
291
  // --- Steps ---
297
292
  async function stepPonytail(pluginsDir, userDir, options) {
298
- console.log("\n[1/8] Installing Ponytail plugin...");
293
+ console.log('\n[1/7] Installing Ponytail plugin...');
299
294
  await fs.mkdir(pluginsDir, { recursive: true });
300
- const pkgPath = path.join(pluginsDir, "package.json");
301
- let pkg = {};
302
- const existing = await readTextIfExists(pkgPath);
303
- if (existing)
304
- pkg = JSON.parse(existing);
305
- pkg.name = pkg.name || "omp-plugins";
306
- pkg.private = true;
307
- pkg.dependencies = pkg.dependencies || {};
308
- pkg.dependencies["@dietrichgebert/ponytail"] = "github:DietrichGebert/ponytail";
295
+ const pkgPath = path.join(pluginsDir, 'package.json');
296
+ const pkg = await readPluginsPackage(pkgPath);
297
+ pkg.dependencies['@dietrichgebert/ponytail'] = PONYTAIL_GITHUB_SPEC;
309
298
  if (options.dryRun) {
310
299
  console.log(` [dry-run] would write ${pkgPath}`);
311
300
  }
312
301
  else {
313
- await fs.writeFile(pkgPath, JSON.stringify(pkg, null, 2) + "\n");
314
- console.log(` [write] package.json`);
315
- }
316
- if (options.dryRun) {
317
- console.log(" [dry-run] would run: omp plugin install github:DietrichGebert/ponytail");
302
+ await fs.writeFile(pkgPath, JSON.stringify(pkg, null, 2) + '\n');
303
+ console.log(' [write] package.json');
304
+ }
305
+ const ponytailExtPath = path.join(pluginsDir, 'node_modules', '@dietrichgebert', 'ponytail', 'pi-extension', 'index.js');
306
+ const probeExt = async () => (await readTextIfExists(ponytailExtPath)) !== null;
307
+ // Fast path: extension already present and no refresh asked — skip network.
308
+ let ponytailExtExists = !options.reinstall && !options.dryRun ? await probeExt() : false;
309
+ if (ponytailExtExists) {
310
+ debug('Ponytail pi-extension already installed; skipping network refresh');
311
+ }
312
+ else if (options.dryRun) {
313
+ // Dry runs preview the wiring below without touching the network.
314
+ console.log(` [dry-run] would run: omp plugin install ${PONYTAIL_GITHUB_SPEC}`);
318
315
  if (options.reinstall) {
319
- console.log(" [dry-run] would run: npm install @dietrichgebert/ponytail@latest --save --no-audit --no-fund");
316
+ console.log(` [dry-run] would run: npm install ${PONYTAIL_NPM_SPEC} --save --no-audit --no-fund`);
320
317
  }
321
- const ponytailExtPath = path.join(pluginsDir, "node_modules", "@dietrichgebert", "ponytail", "pi-extension", "index.js");
322
- const configPath = path.join(userDir, "config.yml");
323
- await ensureExtensionInConfig(configPath, ponytailExtPath, "ponytail", options);
324
- await ensurePonytailDefaultOff(options);
325
- await ensurePonytailHideStatus(options);
326
- return;
318
+ ponytailExtExists = true;
327
319
  }
328
- // Try omp plugin install first
329
- try {
330
- await execP(IS_WINDOWS ? "omp.cmd" : "omp", ["plugin", "install", "github:DietrichGebert/ponytail"], { cwd: pluginsDir });
331
- console.log(" [ok] omp plugin install ran");
332
- }
333
- catch (e) {
334
- console.log(` [warn] omp plugin install failed: ${e.message}`);
335
- }
336
- if (options.reinstall) {
320
+ else {
321
+ // Try omp plugin install first
337
322
  try {
338
- await execP("npm", [
339
- "install",
340
- "@dietrichgebert/ponytail@latest",
341
- "--save",
342
- "--no-audit",
343
- "--no-fund",
344
- ], { cwd: pluginsDir, timeout: 120000 });
345
- console.log(" [ok] Ponytail refreshed");
323
+ await execP(OMP_BIN, ['plugin', 'install', PONYTAIL_GITHUB_SPEC], { cwd: pluginsDir });
324
+ console.log(' [ok] omp plugin install ran');
346
325
  }
347
326
  catch (e) {
348
- console.log(` [fail] Could not refresh ponytail: ${e.message}`);
349
- console.log(` [hint] Manual: cd ~/.omp/plugins && npm install @dietrichgebert/ponytail@latest --save --no-audit --no-fund`);
327
+ console.log(` [warn] omp plugin install failed: ${e.message}`);
350
328
  }
351
- }
352
- // Verify the pi-extension/index.js actually exists
353
- const ponytailExtPath = path.join(pluginsDir, "node_modules", "@dietrichgebert", "ponytail", "pi-extension", "index.js");
354
- let ponytailExtExists = await readTextIfExists(ponytailExtPath);
355
- // Fallback: try bun install or npm install
356
- if (!ponytailExtExists) {
357
- console.log(" [info] pi-extension/index.js not found after omp plugin install — trying npm/bun install...");
358
- try {
359
- await execP("npm", ["install"], { cwd: pluginsDir, timeout: 120000 });
360
- console.log(" [ok] npm install completed");
361
- }
362
- catch {
329
+ if (options.reinstall) {
363
330
  try {
364
- await execP("bun", ["install"], { cwd: pluginsDir, timeout: 120000 });
365
- console.log(" [ok] bun install completed");
331
+ await execP('npm', ['install', PONYTAIL_NPM_SPEC, '--save', '--no-audit', '--no-fund'], { cwd: pluginsDir, timeout: 120000 });
332
+ console.log(' [ok] Ponytail refreshed');
366
333
  }
367
- catch (e2) {
368
- console.log(` [fail] Could not install ponytail: ${e2.message}`);
369
- console.log(` [hint] Manual: cd ~/.omp/plugins && npm install`);
334
+ catch (e) {
335
+ console.log(` [fail] Could not refresh ponytail: ${e.message}`);
336
+ console.log(` [hint] Manual: cd ~/.omp/plugins && npm install ${PONYTAIL_NPM_SPEC} --save --no-audit --no-fund`);
370
337
  }
371
338
  }
372
- ponytailExtExists = await readTextIfExists(ponytailExtPath);
373
- }
374
- // Last-resort fallback: git clone the repo into node_modules
375
- if (!ponytailExtExists) {
376
- console.log(" [info] npm/bun did not produce pi-extension — trying git clone...");
377
- try {
378
- const dest = path.join(pluginsDir, "node_modules", "@dietrichgebert", "ponytail");
379
- await fs.mkdir(path.dirname(dest), { recursive: true });
380
- await execP("git", ["clone", "--depth", "1", "https://github.com/DietrichGebert/ponytail.git", dest], { timeout: 180000 });
381
- console.log(" [ok] git clone completed");
382
- ponytailExtExists = await readTextIfExists(ponytailExtPath);
339
+ // Verify the pi-extension/index.js actually exists
340
+ ponytailExtExists = await probeExt();
341
+ // Fallback: try bun install or npm install
342
+ if (!ponytailExtExists) {
343
+ console.log(' [info] pi-extension/index.js not found after omp plugin install — trying npm/bun install...');
344
+ try {
345
+ await execP('npm', ['install'], { cwd: pluginsDir, timeout: 120000 });
346
+ console.log(' [ok] npm install completed');
347
+ }
348
+ catch {
349
+ try {
350
+ await execP('bun', ['install'], { cwd: pluginsDir, timeout: 120000 });
351
+ console.log(' [ok] bun install completed');
352
+ }
353
+ catch (e2) {
354
+ console.log(` [fail] Could not install ponytail: ${e2.message}`);
355
+ console.log(' [hint] Manual: cd ~/.omp/plugins && npm install');
356
+ }
357
+ }
358
+ ponytailExtExists = await probeExt();
383
359
  }
384
- catch (e3) {
385
- console.log(` [fail] git clone failed: ${e3.message}`);
386
- console.log(` [hint] Install git or check network: https://github.com/DietrichGebert/ponytail`);
360
+ // Last-resort fallback: git clone the repo into node_modules
361
+ if (!ponytailExtExists) {
362
+ console.log(' [info] npm/bun did not produce pi-extension — trying git clone...');
363
+ try {
364
+ const dest = path.join(pluginsDir, 'node_modules', '@dietrichgebert', 'ponytail');
365
+ await fs.mkdir(path.dirname(dest), { recursive: true });
366
+ await execP('git', ['clone', '--depth', '1', 'https://github.com/DietrichGebert/ponytail.git', dest], { timeout: 180000 });
367
+ console.log(' [ok] git clone completed');
368
+ ponytailExtExists = await probeExt();
369
+ }
370
+ catch (e3) {
371
+ console.log(` [fail] git clone failed: ${e3.message}`);
372
+ console.log(' [hint] Install git or check network: https://github.com/DietrichGebert/ponytail');
373
+ }
387
374
  }
388
375
  }
389
376
  if (!ponytailExtExists) {
390
- console.log(" [skip] Ponytail pi-extension/index.js still not found — skill-only mode");
391
- console.log(" [hint] The /ponytail command won't work, but ponytail skills will still load");
392
- // Still set Ponytail config defaultMode=off even without the extension command
393
- await ensurePonytailDefaultOff(options);
394
- await ensurePonytailHideStatus(options);
395
- return;
396
- }
397
- // Wire extension into config.yml so /ponytail command loads
398
- console.log(" [ok] Ponytail pi-extension found");
399
- const configPath = path.join(userDir, "config.yml");
400
- await ensureExtensionInConfig(configPath, ponytailExtPath, "ponytail", options);
401
- await ensurePonytailDefaultOff(options);
402
- await ensurePonytailHideStatus(options);
377
+ console.log(' [skip] Ponytail pi-extension/index.js still not found — skill-only mode');
378
+ console.log(' [hint] The /ponytail command won\'t work, but ponytail skills will still load');
379
+ }
380
+ else if (!options.dryRun) {
381
+ // Wire extension into config.yml so /ponytail command loads
382
+ console.log(' [ok] Ponytail pi-extension found');
383
+ }
384
+ // Still set Ponytail config defaults even without the extension command
385
+ const configPath = path.join(userDir, 'config.yml');
386
+ await ensureExtensionInConfig(configPath, ponytailExtPath, 'ponytail', options);
387
+ await ensurePonytailConfigValue('defaultMode', 'off', options);
388
+ // hideStatus=true keeps the upstream ponytail bar hidden; combo owns the bar.
389
+ await ensurePonytailConfigValue('hideStatus', true, options);
403
390
  }
404
391
  // Registers this package in ~/.omp/plugins so OMP lists it on the
405
392
  // Settings → Plugins page (OMP enumerates plugins/package.json dependencies).
406
- // Returns true when the package is verified in plugins/node_modules — the
407
- // Amanai detector then loads via the plugin's `omp.extensions` manifest, so
408
- // the caller must skip copying it into agent/extensions to avoid a double load.
393
+ // Returns true when the package is verified in plugins/node_modules.
409
394
  async function stepSelfPlugin(pluginsDir, options) {
410
- console.log("\n[2/8] Registering tersio as OMP plugin...");
411
- const pkgPath = path.join(pluginsDir, "package.json");
412
- let pkg = {};
413
- const existing = await readTextIfExists(pkgPath);
414
- if (existing) {
415
- try {
416
- pkg = JSON.parse(existing);
417
- }
418
- catch {
419
- pkg = {};
420
- }
421
- }
422
- pkg.name = pkg.name || "omp-plugins";
423
- pkg.private = true;
424
- pkg.dependencies = pkg.dependencies || {};
395
+ console.log('\n[2/7] Registering tersio as OMP plugin...');
396
+ const pkgPath = path.join(pluginsDir, 'package.json');
397
+ const pkg = await readPluginsPackage(pkgPath);
425
398
  pkg.dependencies[PACKAGE_NAME] = `^${PACKAGE_VERSION}`;
426
- for (const legacy of ["oh-my-pi-token-saver", "tersio-omp"]) {
399
+ for (const legacy of ['oh-my-pi-token-saver', 'tersio-omp']) {
427
400
  if (legacy in pkg.dependencies) {
428
401
  delete pkg.dependencies[legacy];
429
402
  if (!options.dryRun)
@@ -435,14 +408,23 @@ async function stepSelfPlugin(pluginsDir, options) {
435
408
  console.log(` [dry-run] would run: npm install --no-audit --no-fund (in ${pluginsDir})`);
436
409
  return false;
437
410
  }
411
+ // pkg.dependencies[PACKAGE_NAME] was assigned above, so only reinstall skips this fast path.
412
+ if (!options.reinstall) {
413
+ const installedPkgRaw = await readTextIfExists(path.join(pluginsDir, 'node_modules', PACKAGE_NAME, 'package.json'));
414
+ if (parseJsonObject(installedPkgRaw)?.version === PACKAGE_VERSION) {
415
+ debug(`${PACKAGE_NAME} already installed at v${PACKAGE_VERSION}; skipping npm install`);
416
+ console.log(' [ok] Listed in OMP Settings → Plugins as tersio');
417
+ return true;
418
+ }
419
+ }
438
420
  await fs.mkdir(pluginsDir, { recursive: true });
439
- await fs.writeFile(pkgPath, JSON.stringify(pkg, null, 2) + "\n", "utf8");
421
+ await fs.writeFile(pkgPath, JSON.stringify(pkg, null, 2) + '\n', 'utf8');
440
422
  try {
441
- await execP("npm", ["install", "--no-audit", "--no-fund"], { cwd: pluginsDir, timeout: 180000 });
423
+ await execP('npm', ['install', '--no-audit', '--no-fund'], { cwd: pluginsDir, timeout: 180000 });
442
424
  }
443
425
  catch {
444
426
  try {
445
- await execP("bun", ["install"], { cwd: pluginsDir, timeout: 180000 });
427
+ await execP('bun', ['install'], { cwd: pluginsDir, timeout: 180000 });
446
428
  }
447
429
  catch (e) {
448
430
  console.log(` [fail] Could not install ${PACKAGE_NAME} into plugins dir: ${e.message}`);
@@ -450,374 +432,395 @@ async function stepSelfPlugin(pluginsDir, options) {
450
432
  return false;
451
433
  }
452
434
  }
453
- const installedPkg = path.join(pluginsDir, "node_modules", PACKAGE_NAME, "package.json");
435
+ const installedPkg = path.join(pluginsDir, 'node_modules', PACKAGE_NAME, 'package.json');
454
436
  if ((await readTextIfExists(installedPkg)) === null) {
455
437
  console.log(` [warn] ${PACKAGE_NAME} not found in plugins/node_modules after install`);
456
438
  return false;
457
439
  }
458
- console.log(" [ok] Listed in OMP Settings → Plugins as tersio");
440
+ console.log(' [ok] Listed in OMP Settings → Plugins as tersio');
459
441
  return true;
460
442
  }
461
- async function stepRtk(binDir, options) {
462
- console.log("\n[3/8] Installing RTK binary...");
443
+ function resolveRtkTriple() {
444
+ const triple = rtkPlatformSpec()?.triple;
445
+ if (triple)
446
+ return triple;
447
+ console.log(` [fail] Unsupported platform: ${process.platform}/${process.arch}`);
448
+ console.log(' [hint] Manual: https://github.com/rtk-ai/rtk/releases');
449
+ return null;
450
+ }
451
+ function findRtkAsset(release, triple) {
452
+ const assets = release.assets || [];
453
+ const asset = assets.find((a) => a.name === `rtk-${triple}.zip` || a.name === `rtk-${triple}.tar.gz`);
454
+ if (asset)
455
+ return asset;
456
+ console.log(` [fail] No rtk-${triple}.<zip|tar.gz> in release ${release.tag_name}`);
457
+ console.log(` [hint] Available: ${assets.map((a) => a.name).filter((n) => n.startsWith('rtk-')).join(', ')}`);
458
+ return null;
459
+ }
460
+ async function downloadRtkChecksums(release) {
461
+ const checksumsAsset = (release.assets || []).find((a) => a.name === 'checksums.txt');
462
+ if (!checksumsAsset)
463
+ return null;
463
464
  try {
464
- const raw = await httpsGet(RTK_RELEASE_API);
465
- const release = JSON.parse(raw);
466
- const tag = release.tag_name;
467
- // Map (platform, arch) → Rust triple stem.
468
- const PLATFORM = process.platform;
469
- const ARCH = process.arch;
470
- let assetTriple;
471
- if (PLATFORM === "win32" && ARCH === "x64") {
472
- assetTriple = "x86_64-pc-windows-msvc";
473
- }
474
- else if (PLATFORM === "linux" && ARCH === "x64") {
475
- assetTriple = "x86_64-unknown-linux-musl";
465
+ return await httpsGet(checksumsAsset.browser_download_url);
466
+ }
467
+ catch (e) {
468
+ debug(`Could not download checksums.txt: ${e.message}`);
469
+ return null;
470
+ }
471
+ }
472
+ async function verifyRtkArchive(archivePath, assetName, checksumsText) {
473
+ if (!checksumsText) {
474
+ console.log(' [warn] No checksums.txt available — skipping verification');
475
+ return true;
476
+ }
477
+ const expected = parseChecksum(checksumsText, assetName);
478
+ const actual = await sha256File(archivePath);
479
+ if (!expected) {
480
+ console.log(` [warn] checksums.txt missing entry for ${assetName} — skipping verification`);
481
+ return true;
482
+ }
483
+ if (actual === expected) {
484
+ console.log(` [ok] Checksum verified for ${assetName}`);
485
+ return true;
486
+ }
487
+ console.log(` [fail] Checksum mismatch for ${assetName}`);
488
+ console.log(` [fail] Expected: ${expected}`);
489
+ console.log(` [fail] Got: ${actual}`);
490
+ // Caller finally removes the temp dir.
491
+ return false;
492
+ }
493
+ function shortError(e) {
494
+ return ((e.stderr) || e.message || '').trim().slice(0, 200);
495
+ }
496
+ async function extractRtkArchive(archivePath, extractDir) {
497
+ await fs.mkdir(extractDir, { recursive: true });
498
+ if (archivePath.endsWith('.zip')) {
499
+ if (IS_WINDOWS) {
500
+ await execP('powershell', ['Expand-Archive', '-Path', archivePath, '-DestinationPath', extractDir, '-Force'], { timeout: 60000 });
476
501
  }
477
- else if (PLATFORM === "linux" && ARCH === "arm64") {
478
- assetTriple = "aarch64-unknown-linux-gnu";
502
+ else {
503
+ await execP('unzip', [archivePath, '-d', extractDir], { timeout: 60000 });
479
504
  }
480
- else if (PLATFORM === "darwin" && ARCH === "x64") {
481
- assetTriple = "x86_64-apple-darwin";
505
+ return true;
506
+ }
507
+ if (archivePath.endsWith('.tar.gz') || archivePath.endsWith('.tgz')) {
508
+ try {
509
+ await execP('tar', ['xzf', archivePath, '-C', extractDir], { timeout: 60000 });
510
+ debug('tar xzf ok');
482
511
  }
483
- else if (PLATFORM === "darwin" && ARCH === "arm64") {
484
- assetTriple = "aarch64-apple-darwin";
512
+ catch (e) {
513
+ debug(`tar xzf failed: ${shortError(e)}`);
514
+ console.log(` [fail] tar could not extract ${path.basename(archivePath)}`);
515
+ console.log(' [hint] Manual: https://github.com/rtk-ai/rtk/releases');
516
+ return false;
485
517
  }
486
- else {
487
- console.log(` [fail] Unsupported platform: ${PLATFORM}/${ARCH}`);
488
- console.log(` [hint] Manual: https://github.com/rtk-ai/rtk/releases`);
518
+ return true;
519
+ }
520
+ console.log(` [fail] Unknown archive format: ${archivePath}`);
521
+ return false;
522
+ }
523
+ async function stepRtk(binDir, options) {
524
+ console.log('\n[3/7] Installing RTK binary...');
525
+ try {
526
+ const release = await fetchJson(RTK_RELEASE_API);
527
+ const triple = resolveRtkTriple();
528
+ if (!triple)
489
529
  return;
490
- }
491
- const asset = (release.assets || []).find((a) => a.name === `rtk-${assetTriple}.zip` || a.name === `rtk-${assetTriple}.tar.gz`);
492
- if (!asset) {
493
- console.log(` [fail] No rtk-${assetTriple}.<zip|tar.gz> in release ${tag}`);
494
- console.log(` [hint] Available: ${(release.assets || []).map((a) => a.name).filter((n) => n.startsWith("rtk-")).join(", ")}`);
530
+ const asset = findRtkAsset(release, triple);
531
+ if (!asset)
495
532
  return;
496
- }
497
- const binDest = path.join(binDir, IS_WINDOWS ? "rtk.exe" : "rtk");
533
+ const binDest = path.join(binDir, RTK_BINARY_NAME);
498
534
  if (options.dryRun) {
499
- console.log(` [dry-run] would download ${asset.name} from release ${tag}`);
500
- console.log(` [dry-run] would verify checksum against checksums.txt`);
535
+ console.log(` [dry-run] would download ${asset.name} from release ${release.tag_name}`);
536
+ console.log(' [dry-run] would verify checksum against checksums.txt');
501
537
  console.log(` [dry-run] would extract and install to ${binDest}`);
502
538
  return;
503
539
  }
504
- // Also download checksums.txt for verification
505
- const checksumsAsset = (release.assets || []).find((a) => a.name === "checksums.txt");
506
- let checksumsText = null;
507
- if (checksumsAsset) {
508
- try {
509
- checksumsText = await httpsGet(checksumsAsset.browser_download_url);
510
- }
511
- catch (e) {
512
- debug(`Could not download checksums.txt: ${e.message}`);
513
- }
514
- }
515
- const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "omp-rtk-"));
516
- const archivePath = path.join(tmpDir, asset.name);
517
- await httpsDownload(asset.browser_download_url, archivePath);
518
- // Verify checksum
519
- if (checksumsText) {
520
- const expected = parseChecksum(checksumsText, asset.name);
521
- const actual = await sha256File(archivePath);
522
- if (!expected) {
523
- console.log(` [warn] checksums.txt missing entry for ${asset.name} — skipping verification`);
524
- }
525
- else if (actual !== expected) {
526
- console.log(` [fail] Checksum mismatch for ${asset.name}`);
527
- console.log(` [fail] Expected: ${expected}`);
528
- console.log(` [fail] Got: ${actual}`);
529
- await fs.rm(tmpDir, { recursive: true, force: true });
540
+ const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'omp-rtk-'));
541
+ try {
542
+ const archivePath = path.join(tmpDir, asset.name);
543
+ const [checksumsText] = await Promise.all([
544
+ downloadRtkChecksums(release),
545
+ httpsDownload(asset.browser_download_url, archivePath),
546
+ ]);
547
+ if (!await verifyRtkArchive(archivePath, asset.name, checksumsText))
548
+ return;
549
+ const extractDir = path.join(tmpDir, 'extracted');
550
+ if (!await extractRtkArchive(archivePath, extractDir))
551
+ return;
552
+ const found = await findFile(extractDir, RTK_BINARY_NAME);
553
+ if (!found) {
554
+ console.log(` [fail] Could not find ${RTK_BINARY_NAME} in extracted archive`);
530
555
  return;
531
556
  }
532
- else {
533
- console.log(` [ok] Checksum verified for ${asset.name}`);
557
+ await fs.mkdir(path.dirname(binDest), { recursive: true });
558
+ await fs.copyFile(binDest, `${binDest}.bak`).catch(() => { });
559
+ await fs.copyFile(found, binDest);
560
+ console.log(` [write] ${binDest}`);
561
+ if (!IS_WINDOWS) {
562
+ await fs.chmod(binDest, 0o755);
563
+ debug(`chmod 755 ${binDest}`);
534
564
  }
535
- }
536
- else {
537
- console.log(` [warn] No checksums.txt available — skipping verification`);
538
- }
539
- // Extract by extension (not OS)
540
- const extractDir = path.join(tmpDir, "extracted");
541
- await fs.mkdir(extractDir, { recursive: true });
542
- if (asset.name.endsWith(".zip")) {
543
- if (IS_WINDOWS) {
544
- await execP("powershell", ["Expand-Archive", "-Path", archivePath, "-DestinationPath", extractDir, "-Force"], { timeout: 60000 });
545
- }
546
- else {
547
- await execP("unzip", [archivePath, "-d", extractDir], { timeout: 60000 });
548
- }
549
- }
550
- else if (asset.name.endsWith(".tar.gz") || asset.name.endsWith(".tgz")) {
551
565
  try {
552
- await execP("tar", ["xzf", archivePath, "-C", extractDir], { timeout: 60000 });
553
- debug("tar xzf ok");
566
+ const v = (await execP(binDest, ['--version'], { timeout: 10000, shell: false })).stdout.trim();
567
+ console.log(` [ok] ${binDest} → ${v}`);
554
568
  }
555
- catch (e) {
556
- debug(`tar xzf failed: ${((e.stderr) || e.message || "").trim().slice(0, 200)}`);
557
- try {
558
- await execP("sh", ["-c", `gunzip < "${archivePath}" | tar xf - -C "${extractDir}"`], { timeout: 60000 });
559
- debug("gunzip|tar fallback ok");
560
- }
561
- catch (e2) {
562
- debug(`gunzip|tar fallback failed: ${((e2.stderr) || e2.message || "").trim().slice(0, 200)}`);
563
- }
569
+ catch {
570
+ console.log(` [hint] Verify manually: ${binDest} --version`);
564
571
  }
565
572
  }
566
- else {
567
- console.log(` [fail] Unknown archive format: ${asset.name}`);
568
- await fs.rm(tmpDir, { recursive: true, force: true });
569
- return;
570
- }
571
- // Find binary
572
- const binaryName = IS_WINDOWS ? "rtk.exe" : "rtk";
573
- const entries = await fs.readdir(extractDir, { recursive: true });
574
- debug(`extracted entries: ${entries.join(", ")}`);
575
- const found = entries.find((e) => path.basename(e) === binaryName);
576
- if (!found) {
577
- console.log(` [fail] Could not find ${binaryName} in extracted archive`);
578
- await fs.rm(tmpDir, { recursive: true, force: true });
579
- return;
580
- }
581
- await fs.mkdir(path.dirname(binDest), { recursive: true });
582
- await fs.copyFile(path.join(extractDir, found), binDest);
583
- console.log(` [write] ${binDest}`);
584
- // Set executable bit on Unix
585
- if (!IS_WINDOWS) {
586
- await fs.chmod(binDest, 0o755);
587
- debug(`chmod 755 ${binDest}`);
588
- }
589
- // Verify
590
- try {
591
- const v = (await execP(binDest, ["--version"], { timeout: 10000, shell: false })).stdout.trim();
592
- console.log(` [ok] ${binDest} → ${v}`);
593
- }
594
- catch {
595
- console.log(` [hint] Verify manually: ${binDest} --version`);
573
+ finally {
574
+ await fs.rm(tmpDir, { recursive: true, force: true }).catch(() => { });
596
575
  }
597
- // Cleanup
598
- await fs.rm(tmpDir, { recursive: true, force: true });
599
576
  }
600
577
  catch (e) {
601
578
  console.log(` [fail] RTK: ${e.message}`);
602
- console.log(` [hint] Manual: https://github.com/rtk-ai/rtk/releases`);
579
+ console.log(' [hint] Manual: https://github.com/rtk-ai/rtk/releases');
603
580
  }
604
581
  }
605
- async function stepSharedSessionState(extDir, options) {
606
- const src = await readTextIfExists(SHARED_SESSION_STATE);
582
+ // Copy repo source files into the target extension dir. First entry is
583
+ // required (skip label on missing); rest are optional companions.
584
+ async function copySources(extDir, files, skipLabel, options) {
585
+ const src = await readTextIfExists(files[0][0]);
607
586
  if (!src) {
608
- console.log(" [skip] shared/session-state.js not found in repo");
609
- return;
587
+ console.log(` [skip] ${skipLabel} not found in repo`);
588
+ return false;
610
589
  }
611
- await writeIfChanged(path.join(extDir, "shared", "session-state.js"), src, options);
612
- const typesSrc = await readTextIfExists(SHARED_TYPES);
613
- if (typesSrc)
614
- await writeIfChanged(path.join(extDir, "shared", "types.js"), typesSrc, options);
615
- const utilsSrc = await readTextIfExists(LIB_UTILS);
616
- if (utilsSrc)
617
- await writeIfChanged(path.join(extDir, "lib", "utils.js"), utilsSrc, options);
618
- const settingsSrc = await readTextIfExists(SHARED_PLUGIN_SETTINGS);
619
- if (settingsSrc)
620
- await writeIfChanged(path.join(extDir, "shared", "plugin-settings.js"), settingsSrc, options);
590
+ await writeIfChanged(path.join(extDir, files[0][1]), src, options);
591
+ const rest = files.slice(1);
592
+ const extras = await Promise.all(rest.map(([from]) => readTextIfExists(from)));
593
+ for (let i = 0; i < rest.length; i++) {
594
+ const extra = extras[i];
595
+ if (extra)
596
+ await writeIfChanged(path.join(extDir, rest[i][1]), extra, options);
597
+ }
598
+ return true;
599
+ }
600
+ async function stepSharedSessionState(extDir, options) {
601
+ await copySources(extDir, [
602
+ [SHARED_SESSION_STATE, path.join('shared', 'session-state.js')],
603
+ [SHARED_TYPES, path.join('shared', 'types.js')],
604
+ [LIB_UTILS, path.join('lib', 'utils.js')],
605
+ [SHARED_PLUGIN_SETTINGS, path.join('shared', 'plugin-settings.js')],
606
+ ], 'shared/session-state.js', options);
621
607
  }
622
608
  async function stepModeReinforcement(extDir, ponytailExtPath, options) {
623
- console.log("\n[7/8] Installing mode reinforcement extension...");
624
- const src = await readTextIfExists(MODE_REINFORCEMENT_INDEX);
625
- if (!src) {
626
- console.log(" [skip] shared/mode-reinforcement.js not found in repo");
609
+ console.log('\n[7/7] Installing mode reinforcement extension...');
610
+ const dest = path.join(extDir, 'shared', 'mode-reinforcement.js');
611
+ if (!await copySources(extDir, [[MODE_REINFORCEMENT_INDEX, path.join('shared', 'mode-reinforcement.js')]], 'shared/mode-reinforcement.js', options))
627
612
  return;
628
- }
629
- const dest = path.join(extDir, "shared", "mode-reinforcement.js");
630
- await writeIfChanged(dest, src, options);
631
- await ensureExtensionAfterConfigEntry(path.join(path.dirname(extDir), "config.yml"), dest, ponytailExtPath, "mode reinforcement", options);
613
+ await ensureExtensionAfterConfigEntry(path.join(path.dirname(extDir), 'config.yml'), dest, ponytailExtPath, 'mode reinforcement', options);
632
614
  }
633
615
  async function stepRtkSession(extDir, options) {
634
- console.log("\n[4/8] Installing RTK session extension...");
635
- const src = await readTextIfExists(RTK_SESSION_INDEX);
636
- if (!src) {
637
- console.log(" [skip] rtk-session/index.js not found in repo");
638
- return;
616
+ console.log('\n[4/7] Installing RTK session extension...');
617
+ await copySources(extDir, [[RTK_SESSION_INDEX, path.join('rtk-session', 'index.js')]], 'rtk-session/index.js', options);
618
+ }
619
+ // One rule fetch serves user- and project-level installs; dry runs stay offline
620
+ // and fall back to the bundled rule to preview its destination.
621
+ async function fetchCavemanRule(options) {
622
+ if (options.dryRun)
623
+ return (await readTextIfExists(path.join(path.dirname(CAVEMAN_INDEX), 'rule.md'))) || '';
624
+ try {
625
+ return await httpsGet(CAVEMAN_REMOTE_RULE);
626
+ }
627
+ catch (e) {
628
+ console.log(` [warn] Could not fetch caveman rule: ${e.message}`);
629
+ return null;
639
630
  }
640
- const dest = path.join(extDir, "rtk-session", "index.js");
641
- await writeIfChanged(dest, src, options);
642
631
  }
643
- async function stepCaveman(extDir, options) {
644
- console.log("\n[5/8] Installing Caveman session extension...");
645
- const cavemanDir = path.join(extDir, "caveman-session");
632
+ async function stepCaveman(extDir, rule, options) {
633
+ console.log('\n[5/7] Installing Caveman session extension...');
634
+ const cavemanDir = path.join(extDir, 'caveman-session');
646
635
  if (!options.dryRun)
647
636
  await fs.mkdir(cavemanDir, { recursive: true });
648
- // Dry runs stay offline; the bundled rule is enough to preview its destination.
649
- const rule = options.dryRun ? await readTextIfExists(path.join(path.dirname(CAVEMAN_INDEX), "rule.md")) || "" : await httpsGet(CAVEMAN_REMOTE_RULE);
650
- await writeIfChanged(path.join(cavemanDir, "rule.md"), rule, options);
651
- // Write index.js
652
- const src = await readTextIfExists(CAVEMAN_INDEX);
653
- if (!src) {
654
- console.log(" [skip] caveman-session/index.js not found in repo");
655
- return;
637
+ const ruleDest = path.join(cavemanDir, 'rule.md');
638
+ if (rule === null && (await readTextIfExists(ruleDest)) !== null) {
639
+ console.log(' [info] Keeping existing rule.md');
640
+ }
641
+ else if (rule === null) {
642
+ console.log(' [skip] Caveman rule.md unavailable');
643
+ console.log(` [hint] Manual: ${CAVEMAN_REMOTE_RULE}`);
656
644
  }
657
- await writeIfChanged(path.join(cavemanDir, "index.js"), src, options);
645
+ else {
646
+ await writeIfChanged(ruleDest, rule, options);
647
+ }
648
+ await copySources(extDir, [[CAVEMAN_INDEX, path.join('caveman-session', 'index.js')]], 'caveman-session/index.js', options);
658
649
  }
659
650
  async function stepUpdater(extDir, options) {
660
- const updaterSrc = await readTextIfExists(UPDATER_INDEX);
661
- if (!updaterSrc) {
662
- console.log(" [skip] ai-addons-updater/index.js not found in repo");
663
- return;
664
- }
665
- await writeIfChanged(path.join(extDir, "ai-addons-updater", "index.js"), updaterSrc, options);
651
+ await copySources(extDir, [[UPDATER_INDEX, path.join('ai-addons-updater', 'index.js')]], 'ai-addons-updater/index.js', options);
666
652
  }
667
653
  async function stepCombo(extDir, options) {
668
- console.log("\n[6/8] Installing Combo toggle extension...");
669
- const src = await readTextIfExists(COMBO_TOGGLE_INDEX);
670
- if (!src) {
671
- console.log(" [skip] combo-toggle/index.js not found in repo");
654
+ console.log('\n[6/7] Installing Combo toggle extension...');
655
+ const dest = path.join(extDir, 'combo-toggle', 'index.js');
656
+ if (!await copySources(extDir, [[COMBO_TOGGLE_INDEX, path.join('combo-toggle', 'index.js')]], 'combo-toggle/index.js', options))
672
657
  return;
673
- }
674
- const dest = path.join(extDir, "combo-toggle", "index.js");
675
- await writeIfChanged(dest, src, options);
676
658
  // Auto-register combo in config.yml
677
- const configPath = path.join(path.dirname(extDir), "config.yml");
678
- await ensureExtensionInConfig(configPath, dest, "combo", options);
679
- }
680
- async function stepAmanaiReward(extDir, options, pluginProvided = false) {
681
- console.log("\n[8/8] Installing Amanai reward detector...");
682
- const destDir = path.join(extDir, "amanai-reward");
683
- if (pluginProvided) {
684
- // The plugin's omp.extensions manifest loads the detector from
685
- // plugins/node_modules; a copied agent/extensions entry would double-load.
686
- if ((await readTextIfExists(path.join(destDir, "index.js"))) !== null) {
687
- if (options.dryRun)
688
- console.log(` [dry-run] would remove ${destDir} (now provided by the plugin)`);
689
- else {
690
- await fs.rm(destDir, { recursive: true, force: true });
691
- console.log(` [rm] ${destDir} (now provided by the plugin)`);
692
- }
693
- }
694
- console.log(" [ok] detector loads via the tersio plugin manifest");
695
- return;
696
- }
697
- const src = await readTextIfExists(AMANAI_REWARD_INDEX);
698
- if (!src) {
699
- console.log(" [skip] amanai-reward/index.js not found in repo");
700
- return;
701
- }
702
- await writeIfChanged(path.join(destDir, "index.js"), src, options);
659
+ const configPath = path.join(path.dirname(extDir), 'config.yml');
660
+ await ensureExtensionInConfig(configPath, dest, 'combo', options);
703
661
  }
704
662
  // --- Doctor ---
663
+ function presence(label, text) {
664
+ console.log(` ${label}: ${text !== null ? 'installed' : 'MISSING'}`);
665
+ }
666
+ function locationLine(label, ok, loc) {
667
+ console.log(` ${label}: ${ok ? 'ok' : 'MISSING'} ${loc}`);
668
+ }
705
669
  async function runDoctor() {
706
- console.log("\n=== Tersio Doctor ===\n");
670
+ console.log('\n=== Tersio Doctor ===\n');
707
671
  // Node
708
- console.log(` Node: ok v${process.version}`);
709
- // OMP CLI
710
- try {
711
- const v = (await execP(IS_WINDOWS ? "omp.cmd" : "omp", ["--version"])).stdout.trim();
712
- console.log(` OMP CLI: ok ${v}`);
713
- }
714
- catch {
715
- console.log(" OMP CLI: MISSING");
716
- }
717
- // Home
718
- console.log(` Home: ${HOME}`);
672
+ console.log(` Node: ok ${process.version}`);
673
+ // OMP CLI version joins FS probes below; logs keep fixed order.
719
674
  // Directories
720
- const agentDir = path.join(HOME, ".omp", "agent");
721
- const extDir = path.join(agentDir, "extensions");
722
- const configPath = path.join(agentDir, "config.yml");
723
- const pluginsDir = path.join(HOME, ".omp", "plugins");
724
- const rtkBin = path.join(HOME, ".bun", "bin", IS_WINDOWS ? "rtk.exe" : "rtk");
725
- const agentOk = await readTextIfExists(agentDir) !== null || (await fs.readdir(agentDir).catch(() => null)) !== null;
726
- console.log(` OMP agent dir: ${agentOk ? "ok" : "MISSING"} ${agentDir}`);
727
- const extOk = (await fs.readdir(extDir).catch(() => null)) !== null;
728
- console.log(` OMP extensions dir: ${extOk ? "ok" : "MISSING"} ${extDir}`);
729
- const sharedState = path.join(extDir, "shared", "session-state.js");
730
- console.log(` Shared session bridge: ${(await readTextIfExists(sharedState)) !== null ? "installed" : "MISSING"}`);
731
- const configOk = (await readTextIfExists(configPath)) !== null;
732
- console.log(` OMP config.yml: ${configOk ? "ok" : "MISSING"} ${configPath}`);
675
+ const agentDir = OMP_AGENT_DIR;
676
+ const extDir = path.join(agentDir, 'extensions');
677
+ const configPath = path.join(agentDir, 'config.yml');
678
+ const pluginsDir = OMP_PLUGINS_DIR;
679
+ const rtkBin = path.join(BUN_BIN_DIR, RTK_BINARY_NAME);
733
680
  // Ponytail
734
- const ponytailPkg = path.join(pluginsDir, "node_modules", "@dietrichgebert", "ponytail", "package.json");
735
- const ponytailExt = path.join(pluginsDir, "node_modules", "@dietrichgebert", "ponytail", "pi-extension", "index.js");
736
- const ponytailInstalled = (await readTextIfExists(ponytailPkg)) !== null;
737
- const ponytailExtInstalled = (await readTextIfExists(ponytailExt)) !== null;
738
- console.log(` Ponytail package: ${ponytailInstalled ? "installed" : "MISSING"}`);
739
- console.log(` Ponytail extension: ${ponytailExtInstalled ? "installed" : "MISSING"}`);
740
- if (configOk) {
741
- const configText = (await readTextIfExists(configPath));
742
- const hasPonytailPath = configText.includes("ponytail") && configText.includes("pi-extension");
743
- console.log(` Ponytail in config.yml: ${hasPonytailPath ? "registered" : "MISSING"}`);
681
+ const ponytailPkg = path.join(pluginsDir, 'node_modules', '@dietrichgebert', 'ponytail', 'package.json');
682
+ const ponytailExt = path.join(pluginsDir, 'node_modules', '@dietrichgebert', 'ponytail', 'pi-extension', 'index.js');
683
+ const cavemanIndex = path.join(extDir, 'caveman-session', 'index.js');
684
+ const cavemanRule = path.join(extDir, 'caveman-session', 'rule.md');
685
+ const rtkIndex = path.join(extDir, 'rtk-session', 'index.js');
686
+ const updaterIndex = path.join(extDir, 'ai-addons-updater', 'index.js');
687
+ const comboIndex = path.join(extDir, 'combo-toggle', 'index.js');
688
+ const modeReinforcement = path.join(extDir, 'shared', 'mode-reinforcement.js');
689
+ const selfPkg = path.join(pluginsDir, 'node_modules', PACKAGE_NAME, 'package.json');
690
+ // Independent probes run concurrently; logs below keep fixed order.
691
+ const [ompVersion, agentEntries, extEntries, sharedStateText, configText, ponytailPkgText, ponytailExtText, pluginsPkgRaw, selfPkgText, rtkBinText, cavemanIndexText, cavemanRuleText, rtkIndexText, updaterIndexText, comboIndexText, modeReinforcementText,] = await Promise.all([
692
+ execP(OMP_BIN, ['--version']).then((r) => r.stdout.trim(), () => null),
693
+ fs.readdir(agentDir).catch(() => null),
694
+ fs.readdir(extDir).catch(() => null),
695
+ readTextIfExists(path.join(extDir, 'shared', 'session-state.js')),
696
+ readTextIfExists(configPath),
697
+ readTextIfExists(ponytailPkg),
698
+ readTextIfExists(ponytailExt),
699
+ readTextIfExists(path.join(pluginsDir, 'package.json')),
700
+ readTextIfExists(selfPkg),
701
+ readTextIfExists(rtkBin),
702
+ readTextIfExists(cavemanIndex),
703
+ readTextIfExists(cavemanRule),
704
+ readTextIfExists(rtkIndex),
705
+ readTextIfExists(updaterIndex),
706
+ readTextIfExists(comboIndex),
707
+ readTextIfExists(modeReinforcement),
708
+ ]);
709
+ console.log(ompVersion !== null ? ` OMP CLI: ok ${ompVersion}` : ' OMP CLI: MISSING');
710
+ console.log(` Home: ${HOME}`);
711
+ locationLine('OMP agent dir', agentEntries !== null, agentDir);
712
+ locationLine('OMP extensions dir', extEntries !== null, extDir);
713
+ console.log(` Shared session bridge: ${sharedStateText !== null ? 'installed' : 'MISSING'}`);
714
+ locationLine('OMP config.yml', configText !== null, configPath);
715
+ presence('Ponytail package', ponytailPkgText);
716
+ presence('Ponytail extension', ponytailExtText);
717
+ if (configText) {
718
+ const hasPonytailPath = configText.includes('ponytail') && configText.includes('pi-extension');
719
+ console.log(` Ponytail in config.yml: ${hasPonytailPath ? 'registered' : 'MISSING'}`);
744
720
  }
745
721
  // Self plugin registration (Settings → Plugins listing)
746
- const pluginsPkgRaw = await readTextIfExists(path.join(pluginsDir, "package.json"));
747
- let selfDep = false;
748
- if (pluginsPkgRaw) {
749
- try {
750
- selfDep = PACKAGE_NAME in (JSON.parse(pluginsPkgRaw).dependencies || {});
751
- }
752
- catch { /* ignore */ }
753
- }
754
- console.log(` Self plugin in plugins/package.json: ${selfDep ? "registered" : "MISSING"}`);
755
- const selfPkg = path.join(pluginsDir, "node_modules", PACKAGE_NAME, "package.json");
756
- console.log(` Self plugin package: ${(await readTextIfExists(selfPkg)) !== null ? "installed" : "MISSING"}`);
722
+ const selfDep = PACKAGE_NAME in (parseJsonObject(pluginsPkgRaw)?.dependencies || {});
723
+ console.log(` Self plugin in plugins/package.json: ${selfDep ? 'registered' : 'MISSING'}`);
724
+ console.log(` Self plugin package: ${selfPkgText !== null ? 'installed' : 'MISSING'}`);
757
725
  // RTK
758
- const rtkExists = (await readTextIfExists(rtkBin)) !== null;
759
- console.log(` RTK binary: ${rtkExists ? "installed" : "MISSING"} ${rtkBin}`);
726
+ const rtkExists = rtkBinText !== null;
727
+ console.log(` RTK binary: ${rtkExists ? 'installed' : 'MISSING'} ${rtkBin}`);
760
728
  if (rtkExists) {
761
729
  try {
762
- const v = (await execP(rtkBin, ["--version"], { timeout: 5000 })).stdout.trim();
730
+ const v = (await execP(rtkBin, ['--version'], { timeout: 5000 })).stdout.trim();
763
731
  console.log(` RTK version: ${v}`);
764
732
  }
765
733
  catch {
766
- console.log(" RTK version: unavailable (may not be executable)");
734
+ console.log(' RTK version: unavailable (may not be executable)');
767
735
  }
768
736
  }
769
737
  // Caveman
770
- const cavemanIndex = path.join(extDir, "caveman-session", "index.js");
771
- const cavemanRule = path.join(extDir, "caveman-session", "rule.md");
772
- console.log(` Caveman extension: ${(await readTextIfExists(cavemanIndex)) !== null ? "installed" : "MISSING"}`);
773
- console.log(` Caveman rule.md: ${(await readTextIfExists(cavemanRule)) !== null ? "installed" : "MISSING"}`);
738
+ presence('Caveman extension', cavemanIndexText);
739
+ presence('Caveman rule.md', cavemanRuleText);
774
740
  // RTK extension
775
- const rtkIndex = path.join(extDir, "rtk-session", "index.js");
776
- console.log(` RTK extension: ${(await readTextIfExists(rtkIndex)) !== null ? "installed" : "MISSING"}`);
741
+ presence('RTK extension', rtkIndexText);
777
742
  // Updater
778
- const updaterIndex = path.join(extDir, "ai-addons-updater", "index.js");
779
- console.log(` Updater extension: ${(await readTextIfExists(updaterIndex)) !== null ? "installed" : "MISSING"}`);
743
+ presence('Updater extension', updaterIndexText);
780
744
  // Combo
781
- const comboIndex = path.join(extDir, "combo-toggle", "index.js");
782
- console.log(` Combo extension: ${(await readTextIfExists(comboIndex)) !== null ? "installed" : "MISSING"}`);
783
- const modeReinforcement = path.join(extDir, "shared", "mode-reinforcement.js");
784
- console.log(` Mode reinforcement extension: ${(await readTextIfExists(modeReinforcement)) !== null ? "installed" : "MISSING"}`);
785
- // Amanai reward detector
786
- const amanaiRewardIndex = path.join(extDir, "amanai-reward", "index.js");
787
- const amanaiViaPlugin = selfDep && (await readTextIfExists(selfPkg)) !== null;
788
- const amanaiState = (await readTextIfExists(amanaiRewardIndex)) !== null ? "installed" : amanaiViaPlugin ? "installed (via plugin manifest)" : "MISSING";
789
- console.log(` Amanai reward detector: ${amanaiState}`);
790
- if (configOk) {
791
- const configText = (await readTextIfExists(configPath));
792
- const hasComboPath = configText.includes("combo-toggle");
793
- console.log(` Combo in config.yml: ${hasComboPath ? "registered" : "MISSING"}`);
745
+ presence('Combo extension', comboIndexText);
746
+ presence('Mode reinforcement extension', modeReinforcementText);
747
+ if (configText) {
748
+ const hasComboPath = configText.includes('combo-toggle');
749
+ console.log(` Combo in config.yml: ${hasComboPath ? 'registered' : 'MISSING'}`);
750
+ }
751
+ }
752
+ // Read-modify-write a JSON file. mutate returns true when it changed
753
+ // something; no change (or unreadable file) means no output at all.
754
+ async function updateJsonFile(filePath, mutate, dryRunNote, writeNote, dryRun) {
755
+ const raw = await readTextIfExists(filePath);
756
+ if (!raw)
757
+ return;
758
+ let data;
759
+ try {
760
+ data = JSON.parse(raw);
761
+ }
762
+ catch {
763
+ debug(`Could not parse ${filePath}`);
764
+ return;
765
+ }
766
+ if (!mutate(data))
767
+ return;
768
+ if (dryRun) {
769
+ console.log(` [dry-run] ${dryRunNote}`);
770
+ return;
771
+ }
772
+ await fs.writeFile(filePath, JSON.stringify(data, null, 2) + '\n', 'utf8');
773
+ console.log(` [write] ${writeNote}`);
774
+ }
775
+ function dropKey(section, key) {
776
+ if (!section || typeof section !== 'object')
777
+ return false;
778
+ const record = section;
779
+ if (!(key in record))
780
+ return false;
781
+ delete record[key];
782
+ return true;
783
+ }
784
+ async function removeUninstallTarget(target, shouldDryRun, recursive = true) {
785
+ try {
786
+ if (shouldDryRun) {
787
+ console.log(` [dry-run] would remove ${target}`);
788
+ return;
789
+ }
790
+ if (recursive)
791
+ await fs.rm(target, { recursive: true, force: true });
792
+ else
793
+ await fs.unlink(target);
794
+ console.log(` [rm] ${target}`);
795
+ }
796
+ catch {
797
+ debug(`Could not remove ${target}`);
794
798
  }
795
799
  }
796
800
  async function runUninstall(options = {}) {
797
- const confirmed = options.yes ?? yes;
801
+ const shouldDryRun = options.dryRun ?? dryRun;
802
+ const confirmed = (options.yes ?? yes) || shouldDryRun;
798
803
  const shouldRemovePonytail = options.removePonytail ?? removePonytail;
799
804
  const shouldRemoveRtk = options.removeRtk ?? removeRtk;
800
- const shouldDryRun = options.dryRun ?? dryRun;
801
- console.log("\n=== Tersio Uninstall ===\n");
802
- const extDir = path.join(HOME, ".omp", "agent", "extensions");
803
- const configPath = path.join(HOME, ".omp", "agent", "config.yml");
804
- const rtkBin = path.join(HOME, ".bun", "bin", IS_WINDOWS ? "rtk.exe" : "rtk");
805
- const pluginsDir = path.join(HOME, ".omp", "plugins");
806
- const ponytailPkgDir = path.join(pluginsDir, "node_modules", "@dietrichgebert", "ponytail");
805
+ console.log('\n=== Tersio Uninstall ===\n');
806
+ const extDir = path.join(OMP_AGENT_DIR, 'extensions');
807
+ const configPath = path.join(OMP_AGENT_DIR, 'config.yml');
808
+ const rtkBin = path.join(BUN_BIN_DIR, RTK_BINARY_NAME);
809
+ const pluginsDir = OMP_PLUGINS_DIR;
810
+ const ponytailPkgDir = path.join(pluginsDir, 'node_modules', '@dietrichgebert', 'ponytail');
807
811
  const targets = [
808
- path.join(extDir, "caveman-session"),
809
- path.join(extDir, "rtk-session"),
810
- path.join(extDir, "ai-addons-updater"),
811
- path.join(extDir, "combo-toggle"),
812
- path.join(extDir, "shared"),
813
- path.join(extDir, "amanai-reward"),
812
+ 'caveman-session',
813
+ 'rtk-session',
814
+ 'ai-addons-updater',
815
+ 'combo-toggle',
816
+ 'shared',
814
817
  // Only consumed by ai-addons-updater (removed above); otherwise orphaned.
815
- path.join(extDir, "lib"),
818
+ 'lib',
816
819
  // Legacy always-on combo helper; imports shared/session-state.js, so it
817
820
  // breaks with a module-not-found warning once the shared dir is removed.
818
- path.join(extDir, "aaa-combo-boot"),
819
- ];
820
- console.log("Will remove:");
821
+ 'aaa-combo-boot',
822
+ ].map((dir) => path.join(extDir, dir));
823
+ console.log('Will remove:');
821
824
  for (const t of targets) {
822
825
  console.log(` ${t}`);
823
826
  }
@@ -828,70 +831,39 @@ async function runUninstall(options = {}) {
828
831
  console.log(` ${rtkBin}`);
829
832
  }
830
833
  if (!confirmed) {
831
- const answer = await ask("\nProceed? [y/N]: ");
832
- if (!answer.toLowerCase().startsWith("y")) {
833
- console.log("Aborted.");
834
+ const answer = await ask('\nProceed? [y/N]: ');
835
+ if (!answer.toLowerCase().startsWith('y')) {
836
+ console.log('Aborted.');
834
837
  closeRL();
835
838
  return false;
836
839
  }
837
840
  }
838
- // Remove extension directories
839
- for (const t of targets) {
840
- try {
841
- if (shouldDryRun)
842
- console.log(` [dry-run] would remove ${t}`);
843
- else {
844
- await fs.rm(t, { recursive: true, force: true });
845
- console.log(` [rm] ${t}`);
846
- }
847
- }
848
- catch {
849
- debug(`Could not remove ${t}`);
850
- }
851
- }
841
+ // Remove extension directories (independent paths, so concurrently).
842
+ await Promise.all(targets.map((t) => removeUninstallTarget(t, shouldDryRun)));
852
843
  // Remove Combo and mode-reinforcement registrations; Ponytail only when requested.
853
844
  const configRaw = await readTextIfExists(configPath);
854
845
  if (configRaw) {
855
- let lines = configRaw.split("\n");
846
+ let lines = configRaw.split('\n');
856
847
  const before = lines.length;
857
848
  lines = lines.filter((l) => {
858
- if (l.includes("combo-toggle") || l.includes("mode-reinforcement"))
849
+ if (l.includes('combo-toggle') || l.includes('mode-reinforcement'))
859
850
  return false;
860
- if (shouldRemovePonytail && l.includes("ponytail") && l.includes("pi-extension"))
851
+ if (shouldRemovePonytail && l.includes('ponytail') && l.includes('pi-extension'))
861
852
  return false;
862
853
  return true;
863
854
  });
864
855
  if (lines.length !== before) {
865
856
  if (shouldDryRun)
866
857
  console.log(` [dry-run] would remove ${before - lines.length} config.yml entries`);
867
- else {
868
- await fs.writeFile(configPath, lines.join("\n"), "utf8");
869
- console.log(` [write] Updated config.yml (removed ${before - lines.length} entries)`);
870
- }
858
+ else
859
+ await writeConfigLines(configPath, lines, ` [write] Updated config.yml (removed ${before - lines.length} entries)`);
871
860
  }
872
861
  }
873
862
  // Remove the Ponytail plugin package the installer added (dep, files,
874
863
  // lock entry); the config.yml entry was filtered above.
875
864
  if (shouldRemovePonytail) {
876
- const pluginsPkgPath = path.join(pluginsDir, "package.json");
877
- const ponytailRaw = await readTextIfExists(pluginsPkgPath);
878
- if (ponytailRaw) {
879
- try {
880
- const pkg = JSON.parse(ponytailRaw);
881
- if (pkg.dependencies && "@dietrichgebert/ponytail" in pkg.dependencies) {
882
- if (shouldDryRun)
883
- console.log(` [dry-run] would remove @dietrichgebert/ponytail from ${pluginsPkgPath}`);
884
- else {
885
- delete pkg.dependencies["@dietrichgebert/ponytail"];
886
- await fs.writeFile(pluginsPkgPath, JSON.stringify(pkg, null, 2) + "\n", "utf8");
887
- console.log(" [write] Removed @dietrichgebert/ponytail from plugins/package.json");
888
- }
889
- }
890
- }
891
- catch {
892
- debug("Could not update plugins/package.json for ponytail");
893
- }
894
- }
865
+ const pluginsPkgPath = path.join(pluginsDir, 'package.json');
866
+ await updateJsonFile(pluginsPkgPath, (data) => dropKey(data.dependencies, '@dietrichgebert/ponytail'), `would remove @dietrichgebert/ponytail from ${pluginsPkgPath}`, 'Removed @dietrichgebert/ponytail from plugins/package.json', shouldDryRun);
895
867
  try {
896
868
  if (shouldDryRun)
897
869
  console.log(` [dry-run] would remove ${ponytailPkgDir}`);
@@ -903,104 +875,50 @@ async function runUninstall(options = {}) {
903
875
  }
904
876
  }
905
877
  catch {
906
- debug("Could not remove ponytail package dir (scope may hold other packages)");
907
- }
908
- const lockPath = path.join(pluginsDir, "omp-plugins.lock.json");
909
- const lockRaw = await readTextIfExists(lockPath);
910
- if (lockRaw) {
911
- try {
912
- const lock = JSON.parse(lockRaw);
913
- if (lock.plugins && "@dietrichgebert/ponytail" in lock.plugins) {
914
- if (shouldDryRun)
915
- console.log(` [dry-run] would remove @dietrichgebert/ponytail from ${lockPath}`);
916
- else {
917
- delete lock.plugins["@dietrichgebert/ponytail"];
918
- await fs.writeFile(lockPath, JSON.stringify(lock, null, 2) + "\n", "utf8");
919
- console.log(` [write] Removed @dietrichgebert/ponytail from ${lockPath}`);
920
- }
921
- }
922
- }
923
- catch {
924
- debug("Could not update omp-plugins.lock.json for ponytail");
925
- }
878
+ debug('Could not remove ponytail package dir (scope may hold other packages)');
926
879
  }
880
+ const lockPath = path.join(pluginsDir, 'omp-plugins.lock.json');
881
+ await updateJsonFile(lockPath, (data) => dropKey(data.plugins, '@dietrichgebert/ponytail'), `would remove @dietrichgebert/ponytail from ${lockPath}`, `Removed @dietrichgebert/ponytail from ${lockPath}`, shouldDryRun);
927
882
  }
928
883
  // Remove our plugin registration from ~/.omp/plugins
929
- const pluginsPkgPath = path.join(pluginsDir, "package.json");
930
- const pluginsPkgRaw = await readTextIfExists(pluginsPkgPath);
931
- if (pluginsPkgRaw) {
932
- try {
933
- const pkg = JSON.parse(pluginsPkgRaw);
934
- if (pkg.dependencies && PACKAGE_NAME in pkg.dependencies) {
935
- if (shouldDryRun)
936
- console.log(` [dry-run] would remove ${PACKAGE_NAME} from ${pluginsPkgPath}`);
937
- else {
938
- delete pkg.dependencies[PACKAGE_NAME];
939
- await fs.writeFile(pluginsPkgPath, JSON.stringify(pkg, null, 2) + "\n", "utf8");
940
- console.log(` [write] Removed ${PACKAGE_NAME} from plugins/package.json`);
941
- }
942
- }
943
- }
944
- catch {
945
- debug("Could not update plugins/package.json");
946
- }
947
- }
948
- const selfPluginDir = path.join(pluginsDir, "node_modules", PACKAGE_NAME);
949
- if ((await readTextIfExists(path.join(selfPluginDir, "package.json"))) !== null) {
950
- try {
951
- if (shouldDryRun)
952
- console.log(` [dry-run] would remove ${selfPluginDir}`);
953
- else {
954
- await fs.rm(selfPluginDir, { recursive: true, force: true });
955
- console.log(` [rm] ${selfPluginDir}`);
956
- }
957
- }
958
- catch {
959
- debug(`Could not remove ${selfPluginDir}`);
960
- }
884
+ const pluginsPkgPath = path.join(pluginsDir, 'package.json');
885
+ await updateJsonFile(pluginsPkgPath, (data) => dropKey(data.dependencies, PACKAGE_NAME), `would remove ${PACKAGE_NAME} from ${pluginsPkgPath}`, `Removed ${PACKAGE_NAME} from plugins/package.json`, shouldDryRun);
886
+ const selfPluginDir = path.join(pluginsDir, 'node_modules', PACKAGE_NAME);
887
+ if ((await readTextIfExists(path.join(selfPluginDir, 'package.json'))) !== null) {
888
+ await removeUninstallTarget(selfPluginDir, shouldDryRun);
961
889
  }
962
890
  // Remove RTK binary if requested
963
- if (shouldRemoveRtk) {
964
- try {
965
- if (shouldDryRun)
966
- console.log(` [dry-run] would remove ${rtkBin}`);
967
- else {
968
- await fs.unlink(rtkBin);
969
- console.log(` [rm] ${rtkBin}`);
970
- }
971
- }
972
- catch {
973
- debug(`Could not remove ${rtkBin}`);
974
- }
975
- }
976
- console.log("\nDone. Restart OMP for changes to take effect.");
891
+ if (shouldRemoveRtk)
892
+ await removeUninstallTarget(rtkBin, shouldDryRun, false);
893
+ console.log('\nDone. Restart OMP for changes to take effect.');
977
894
  return true;
978
895
  }
896
+ const SCOPE_MAP = { user: '1', project: '2', both: '3' };
979
897
  async function runLatestUpdate() {
980
- const updateScope = scopeFlag || "user";
981
- if (!["user", "project", "both"].includes(updateScope)) {
898
+ const updateScope = scopeFlag || 'user';
899
+ if (!Object.hasOwn(SCOPE_MAP, updateScope)) {
982
900
  console.error(`[fail] Invalid --scope: ${updateScope}. Use: user, project, both`);
983
901
  process.exitCode = 1;
984
902
  return;
985
903
  }
986
- const forwardedArgs = ["--yes", "--scope", updateScope];
904
+ const forwardedArgs = ['--yes', '--scope', updateScope];
987
905
  if (dryRun)
988
- forwardedArgs.push("--dry-run");
906
+ forwardedArgs.push('--dry-run');
989
907
  if (verbose)
990
- forwardedArgs.push("--verbose");
908
+ forwardedArgs.push('--verbose');
991
909
  const npmArgs = [
992
- "exec",
993
- "--yes",
994
- "--prefer-online",
910
+ 'exec',
911
+ '--yes',
912
+ '--prefer-online',
995
913
  `--package=${PACKAGE_NAME}@latest`,
996
- "--",
914
+ '--',
997
915
  PACKAGE_BIN,
998
- "--apply-update",
916
+ '--apply-update',
999
917
  ...forwardedArgs,
1000
918
  ];
1001
- const npmCommand = IS_WINDOWS ? process.env.ComSpec || "cmd.exe" : "npm";
1002
- const npmCommandArgs = IS_WINDOWS ? ["/d", "/s", "/c", "npm", ...npmArgs] : npmArgs;
1003
- console.log("=== Updating Tersio ===");
919
+ const npmCommand = IS_WINDOWS ? process.env.ComSpec || 'cmd.exe' : 'npm';
920
+ const npmCommandArgs = IS_WINDOWS ? ['/d', '/s', '/c', 'npm', ...npmArgs] : npmArgs;
921
+ console.log('=== Updating Tersio ===');
1004
922
  console.log(` Running the latest ${PACKAGE_NAME} installer...\n`);
1005
923
  try {
1006
924
  const result = await execP(npmCommand, npmCommandArgs, {
@@ -1013,7 +931,7 @@ async function runLatestUpdate() {
1013
931
  process.stdout.write(result.stdout);
1014
932
  if (result.stderr)
1015
933
  process.stderr.write(result.stderr);
1016
- console.log("\n=== Update complete ===");
934
+ console.log('\n=== Update complete ===');
1017
935
  }
1018
936
  catch (e) {
1019
937
  const err = e;
@@ -1028,26 +946,57 @@ async function runLatestUpdate() {
1028
946
  // --- Install profile (session-start mode defaults) ---
1029
947
  function defaultProfile() {
1030
948
  return {
1031
- comboDefault: "off",
1032
- cavemanDefault: "off",
949
+ comboDefault: 'off',
950
+ cavemanDefault: 'off',
1033
951
  rtkDefault: false,
1034
- ponytailDefault: "off",
952
+ ponytailDefault: 'off',
1035
953
  };
1036
954
  }
1037
955
  function tty() {
1038
956
  return Boolean(process.stdin.isTTY && process.stdout.isTTY);
1039
957
  }
958
+ // Determine install scope: reinstall > flag > non-interactive default > prompt.
959
+ async function resolveScope() {
960
+ if (reinstall) {
961
+ console.log(' Scope: user (reinstall)');
962
+ return '1';
963
+ }
964
+ if (scopeFlag) {
965
+ const scope = SCOPE_MAP[scopeFlag];
966
+ if (!scope) {
967
+ console.log(` [fail] Invalid --scope: ${scopeFlag}. Use: user, project, both`);
968
+ closeRL();
969
+ process.exit(1);
970
+ }
971
+ console.log(` Scope: ${scopeFlag}`);
972
+ return scope;
973
+ }
974
+ if (install || yes) {
975
+ console.log(` Scope: user (${install ? 'install default' : '--scope omitted, defaulting to user with --yes'})`);
976
+ return '1';
977
+ }
978
+ console.log('\nInstall scope:');
979
+ console.log(' 1) User-level (all OMP sessions)');
980
+ console.log(' 2) Project-level (this repo only)');
981
+ console.log(' 3) Both');
982
+ while (true) {
983
+ const answer = (await ask('\nChoose [1-3] (default 1): ')).trim() || '1';
984
+ if (answer === '1' || answer === '2' || answer === '3')
985
+ return answer;
986
+ console.log(` [fail] Invalid scope: ${answer}. Choose 1, 2, or 3.`);
987
+ }
988
+ }
1040
989
  async function resolveProfile() {
1041
990
  const profile = defaultProfile();
1042
991
  // Single interactive prompt: the Combo preset implies all three modes.
1043
992
  // Only for a real user at a terminal, only when no default flags were
1044
993
  // given, and never for --apply-update runs.
1045
994
  if (tty() && !profileFlagsGiven && !applyUpdate && (install || reinstall)) {
1046
- const comboAnswer = (await ask(" Default Combo preset on session start? [off/medium/balanced/max] (off): ")).trim().toLowerCase();
1047
- if (comboAnswer && comboAnswer in COMBO_DEFAULTS)
995
+ const comboAnswer = (await ask(' Default Combo preset on session start? [off/medium/balanced/max] (off): ')).trim().toLowerCase();
996
+ if (comboAnswer && COMBO_DEFAULTS.has(comboAnswer))
1048
997
  profile.comboDefault = comboAnswer;
1049
998
  }
1050
- // Explicit flags always win. Caveman/RTK flags override the Combo preset.
999
+ // Explicit flags always win over the Combo preset.
1051
1000
  if (comboDefaultFlag !== undefined)
1052
1001
  profile.comboDefault = comboDefaultFlag;
1053
1002
  const preset = COMBO_PRESET_MODES[profile.comboDefault] ?? COMBO_PRESET_MODES.off;
@@ -1057,8 +1006,10 @@ async function resolveProfile() {
1057
1006
  if (cavemanDefaultFlag !== undefined)
1058
1007
  profile.cavemanDefault = cavemanDefaultFlag;
1059
1008
  if (rtkDefaultFlag !== undefined)
1060
- profile.rtkDefault = rtkDefaultFlag === "on";
1061
- console.log(` Profile: combo default=${profile.comboDefault} (caveman=${profile.cavemanDefault} · rtk=${profile.rtkDefault ? "on" : "off"} · ponytail=${profile.ponytailDefault})`);
1009
+ profile.rtkDefault = rtkDefaultFlag === 'on';
1010
+ if (ponytailDefaultFlag !== undefined)
1011
+ profile.ponytailDefault = ponytailDefaultFlag;
1012
+ console.log(` Profile: combo default=${profile.comboDefault} (caveman=${profile.cavemanDefault} · rtk=${profile.rtkDefault ? 'on' : 'off'} · ponytail=${profile.ponytailDefault})`);
1062
1013
  return profile;
1063
1014
  }
1064
1015
  // ponytail: post-install guard. List entries under `extensions: null` break
@@ -1066,29 +1017,29 @@ async function resolveProfile() {
1066
1017
  async function validateConfigExtensions(agentDir, options) {
1067
1018
  if (options.dryRun)
1068
1019
  return;
1069
- const raw = await readTextIfExists(path.join(agentDir, "config.yml"));
1020
+ const raw = await readTextIfExists(path.join(agentDir, 'config.yml'));
1070
1021
  if (!raw)
1071
1022
  return;
1072
- const lines = raw.split("\n");
1073
- const keyIdx = lines.findIndex((l) => /^\s*extensions\s*:/i.test(l));
1023
+ const lines = raw.split('\n');
1024
+ const keyIdx = lines.findIndex((l) => EXTENSIONS_KEY_RE.test(l));
1074
1025
  if (keyIdx === -1)
1075
1026
  return;
1076
1027
  const scalarNull = /^\s*extensions\s*:\s*(null|~|\[\s*\])\s*$/i.test(lines[keyIdx]);
1077
1028
  const hasEntries = lines.slice(keyIdx + 1).some((l) => /^\s*-\s+\S/.test(l));
1078
1029
  if (scalarNull && hasEntries) {
1079
- console.log(" [fail] config.yml lists extensions under `extensions: null` — OMP will fail to launch.");
1080
- console.log(" [hint] Replace the `extensions: null` line with `extensions:`, then reinstall.");
1030
+ console.log(' [fail] config.yml lists extensions under `extensions: null` — OMP will fail to launch.');
1031
+ console.log(' [hint] Replace the `extensions: null` line with `extensions:`, then reinstall.');
1081
1032
  process.exitCode = 1;
1082
1033
  }
1083
1034
  else {
1084
- console.log(" [ok] config.yml extensions key valid");
1035
+ console.log(' [ok] config.yml extensions key valid');
1085
1036
  }
1086
1037
  }
1087
1038
  // Persist the profile as omp plugin settings so `omp plugin config get`
1088
1039
  // reflects the choice and the extensions pick it up on session start.
1089
1040
  async function writePluginSettings(profile, options) {
1090
- const pluginsDir = path.join(HOME, ".omp", "plugins");
1091
- const lockPath = path.join(pluginsDir, "omp-plugins.lock.json");
1041
+ const pluginsDir = OMP_PLUGINS_DIR;
1042
+ const lockPath = path.join(pluginsDir, 'omp-plugins.lock.json');
1092
1043
  let config = {};
1093
1044
  const existing = await readTextIfExists(lockPath);
1094
1045
  if (existing) {
@@ -1101,18 +1052,19 @@ async function writePluginSettings(profile, options) {
1101
1052
  }
1102
1053
  config.plugins = config.plugins || {};
1103
1054
  config.settings = config.settings || {};
1104
- const settings = { ...(config.settings[PACKAGE_NAME] || {}) };
1105
- settings.comboDefault = profile.comboDefault;
1106
- settings.cavemanDefault = profile.cavemanDefault;
1107
- settings.rtkDefault = profile.rtkDefault;
1108
- settings.ponytailDefault = profile.ponytailDefault;
1109
- config.settings[PACKAGE_NAME] = settings;
1055
+ config.settings[PACKAGE_NAME] = {
1056
+ ...(config.settings[PACKAGE_NAME] || {}),
1057
+ comboDefault: profile.comboDefault,
1058
+ cavemanDefault: profile.cavemanDefault,
1059
+ rtkDefault: profile.rtkDefault,
1060
+ ponytailDefault: profile.ponytailDefault,
1061
+ };
1110
1062
  if (options.dryRun) {
1111
1063
  console.log(` [dry-run] would write plugin settings (${PACKAGE_NAME}) to ${lockPath}`);
1112
1064
  return;
1113
1065
  }
1114
1066
  await fs.mkdir(pluginsDir, { recursive: true });
1115
- await fs.writeFile(lockPath, JSON.stringify(config, null, 2) + "\n", "utf8");
1067
+ await fs.writeFile(lockPath, JSON.stringify(config, null, 2) + '\n', 'utf8');
1116
1068
  console.log(` [write] Plugin settings in ${lockPath}`);
1117
1069
  }
1118
1070
  // --- Main ---
@@ -1153,89 +1105,60 @@ async function main() {
1153
1105
  await runUninstall({ yes: true, removePonytail: false, removeRtk: true });
1154
1106
  }
1155
1107
  if (dryRun)
1156
- console.log("[dry-run] No changes will be written.\n");
1108
+ console.log('[dry-run] No changes will be written.\n');
1157
1109
  console.log(`=== Tersio v${PACKAGE_VERSION} ===`);
1158
1110
  console.log(` Platform: ${process.platform}`);
1159
1111
  console.log(` Arch: ${process.arch}`);
1160
1112
  console.log(` Home: ${HOME}`);
1161
- // Determine install scope
1162
- let scope;
1163
- if (reinstall) {
1164
- scope = "1";
1165
- console.log(" Scope: user (reinstall)");
1166
- }
1167
- else if (scopeFlag) {
1168
- const map = { user: "1", project: "2", both: "3" };
1169
- scope = map[scopeFlag];
1170
- if (!scope) {
1171
- console.log(` [fail] Invalid --scope: ${scopeFlag}. Use: user, project, both`);
1172
- closeRL();
1173
- process.exit(1);
1174
- }
1175
- console.log(` Scope: ${scopeFlag}`);
1176
- }
1177
- else if (install || yes) {
1178
- scope = "1";
1179
- console.log(` Scope: user (${install ? "install default" : "--scope omitted, defaulting to user with --yes"})`);
1180
- }
1181
- else {
1182
- console.log("\nInstall scope:");
1183
- console.log(" 1) User-level (all OMP sessions)");
1184
- console.log(" 2) Project-level (this repo only)");
1185
- console.log(" 3) Both");
1186
- scope = (await ask("\nChoose [1-3] (default 1): ")).trim() || "1";
1187
- }
1113
+ const scope = await resolveScope();
1188
1114
  // Resolve session defaults: flags > interactive prompt > defaults.
1189
1115
  const profile = await resolveProfile();
1190
- const userDir = path.join(HOME, ".omp", "agent");
1191
- const userExtDir = path.join(userDir, "extensions");
1192
- const userPluginsDir = path.join(userDir, "..", "plugins");
1193
- const bunBinDir = path.join(HOME, ".bun", "bin");
1194
- const projectExtDir = path.join(process.cwd(), ".omp", "extensions");
1116
+ const userDir = OMP_AGENT_DIR;
1117
+ const userExtDir = path.join(userDir, 'extensions');
1118
+ const projectExtDir = path.join(process.cwd(), '.omp', 'extensions');
1195
1119
  const options = { dryRun, verbose, yes, scope, reinstall };
1196
1120
  // Check prerequisites
1197
- console.log("\nPrerequisites:");
1121
+ console.log('\nPrerequisites:');
1198
1122
  try {
1199
- const v = (await execP(IS_WINDOWS ? "omp.cmd" : "omp", ["--version"])).stdout.trim();
1123
+ const v = (await execP(OMP_BIN, ['--version'])).stdout.trim();
1200
1124
  console.log(` [ok] omp ${v}`);
1201
1125
  }
1202
1126
  catch {
1203
- console.log(" [fail] omp not found — ensure it's installed");
1127
+ console.log(' [fail] omp not found — ensure it\'s installed');
1204
1128
  }
1205
- if (scope === "1" || scope === "3") {
1206
- console.log("\n--- User-level install ---");
1129
+ const cavemanRule = await fetchCavemanRule(options);
1130
+ if (scope === '1' || scope === '3') {
1131
+ console.log('\n--- User-level install ---');
1207
1132
  await stepSharedSessionState(userExtDir, options);
1208
- await stepPonytail(userPluginsDir, userDir, options);
1209
- const selfPlugin = await stepSelfPlugin(userPluginsDir, options);
1210
- const ponytailExtPath = path.join(userPluginsDir, "node_modules", "@dietrichgebert", "ponytail", "pi-extension", "index.js");
1211
- await stepRtk(bunBinDir, options);
1133
+ await stepPonytail(OMP_PLUGINS_DIR, userDir, options);
1134
+ const selfPlugin = await stepSelfPlugin(OMP_PLUGINS_DIR, options);
1135
+ const ponytailExtPath = path.join(OMP_PLUGINS_DIR, 'node_modules', '@dietrichgebert', 'ponytail', 'pi-extension', 'index.js');
1136
+ await stepRtk(BUN_BIN_DIR, options);
1212
1137
  await stepRtkSession(userExtDir, options);
1213
- await stepCaveman(userExtDir, options);
1138
+ await stepCaveman(userExtDir, cavemanRule, options);
1214
1139
  await stepCombo(userExtDir, options);
1215
1140
  await stepModeReinforcement(userExtDir, ponytailExtPath, options);
1216
1141
  await stepUpdater(userExtDir, options);
1217
- await stepAmanaiReward(userExtDir, options, selfPlugin);
1218
1142
  if (selfPlugin)
1219
1143
  await writePluginSettings(profile, options);
1220
1144
  await validateConfigExtensions(userDir, options);
1221
1145
  }
1222
- if (scope === "2" || scope === "3") {
1223
- console.log("\n--- Project-level install ---");
1146
+ if (scope === '2' || scope === '3') {
1147
+ console.log('\n--- Project-level install ---');
1224
1148
  await stepSharedSessionState(projectExtDir, options);
1225
1149
  await stepRtkSession(projectExtDir, options);
1226
- await stepCaveman(projectExtDir, options);
1150
+ await stepCaveman(projectExtDir, cavemanRule, options);
1227
1151
  await stepUpdater(projectExtDir, options);
1228
- await stepAmanaiReward(projectExtDir, options);
1229
- console.log(" [note] Ponytail, RTK binary, and Combo toggle require user-level (global) install");
1230
- }
1231
- console.log("\n=== Installation complete ===");
1232
- console.log("\nNext steps:");
1233
- console.log(" 1. Restart OMP");
1234
- console.log(" 2. /caveman full");
1235
- console.log(" 3. /rtk on");
1236
- console.log(" 4. /ponytail full");
1237
- console.log(" 5. /ai-addons check");
1238
- console.log(" 6. /combo medium (toggle all 3 at once — off by default)");
1152
+ console.log(' [note] Ponytail, RTK binary, and Combo toggle require user-level (global) install');
1153
+ }
1154
+ console.log('\n=== Installation complete ===');
1155
+ console.log('\nNext steps:');
1156
+ console.log(' 1. Restart OMP');
1157
+ console.log(' 2. /caveman full');
1158
+ console.log(' 3. /rtk on');
1159
+ console.log(' 4. /ponytail full');
1160
+ console.log(' 5. /ai-addons check');
1161
+ console.log(' 6. /combo medium (toggle all 3 at once — off by default)');
1239
1162
  closeRL();
1240
1163
  }
1241
- main().catch((e) => { closeRL(); console.error(e); });
1164
+ main().catch((e) => { closeRL(); console.error(e); process.exitCode = 1; });