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