@krtclcdy/tersio 2.22.1 → 2.24.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 +62 -46
- package/cli/agent-hosts.js +42 -0
- package/cli/agents.js +266 -0
- package/cli/common.js +55 -55
- package/cli/currency.js +8 -12
- package/cli/dashboard.js +76 -49
- package/cli/doctor-fix.js +61 -44
- package/cli/doctor.js +66 -44
- package/cli/hosts.js +118 -0
- package/cli/install.js +187 -143
- package/cli/interactive.js +27 -1
- package/cli/layer.js +24 -0
- package/cli/manifest.js +36 -0
- package/cli/omp-layer.js +51 -0
- package/cli/pi-layer.js +37 -0
- package/cli/pi-wiring.js +92 -0
- package/cli/profile.js +45 -46
- package/cli/reset.js +3 -26
- package/cli/rtk-wiring.js +10 -15
- package/cli/settings.js +1 -1
- package/cli/uninstall.js +135 -44
- package/cli/update.js +24 -30
- package/cli/usage.js +6 -1
- package/dashboard/dist/index.html +16 -16
- package/extensions/ai-addons-updater/index.js +36 -25
- package/extensions/ai-addons-updater/index.ts +36 -24
- package/extensions/caveman-session/index.js +23 -18
- package/extensions/caveman-session/index.ts +23 -18
- package/extensions/caveman-session/rule.md +90 -0
- package/extensions/combo-toggle/index.js +63 -41
- package/extensions/combo-toggle/index.ts +61 -48
- package/extensions/lib/utils.js +56 -13
- package/extensions/lib/utils.ts +55 -13
- package/extensions/rtk-session/index.js +20 -19
- package/extensions/rtk-session/index.ts +18 -24
- package/extensions/shared/carbon.js +5 -8
- package/extensions/shared/carbon.ts +5 -8
- package/extensions/shared/host.js +68 -0
- package/extensions/shared/host.ts +104 -0
- package/extensions/shared/omp-prompt.js +12 -0
- package/extensions/shared/plugin-settings.js +63 -16
- package/extensions/shared/plugin-settings.ts +67 -17
- package/extensions/shared/pricing.js +26 -15
- package/extensions/shared/pricing.ts +28 -16
- package/extensions/shared/rtk-gain.js +13 -24
- package/extensions/shared/rtk-gain.ts +14 -25
- package/extensions/shared/session-state.js +26 -28
- package/extensions/shared/session-state.ts +26 -29
- package/extensions/shared/types.ts +15 -3
- package/extensions/shared/usage-ledger.js +136 -38
- package/extensions/shared/usage-ledger.ts +149 -52
- package/extensions/shared/usage-store.js +14 -12
- package/extensions/shared/usage-store.ts +16 -14
- package/extensions/tersio-commands/index.js +2 -1
- package/extensions/tersio-commands/index.ts +2 -1
- package/package.json +15 -7
- package/tersio.js +6 -6
- package/extensions/shared/mode-reinforcement.js +0 -26
- package/extensions/shared/mode-reinforcement.ts +0 -29
package/cli/install.js
CHANGED
|
@@ -1,44 +1,29 @@
|
|
|
1
|
-
// cli/install.ts — install
|
|
1
|
+
// cli/install.ts — install flow and all setup steps.
|
|
2
2
|
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, COMBO_PRESET_MODES, IS_WINDOWS, OMP_AGENT_DIR, OMP_PLUGINS_DIR, OMP_BIN, PACKAGE_NAME, PACKAGE_VERSION, RTK_BINARY_NAME, applyUpdate, cavemanDefaultFlag, comboDefaultFlag, command, dryRun,
|
|
6
|
+
import { BUN_BIN_DIR, COMBO_PRESET_MODES, IS_WINDOWS, OMP_AGENT_DIR, OMP_PLUGINS_DIR, OMP_BIN, PACKAGE_NAME, PACKAGE_VERSION, RTK_BINARY_NAME, args, applyUpdate, cavemanDefaultFlag, comboDefaultFlag, command, dryRun, ponytailDefaultFlag, profileFlagsGiven, rtkDefaultFlag, verbose, yes, dashboardExport, dashboardPort, debug, 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';
|
|
10
10
|
import { runUninstall } from './uninstall.js';
|
|
11
11
|
import { runDoctor } from './doctor.js';
|
|
12
12
|
import { runReset } from './reset.js';
|
|
13
|
-
import { runSettings } from './settings.js';
|
|
14
13
|
import { runUsage } from './usage.js';
|
|
15
14
|
import { runDashboard } from './dashboard.js';
|
|
16
15
|
import { wireRtkOmp } from './rtk-wiring.js';
|
|
17
|
-
import { CAVEMAN_REMOTE_RULE, RTK_RELEASE_API, fetchJson, findFile, httpsGet, httpsDownload, parseChecksum, readTextIfExists, rtkPlatformSpec, sha256File, } from '../extensions/lib/utils.js';
|
|
16
|
+
import { CAVEMAN_REMOTE_RULE, RTK_RELEASE_API, fetchJson, findFile, findHoistedPackage, httpsGet, httpsDownload, parseChecksum, piAgentDir, readTextIfExists, resolveRtkBinary, rtkPlatformSpec, sha256File, } from '../extensions/lib/utils.js';
|
|
18
17
|
import { storedProfile, writePluginSettings } from './profile.js';
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
const CAVEMAN_INDEX = path.join(EXT_DIR, 'caveman-session', 'index.ts');
|
|
23
|
-
const RTK_SESSION_INDEX = path.join(EXT_DIR, 'rtk-session', 'index.ts');
|
|
24
|
-
const UPDATER_INDEX = path.join(EXT_DIR, 'ai-addons-updater', 'index.ts');
|
|
25
|
-
const COMBO_TOGGLE_INDEX = path.join(EXT_DIR, 'combo-toggle', 'index.ts');
|
|
26
|
-
const TERSIO_COMMANDS_INDEX = path.join(EXT_DIR, 'tersio-commands', 'index.ts');
|
|
27
|
-
const MODE_REINFORCEMENT_INDEX = path.join(EXT_DIR, 'shared', 'mode-reinforcement.ts');
|
|
28
|
-
const SHARED_TYPES = path.join(EXT_DIR, 'shared', 'types.ts');
|
|
29
|
-
const LIB_UTILS = path.join(EXT_DIR, 'lib', 'utils.ts');
|
|
30
|
-
const SHARED_PLUGIN_SETTINGS = path.join(EXT_DIR, 'shared', 'plugin-settings.ts');
|
|
31
|
-
const SHARED_USAGE_LEDGER = path.join(EXT_DIR, 'shared', 'usage-ledger.ts');
|
|
32
|
-
const SHARED_PRICING = path.join(EXT_DIR, 'shared', 'pricing.ts');
|
|
33
|
-
const SHARED_CARBON = path.join(EXT_DIR, 'shared', 'carbon.ts');
|
|
34
|
-
async function stepPonytail(pluginsDir, userDir, options) {
|
|
18
|
+
import { filesUnder, sourcePath } from './manifest.js';
|
|
19
|
+
import { detectHosts, hostHint, hostLabel, parseHostArg, piTersioSource } from './hosts.js';
|
|
20
|
+
async function stepPonytail(pluginsDir, options) {
|
|
35
21
|
if (!options.quiet)
|
|
36
22
|
console.log(' Ponytail — ensure bundled plugin');
|
|
37
23
|
await fs.mkdir(pluginsDir, { recursive: true });
|
|
38
24
|
const pkgPath = path.join(pluginsDir, 'package.json');
|
|
39
25
|
const pkg = await readPluginsPackage(pkgPath);
|
|
40
|
-
//
|
|
41
|
-
// dependency, so drop the legacy row. Runs after stepSelfPlugin.
|
|
26
|
+
// Drop the legacy Ponytail plugin row; it is a tersio dependency now.
|
|
42
27
|
let migrated = false;
|
|
43
28
|
if ('@dietrichgebert/ponytail' in pkg.dependencies) {
|
|
44
29
|
delete pkg.dependencies['@dietrichgebert/ponytail'];
|
|
@@ -57,9 +42,7 @@ async function stepPonytail(pluginsDir, userDir, options) {
|
|
|
57
42
|
}
|
|
58
43
|
const ponytailExtPath = path.join(pluginsDir, 'node_modules', '@dietrichgebert', 'ponytail', 'pi-extension', 'index.js');
|
|
59
44
|
const probeExt = async () => (await readTextIfExists(ponytailExtPath)) !== null;
|
|
60
|
-
//
|
|
61
|
-
// network. Migration always reinstalls: the probe may hit the stale legacy
|
|
62
|
-
// copy, and only npm install prunes it into the tersio-owned one.
|
|
45
|
+
// Skip the network unless a refresh was asked; migration always reinstalls.
|
|
63
46
|
let ponytailExtExists = !options.reinstall && !options.dryRun && !migrated ? await probeExt() : false;
|
|
64
47
|
if (ponytailExtExists) {
|
|
65
48
|
debug('Bundled Ponytail pi-extension already installed; skipping network refresh');
|
|
@@ -71,8 +54,7 @@ async function stepPonytail(pluginsDir, userDir, options) {
|
|
|
71
54
|
ponytailExtExists = true;
|
|
72
55
|
}
|
|
73
56
|
else {
|
|
74
|
-
//
|
|
75
|
-
// Reinstall here only when the probe still misses.
|
|
57
|
+
// Reinstall only when the self-plugin step did not materialize it.
|
|
76
58
|
try {
|
|
77
59
|
await execNetwork('Installing bundled Ponytail', 'npm', ['install', '--no-audit', '--no-fund'], { cwd: pluginsDir, timeout: 180000 });
|
|
78
60
|
}
|
|
@@ -92,19 +74,13 @@ async function stepPonytail(pluginsDir, userDir, options) {
|
|
|
92
74
|
console.log(' [hint] The /ponytail command won\'t work, but ponytail skills will still load');
|
|
93
75
|
}
|
|
94
76
|
else if (!options.dryRun && !options.quiet) {
|
|
95
|
-
|
|
96
|
-
debug('Bundled Ponytail pi-extension found');
|
|
77
|
+
debug('Bundled Ponytail pi-extension found; OMP loads it from the plugin manifest');
|
|
97
78
|
}
|
|
98
|
-
// Still set Ponytail config defaults even without the extension command
|
|
99
|
-
const configPath = path.join(userDir, 'config.yml');
|
|
100
|
-
await ensureExtensionInConfig(configPath, ponytailExtPath, 'ponytail', options);
|
|
101
79
|
await ensurePonytailConfigValue('defaultMode', 'off', options);
|
|
102
80
|
// hideStatus=true keeps the upstream ponytail bar hidden; combo owns the bar.
|
|
103
81
|
await ensurePonytailConfigValue('hideStatus', true, options);
|
|
104
82
|
}
|
|
105
|
-
// Registers
|
|
106
|
-
// Settings → Plugins page (OMP enumerates plugins/package.json dependencies).
|
|
107
|
-
// Returns true when the package is verified in plugins/node_modules.
|
|
83
|
+
// Registers the package so OMP lists it under Settings → Plugins.
|
|
108
84
|
async function stepSelfPlugin(pluginsDir, options) {
|
|
109
85
|
if (!options.quiet)
|
|
110
86
|
console.log(' Tersio — register plugin');
|
|
@@ -247,17 +223,38 @@ async function extractRtkArchive(archivePath, extractDir) {
|
|
|
247
223
|
console.log(` [fail] Unknown archive format: ${archivePath}`);
|
|
248
224
|
return false;
|
|
249
225
|
}
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
226
|
+
// The binary is machine-wide and only OMP needs the wiring, so an install that
|
|
227
|
+
// finds one present only rebinds. `tersio update` still refreshes it.
|
|
228
|
+
async function stepRtk(binDir, options, target = 'omp') {
|
|
253
229
|
const binDest = path.join(binDir, RTK_BINARY_NAME);
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
230
|
+
const found = resolveRtkBinary();
|
|
231
|
+
const bind = async (binary) => {
|
|
232
|
+
if (target !== 'omp')
|
|
233
|
+
return;
|
|
234
|
+
if (!(await fileExists(binary))) {
|
|
235
|
+
console.log(' [skip] no rtk binary to wire — install rtk, then run: rtk init -g --agent omp');
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
await wireRtkOmp(binary, options);
|
|
239
|
+
};
|
|
240
|
+
if (found && !applyUpdate) {
|
|
241
|
+
// The path only under --verbose: a plain run, and a dry run, stay free of
|
|
242
|
+
// real user paths.
|
|
243
|
+
if (!options.quiet) {
|
|
244
|
+
const where = verbose ? ` (${found})` : '';
|
|
245
|
+
console.log(` RTK — already installed${where}${target === 'omp' ? ', binding into OMP' : ', nothing to bind on pi'}`);
|
|
246
|
+
}
|
|
247
|
+
await bind(found);
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
if (!options.quiet)
|
|
251
|
+
console.log(` RTK — download binary and ${target === 'omp' ? 'wire into OMP' : 'put it on PATH'}`);
|
|
252
|
+
// A failed download must not skip wiring: a pre-existing binary serves the
|
|
253
|
+
// OMP hook just as well. Dry runs stay offline.
|
|
257
254
|
if (options.dryRun) {
|
|
258
255
|
if (verbose && !options.quiet)
|
|
259
256
|
console.log(` [dry-run] would download rtk binary and install to ${binDest}`);
|
|
260
|
-
await
|
|
257
|
+
await bind(found ?? binDest);
|
|
261
258
|
return;
|
|
262
259
|
}
|
|
263
260
|
try {
|
|
@@ -311,20 +308,11 @@ async function stepRtk(binDir, options) {
|
|
|
311
308
|
console.log(` [fail] RTK: ${e.message}`);
|
|
312
309
|
console.log(' [hint] Manual: https://github.com/rtk-ai/rtk/releases');
|
|
313
310
|
}
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
return;
|
|
317
|
-
}
|
|
318
|
-
if (!(await fileExists(binDest))) {
|
|
319
|
-
console.log(' [skip] no rtk binary to wire — install rtk, then run: rtk init -g --agent omp');
|
|
320
|
-
return;
|
|
321
|
-
}
|
|
322
|
-
// Binary alone never meters OMP sessions — wire rtk's tool_call
|
|
323
|
-
// extension so bash commands rewrite to rtk and land in history.db.
|
|
324
|
-
await wireRtkOmp(binDest, options);
|
|
311
|
+
// Wire rtk's tool_call extension: the binary alone never meters a session.
|
|
312
|
+
await bind(binDest);
|
|
325
313
|
}
|
|
326
|
-
// Copy
|
|
327
|
-
//
|
|
314
|
+
// Copy sources into the target extension dir. First entry is required; the
|
|
315
|
+
// rest are optional companions.
|
|
328
316
|
async function copySources(extDir, files, skipLabel, options) {
|
|
329
317
|
const src = await readTextIfExists(files[0][0]);
|
|
330
318
|
if (!src) {
|
|
@@ -345,38 +333,28 @@ async function copySources(extDir, files, skipLabel, options) {
|
|
|
345
333
|
async function stepSharedSessionState(extDir, options) {
|
|
346
334
|
if (!options.quiet)
|
|
347
335
|
console.log(' Shared files — sync session bridge');
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
[SHARED_TYPES, path.join('shared', 'types.ts')],
|
|
351
|
-
[LIB_UTILS, path.join('lib', 'utils.ts')],
|
|
352
|
-
[SHARED_PLUGIN_SETTINGS, path.join('shared', 'plugin-settings.ts')],
|
|
353
|
-
[SHARED_USAGE_LEDGER, path.join('shared', 'usage-ledger.ts')],
|
|
354
|
-
[SHARED_PRICING, path.join('shared', 'pricing.ts')],
|
|
355
|
-
[SHARED_CARBON, path.join('shared', 'carbon.ts')],
|
|
356
|
-
], 'shared/session-state.js', options);
|
|
357
|
-
}
|
|
358
|
-
async function stepModeReinforcement(extDir, ponytailExtPath, options) {
|
|
359
|
-
if (!options.quiet)
|
|
360
|
-
console.log(' Session helpers — sync shared files');
|
|
361
|
-
const dest = path.join(extDir, 'shared', 'mode-reinforcement.ts');
|
|
362
|
-
if (!await copySources(extDir, [[MODE_REINFORCEMENT_INDEX, path.join('shared', 'mode-reinforcement.ts')]], 'shared/mode-reinforcement.ts', options))
|
|
363
|
-
return;
|
|
364
|
-
await ensureExtensionAfterConfigEntry(path.join(path.dirname(extDir), 'config.yml'), dest, ponytailExtPath, 'mode reinforcement', options);
|
|
336
|
+
const shared = [...filesUnder('shared'), ...filesUnder('lib')];
|
|
337
|
+
await copySources(extDir, shared, 'shared/session-state.js', options);
|
|
365
338
|
}
|
|
366
339
|
async function stepRtkSession(extDir, options) {
|
|
367
340
|
if (!options.quiet)
|
|
368
341
|
console.log(' RTK session — install session mode');
|
|
369
|
-
await copySources(extDir,
|
|
342
|
+
await copySources(extDir, filesUnder('rtk-session'), 'rtk-session/index.ts', options);
|
|
370
343
|
}
|
|
371
|
-
// One
|
|
372
|
-
//
|
|
344
|
+
// One fetch serves the install; dry runs stay offline and preview the bundled
|
|
345
|
+
// rule's destination.
|
|
373
346
|
async function fetchCavemanRule(options) {
|
|
347
|
+
const bundled = await readTextIfExists(sourcePath('caveman-session/rule.md'));
|
|
374
348
|
if (options.dryRun)
|
|
375
|
-
return
|
|
349
|
+
return bundled;
|
|
376
350
|
try {
|
|
377
351
|
return await withInteractiveSpinner('Fetching Caveman rule', () => httpsGet(CAVEMAN_REMOTE_RULE));
|
|
378
352
|
}
|
|
379
353
|
catch (e) {
|
|
354
|
+
if (bundled !== null) {
|
|
355
|
+
debug(`Using bundled Caveman rule: ${e.message}`);
|
|
356
|
+
return bundled;
|
|
357
|
+
}
|
|
380
358
|
console.log(` [warn] Could not fetch caveman rule: ${e.message}`);
|
|
381
359
|
return null;
|
|
382
360
|
}
|
|
@@ -398,35 +376,27 @@ async function stepCaveman(extDir, rule, options) {
|
|
|
398
376
|
else {
|
|
399
377
|
await writeIfChanged(ruleDest, rule, options);
|
|
400
378
|
}
|
|
401
|
-
await copySources(extDir,
|
|
379
|
+
await copySources(extDir, filesUnder('caveman-session').filter(([, to]) => to.endsWith('index.ts')), 'caveman-session/index.ts', options);
|
|
402
380
|
}
|
|
403
381
|
async function stepTersioCommands(extDir, options) {
|
|
404
382
|
if (!options.quiet)
|
|
405
383
|
console.log(' Tersio commands — install /tersio root command');
|
|
406
|
-
await copySources(extDir,
|
|
384
|
+
await copySources(extDir, filesUnder('tersio-commands'), 'tersio-commands/index.ts', options);
|
|
407
385
|
}
|
|
408
386
|
async function stepUpdater(extDir, options) {
|
|
409
|
-
await copySources(extDir,
|
|
387
|
+
await copySources(extDir, filesUnder('ai-addons-updater'), 'ai-addons-updater/index.ts', options);
|
|
410
388
|
}
|
|
411
389
|
async function stepCombo(extDir, options) {
|
|
412
390
|
if (!options.quiet)
|
|
413
391
|
console.log(' Combo — install preset switch');
|
|
414
|
-
|
|
415
|
-
if (!await copySources(extDir, [[COMBO_TOGGLE_INDEX, path.join('combo-toggle', 'index.ts')]], 'combo-toggle/index.ts', options))
|
|
416
|
-
return;
|
|
417
|
-
// Auto-register combo in config.yml
|
|
418
|
-
const configPath = path.join(path.dirname(extDir), 'config.yml');
|
|
419
|
-
await ensureExtensionInConfig(configPath, dest, 'combo', options);
|
|
392
|
+
await copySources(extDir, filesUnder('combo-toggle'), 'combo-toggle/index.ts', options);
|
|
420
393
|
}
|
|
421
|
-
async function resolveProfile(
|
|
422
|
-
// Seed from the
|
|
423
|
-
// user's configured defaults instead of resetting them to off.
|
|
394
|
+
async function resolveProfile(opts = {}) {
|
|
395
|
+
// Seed from the stored defaults so flag-less runs keep them.
|
|
424
396
|
const profile = await storedProfile();
|
|
425
|
-
//
|
|
426
|
-
//
|
|
427
|
-
|
|
428
|
-
// given, and never for --apply-update runs.
|
|
429
|
-
if (tty() && !profileFlagsGiven && !applyUpdate && (install || forceReinstall || reinstall)) {
|
|
397
|
+
// One numbered prompt for all three modes, for a real terminal user with no
|
|
398
|
+
// default flags. Never for --apply-update, and never for a script.
|
|
399
|
+
if (tty() && !profileFlagsGiven && !applyUpdate) {
|
|
430
400
|
const choice = await askInteractiveChoice('Session-start defaults — Combo preset', [
|
|
431
401
|
{ value: 'off', label: 'off' },
|
|
432
402
|
{ value: 'medium', label: 'medium', hint: 'caveman=lite, rtk=on, ponytail=lite' },
|
|
@@ -458,9 +428,7 @@ async function resolveProfile(forceReinstall = false, opts = {}) {
|
|
|
458
428
|
return profile;
|
|
459
429
|
}
|
|
460
430
|
let updatePromptDone = false;
|
|
461
|
-
// Bare `tersio` at a terminal
|
|
462
|
-
// update offer first (when pending), then a Clack menu over every command.
|
|
463
|
-
// Scripts, pipes, --yes, and --dry-run keep the old straight-to-install path.
|
|
431
|
+
// Bare `tersio` at a terminal picks a command; scripts and flags install.
|
|
464
432
|
async function runCommandMenu() {
|
|
465
433
|
printWelcome();
|
|
466
434
|
const newer = await checkForUpdate();
|
|
@@ -481,12 +449,10 @@ async function runCommandMenu() {
|
|
|
481
449
|
const choice = await askInteractiveChoice('Tersio — what next?', [
|
|
482
450
|
{ value: 'install', label: 'Install add-ons', hint: 'user scope + combo defaults' },
|
|
483
451
|
{ value: 'update', label: 'Update', hint: 'CLI version check, then refresh add-ons (RTK, Caveman rule, Ponytail)' },
|
|
484
|
-
{ value: 'reinstall', label: 'Reinstall', hint: 'clean and reinstall the add-ons, Ponytail package kept' },
|
|
485
452
|
{ value: 'doctor', label: 'Doctor', hint: 'verify the installation' },
|
|
486
453
|
{ value: 'usage', label: 'Usage', hint: 'token usage and savings report' },
|
|
487
454
|
{ value: 'dashboard', label: 'Dashboard', hint: 'open the report in your browser' },
|
|
488
455
|
{ value: 'reset', label: 'Reset statistics', hint: 'clear statistics; transcripts and RTK history stay' },
|
|
489
|
-
{ value: 'settings', label: 'Settings', hint: 'defaults: combo, caveman, rtk, ponytail, currency' },
|
|
490
456
|
{ value: 'uninstall', label: 'Uninstall', hint: 'remove tersio' },
|
|
491
457
|
], 'install');
|
|
492
458
|
if (choice.status !== 'selected') {
|
|
@@ -518,10 +484,6 @@ async function runCommandMenu() {
|
|
|
518
484
|
closeRL();
|
|
519
485
|
break;
|
|
520
486
|
}
|
|
521
|
-
case 'reinstall':
|
|
522
|
-
await runInstall({ reinstall: true });
|
|
523
|
-
closeRL();
|
|
524
|
-
break;
|
|
525
487
|
case 'doctor': {
|
|
526
488
|
const summary = await runDoctor();
|
|
527
489
|
if (!dryRun && summary.missing + summary.warn > 0) {
|
|
@@ -553,49 +515,123 @@ async function runCommandMenu() {
|
|
|
553
515
|
await runReset();
|
|
554
516
|
closeRL();
|
|
555
517
|
break;
|
|
556
|
-
case 'settings':
|
|
557
|
-
await runSettings();
|
|
558
|
-
break;
|
|
559
518
|
case 'uninstall':
|
|
560
519
|
await runUninstall();
|
|
561
520
|
closeRL();
|
|
562
521
|
break;
|
|
563
522
|
}
|
|
564
523
|
}
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
524
|
+
// Which host this run targets. --host pins it; a TTY user picks; scripts,
|
|
525
|
+
// pipes, --yes, and --dry-run keep the pre-multi-host default of Oh My Pi.
|
|
526
|
+
let targetHost = 'omp';
|
|
527
|
+
async function chooseHost(title, only) {
|
|
528
|
+
const hosts = detectHosts().filter((host) => !only || only.includes(host.id));
|
|
529
|
+
const choice = await askInteractiveChoice(title, hosts.map((host) => ({
|
|
530
|
+
value: host.id,
|
|
531
|
+
label: hostLabel(host),
|
|
532
|
+
hint: hostHint(host),
|
|
533
|
+
})), targetHost);
|
|
534
|
+
if (choice.status !== 'selected') {
|
|
535
|
+
closeRL();
|
|
536
|
+
process.exit(130);
|
|
537
|
+
}
|
|
538
|
+
return choice.value;
|
|
539
|
+
}
|
|
540
|
+
function hostEntry(id) {
|
|
541
|
+
return detectHosts().find((host) => host.id === id);
|
|
542
|
+
}
|
|
543
|
+
// pi auto-discovers `<agent-dir>/extensions/**/index.ts`, so this route writes
|
|
544
|
+
// the same tree as OMP's and registers nothing.
|
|
545
|
+
async function stepPiLayer(profile, options) {
|
|
546
|
+
const agentDir = piAgentDir();
|
|
547
|
+
const extDir = path.join(agentDir, 'extensions');
|
|
548
|
+
const declared = piTersioSource(agentDir);
|
|
549
|
+
if (declared)
|
|
550
|
+
console.log(` [note] pi also has a tersio package (${declared}) — remove one, or both copies load: pi remove npm:${PACKAGE_NAME}`);
|
|
551
|
+
if (!options.quiet)
|
|
552
|
+
console.log(` Pi — write the extension tree (${extDir})`);
|
|
553
|
+
const cavemanRule = await fetchCavemanRule(options);
|
|
554
|
+
await stepSharedSessionState(extDir, options);
|
|
555
|
+
// OMP loads rtk-session from its plugin manifest, so only the pi tree copies it.
|
|
556
|
+
await copySources(extDir, filesUnder('rtk-session'), 'rtk-session/index.ts', options);
|
|
557
|
+
await stepCaveman(extDir, cavemanRule, options);
|
|
558
|
+
await stepCombo(extDir, options);
|
|
559
|
+
await stepTersioCommands(extDir, options);
|
|
560
|
+
await stepUpdater(extDir, options);
|
|
561
|
+
await stepPonytailForPi(agentDir, options);
|
|
562
|
+
}
|
|
563
|
+
// Ponytail for pi: install the package, or copy the bundled skills offline.
|
|
564
|
+
async function stepPonytailForPi(agentDir, options) {
|
|
565
|
+
if (!options.quiet)
|
|
566
|
+
console.log(' Ponytail — ensure the pi package');
|
|
567
|
+
if (!options.dryRun) {
|
|
568
|
+
try {
|
|
569
|
+
await execNetwork('Installing Ponytail for pi', 'pi', ['install', 'npm:@dietrichgebert/ponytail'], { timeout: 300000 });
|
|
570
|
+
return;
|
|
571
|
+
}
|
|
572
|
+
catch (e) {
|
|
573
|
+
console.log(` [fail] pi install ponytail: ${shortError(e)}`);
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
const skills = findHoistedPackage('@dietrichgebert/ponytail', path.dirname(fileURLToPath(import.meta.url)), 'skills');
|
|
577
|
+
if (!skills) {
|
|
578
|
+
console.log(' [skip] Ponytail package not found next to the CLI — skills not copied');
|
|
579
|
+
console.log(' [hint] The /ponytail command will not work; run: pi install npm:@dietrichgebert/ponytail');
|
|
580
|
+
return;
|
|
581
|
+
}
|
|
582
|
+
const dest = path.join(agentDir, 'skills');
|
|
583
|
+
if (options.dryRun) {
|
|
584
|
+
if (verbose && !options.quiet)
|
|
585
|
+
console.log(` [dry-run] would copy Ponytail skills to ${dest}`);
|
|
586
|
+
return;
|
|
587
|
+
}
|
|
588
|
+
for (const entry of await fs.readdir(skills, { withFileTypes: true })) {
|
|
589
|
+
if (!entry.isDirectory())
|
|
590
|
+
continue;
|
|
591
|
+
const to = path.join(dest, entry.name);
|
|
592
|
+
await fs.rm(to, { recursive: true, force: true });
|
|
593
|
+
await fs.cp(path.join(skills, entry.name), to, { recursive: true });
|
|
594
|
+
if (!options.quiet)
|
|
595
|
+
console.log(` [write] skill ${entry.name} → ${to}`);
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
async function runInstall() {
|
|
599
|
+
// Without this guard a picked command re-enters here with command === null
|
|
600
|
+
// and loops straight back into the picker.
|
|
570
601
|
if (command === null && tty() && !yes && !updatePromptDone) {
|
|
571
602
|
await runCommandMenu();
|
|
572
603
|
return;
|
|
573
604
|
}
|
|
574
|
-
//
|
|
575
|
-
|
|
576
|
-
|
|
605
|
+
// Ask the host first: it decides the tree, the cleanup, and the defaults file.
|
|
606
|
+
let pinned;
|
|
607
|
+
try {
|
|
608
|
+
pinned = parseHostArg(args);
|
|
609
|
+
}
|
|
610
|
+
catch (e) {
|
|
611
|
+
console.error(`[fail] ${e.message}`);
|
|
612
|
+
process.exit(1);
|
|
613
|
+
}
|
|
614
|
+
if (pinned)
|
|
615
|
+
targetHost = pinned;
|
|
616
|
+
else if (tty() && !yes && !dryRun && !applyUpdate && command !== 'uninstall') {
|
|
617
|
+
targetHost = await chooseHost('Install Tersio into which agent?');
|
|
618
|
+
}
|
|
619
|
+
const host = hostEntry(targetHost);
|
|
620
|
+
// apply-update stays silent: the parent printed the plan.
|
|
577
621
|
const quiet = applyUpdate;
|
|
578
622
|
if (!quiet)
|
|
579
623
|
printWelcome();
|
|
580
|
-
if (isReinstall) {
|
|
581
|
-
// removeRtk stays false: reinstall is about to replace the binary, and
|
|
582
|
-
// deleting it first would leave nothing to wire if the fresh download
|
|
583
|
-
// fails (rate limit, offline). rtk.ts is removed here and re-wired below.
|
|
584
|
-
await runUninstall({ yes: true, removePonytail: false, removeRtk: false });
|
|
585
|
-
}
|
|
586
624
|
if (dryRun && !quiet)
|
|
587
625
|
console.log('Preview — no changes will be written.\n');
|
|
588
626
|
if (!quiet) {
|
|
589
627
|
console.log(`Installing Tersio v${PACKAGE_VERSION}`);
|
|
590
628
|
}
|
|
591
|
-
//
|
|
592
|
-
// the apply-update payload, which is itself an update run.
|
|
629
|
+
// Nudge about a newer release; silent without a TTY.
|
|
593
630
|
if (tty() && !applyUpdate && command !== 'uninstall') {
|
|
594
631
|
const newer = await checkForUpdate();
|
|
595
632
|
if (typeof newer === 'string') {
|
|
596
|
-
//
|
|
597
|
-
//
|
|
598
|
-
// update and stops here — the fresh binary owns what follows.
|
|
633
|
+
// Update pending: offer it here instead of burying it above the install
|
|
634
|
+
// prompts. Yes runs the full update and stops.
|
|
599
635
|
if (command === null && !dryRun && !yes && !updatePromptDone) {
|
|
600
636
|
const answer = await askInteractiveConfirm(`tersio ${newer} is available (installed ${PACKAGE_VERSION}). Install it now?`);
|
|
601
637
|
if (answer.status === 'confirmed' && answer.value) {
|
|
@@ -615,22 +651,20 @@ async function runInstall(overrides = {}) {
|
|
|
615
651
|
}
|
|
616
652
|
}
|
|
617
653
|
// Resolve session defaults: flags > interactive prompt > defaults.
|
|
618
|
-
const profile = await resolveProfile(
|
|
654
|
+
const profile = await resolveProfile({ quiet });
|
|
619
655
|
const userDir = OMP_AGENT_DIR;
|
|
620
656
|
const userExtDir = path.join(userDir, 'extensions');
|
|
621
|
-
// apply-update
|
|
622
|
-
|
|
623
|
-
// binary (always re-downloaded), and the Caveman rule (always re-fetched).
|
|
624
|
-
const options = { dryRun, verbose, yes, reinstall: isReinstall || applyUpdate, quiet };
|
|
657
|
+
// apply-update refreshes every add-on like a reinstall.
|
|
658
|
+
const installOptions = { dryRun, verbose, yes, reinstall: applyUpdate, quiet };
|
|
625
659
|
try {
|
|
626
660
|
const v = (await execP(OMP_BIN, ['--version'])).stdout.trim();
|
|
627
661
|
if (verbose && !quiet)
|
|
628
662
|
console.log(` omp ${v}`);
|
|
629
663
|
}
|
|
630
664
|
catch {
|
|
631
|
-
|
|
665
|
+
if (targetHost === 'omp')
|
|
666
|
+
console.log(' [fail] omp not found — ensure it\'s installed');
|
|
632
667
|
}
|
|
633
|
-
const cavemanRule = await fetchCavemanRule(options);
|
|
634
668
|
const failures = [];
|
|
635
669
|
const capture = async (label, work) => {
|
|
636
670
|
try {
|
|
@@ -641,20 +675,30 @@ async function runInstall(overrides = {}) {
|
|
|
641
675
|
console.log(` [fail] ${label}: ${shortError(e)}`);
|
|
642
676
|
}
|
|
643
677
|
};
|
|
644
|
-
|
|
678
|
+
if (targetHost === 'pi') {
|
|
679
|
+
await capture('pi tree', () => stepPiLayer(profile, installOptions));
|
|
680
|
+
await capture('rtk', () => stepRtk(BUN_BIN_DIR, installOptions, 'pi'));
|
|
681
|
+
await capture('settings', () => writePluginSettings(profile, installOptions));
|
|
682
|
+
if (failures.length > 0)
|
|
683
|
+
console.log(`\nDone with ${failures.length} failure(s) — see [fail] lines above.`);
|
|
684
|
+
else
|
|
685
|
+
console.log('\nDone — restart pi, then /combo medium.');
|
|
686
|
+
closeRL();
|
|
687
|
+
return;
|
|
688
|
+
}
|
|
689
|
+
const cavemanRule = await fetchCavemanRule(installOptions);
|
|
690
|
+
await capture('shared', () => stepSharedSessionState(userExtDir, installOptions));
|
|
645
691
|
let selfPlugin = false;
|
|
646
|
-
await capture('self-plugin', async () => { selfPlugin = await stepSelfPlugin(OMP_PLUGINS_DIR,
|
|
647
|
-
await capture('ponytail', () => stepPonytail(OMP_PLUGINS_DIR,
|
|
648
|
-
|
|
649
|
-
await capture('rtk', () =>
|
|
650
|
-
await capture('
|
|
651
|
-
await capture('
|
|
652
|
-
await capture('
|
|
653
|
-
await capture('
|
|
654
|
-
await capture('reinforcement', () => stepModeReinforcement(userExtDir, ponytailExtPath, options));
|
|
655
|
-
await capture('updater', () => stepUpdater(userExtDir, options));
|
|
692
|
+
await capture('self-plugin', async () => { selfPlugin = await stepSelfPlugin(OMP_PLUGINS_DIR, installOptions); });
|
|
693
|
+
await capture('ponytail', () => stepPonytail(OMP_PLUGINS_DIR, installOptions));
|
|
694
|
+
await capture('rtk', () => stepRtk(BUN_BIN_DIR, installOptions));
|
|
695
|
+
await capture('rtk session', () => stepRtkSession(userExtDir, installOptions));
|
|
696
|
+
await capture('caveman', () => stepCaveman(path.join(OMP_PLUGINS_DIR, 'node_modules', '@krtclcdy', 'tersio', 'extensions'), cavemanRule, installOptions));
|
|
697
|
+
await capture('combo', () => stepCombo(userExtDir, installOptions));
|
|
698
|
+
await capture('commands', () => stepTersioCommands(userExtDir, installOptions));
|
|
699
|
+
await capture('updater', () => stepUpdater(userExtDir, installOptions));
|
|
656
700
|
if (selfPlugin)
|
|
657
|
-
await capture('settings', () => writePluginSettings(profile,
|
|
701
|
+
await capture('settings', () => writePluginSettings(profile, installOptions));
|
|
658
702
|
if (quiet) {
|
|
659
703
|
if (failures.length > 0)
|
|
660
704
|
console.log(` add-ons: ${failures.length} failed (${failures.join(', ')}) — see [fail] lines above`);
|
package/cli/interactive.js
CHANGED
|
@@ -67,6 +67,32 @@ async function askInteractiveConfirm(message, initialValue = true) {
|
|
|
67
67
|
}
|
|
68
68
|
return { status: 'confirmed', value: answer };
|
|
69
69
|
}
|
|
70
|
+
// Confirm a destructive run: a Clack dialog at a terminal, a plain prompt in a
|
|
71
|
+
// pipe or script. False means the user declined or aborted.
|
|
72
|
+
async function confirmDestructive(message) {
|
|
73
|
+
if (tty()) {
|
|
74
|
+
closeRL();
|
|
75
|
+
const answer = await clackConfirm({ message, initialValue: false });
|
|
76
|
+
if (typeof answer !== 'boolean') {
|
|
77
|
+
clackCancel('Aborted.');
|
|
78
|
+
closeRL();
|
|
79
|
+
return false;
|
|
80
|
+
}
|
|
81
|
+
if (!answer) {
|
|
82
|
+
console.log('Aborted.');
|
|
83
|
+
closeRL();
|
|
84
|
+
return false;
|
|
85
|
+
}
|
|
86
|
+
return true;
|
|
87
|
+
}
|
|
88
|
+
const typed = await ask('\nProceed? [y/N]: ');
|
|
89
|
+
if (!typed.toLowerCase().startsWith('y')) {
|
|
90
|
+
console.log('Aborted.');
|
|
91
|
+
closeRL();
|
|
92
|
+
return false;
|
|
93
|
+
}
|
|
94
|
+
return true;
|
|
95
|
+
}
|
|
70
96
|
// Run collecting work under one TTY-only Clack task. Callers print after the
|
|
71
97
|
// task completes, keeping normal output out of the spinner animation.
|
|
72
98
|
async function runInteractivePhase(title, collect) {
|
|
@@ -80,4 +106,4 @@ async function runInteractivePhase(title, collect) {
|
|
|
80
106
|
async function execNetwork(label, cmd, args, opts = {}) {
|
|
81
107
|
return withInteractiveSpinner(label, () => execP(cmd, args, opts));
|
|
82
108
|
}
|
|
83
|
-
export { ask, closeRL, tty, withInteractiveSpinner, execNetwork, askInteractiveChoice, askInteractiveConfirm, runInteractivePhase, };
|
|
109
|
+
export { ask, closeRL, tty, withInteractiveSpinner, execNetwork, askInteractiveChoice, askInteractiveConfirm, confirmDestructive, runInteractivePhase, };
|
package/cli/layer.js
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
// cli/layer.ts — the shared shape of an extension tree.
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
// Turns a `[dir, label]` table into resolved paths under `extDir`.
|
|
4
|
+
export function layerEntries(extDir, table) {
|
|
5
|
+
return table.map(([dir, label]) => ({ label, path: path.join(extDir, dir) }));
|
|
6
|
+
}
|
|
7
|
+
// Directory names only, for callers that key off the names alone.
|
|
8
|
+
export function layerDirs(table) {
|
|
9
|
+
return table.map(([dir]) => dir);
|
|
10
|
+
}
|
|
11
|
+
// A port source sits two levels below extensions/; installed, it sits one level
|
|
12
|
+
// below <agent-dir>/extensions/. Rewriting on the way in keeps one spelling in
|
|
13
|
+
// the repo and the same other spelling in every installed tree.
|
|
14
|
+
export function rewritePortImports(content) {
|
|
15
|
+
return content.replace(/from '\.\.\/\.\.\/(shared|lib)\//g, "from '../$1/");
|
|
16
|
+
}
|
|
17
|
+
// What of the layer is on disk.
|
|
18
|
+
export function reportLayer(extDir, tables, exists) {
|
|
19
|
+
const all = tables.flatMap((table) => layerEntries(extDir, table));
|
|
20
|
+
return {
|
|
21
|
+
present: all.filter((entry) => exists(entry.path)),
|
|
22
|
+
missing: all.filter((entry) => !exists(entry.path)),
|
|
23
|
+
};
|
|
24
|
+
}
|
package/cli/manifest.js
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
// cli/manifest.ts — the one list of what an install writes into a host's
|
|
2
|
+
// extension tree. The installer, `doctor --fix extensions`, the doctor's
|
|
3
|
+
// per-host tree row, and the loadability test all read it, so none of them
|
|
4
|
+
// can drift from what actually ships.
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import { fileURLToPath } from 'node:url';
|
|
7
|
+
export const EXT_DIR = path.join(path.dirname(fileURLToPath(import.meta.url)), '..', 'extensions');
|
|
8
|
+
/** Every file in the tree, as paths relative to the extensions directory. */
|
|
9
|
+
export const TREE_FILES = [
|
|
10
|
+
'shared/host.ts',
|
|
11
|
+
'shared/session-state.ts',
|
|
12
|
+
'shared/types.ts',
|
|
13
|
+
'shared/plugin-settings.ts',
|
|
14
|
+
'shared/usage-ledger.ts',
|
|
15
|
+
'shared/pricing.ts',
|
|
16
|
+
'shared/carbon.ts',
|
|
17
|
+
'lib/utils.ts',
|
|
18
|
+
'caveman-session/index.ts',
|
|
19
|
+
'caveman-session/rule.md',
|
|
20
|
+
'rtk-session/index.ts',
|
|
21
|
+
'combo-toggle/index.ts',
|
|
22
|
+
'tersio-commands/index.ts',
|
|
23
|
+
'ai-addons-updater/index.ts',
|
|
24
|
+
];
|
|
25
|
+
export function sourcePath(relative) {
|
|
26
|
+
return path.join(EXT_DIR, ...relative.split('/'));
|
|
27
|
+
}
|
|
28
|
+
/** The entries under one directory, as source paths and tree-relative targets. */
|
|
29
|
+
export function filesUnder(dir) {
|
|
30
|
+
const found = [];
|
|
31
|
+
for (const file of TREE_FILES) {
|
|
32
|
+
if (file.startsWith(`${dir}/`))
|
|
33
|
+
found.push([sourcePath(file), file]);
|
|
34
|
+
}
|
|
35
|
+
return found;
|
|
36
|
+
}
|