@krtclcdy/tersio 2.14.0 → 2.16.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 +9 -27
- package/cli/banner.js +9 -2
- package/cli/common.js +17 -10
- package/cli/doctor.js +52 -60
- package/cli/install.js +144 -115
- package/cli/rtk-wiring.js +66 -8
- package/cli/update.js +54 -42
- package/cli/usage.js +2 -1
- package/dashboard/app.js +16 -1
- package/extensions/combo-toggle/index.js +20 -5
- package/extensions/shared/plugin-settings.js +4 -0
- package/extensions/shared/pricing.js +112 -70
- package/extensions/shared/usage-ledger.js +2 -2
- package/extensions/tersio-commands/index.js +15 -79
- package/package.json +1 -1
package/cli/install.js
CHANGED
|
@@ -3,7 +3,7 @@ import { promises as fs } from 'node:fs';
|
|
|
3
3
|
import os from 'node:os';
|
|
4
4
|
import path from 'node:path';
|
|
5
5
|
import { fileURLToPath } from 'node:url';
|
|
6
|
-
import { BUN_BIN_DIR,
|
|
6
|
+
import { BUN_BIN_DIR, CAVEMAN_DEFAULTS, COMBO_PRESET_MODES, IS_WINDOWS, OMP_AGENT_DIR, OMP_PLUGINS_DIR, OMP_BIN, PACKAGE_NAME, PACKAGE_VERSION, PONYTAIL_DEFAULTS, RTK_BINARY_NAME, applyUpdate, cavemanDefaultFlag, comboDefaultFlag, command, dryRun, install, ponytailDefaultFlag, profileFlagsGiven, reinstall, rtkDefaultFlag, verbose, yes, dashboardExport, dashboardPort, debug, ensureExtensionAfterConfigEntry, ensureExtensionInConfig, ensurePonytailConfigValue, execP, parseJsonObject, readPluginsPackage, writeIfChanged, } from './common.js';
|
|
7
7
|
import { askInteractiveChoice, askInteractiveConfirm, closeRL, execNetwork, tty, withInteractiveSpinner, } from './interactive.js';
|
|
8
8
|
import { printWelcome } from './banner.js';
|
|
9
9
|
import { checkForUpdate, runLatestUpdate } from './update.js';
|
|
@@ -32,17 +32,18 @@ const SHARED_CARBON = path.join(EXT_DIR, 'shared', 'carbon.js');
|
|
|
32
32
|
const PONYTAIL_GITHUB_SPEC = 'github:DietrichGebert/ponytail';
|
|
33
33
|
const PONYTAIL_NPM_SPEC = '@dietrichgebert/ponytail@latest';
|
|
34
34
|
async function stepPonytail(pluginsDir, userDir, options) {
|
|
35
|
-
|
|
35
|
+
if (!options.quiet)
|
|
36
|
+
console.log(' Ponytail — refresh plugin');
|
|
36
37
|
await fs.mkdir(pluginsDir, { recursive: true });
|
|
37
38
|
const pkgPath = path.join(pluginsDir, 'package.json');
|
|
38
39
|
const pkg = await readPluginsPackage(pkgPath);
|
|
39
40
|
pkg.dependencies['@dietrichgebert/ponytail'] = PONYTAIL_GITHUB_SPEC;
|
|
40
41
|
if (options.dryRun) {
|
|
41
|
-
|
|
42
|
+
if (verbose && !options.quiet)
|
|
43
|
+
console.log(` [dry-run] would write ${pkgPath}`);
|
|
42
44
|
}
|
|
43
45
|
else {
|
|
44
46
|
await fs.writeFile(pkgPath, JSON.stringify(pkg, null, 2) + '\n');
|
|
45
|
-
console.log(' [write] package.json');
|
|
46
47
|
}
|
|
47
48
|
const ponytailExtPath = path.join(pluginsDir, 'node_modules', '@dietrichgebert', 'ponytail', 'pi-extension', 'index.js');
|
|
48
49
|
const probeExt = async () => (await readTextIfExists(ponytailExtPath)) !== null;
|
|
@@ -53,8 +54,9 @@ async function stepPonytail(pluginsDir, userDir, options) {
|
|
|
53
54
|
}
|
|
54
55
|
else if (options.dryRun) {
|
|
55
56
|
// Dry runs preview the wiring below without touching the network.
|
|
56
|
-
|
|
57
|
-
|
|
57
|
+
if (verbose && !options.quiet)
|
|
58
|
+
console.log(` [dry-run] would run: omp plugin install ${PONYTAIL_GITHUB_SPEC}`);
|
|
59
|
+
if (options.reinstall && verbose && !options.quiet) {
|
|
58
60
|
console.log(` [dry-run] would run: npm install ${PONYTAIL_NPM_SPEC} --save --no-audit --no-fund`);
|
|
59
61
|
}
|
|
60
62
|
ponytailExtExists = true;
|
|
@@ -63,7 +65,6 @@ async function stepPonytail(pluginsDir, userDir, options) {
|
|
|
63
65
|
// Try omp plugin install first
|
|
64
66
|
try {
|
|
65
67
|
await execNetwork('Installing Ponytail plugin', OMP_BIN, ['plugin', 'install', PONYTAIL_GITHUB_SPEC], { cwd: pluginsDir });
|
|
66
|
-
console.log(' [ok] omp plugin install ran');
|
|
67
68
|
}
|
|
68
69
|
catch (e) {
|
|
69
70
|
console.log(` [warn] omp plugin install failed: ${e.message}`);
|
|
@@ -71,7 +72,6 @@ async function stepPonytail(pluginsDir, userDir, options) {
|
|
|
71
72
|
if (options.reinstall) {
|
|
72
73
|
try {
|
|
73
74
|
await execNetwork('Refreshing Ponytail package', 'npm', ['install', PONYTAIL_NPM_SPEC, '--save', '--no-audit', '--no-fund'], { cwd: pluginsDir, timeout: 120000 });
|
|
74
|
-
console.log(' [ok] Ponytail refreshed');
|
|
75
75
|
}
|
|
76
76
|
catch (e) {
|
|
77
77
|
console.log(` [fail] Could not refresh ponytail: ${e.message}`);
|
|
@@ -82,15 +82,14 @@ async function stepPonytail(pluginsDir, userDir, options) {
|
|
|
82
82
|
ponytailExtExists = await probeExt();
|
|
83
83
|
// Fallback: try bun install or npm install
|
|
84
84
|
if (!ponytailExtExists) {
|
|
85
|
-
|
|
85
|
+
if (!options.quiet)
|
|
86
|
+
console.log(' [info] pi-extension/index.js not found after omp plugin install — trying npm/bun install...');
|
|
86
87
|
try {
|
|
87
88
|
await execNetwork('Installing Ponytail dependencies', 'npm', ['install'], { cwd: pluginsDir, timeout: 120000 });
|
|
88
|
-
console.log(' [ok] npm install completed');
|
|
89
89
|
}
|
|
90
90
|
catch {
|
|
91
91
|
try {
|
|
92
92
|
await execNetwork('Installing Ponytail dependencies', 'bun', ['install'], { cwd: pluginsDir, timeout: 120000 });
|
|
93
|
-
console.log(' [ok] bun install completed');
|
|
94
93
|
}
|
|
95
94
|
catch (e2) {
|
|
96
95
|
console.log(` [fail] Could not install ponytail: ${e2.message}`);
|
|
@@ -101,12 +100,12 @@ async function stepPonytail(pluginsDir, userDir, options) {
|
|
|
101
100
|
}
|
|
102
101
|
// Last-resort fallback: git clone the repo into node_modules
|
|
103
102
|
if (!ponytailExtExists) {
|
|
104
|
-
|
|
103
|
+
if (!options.quiet)
|
|
104
|
+
console.log(' [info] npm/bun did not produce pi-extension — trying git clone...');
|
|
105
105
|
try {
|
|
106
106
|
const dest = path.join(pluginsDir, 'node_modules', '@dietrichgebert', 'ponytail');
|
|
107
107
|
await fs.mkdir(path.dirname(dest), { recursive: true });
|
|
108
108
|
await execNetwork('Cloning Ponytail repository', 'git', ['clone', '--depth', '1', 'https://github.com/DietrichGebert/ponytail.git', dest], { timeout: 180000 });
|
|
109
|
-
console.log(' [ok] git clone completed');
|
|
110
109
|
ponytailExtExists = await probeExt();
|
|
111
110
|
}
|
|
112
111
|
catch (e3) {
|
|
@@ -119,9 +118,9 @@ async function stepPonytail(pluginsDir, userDir, options) {
|
|
|
119
118
|
console.log(' [skip] Ponytail pi-extension/index.js still not found — skill-only mode');
|
|
120
119
|
console.log(' [hint] The /ponytail command won\'t work, but ponytail skills will still load');
|
|
121
120
|
}
|
|
122
|
-
else if (!options.dryRun) {
|
|
121
|
+
else if (!options.dryRun && !options.quiet) {
|
|
123
122
|
// Wire extension into config.yml so /ponytail command loads
|
|
124
|
-
|
|
123
|
+
debug('Ponytail pi-extension found');
|
|
125
124
|
}
|
|
126
125
|
// Still set Ponytail config defaults even without the extension command
|
|
127
126
|
const configPath = path.join(userDir, 'config.yml');
|
|
@@ -134,20 +133,23 @@ async function stepPonytail(pluginsDir, userDir, options) {
|
|
|
134
133
|
// Settings → Plugins page (OMP enumerates plugins/package.json dependencies).
|
|
135
134
|
// Returns true when the package is verified in plugins/node_modules.
|
|
136
135
|
async function stepSelfPlugin(pluginsDir, options) {
|
|
137
|
-
|
|
136
|
+
if (!options.quiet)
|
|
137
|
+
console.log(' Tersio — register plugin');
|
|
138
138
|
const pkgPath = path.join(pluginsDir, 'package.json');
|
|
139
139
|
const pkg = await readPluginsPackage(pkgPath);
|
|
140
140
|
pkg.dependencies[PACKAGE_NAME] = `^${PACKAGE_VERSION}`;
|
|
141
141
|
for (const legacy of ['oh-my-pi-token-saver', 'tersio-omp']) {
|
|
142
142
|
if (legacy in pkg.dependencies) {
|
|
143
143
|
delete pkg.dependencies[legacy];
|
|
144
|
-
if (!options.dryRun)
|
|
144
|
+
if (!options.dryRun && !options.quiet)
|
|
145
145
|
console.log(` [migrate] dropped legacy ${legacy} dependency`);
|
|
146
146
|
}
|
|
147
147
|
}
|
|
148
148
|
if (options.dryRun) {
|
|
149
|
-
|
|
150
|
-
|
|
149
|
+
if (verbose && !options.quiet)
|
|
150
|
+
console.log(` [dry-run] would add ${PACKAGE_NAME}@^${PACKAGE_VERSION} to ${pkgPath}`);
|
|
151
|
+
if (verbose && !options.quiet)
|
|
152
|
+
console.log(` [dry-run] would run: npm install --no-audit --no-fund (in ${pluginsDir})`);
|
|
151
153
|
return false;
|
|
152
154
|
}
|
|
153
155
|
// pkg.dependencies[PACKAGE_NAME] was assigned above, so only reinstall skips this fast path.
|
|
@@ -155,7 +157,6 @@ async function stepSelfPlugin(pluginsDir, options) {
|
|
|
155
157
|
const installedPkgRaw = await readTextIfExists(path.join(pluginsDir, 'node_modules', PACKAGE_NAME, 'package.json'));
|
|
156
158
|
if (parseJsonObject(installedPkgRaw)?.version === PACKAGE_VERSION) {
|
|
157
159
|
debug(`${PACKAGE_NAME} already installed at v${PACKAGE_VERSION}; skipping npm install`);
|
|
158
|
-
console.log(' [ok] Listed in OMP Settings → Plugins as tersio');
|
|
159
160
|
return true;
|
|
160
161
|
}
|
|
161
162
|
}
|
|
@@ -179,7 +180,7 @@ async function stepSelfPlugin(pluginsDir, options) {
|
|
|
179
180
|
console.log(` [warn] ${PACKAGE_NAME} not found in plugins/node_modules after install`);
|
|
180
181
|
return false;
|
|
181
182
|
}
|
|
182
|
-
|
|
183
|
+
debug('tersio listed in OMP plugins');
|
|
183
184
|
return true;
|
|
184
185
|
}
|
|
185
186
|
function resolveRtkTriple() {
|
|
@@ -211,19 +212,21 @@ async function downloadRtkChecksums(release) {
|
|
|
211
212
|
return null;
|
|
212
213
|
}
|
|
213
214
|
}
|
|
214
|
-
async function verifyRtkArchive(archivePath, assetName, checksumsText) {
|
|
215
|
+
async function verifyRtkArchive(archivePath, assetName, checksumsText, options = {}) {
|
|
215
216
|
if (!checksumsText) {
|
|
216
|
-
|
|
217
|
+
if (!options.quiet)
|
|
218
|
+
console.log(' [warn] No checksums.txt available — skipping verification');
|
|
217
219
|
return true;
|
|
218
220
|
}
|
|
219
221
|
const expected = parseChecksum(checksumsText, assetName);
|
|
220
222
|
const actual = await sha256File(archivePath);
|
|
221
223
|
if (!expected) {
|
|
222
|
-
|
|
224
|
+
if (!options.quiet)
|
|
225
|
+
console.log(` [warn] checksums.txt missing entry for ${assetName} — skipping verification`);
|
|
223
226
|
return true;
|
|
224
227
|
}
|
|
225
228
|
if (actual === expected) {
|
|
226
|
-
|
|
229
|
+
debug(`Checksum verified for ${assetName}`);
|
|
227
230
|
return true;
|
|
228
231
|
}
|
|
229
232
|
console.log(` [fail] Checksum mismatch for ${assetName}`);
|
|
@@ -272,10 +275,18 @@ async function extractRtkArchive(archivePath, extractDir) {
|
|
|
272
275
|
return false;
|
|
273
276
|
}
|
|
274
277
|
async function stepRtk(binDir, options) {
|
|
275
|
-
|
|
278
|
+
if (!options.quiet)
|
|
279
|
+
console.log(' RTK — download binary and wire into OMP');
|
|
276
280
|
const binDest = path.join(binDir, RTK_BINARY_NAME);
|
|
277
281
|
// Download failure must not skip wiring: a pre-existing rtk binary is
|
|
278
282
|
// exactly as good for the OMP hook, so wire whatever ends up at binDest.
|
|
283
|
+
// Dry runs stay offline: no registry probe, just the plan line above.
|
|
284
|
+
if (options.dryRun) {
|
|
285
|
+
if (verbose && !options.quiet)
|
|
286
|
+
console.log(` [dry-run] would download rtk binary and install to ${binDest}`);
|
|
287
|
+
await wireRtkOmp(binDest, options);
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
279
290
|
try {
|
|
280
291
|
const release = await withInteractiveSpinner('Finding latest RTK release', () => fetchJson(RTK_RELEASE_API));
|
|
281
292
|
const triple = resolveRtkTriple();
|
|
@@ -284,12 +295,6 @@ async function stepRtk(binDir, options) {
|
|
|
284
295
|
const asset = findRtkAsset(release, triple);
|
|
285
296
|
if (!asset)
|
|
286
297
|
return;
|
|
287
|
-
if (options.dryRun) {
|
|
288
|
-
console.log(` [dry-run] would download ${asset.name} from release ${release.tag_name}`);
|
|
289
|
-
console.log(' [dry-run] would verify checksum against checksums.txt');
|
|
290
|
-
console.log(` [dry-run] would extract and install to ${binDest}`);
|
|
291
|
-
return;
|
|
292
|
-
}
|
|
293
298
|
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'omp-rtk-'));
|
|
294
299
|
try {
|
|
295
300
|
const archivePath = path.join(tmpDir, asset.name);
|
|
@@ -301,7 +306,7 @@ async function stepRtk(binDir, options) {
|
|
|
301
306
|
update('Verifying RTK checksum');
|
|
302
307
|
return downloadedChecksums;
|
|
303
308
|
});
|
|
304
|
-
if (!await verifyRtkArchive(archivePath, asset.name, checksumsText))
|
|
309
|
+
if (!await verifyRtkArchive(archivePath, asset.name, checksumsText, options))
|
|
305
310
|
return;
|
|
306
311
|
const extractDir = path.join(tmpDir, 'extracted');
|
|
307
312
|
if (!await extractRtkArchive(archivePath, extractDir))
|
|
@@ -314,14 +319,12 @@ async function stepRtk(binDir, options) {
|
|
|
314
319
|
await fs.mkdir(path.dirname(binDest), { recursive: true });
|
|
315
320
|
await fs.copyFile(binDest, `${binDest}.bak`).catch(() => { });
|
|
316
321
|
await fs.copyFile(found, binDest);
|
|
317
|
-
console.log(` [write] ${binDest}`);
|
|
318
322
|
if (!IS_WINDOWS) {
|
|
319
323
|
await fs.chmod(binDest, 0o755);
|
|
320
324
|
debug(`chmod 755 ${binDest}`);
|
|
321
325
|
}
|
|
322
326
|
try {
|
|
323
|
-
|
|
324
|
-
console.log(` [ok] ${binDest} → ${v}`);
|
|
327
|
+
await execP(binDest, ['--version'], { timeout: 30000, shell: false });
|
|
325
328
|
}
|
|
326
329
|
catch {
|
|
327
330
|
console.log(` [hint] Verify manually: ${binDest} --version`);
|
|
@@ -352,7 +355,8 @@ async function stepRtk(binDir, options) {
|
|
|
352
355
|
async function copySources(extDir, files, skipLabel, options) {
|
|
353
356
|
const src = await readTextIfExists(files[0][0]);
|
|
354
357
|
if (!src) {
|
|
355
|
-
|
|
358
|
+
if (!options.quiet && options.dryRun)
|
|
359
|
+
console.log(` [skip] ${skipLabel} not found in repo`);
|
|
356
360
|
return false;
|
|
357
361
|
}
|
|
358
362
|
await writeIfChanged(path.join(extDir, files[0][1]), src, options);
|
|
@@ -366,6 +370,8 @@ async function copySources(extDir, files, skipLabel, options) {
|
|
|
366
370
|
return true;
|
|
367
371
|
}
|
|
368
372
|
async function stepSharedSessionState(extDir, options) {
|
|
373
|
+
if (!options.quiet)
|
|
374
|
+
console.log(' Shared files — sync session bridge');
|
|
369
375
|
await copySources(extDir, [
|
|
370
376
|
[SHARED_SESSION_STATE, path.join('shared', 'session-state.js')],
|
|
371
377
|
[SHARED_TYPES, path.join('shared', 'types.js')],
|
|
@@ -377,14 +383,16 @@ async function stepSharedSessionState(extDir, options) {
|
|
|
377
383
|
], 'shared/session-state.js', options);
|
|
378
384
|
}
|
|
379
385
|
async function stepModeReinforcement(extDir, ponytailExtPath, options) {
|
|
380
|
-
|
|
386
|
+
if (!options.quiet)
|
|
387
|
+
console.log(' Session helpers — sync shared files');
|
|
381
388
|
const dest = path.join(extDir, 'shared', 'mode-reinforcement.js');
|
|
382
389
|
if (!await copySources(extDir, [[MODE_REINFORCEMENT_INDEX, path.join('shared', 'mode-reinforcement.js')]], 'shared/mode-reinforcement.js', options))
|
|
383
390
|
return;
|
|
384
391
|
await ensureExtensionAfterConfigEntry(path.join(path.dirname(extDir), 'config.yml'), dest, ponytailExtPath, 'mode reinforcement', options);
|
|
385
392
|
}
|
|
386
393
|
async function stepRtkSession(extDir, options) {
|
|
387
|
-
|
|
394
|
+
if (!options.quiet)
|
|
395
|
+
console.log(' RTK session — install session mode');
|
|
388
396
|
await copySources(extDir, [[RTK_SESSION_INDEX, path.join('rtk-session', 'index.js')]], 'rtk-session/index.js', options);
|
|
389
397
|
}
|
|
390
398
|
// One rule fetch serves the install; dry runs stay offline
|
|
@@ -401,13 +409,14 @@ async function fetchCavemanRule(options) {
|
|
|
401
409
|
}
|
|
402
410
|
}
|
|
403
411
|
async function stepCaveman(extDir, rule, options) {
|
|
404
|
-
|
|
412
|
+
if (!options.quiet)
|
|
413
|
+
console.log(' Caveman — fetch rule and install session mode');
|
|
405
414
|
const cavemanDir = path.join(extDir, 'caveman-session');
|
|
406
415
|
if (!options.dryRun)
|
|
407
416
|
await fs.mkdir(cavemanDir, { recursive: true });
|
|
408
417
|
const ruleDest = path.join(cavemanDir, 'rule.md');
|
|
409
418
|
if (rule === null && (await readTextIfExists(ruleDest)) !== null) {
|
|
410
|
-
|
|
419
|
+
debug('Keeping existing rule.md');
|
|
411
420
|
}
|
|
412
421
|
else if (rule === null) {
|
|
413
422
|
console.log(' [skip] Caveman rule.md unavailable');
|
|
@@ -419,14 +428,16 @@ async function stepCaveman(extDir, rule, options) {
|
|
|
419
428
|
await copySources(extDir, [[CAVEMAN_INDEX, path.join('caveman-session', 'index.js')]], 'caveman-session/index.js', options);
|
|
420
429
|
}
|
|
421
430
|
async function stepTersioCommands(extDir, options) {
|
|
422
|
-
|
|
431
|
+
if (!options.quiet)
|
|
432
|
+
console.log(' Tersio commands — install /tersio root command');
|
|
423
433
|
await copySources(extDir, [[TERSIO_COMMANDS_INDEX, path.join('tersio-commands', 'index.js')]], 'tersio-commands/index.js', options);
|
|
424
434
|
}
|
|
425
435
|
async function stepUpdater(extDir, options) {
|
|
426
436
|
await copySources(extDir, [[UPDATER_INDEX, path.join('ai-addons-updater', 'index.js')]], 'ai-addons-updater/index.js', options);
|
|
427
437
|
}
|
|
428
438
|
async function stepCombo(extDir, options) {
|
|
429
|
-
|
|
439
|
+
if (!options.quiet)
|
|
440
|
+
console.log(' Combo — install preset switch');
|
|
430
441
|
const dest = path.join(extDir, 'combo-toggle', 'index.js');
|
|
431
442
|
if (!await copySources(extDir, [[COMBO_TOGGLE_INDEX, path.join('combo-toggle', 'index.js')]], 'combo-toggle/index.js', options))
|
|
432
443
|
return;
|
|
@@ -442,10 +453,41 @@ function defaultProfile() {
|
|
|
442
453
|
ponytailDefault: 'off',
|
|
443
454
|
};
|
|
444
455
|
}
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
456
|
+
async function storedProfile() {
|
|
457
|
+
const base = defaultProfile();
|
|
458
|
+
const raw = await readTextIfExists(path.join(OMP_PLUGINS_DIR, 'omp-plugins.lock.json'));
|
|
459
|
+
if (!raw)
|
|
460
|
+
return base;
|
|
461
|
+
let stored;
|
|
462
|
+
try {
|
|
463
|
+
const parsed = JSON.parse(raw);
|
|
464
|
+
if (!parsed || typeof parsed !== 'object' || !('settings' in parsed))
|
|
465
|
+
return base;
|
|
466
|
+
const settings = parsed.settings;
|
|
467
|
+
if (!settings || typeof settings !== 'object' || !(PACKAGE_NAME in settings))
|
|
468
|
+
return base;
|
|
469
|
+
const entry = settings[PACKAGE_NAME];
|
|
470
|
+
if (!entry || typeof entry !== 'object')
|
|
471
|
+
return base;
|
|
472
|
+
stored = entry;
|
|
473
|
+
}
|
|
474
|
+
catch {
|
|
475
|
+
return base;
|
|
476
|
+
}
|
|
477
|
+
if (typeof stored.comboDefault === 'string' && stored.comboDefault in COMBO_PRESET_MODES)
|
|
478
|
+
base.comboDefault = stored.comboDefault;
|
|
479
|
+
if (typeof stored.cavemanDefault === 'string' && CAVEMAN_DEFAULTS.has(stored.cavemanDefault))
|
|
480
|
+
base.cavemanDefault = stored.cavemanDefault;
|
|
481
|
+
if (typeof stored.rtkDefault === 'boolean')
|
|
482
|
+
base.rtkDefault = stored.rtkDefault;
|
|
483
|
+
if (typeof stored.ponytailDefault === 'string' && PONYTAIL_DEFAULTS.has(stored.ponytailDefault))
|
|
484
|
+
base.ponytailDefault = stored.ponytailDefault;
|
|
485
|
+
return base;
|
|
486
|
+
}
|
|
487
|
+
async function resolveProfile(forceReinstall = false, opts = {}) {
|
|
488
|
+
// Seed from the lock file so flag-less update/reinstall runs keep the
|
|
489
|
+
// user's configured defaults instead of resetting them to off.
|
|
490
|
+
const profile = await storedProfile();
|
|
449
491
|
// Single interactive prompt: the Combo preset implies all three modes.
|
|
450
492
|
// Numbered menu — no typing preset names.
|
|
451
493
|
// Only for a real user at a terminal, only when no default flags were
|
|
@@ -456,7 +498,7 @@ async function resolveProfile(forceReinstall = false) {
|
|
|
456
498
|
{ value: 'medium', label: 'medium', hint: 'caveman=lite, rtk=on, ponytail=lite' },
|
|
457
499
|
{ value: 'balanced', label: 'balanced', hint: 'caveman=full, rtk=on, ponytail=full' },
|
|
458
500
|
{ value: 'max', label: 'max', hint: 'caveman=ultra, rtk=on, ponytail=ultra' },
|
|
459
|
-
],
|
|
501
|
+
], profile.comboDefault);
|
|
460
502
|
if (choice.status === 'selected')
|
|
461
503
|
profile.comboDefault = choice.value;
|
|
462
504
|
else {
|
|
@@ -477,32 +519,10 @@ async function resolveProfile(forceReinstall = false) {
|
|
|
477
519
|
profile.rtkDefault = rtkDefaultFlag === 'on';
|
|
478
520
|
if (ponytailDefaultFlag !== undefined)
|
|
479
521
|
profile.ponytailDefault = ponytailDefaultFlag;
|
|
480
|
-
|
|
522
|
+
if ((!opts.quiet && verbose) || dryRun)
|
|
523
|
+
console.log(` Defaults: combo=${profile.comboDefault} (caveman=${profile.cavemanDefault} · rtk=${profile.rtkDefault ? 'on' : 'off'} · ponytail=${profile.ponytailDefault})`);
|
|
481
524
|
return profile;
|
|
482
525
|
}
|
|
483
|
-
// ponytail: post-install guard. List entries under `extensions: null` break
|
|
484
|
-
// OMP launch, so fail loudly here instead of at the next OMP start.
|
|
485
|
-
async function validateConfigExtensions(agentDir, options) {
|
|
486
|
-
if (options.dryRun)
|
|
487
|
-
return;
|
|
488
|
-
const raw = await readTextIfExists(path.join(agentDir, 'config.yml'));
|
|
489
|
-
if (!raw)
|
|
490
|
-
return;
|
|
491
|
-
const lines = raw.split('\n');
|
|
492
|
-
const keyIdx = lines.findIndex((l) => EXTENSIONS_KEY_RE.test(l));
|
|
493
|
-
if (keyIdx === -1)
|
|
494
|
-
return;
|
|
495
|
-
const scalarNull = /^\s*extensions\s*:\s*(null|~|\[\s*\])\s*$/i.test(lines[keyIdx]);
|
|
496
|
-
const hasEntries = lines.slice(keyIdx + 1).some((l) => /^\s*-\s+\S/.test(l));
|
|
497
|
-
if (scalarNull && hasEntries) {
|
|
498
|
-
console.log(' [fail] config.yml lists extensions under `extensions: null` — OMP will fail to launch.');
|
|
499
|
-
console.log(' [hint] Replace the `extensions: null` line with `extensions:`, then reinstall.');
|
|
500
|
-
process.exitCode = 1;
|
|
501
|
-
}
|
|
502
|
-
else {
|
|
503
|
-
console.log(' [ok] config.yml extensions key valid');
|
|
504
|
-
}
|
|
505
|
-
}
|
|
506
526
|
// Persist the profile as omp plugin settings so `omp plugin config get`
|
|
507
527
|
// reflects the choice and the extensions pick it up on session start.
|
|
508
528
|
async function writePluginSettings(profile, options) {
|
|
@@ -528,7 +548,8 @@ async function writePluginSettings(profile, options) {
|
|
|
528
548
|
ponytailDefault: profile.ponytailDefault,
|
|
529
549
|
};
|
|
530
550
|
if (options.dryRun) {
|
|
531
|
-
|
|
551
|
+
if (verbose && !options.quiet)
|
|
552
|
+
console.log(` [dry-run] would write plugin settings (${PACKAGE_NAME}) to ${lockPath}`);
|
|
532
553
|
return;
|
|
533
554
|
}
|
|
534
555
|
await fs.mkdir(pluginsDir, { recursive: true });
|
|
@@ -575,16 +596,13 @@ async function runCommandMenu() {
|
|
|
575
596
|
await runInstall();
|
|
576
597
|
break;
|
|
577
598
|
case 'update': {
|
|
578
|
-
// One bound flow:
|
|
579
|
-
// refresh in the same
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
console.log(` [update] tersio ${latest} available (installed ${PACKAGE_VERSION})`);
|
|
583
|
-
else if (latest === 'unknown')
|
|
584
|
-
console.log(` [warn] could not reach the npm registry — run \`tersio update\` to retry`);
|
|
599
|
+
// One bound flow: the version check already ran above, so report it
|
|
600
|
+
// and offer the refresh in the same breath — no second "update" quiz.
|
|
601
|
+
if (typeof newer === 'string')
|
|
602
|
+
console.log(` tersio ${newer} available (installed ${PACKAGE_VERSION})`);
|
|
585
603
|
else
|
|
586
|
-
console.log(`
|
|
587
|
-
const go = await askInteractiveConfirm('Update
|
|
604
|
+
console.log(` tersio ${PACKAGE_VERSION} is the latest`);
|
|
605
|
+
const go = await askInteractiveConfirm('Update now (CLI + RTK, Caveman rule, Ponytail)?');
|
|
588
606
|
if (go.status === 'confirmed' && go.value) {
|
|
589
607
|
await runLatestUpdate();
|
|
590
608
|
}
|
|
@@ -633,19 +651,23 @@ async function runInstall(overrides = {}) {
|
|
|
633
651
|
await runCommandMenu();
|
|
634
652
|
return;
|
|
635
653
|
}
|
|
636
|
-
|
|
654
|
+
// apply-update is the delegated payload of `tersio update`: stay silent —
|
|
655
|
+
// the parent already printed the plan and owns the closing summary. Banner
|
|
656
|
+
// repeats here otherwise (the reported double-print), burying real output.
|
|
657
|
+
const quiet = applyUpdate;
|
|
658
|
+
if (!quiet)
|
|
659
|
+
printWelcome();
|
|
637
660
|
if (isReinstall) {
|
|
638
661
|
// removeRtk stays false: reinstall is about to replace the binary, and
|
|
639
662
|
// deleting it first would leave nothing to wire if the fresh download
|
|
640
663
|
// fails (rate limit, offline). rtk.ts is removed here and re-wired below.
|
|
641
664
|
await runUninstall({ yes: true, removePonytail: false, removeRtk: false });
|
|
642
665
|
}
|
|
643
|
-
if (dryRun)
|
|
644
|
-
console.log('
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
console.log(` Home: ${HOME}`);
|
|
666
|
+
if (dryRun && !quiet)
|
|
667
|
+
console.log('Preview — no changes will be written.\n');
|
|
668
|
+
if (!quiet) {
|
|
669
|
+
console.log(`Installing Tersio v${PACKAGE_VERSION}`);
|
|
670
|
+
}
|
|
649
671
|
// Remind humans a newer release exists; silent for scripts (no TTY) and for
|
|
650
672
|
// the apply-update payload, which is itself an update run.
|
|
651
673
|
if (tty() && !applyUpdate && command !== 'uninstall') {
|
|
@@ -672,47 +694,54 @@ async function runInstall(overrides = {}) {
|
|
|
672
694
|
}
|
|
673
695
|
}
|
|
674
696
|
}
|
|
675
|
-
console.log(' Scope: user (all OMP sessions)');
|
|
676
697
|
// Resolve session defaults: flags > interactive prompt > defaults.
|
|
677
|
-
const profile = await resolveProfile(isReinstall);
|
|
698
|
+
const profile = await resolveProfile(isReinstall, { quiet });
|
|
678
699
|
const userDir = OMP_AGENT_DIR;
|
|
679
700
|
const userExtDir = path.join(userDir, 'extensions');
|
|
680
701
|
// apply-update is `tersio update`'s payload run: treat it like reinstall so
|
|
681
702
|
// the add-ons refresh too — Ponytail package via npm, self plugin, RTK
|
|
682
703
|
// binary (always re-downloaded), and the Caveman rule (always re-fetched).
|
|
683
|
-
const options = { dryRun, verbose, yes, reinstall: isReinstall || applyUpdate };
|
|
684
|
-
// Check prerequisites
|
|
685
|
-
console.log('\nPrerequisites:');
|
|
704
|
+
const options = { dryRun, verbose, yes, reinstall: isReinstall || applyUpdate, quiet };
|
|
686
705
|
try {
|
|
687
706
|
const v = (await execP(OMP_BIN, ['--version'])).stdout.trim();
|
|
688
|
-
|
|
707
|
+
if (verbose && !quiet)
|
|
708
|
+
console.log(` omp ${v}`);
|
|
689
709
|
}
|
|
690
710
|
catch {
|
|
691
711
|
console.log(' [fail] omp not found — ensure it\'s installed');
|
|
692
712
|
}
|
|
693
713
|
const cavemanRule = await fetchCavemanRule(options);
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
714
|
+
const failures = [];
|
|
715
|
+
const capture = async (label, work) => {
|
|
716
|
+
try {
|
|
717
|
+
await work();
|
|
718
|
+
}
|
|
719
|
+
catch (e) {
|
|
720
|
+
failures.push(label);
|
|
721
|
+
console.log(` [fail] ${label}: ${shortError(e)}`);
|
|
722
|
+
}
|
|
723
|
+
};
|
|
724
|
+
await capture('shared', () => stepSharedSessionState(userExtDir, options));
|
|
725
|
+
await capture('ponytail', () => stepPonytail(OMP_PLUGINS_DIR, userDir, options));
|
|
726
|
+
let selfPlugin = false;
|
|
727
|
+
await capture('self-plugin', async () => { selfPlugin = await stepSelfPlugin(OMP_PLUGINS_DIR, options); });
|
|
698
728
|
const ponytailExtPath = path.join(OMP_PLUGINS_DIR, 'node_modules', '@dietrichgebert', 'ponytail', 'pi-extension', 'index.js');
|
|
699
|
-
await stepRtk(BUN_BIN_DIR, options);
|
|
700
|
-
await stepRtkSession(userExtDir, options);
|
|
701
|
-
await stepCaveman(userExtDir, cavemanRule, options);
|
|
702
|
-
await stepCombo(userExtDir, options);
|
|
703
|
-
await stepTersioCommands(userExtDir, options);
|
|
704
|
-
await stepModeReinforcement(userExtDir, ponytailExtPath, options);
|
|
705
|
-
await stepUpdater(userExtDir, options);
|
|
729
|
+
await capture('rtk', () => stepRtk(BUN_BIN_DIR, options));
|
|
730
|
+
await capture('rtk session', () => stepRtkSession(userExtDir, options));
|
|
731
|
+
await capture('caveman', () => stepCaveman(userExtDir, cavemanRule, options));
|
|
732
|
+
await capture('combo', () => stepCombo(userExtDir, options));
|
|
733
|
+
await capture('commands', () => stepTersioCommands(userExtDir, options));
|
|
734
|
+
await capture('reinforcement', () => stepModeReinforcement(userExtDir, ponytailExtPath, options));
|
|
735
|
+
await capture('updater', () => stepUpdater(userExtDir, options));
|
|
706
736
|
if (selfPlugin)
|
|
707
|
-
await writePluginSettings(profile, options);
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
console.log(' 6. /combo medium (toggle all 3 at once — off by default)');
|
|
737
|
+
await capture('settings', () => writePluginSettings(profile, options));
|
|
738
|
+
if (quiet) {
|
|
739
|
+
if (failures.length > 0)
|
|
740
|
+
console.log(` add-ons: ${failures.length} failed (${failures.join(', ')}) — see [fail] lines above`);
|
|
741
|
+
}
|
|
742
|
+
else {
|
|
743
|
+
console.log(failures.length === 0 ? '\nDone — restart OMP, then /combo medium.' : `\nDone with ${failures.length} failure(s) — see [fail] lines above.`);
|
|
744
|
+
}
|
|
716
745
|
closeRL();
|
|
717
746
|
}
|
|
718
747
|
export { runInstall };
|
package/cli/rtk-wiring.js
CHANGED
|
@@ -1,12 +1,20 @@
|
|
|
1
1
|
// cli/rtk-wiring.ts — wire the installed rtk binary into OMP.
|
|
2
2
|
// `rtk init -g --agent omp` writes rtk's tool_call extension to
|
|
3
|
-
// ~/.omp/agent/extensions/rtk.ts
|
|
4
|
-
//
|
|
5
|
-
//
|
|
3
|
+
// ~/.omp/agent/extensions/rtk.ts, and we register that path in config.yml
|
|
4
|
+
// (OMP only loads listed extensions — rtk init does not register it).
|
|
5
|
+
// Once loaded, OMP rewrites bash commands to rtk before execution, so every
|
|
6
|
+
// rewritten call lands in rtk's history.db and shows metered savings.
|
|
6
7
|
// Fail-open: a failed wire only downgrades to manual `rtk` prefixing.
|
|
7
8
|
// Deliberately free of cli/common.ts imports (argv side effects) so tests
|
|
8
9
|
// can load it directly.
|
|
10
|
+
import { promises as fs } from 'node:fs';
|
|
11
|
+
import os from 'node:os';
|
|
12
|
+
import path from 'node:path';
|
|
9
13
|
import { execFile } from 'node:child_process';
|
|
14
|
+
function debugWire(options, msg) {
|
|
15
|
+
if (!options.quiet)
|
|
16
|
+
console.log(` ${msg}`);
|
|
17
|
+
}
|
|
10
18
|
function shortWiringError(e) {
|
|
11
19
|
const err = e;
|
|
12
20
|
const why = [err.code !== undefined ? `code ${err.code}` : '', err.signal ?? '', err.stderr?.trim(), err.message]
|
|
@@ -29,14 +37,64 @@ function execFileP(cmd, args, timeout) {
|
|
|
29
37
|
// Idempotent: rtk rewrites its extension file on every init run, so
|
|
30
38
|
// reinstalling tersio refreshes the wiring for free.
|
|
31
39
|
export async function wireRtkOmp(rtkBin, options = {}) {
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
40
|
+
if (!options.dryRun)
|
|
41
|
+
debugWire(options, 'Wiring rtk → OMP (bash tool_call rewrite)…');
|
|
42
|
+
if (options.dryRun)
|
|
35
43
|
return true;
|
|
44
|
+
const wired = await runRtkInit(rtkBin, options);
|
|
45
|
+
if (!wired)
|
|
46
|
+
return false;
|
|
47
|
+
await ensureRtkInConfig(options);
|
|
48
|
+
return true;
|
|
49
|
+
}
|
|
50
|
+
// The rtk.ts path rtk init writes (mirrors OMP_AGENT_DIR in cli/common.ts
|
|
51
|
+
// without importing it — see header note on argv side effects).
|
|
52
|
+
function rtkExtensionPath() {
|
|
53
|
+
const home = process.env.HOME || process.env.USERPROFILE || os.homedir();
|
|
54
|
+
return path.join(home, '.omp', 'agent', 'extensions', 'rtk.ts');
|
|
55
|
+
}
|
|
56
|
+
// OMP loads only listed extensions: append rtk.ts after the existing entries
|
|
57
|
+
// (or create the key) so a fresh wire takes effect on next OMP start.
|
|
58
|
+
async function ensureRtkInConfig(options) {
|
|
59
|
+
const home = process.env.HOME || process.env.USERPROFILE || os.homedir();
|
|
60
|
+
const configPath = path.join(home, '.omp', 'agent', 'config.yml');
|
|
61
|
+
const extPath = rtkExtensionPath().replace(/\\/g, '/');
|
|
62
|
+
let raw = null;
|
|
63
|
+
try {
|
|
64
|
+
raw = await fs.readFile(configPath, 'utf8');
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
if (raw.includes(extPath)) {
|
|
70
|
+
debugWire(options, 'rtk already in config.yml');
|
|
71
|
+
return;
|
|
36
72
|
}
|
|
73
|
+
const lines = raw.split('\n');
|
|
74
|
+
const keyIdx = lines.findIndex((l) => /^\s*extensions\s*:/i.test(l));
|
|
75
|
+
const line = ` - ${extPath}`;
|
|
76
|
+
if (keyIdx === -1) {
|
|
77
|
+
lines.push('extensions:', line, '');
|
|
78
|
+
}
|
|
79
|
+
else {
|
|
80
|
+
if (/^\s*extensions\s*:\s*(?:\[\s*\]|null|~)?\s*$/i.test(lines[keyIdx]))
|
|
81
|
+
lines[keyIdx] = 'extensions:';
|
|
82
|
+
lines.splice(keyIdx + 1, 0, line);
|
|
83
|
+
}
|
|
84
|
+
try {
|
|
85
|
+
await fs.mkdir(path.dirname(configPath), { recursive: true });
|
|
86
|
+
await fs.copyFile(configPath, `${configPath}.bak`).catch(() => { });
|
|
87
|
+
await fs.writeFile(configPath, lines.join('\n'), 'utf8');
|
|
88
|
+
debugWire(options, 'rtk registered in config.yml');
|
|
89
|
+
}
|
|
90
|
+
catch (e) {
|
|
91
|
+
console.log(` [warn] could not register rtk in config.yml: ${e.message}`);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
async function runRtkInit(rtkBin, options) {
|
|
37
95
|
try {
|
|
38
96
|
await execFileP(rtkBin, ['init', '-g', '--agent', 'omp'], 30000);
|
|
39
|
-
|
|
97
|
+
debugWire(options, 'rtk OMP extension wired');
|
|
40
98
|
return true;
|
|
41
99
|
}
|
|
42
100
|
catch (first) {
|
|
@@ -45,7 +103,7 @@ export async function wireRtkOmp(rtkBin, options = {}) {
|
|
|
45
103
|
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
46
104
|
try {
|
|
47
105
|
await execFileP(rtkBin, ['init', '-g', '--agent', 'omp'], 30000);
|
|
48
|
-
|
|
106
|
+
debugWire(options, 'rtk OMP extension wired');
|
|
49
107
|
return true;
|
|
50
108
|
}
|
|
51
109
|
catch (e) {
|