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