@krtclcdy/tersio 2.23.0 → 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 +58 -45
- package/cli/agent-hosts.js +42 -0
- package/cli/agents.js +266 -0
- package/cli/common.js +16 -20
- package/cli/currency.js +8 -12
- package/cli/dashboard.js +49 -37
- package/cli/doctor-fix.js +28 -17
- package/cli/doctor.js +52 -41
- package/cli/hosts.js +118 -0
- package/cli/install.js +180 -117
- 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 +44 -46
- package/cli/reset.js +3 -26
- package/cli/rtk-wiring.js +10 -15
- package/cli/uninstall.js +135 -44
- package/cli/update.js +12 -19
- package/dashboard/dist/index.html +16 -16
- package/extensions/ai-addons-updater/index.js +31 -24
- package/extensions/ai-addons-updater/index.ts +32 -32
- package/extensions/caveman-session/index.js +6 -6
- package/extensions/caveman-session/index.ts +6 -6
- package/extensions/combo-toggle/index.js +42 -40
- package/extensions/combo-toggle/index.ts +36 -47
- package/extensions/lib/utils.js +36 -9
- package/extensions/lib/utils.ts +37 -9
- package/extensions/rtk-session/index.js +9 -9
- package/extensions/rtk-session/index.ts +9 -14
- 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 +43 -30
- package/extensions/shared/plugin-settings.ts +45 -26
- package/extensions/shared/pricing.js +26 -15
- package/extensions/shared/pricing.ts +28 -16
- package/extensions/shared/rtk-gain.js +11 -22
- package/extensions/shared/rtk-gain.ts +11 -22
- package/extensions/shared/session-state.js +21 -25
- package/extensions/shared/session-state.ts +22 -26
- package/extensions/shared/types.ts +10 -3
- package/extensions/shared/usage-ledger.js +24 -36
- package/extensions/shared/usage-ledger.ts +29 -50
- 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 +4 -2
- package/tersio.js +4 -4
- package/extensions/shared/mode-reinforcement.js +0 -26
package/cli/install.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
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';
|
|
@@ -13,31 +13,17 @@ import { runReset } from './reset.js';
|
|
|
13
13
|
import { runUsage } from './usage.js';
|
|
14
14
|
import { runDashboard } from './dashboard.js';
|
|
15
15
|
import { wireRtkOmp } from './rtk-wiring.js';
|
|
16
|
-
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';
|
|
17
17
|
import { storedProfile, writePluginSettings } from './profile.js';
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
const SHARED_SESSION_STATE = path.join(EXT_DIR, 'shared', 'session-state.ts');
|
|
21
|
-
const CAVEMAN_INDEX = path.join(EXT_DIR, 'caveman-session', 'index.ts');
|
|
22
|
-
const CAVEMAN_RULE = path.join(EXT_DIR, 'caveman-session', 'rule.md');
|
|
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 SHARED_TYPES = path.join(EXT_DIR, 'shared', 'types.ts');
|
|
28
|
-
const LIB_UTILS = path.join(EXT_DIR, 'lib', 'utils.ts');
|
|
29
|
-
const SHARED_PLUGIN_SETTINGS = path.join(EXT_DIR, 'shared', 'plugin-settings.ts');
|
|
30
|
-
const SHARED_USAGE_LEDGER = path.join(EXT_DIR, 'shared', 'usage-ledger.ts');
|
|
31
|
-
const SHARED_PRICING = path.join(EXT_DIR, 'shared', 'pricing.ts');
|
|
32
|
-
const SHARED_CARBON = path.join(EXT_DIR, 'shared', 'carbon.ts');
|
|
18
|
+
import { filesUnder, sourcePath } from './manifest.js';
|
|
19
|
+
import { detectHosts, hostHint, hostLabel, parseHostArg, piTersioSource } from './hosts.js';
|
|
33
20
|
async function stepPonytail(pluginsDir, options) {
|
|
34
21
|
if (!options.quiet)
|
|
35
22
|
console.log(' Ponytail — ensure bundled plugin');
|
|
36
23
|
await fs.mkdir(pluginsDir, { recursive: true });
|
|
37
24
|
const pkgPath = path.join(pluginsDir, 'package.json');
|
|
38
25
|
const pkg = await readPluginsPackage(pkgPath);
|
|
39
|
-
//
|
|
40
|
-
// dependency, so drop the legacy row. Runs after stepSelfPlugin.
|
|
26
|
+
// Drop the legacy Ponytail plugin row; it is a tersio dependency now.
|
|
41
27
|
let migrated = false;
|
|
42
28
|
if ('@dietrichgebert/ponytail' in pkg.dependencies) {
|
|
43
29
|
delete pkg.dependencies['@dietrichgebert/ponytail'];
|
|
@@ -56,9 +42,7 @@ async function stepPonytail(pluginsDir, options) {
|
|
|
56
42
|
}
|
|
57
43
|
const ponytailExtPath = path.join(pluginsDir, 'node_modules', '@dietrichgebert', 'ponytail', 'pi-extension', 'index.js');
|
|
58
44
|
const probeExt = async () => (await readTextIfExists(ponytailExtPath)) !== null;
|
|
59
|
-
//
|
|
60
|
-
// network. Migration always reinstalls: the probe may hit the stale legacy
|
|
61
|
-
// copy, and only npm install prunes it into the tersio-owned one.
|
|
45
|
+
// Skip the network unless a refresh was asked; migration always reinstalls.
|
|
62
46
|
let ponytailExtExists = !options.reinstall && !options.dryRun && !migrated ? await probeExt() : false;
|
|
63
47
|
if (ponytailExtExists) {
|
|
64
48
|
debug('Bundled Ponytail pi-extension already installed; skipping network refresh');
|
|
@@ -70,8 +54,7 @@ async function stepPonytail(pluginsDir, options) {
|
|
|
70
54
|
ponytailExtExists = true;
|
|
71
55
|
}
|
|
72
56
|
else {
|
|
73
|
-
//
|
|
74
|
-
// Reinstall here only when the probe still misses.
|
|
57
|
+
// Reinstall only when the self-plugin step did not materialize it.
|
|
75
58
|
try {
|
|
76
59
|
await execNetwork('Installing bundled Ponytail', 'npm', ['install', '--no-audit', '--no-fund'], { cwd: pluginsDir, timeout: 180000 });
|
|
77
60
|
}
|
|
@@ -97,9 +80,7 @@ async function stepPonytail(pluginsDir, options) {
|
|
|
97
80
|
// hideStatus=true keeps the upstream ponytail bar hidden; combo owns the bar.
|
|
98
81
|
await ensurePonytailConfigValue('hideStatus', true, options);
|
|
99
82
|
}
|
|
100
|
-
// Registers
|
|
101
|
-
// Settings → Plugins page (OMP enumerates plugins/package.json dependencies).
|
|
102
|
-
// Returns true when the package is verified in plugins/node_modules.
|
|
83
|
+
// Registers the package so OMP lists it under Settings → Plugins.
|
|
103
84
|
async function stepSelfPlugin(pluginsDir, options) {
|
|
104
85
|
if (!options.quiet)
|
|
105
86
|
console.log(' Tersio — register plugin');
|
|
@@ -242,17 +223,38 @@ async function extractRtkArchive(archivePath, extractDir) {
|
|
|
242
223
|
console.log(` [fail] Unknown archive format: ${archivePath}`);
|
|
243
224
|
return false;
|
|
244
225
|
}
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
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') {
|
|
248
229
|
const binDest = path.join(binDir, RTK_BINARY_NAME);
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
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.
|
|
252
254
|
if (options.dryRun) {
|
|
253
255
|
if (verbose && !options.quiet)
|
|
254
256
|
console.log(` [dry-run] would download rtk binary and install to ${binDest}`);
|
|
255
|
-
await
|
|
257
|
+
await bind(found ?? binDest);
|
|
256
258
|
return;
|
|
257
259
|
}
|
|
258
260
|
try {
|
|
@@ -306,20 +308,11 @@ async function stepRtk(binDir, options) {
|
|
|
306
308
|
console.log(` [fail] RTK: ${e.message}`);
|
|
307
309
|
console.log(' [hint] Manual: https://github.com/rtk-ai/rtk/releases');
|
|
308
310
|
}
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
return;
|
|
312
|
-
}
|
|
313
|
-
if (!(await fileExists(binDest))) {
|
|
314
|
-
console.log(' [skip] no rtk binary to wire — install rtk, then run: rtk init -g --agent omp');
|
|
315
|
-
return;
|
|
316
|
-
}
|
|
317
|
-
// Binary alone never meters OMP sessions — wire rtk's tool_call
|
|
318
|
-
// extension so bash commands rewrite to rtk and land in history.db.
|
|
319
|
-
await wireRtkOmp(binDest, options);
|
|
311
|
+
// Wire rtk's tool_call extension: the binary alone never meters a session.
|
|
312
|
+
await bind(binDest);
|
|
320
313
|
}
|
|
321
|
-
// Copy
|
|
322
|
-
//
|
|
314
|
+
// Copy sources into the target extension dir. First entry is required; the
|
|
315
|
+
// rest are optional companions.
|
|
323
316
|
async function copySources(extDir, files, skipLabel, options) {
|
|
324
317
|
const src = await readTextIfExists(files[0][0]);
|
|
325
318
|
if (!src) {
|
|
@@ -340,25 +333,18 @@ async function copySources(extDir, files, skipLabel, options) {
|
|
|
340
333
|
async function stepSharedSessionState(extDir, options) {
|
|
341
334
|
if (!options.quiet)
|
|
342
335
|
console.log(' Shared files — sync session bridge');
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
[SHARED_TYPES, path.join('shared', 'types.ts')],
|
|
346
|
-
[LIB_UTILS, path.join('lib', 'utils.ts')],
|
|
347
|
-
[SHARED_PLUGIN_SETTINGS, path.join('shared', 'plugin-settings.ts')],
|
|
348
|
-
[SHARED_USAGE_LEDGER, path.join('shared', 'usage-ledger.ts')],
|
|
349
|
-
[SHARED_PRICING, path.join('shared', 'pricing.ts')],
|
|
350
|
-
[SHARED_CARBON, path.join('shared', 'carbon.ts')],
|
|
351
|
-
], 'shared/session-state.js', options);
|
|
336
|
+
const shared = [...filesUnder('shared'), ...filesUnder('lib')];
|
|
337
|
+
await copySources(extDir, shared, 'shared/session-state.js', options);
|
|
352
338
|
}
|
|
353
339
|
async function stepRtkSession(extDir, options) {
|
|
354
340
|
if (!options.quiet)
|
|
355
341
|
console.log(' RTK session — install session mode');
|
|
356
|
-
await copySources(extDir,
|
|
342
|
+
await copySources(extDir, filesUnder('rtk-session'), 'rtk-session/index.ts', options);
|
|
357
343
|
}
|
|
358
|
-
// One
|
|
359
|
-
//
|
|
344
|
+
// One fetch serves the install; dry runs stay offline and preview the bundled
|
|
345
|
+
// rule's destination.
|
|
360
346
|
async function fetchCavemanRule(options) {
|
|
361
|
-
const bundled = await readTextIfExists(
|
|
347
|
+
const bundled = await readTextIfExists(sourcePath('caveman-session/rule.md'));
|
|
362
348
|
if (options.dryRun)
|
|
363
349
|
return bundled;
|
|
364
350
|
try {
|
|
@@ -390,30 +376,27 @@ async function stepCaveman(extDir, rule, options) {
|
|
|
390
376
|
else {
|
|
391
377
|
await writeIfChanged(ruleDest, rule, options);
|
|
392
378
|
}
|
|
393
|
-
await copySources(extDir,
|
|
379
|
+
await copySources(extDir, filesUnder('caveman-session').filter(([, to]) => to.endsWith('index.ts')), 'caveman-session/index.ts', options);
|
|
394
380
|
}
|
|
395
381
|
async function stepTersioCommands(extDir, options) {
|
|
396
382
|
if (!options.quiet)
|
|
397
383
|
console.log(' Tersio commands — install /tersio root command');
|
|
398
|
-
await copySources(extDir,
|
|
384
|
+
await copySources(extDir, filesUnder('tersio-commands'), 'tersio-commands/index.ts', options);
|
|
399
385
|
}
|
|
400
386
|
async function stepUpdater(extDir, options) {
|
|
401
|
-
await copySources(extDir,
|
|
387
|
+
await copySources(extDir, filesUnder('ai-addons-updater'), 'ai-addons-updater/index.ts', options);
|
|
402
388
|
}
|
|
403
389
|
async function stepCombo(extDir, options) {
|
|
404
390
|
if (!options.quiet)
|
|
405
391
|
console.log(' Combo — install preset switch');
|
|
406
|
-
await copySources(extDir,
|
|
392
|
+
await copySources(extDir, filesUnder('combo-toggle'), 'combo-toggle/index.ts', options);
|
|
407
393
|
}
|
|
408
|
-
async function resolveProfile(
|
|
409
|
-
// Seed from the
|
|
410
|
-
// 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.
|
|
411
396
|
const profile = await storedProfile();
|
|
412
|
-
//
|
|
413
|
-
//
|
|
414
|
-
|
|
415
|
-
// given, and never for --apply-update runs.
|
|
416
|
-
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) {
|
|
417
400
|
const choice = await askInteractiveChoice('Session-start defaults — Combo preset', [
|
|
418
401
|
{ value: 'off', label: 'off' },
|
|
419
402
|
{ value: 'medium', label: 'medium', hint: 'caveman=lite, rtk=on, ponytail=lite' },
|
|
@@ -445,9 +428,7 @@ async function resolveProfile(forceReinstall = false, opts = {}) {
|
|
|
445
428
|
return profile;
|
|
446
429
|
}
|
|
447
430
|
let updatePromptDone = false;
|
|
448
|
-
// Bare `tersio` at a terminal
|
|
449
|
-
// update offer first (when pending), then a Clack menu over every command.
|
|
450
|
-
// 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.
|
|
451
432
|
async function runCommandMenu() {
|
|
452
433
|
printWelcome();
|
|
453
434
|
const newer = await checkForUpdate();
|
|
@@ -468,7 +449,6 @@ async function runCommandMenu() {
|
|
|
468
449
|
const choice = await askInteractiveChoice('Tersio — what next?', [
|
|
469
450
|
{ value: 'install', label: 'Install add-ons', hint: 'user scope + combo defaults' },
|
|
470
451
|
{ value: 'update', label: 'Update', hint: 'CLI version check, then refresh add-ons (RTK, Caveman rule, Ponytail)' },
|
|
471
|
-
{ value: 'reinstall', label: 'Reinstall', hint: 'clean and reinstall the add-ons, Ponytail package kept' },
|
|
472
452
|
{ value: 'doctor', label: 'Doctor', hint: 'verify the installation' },
|
|
473
453
|
{ value: 'usage', label: 'Usage', hint: 'token usage and savings report' },
|
|
474
454
|
{ value: 'dashboard', label: 'Dashboard', hint: 'open the report in your browser' },
|
|
@@ -504,10 +484,6 @@ async function runCommandMenu() {
|
|
|
504
484
|
closeRL();
|
|
505
485
|
break;
|
|
506
486
|
}
|
|
507
|
-
case 'reinstall':
|
|
508
|
-
await runInstall({ reinstall: true });
|
|
509
|
-
closeRL();
|
|
510
|
-
break;
|
|
511
487
|
case 'doctor': {
|
|
512
488
|
const summary = await runDoctor();
|
|
513
489
|
if (!dryRun && summary.missing + summary.warn > 0) {
|
|
@@ -545,40 +521,117 @@ async function runCommandMenu() {
|
|
|
545
521
|
break;
|
|
546
522
|
}
|
|
547
523
|
}
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
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.
|
|
553
601
|
if (command === null && tty() && !yes && !updatePromptDone) {
|
|
554
602
|
await runCommandMenu();
|
|
555
603
|
return;
|
|
556
604
|
}
|
|
557
|
-
//
|
|
558
|
-
|
|
559
|
-
|
|
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.
|
|
560
621
|
const quiet = applyUpdate;
|
|
561
622
|
if (!quiet)
|
|
562
623
|
printWelcome();
|
|
563
|
-
if (isReinstall) {
|
|
564
|
-
// removeRtk stays false: reinstall is about to replace the binary, and
|
|
565
|
-
// deleting it first would leave nothing to wire if the fresh download
|
|
566
|
-
// fails (rate limit, offline). rtk.ts is removed here and re-wired below.
|
|
567
|
-
await runUninstall({ yes: true, removePonytail: false, removeRtk: false });
|
|
568
|
-
}
|
|
569
624
|
if (dryRun && !quiet)
|
|
570
625
|
console.log('Preview — no changes will be written.\n');
|
|
571
626
|
if (!quiet) {
|
|
572
627
|
console.log(`Installing Tersio v${PACKAGE_VERSION}`);
|
|
573
628
|
}
|
|
574
|
-
//
|
|
575
|
-
// the apply-update payload, which is itself an update run.
|
|
629
|
+
// Nudge about a newer release; silent without a TTY.
|
|
576
630
|
if (tty() && !applyUpdate && command !== 'uninstall') {
|
|
577
631
|
const newer = await checkForUpdate();
|
|
578
632
|
if (typeof newer === 'string') {
|
|
579
|
-
//
|
|
580
|
-
//
|
|
581
|
-
// 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.
|
|
582
635
|
if (command === null && !dryRun && !yes && !updatePromptDone) {
|
|
583
636
|
const answer = await askInteractiveConfirm(`tersio ${newer} is available (installed ${PACKAGE_VERSION}). Install it now?`);
|
|
584
637
|
if (answer.status === 'confirmed' && answer.value) {
|
|
@@ -598,22 +651,20 @@ async function runInstall(overrides = {}) {
|
|
|
598
651
|
}
|
|
599
652
|
}
|
|
600
653
|
// Resolve session defaults: flags > interactive prompt > defaults.
|
|
601
|
-
const profile = await resolveProfile(
|
|
654
|
+
const profile = await resolveProfile({ quiet });
|
|
602
655
|
const userDir = OMP_AGENT_DIR;
|
|
603
656
|
const userExtDir = path.join(userDir, 'extensions');
|
|
604
|
-
// apply-update
|
|
605
|
-
|
|
606
|
-
// binary (always re-downloaded), and the Caveman rule (always re-fetched).
|
|
607
|
-
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 };
|
|
608
659
|
try {
|
|
609
660
|
const v = (await execP(OMP_BIN, ['--version'])).stdout.trim();
|
|
610
661
|
if (verbose && !quiet)
|
|
611
662
|
console.log(` omp ${v}`);
|
|
612
663
|
}
|
|
613
664
|
catch {
|
|
614
|
-
|
|
665
|
+
if (targetHost === 'omp')
|
|
666
|
+
console.log(' [fail] omp not found — ensure it\'s installed');
|
|
615
667
|
}
|
|
616
|
-
const cavemanRule = await fetchCavemanRule(options);
|
|
617
668
|
const failures = [];
|
|
618
669
|
const capture = async (label, work) => {
|
|
619
670
|
try {
|
|
@@ -624,18 +675,30 @@ async function runInstall(overrides = {}) {
|
|
|
624
675
|
console.log(` [fail] ${label}: ${shortError(e)}`);
|
|
625
676
|
}
|
|
626
677
|
};
|
|
627
|
-
|
|
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));
|
|
628
691
|
let selfPlugin = false;
|
|
629
|
-
await capture('self-plugin', async () => { selfPlugin = await stepSelfPlugin(OMP_PLUGINS_DIR,
|
|
630
|
-
await capture('ponytail', () => stepPonytail(OMP_PLUGINS_DIR,
|
|
631
|
-
await capture('rtk', () => stepRtk(BUN_BIN_DIR,
|
|
632
|
-
await capture('rtk session', () => stepRtkSession(userExtDir,
|
|
633
|
-
await capture('caveman', () => stepCaveman(path.join(OMP_PLUGINS_DIR, 'node_modules', '@krtclcdy', 'tersio', 'extensions'), cavemanRule,
|
|
634
|
-
await capture('combo', () => stepCombo(userExtDir,
|
|
635
|
-
await capture('commands', () => stepTersioCommands(userExtDir,
|
|
636
|
-
await capture('updater', () => stepUpdater(userExtDir,
|
|
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));
|
|
637
700
|
if (selfPlugin)
|
|
638
|
-
await capture('settings', () => writePluginSettings(profile,
|
|
701
|
+
await capture('settings', () => writePluginSettings(profile, installOptions));
|
|
639
702
|
if (quiet) {
|
|
640
703
|
if (failures.length > 0)
|
|
641
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
|
+
}
|
package/cli/omp-layer.js
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
// The Oh My Pi layer as data: the extension directories, the RTK wiring and the Ponytail package, named in one place...
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { layerEntries, reportLayer } from './layer.js';
|
|
4
|
+
const INSTALLED_EXTENSIONS = [
|
|
5
|
+
['caveman-session', 'caveman session mode'],
|
|
6
|
+
['rtk-session', 'rtk session mode'],
|
|
7
|
+
['ai-addons-updater', 'add-on updater'],
|
|
8
|
+
['combo-toggle', 'combo preset switch'],
|
|
9
|
+
['tersio-commands', '/tersio command'],
|
|
10
|
+
['shared', 'session bridge, usage and pricing store'],
|
|
11
|
+
// Read by the add-on updater; the updater itself is removed above.
|
|
12
|
+
['lib', 'shared installer and updater helpers'],
|
|
13
|
+
];
|
|
14
|
+
const RETIRED_EXTENSIONS = [
|
|
15
|
+
// Legacy always-on combo helper; imports shared/session-state.js, so it breaks with a module-not-found warning once the...
|
|
16
|
+
['aaa-combo-boot', 'retired combo boot helper'],
|
|
17
|
+
];
|
|
18
|
+
// `rtkBinaryName` is passed in because it is platform-dependent and already resolved by the caller.
|
|
19
|
+
export function ompLayer(home, rtkBinaryName) {
|
|
20
|
+
const agentDir = path.join(home, '.omp', 'agent');
|
|
21
|
+
const extDir = path.join(agentDir, 'extensions');
|
|
22
|
+
const pluginsDir = path.join(home, '.omp', 'plugins');
|
|
23
|
+
return {
|
|
24
|
+
extDir,
|
|
25
|
+
configFile: path.join(agentDir, 'config.yml'),
|
|
26
|
+
pluginsDir,
|
|
27
|
+
ponytailPackage: path.join(pluginsDir, 'node_modules', '@dietrichgebert', 'ponytail'),
|
|
28
|
+
rtkBinary: path.join(home, '.bun', 'bin', rtkBinaryName),
|
|
29
|
+
rtkExtension: path.join(extDir, 'rtk.ts'),
|
|
30
|
+
installed: layerEntries(extDir, INSTALLED_EXTENSIONS),
|
|
31
|
+
retired: layerEntries(extDir, RETIRED_EXTENSIONS),
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
// Every extension directory the layer owns, installed and retired together.
|
|
35
|
+
export function ompExtensionTargets(layer) {
|
|
36
|
+
return [...layer.installed, ...layer.retired].map((entry) => entry.path);
|
|
37
|
+
}
|
|
38
|
+
export function tersioPluginDir(home) {
|
|
39
|
+
return path.join(home, '.omp', 'plugins', 'node_modules', '@krtclcdy', 'tersio');
|
|
40
|
+
}
|
|
41
|
+
export function reportOmpLayer(home, exists) {
|
|
42
|
+
const extDir = path.join(home, '.omp', 'agent', 'extensions');
|
|
43
|
+
const pluginPath = tersioPluginDir(home);
|
|
44
|
+
const { present, missing } = reportLayer(extDir, [INSTALLED_EXTENSIONS], exists);
|
|
45
|
+
return {
|
|
46
|
+
pluginPath: exists(pluginPath) ? pluginPath : null,
|
|
47
|
+
extensions: present,
|
|
48
|
+
missing,
|
|
49
|
+
installed: exists(pluginPath),
|
|
50
|
+
};
|
|
51
|
+
}
|