@krtclcdy/tersio 2.25.4 → 2.25.6
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 +27 -63
- package/dashboard/dist/index.html +18 -16
- package/dist/cli/backups.js +187 -0
- package/dist/cli/banner.js +4 -3
- package/dist/cli/common.js +29 -13
- package/dist/cli/currency.js +1 -1
- package/dist/cli/dashboard.js +168 -110
- package/dist/cli/doctor-fix.js +232 -50
- package/dist/cli/doctor.js +281 -32
- package/dist/cli/hosts.js +29 -9
- package/dist/cli/install.js +204 -69
- package/dist/cli/interactive.js +3 -3
- package/dist/cli/manifest.js +125 -17
- package/dist/cli/profile.js +14 -9
- package/dist/cli/rtk-wiring.js +1 -1
- package/dist/cli/settings.js +20 -5
- package/dist/cli/uninstall.js +24 -8
- package/dist/cli/update.js +9 -10
- package/dist/cli/usage.js +75 -7
- package/dist/extensions/{caveman-session → caveman}/index.js +4 -2
- package/{extensions/caveman-session → dist/extensions/caveman}/rule-megacave.md +5 -3
- package/{extensions/caveman-session → dist/extensions/caveman}/rule-ultra.md +4 -4
- package/dist/extensions/caveman/rule.md +89 -0
- package/dist/extensions/{combo-toggle → combo}/index.js +62 -21
- package/dist/extensions/{tersio-commands → commands}/index.js +2 -2
- package/dist/extensions/pstack/index.js +119 -0
- package/dist/extensions/pstack/rule.md +65 -0
- package/dist/extensions/{rtk-filter/index.js → rtk/filter.js} +1 -2
- package/dist/extensions/{rtk-session/index.js → rtk/session.js} +5 -5
- package/dist/extensions/{opencode/server.js → server.js} +98 -14
- package/dist/extensions/shared/agent-stats.js +1198 -0
- package/dist/extensions/shared/brand-marks.js +153 -0
- package/dist/extensions/shared/carbon.js +1 -1
- package/dist/extensions/shared/plugin-settings.js +12 -7
- package/dist/extensions/shared/pricing.js +86 -30
- package/dist/extensions/shared/rtk-gain.js +4 -9
- package/dist/extensions/shared/session-state.js +15 -10
- package/dist/extensions/shared/sqlite.js +168 -0
- package/dist/extensions/shared/status.js +8 -8
- package/dist/extensions/shared/types.js +1 -1
- package/dist/extensions/shared/usage-ledger.js +113 -39
- package/dist/extensions/shared/usage-store.js +191 -48
- package/dist/extensions/{lib → shared}/utils.js +6 -6
- package/dist/extensions/timing/index.js +31 -0
- package/dist/extensions/{ai-addons-updater → updater}/index.js +13 -6
- package/dist/probe-catalog.js +48 -0
- package/dist/probe-catalog188.js +44 -0
- package/dist/tersio.js +9 -4
- package/extensions/{caveman-session → caveman}/index.ts +4 -2
- package/{dist/extensions/caveman-session → extensions/caveman}/rule-megacave.md +5 -3
- package/{dist/extensions/caveman-session → extensions/caveman}/rule-ultra.md +4 -4
- package/extensions/caveman/rule.md +89 -0
- package/extensions/{combo-toggle → combo}/index.ts +62 -19
- package/extensions/{tersio-commands → commands}/index.ts +2 -2
- package/extensions/pstack/index.ts +128 -0
- package/extensions/pstack/rule.md +65 -0
- package/extensions/{rtk-filter/index.ts → rtk/filter.ts} +1 -2
- package/extensions/{rtk-session/index.ts → rtk/session.ts} +5 -5
- package/extensions/{opencode/server.ts → server.ts} +81 -16
- package/extensions/shared/agent-stats.ts +1605 -0
- package/extensions/shared/brand-marks.ts +163 -0
- package/extensions/shared/carbon.ts +1 -1
- package/extensions/shared/plugin-settings.ts +13 -7
- package/extensions/shared/pricing.ts +95 -32
- package/extensions/shared/rtk-gain.ts +4 -9
- package/extensions/shared/session-state.ts +18 -13
- package/extensions/shared/sqlite.ts +168 -0
- package/extensions/shared/status.ts +9 -8
- package/extensions/shared/types.ts +2 -1
- package/extensions/shared/usage-ledger.ts +134 -46
- package/extensions/shared/usage-store.ts +202 -50
- package/extensions/{lib → shared}/utils.ts +6 -6
- package/extensions/timing/index.ts +45 -0
- package/extensions/{ai-addons-updater → updater}/index.ts +15 -9
- package/package.json +27 -18
- package/dist/extensions/caveman-session/rule.md +0 -79
- package/extensions/caveman-session/rule.md +0 -79
package/dist/cli/install.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
// cli/install.ts — install flow and all setup steps.
|
|
2
|
-
import { promises as fs } from 'node:fs';
|
|
2
|
+
import { existsSync, 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, HOME, IS_WINDOWS, OMP_AGENT_DIR, OMP_PLUGINS_DIR, OMP_BIN, PACKAGE_NAME, PACKAGE_VERSION, RTK_BINARY_NAME, args, allowUnverified, applyUpdate, cavemanDefaultFlag, comboDefaultFlag, command, dryRun, ponytailDefaultFlag, profileFlagsGiven, rtkDefaultFlag, verbose, yes, dashboardExport, dashboardPort, debug, ensurePonytailConfigValue, execP, parseJsonObject, readPluginsPackage, writeIfChanged, } from './common.js';
|
|
6
|
+
import { BUN_BIN_DIR, COMBO_PRESET_MODES, HOME, IS_WINDOWS, OMP_AGENT_DIR, OMP_PLUGINS_DIR, OMP_BIN, PACKAGE_NAME, PACKAGE_VERSION, RTK_BINARY_NAME, args, allowUnverified, applyUpdate, bundledPonytailSpec, cavemanDefaultFlag, comboDefaultFlag, command, dryRun, ponytailDefaultFlag, pstackDefaultFlag, profileFlagsGiven, rtkDefaultFlag, verbose, yes, dashboardExport, dashboardPort, currency, debug, ensurePonytailConfigValue, execP, parseJsonObject, readPluginsPackage, writeIfChanged, } from './common.js';
|
|
7
7
|
import { askInteractiveChoice, askInteractiveConfirm, closeRL, execNetwork, tty, withInteractiveSpinner, withInteractiveTask, sayTagged } from './interactive.js';
|
|
8
8
|
import { printWelcome } from './banner.js';
|
|
9
9
|
import { checkForUpdate, runLatestUpdate } from './update.js';
|
|
@@ -13,11 +13,11 @@ import { runReset } from './reset.js';
|
|
|
13
13
|
import { runUsage } from './usage.js';
|
|
14
14
|
import { runDashboard } from './dashboard.js';
|
|
15
15
|
import { wireRtkOmp, wireRtkOpencode, wireRtkPi } from './rtk-wiring.js';
|
|
16
|
-
import { CAVEMAN_REMOTE_ULTRA, CAVEMAN_REMOTE_MEGACAVE, RTK_RELEASE_API, fetchJson, findFile, findHoistedPackage, httpsGet, findPackageRoot, httpsDownload, parseChecksum, piAgentDir, readTextIfExists, resolveRtkBinary, ruleBodyProblem, rtkPlatformSpec, sha256File, } from '../extensions/
|
|
16
|
+
import { CAVEMAN_REMOTE_ULTRA, CAVEMAN_REMOTE_MEGACAVE, RTK_RELEASE_API, fetchJson, findFile, findHoistedPackage, httpsGet, findPackageRoot, httpsDownload, parseChecksum, piAgentDir, readTextIfExists, resolveRtkBinary, ruleBodyProblem, rtkPlatformSpec, sha256File, } from '../extensions/shared/utils.js';
|
|
17
17
|
import { formatCliStatus, storedProfile, storedProfileSync, writePluginSettings } from './profile.js';
|
|
18
18
|
import { runSettings } from './settings.js';
|
|
19
19
|
import { tersioSettingsFile } from '../extensions/shared/plugin-settings.js';
|
|
20
|
-
import {
|
|
20
|
+
import { PI_RTK_MANIFEST, PI_PONYTAIL_FILES, PONYTAIL_BUNDLE_FILES, filesUnder, rootFiles, sourcePath, syncPluginManifest, syncTreeFiles } from './manifest.js';
|
|
21
21
|
import { detectHosts, hostHint, hostLabel, parseHostArg, piTersioSource } from './hosts.js';
|
|
22
22
|
async function stepPonytail(pluginsDir, options) {
|
|
23
23
|
if (!options.quiet)
|
|
@@ -25,22 +25,44 @@ async function stepPonytail(pluginsDir, options) {
|
|
|
25
25
|
await fs.mkdir(pluginsDir, { recursive: true });
|
|
26
26
|
const pkgPath = path.join(pluginsDir, 'package.json');
|
|
27
27
|
const pkg = await readPluginsPackage(pkgPath);
|
|
28
|
-
//
|
|
28
|
+
// npm resolves the *published* tersio, so a plain dependency entry lets npm nest the range's version.
|
|
29
29
|
let migrated = false;
|
|
30
|
-
|
|
30
|
+
const localSpec = await bundledPonytailSpec();
|
|
31
|
+
const declared = pkg.dependencies['@dietrichgebert/ponytail'];
|
|
32
|
+
// Node resolves the nested copy first, so a dependency entry alone cannot hold the local version.
|
|
33
|
+
const rawOverrides = typeof pkg.overrides === 'object' && pkg.overrides !== null ? { ...pkg.overrides } : {};
|
|
34
|
+
const overrideNeeded = localSpec !== null && rawOverrides['@dietrichgebert/ponytail'] !== localSpec;
|
|
35
|
+
if (localSpec !== null && declared !== localSpec) {
|
|
36
|
+
pkg.dependencies['@dietrichgebert/ponytail'] = localSpec;
|
|
37
|
+
migrated = true;
|
|
38
|
+
}
|
|
39
|
+
else if (localSpec === null && declared) {
|
|
31
40
|
delete pkg.dependencies['@dietrichgebert/ponytail'];
|
|
32
41
|
migrated = true;
|
|
33
42
|
}
|
|
43
|
+
if (localSpec === null) {
|
|
44
|
+
// No bundled spec means no tersio-owned row, so an override would resurrect the dependency.
|
|
45
|
+
delete rawOverrides['@dietrichgebert/ponytail'];
|
|
46
|
+
}
|
|
47
|
+
else if (overrideNeeded) {
|
|
48
|
+
rawOverrides['@dietrichgebert/ponytail'] = localSpec;
|
|
49
|
+
}
|
|
50
|
+
if (Object.keys(rawOverrides).length === 0)
|
|
51
|
+
delete pkg.overrides;
|
|
52
|
+
else if (JSON.stringify(rawOverrides) !== JSON.stringify(typeof pkg.overrides === 'object' && pkg.overrides !== null ? pkg.overrides : {})) {
|
|
53
|
+
pkg.overrides = rawOverrides;
|
|
54
|
+
migrated = true;
|
|
55
|
+
}
|
|
34
56
|
if (options.dryRun) {
|
|
35
57
|
if (verbose && !options.quiet)
|
|
36
58
|
sayTagged(` [dry-run] would write ${pkgPath}`);
|
|
37
59
|
if (migrated && verbose && !options.quiet)
|
|
38
|
-
sayTagged(` [dry-run] would
|
|
60
|
+
sayTagged(` [dry-run] would set @dietrichgebert/ponytail → ${localSpec ?? 'this tree\x27s range'} (bundled with tersio)`);
|
|
39
61
|
}
|
|
40
62
|
else if (migrated) {
|
|
41
63
|
await fs.writeFile(pkgPath, JSON.stringify(pkg, null, 2) + '\n');
|
|
42
64
|
if (!options.quiet)
|
|
43
|
-
console.log(
|
|
65
|
+
console.log(` [migrate] set @dietrichgebert/ponytail → ${pkg.dependencies['@dietrichgebert/ponytail'] ?? 'this tree\x27s range'} (bundled with tersio)`);
|
|
44
66
|
}
|
|
45
67
|
const ponytailExtPath = path.join(pluginsDir, 'node_modules', '@dietrichgebert', 'ponytail', 'pi-extension', 'index.js');
|
|
46
68
|
const probeExt = async () => (await readTextIfExists(ponytailExtPath)) !== null;
|
|
@@ -50,7 +72,7 @@ async function stepPonytail(pluginsDir, options) {
|
|
|
50
72
|
debug('Bundled Ponytail pi-extension already installed; skipping network refresh');
|
|
51
73
|
}
|
|
52
74
|
else if (options.dryRun) {
|
|
53
|
-
//
|
|
75
|
+
// Pretend the extension is present so the wiring below still runs, without touching the network.
|
|
54
76
|
if (verbose && !options.quiet)
|
|
55
77
|
console.log(' [dry-run] would run: npm install --no-audit --no-fund (in plugins dir)');
|
|
56
78
|
ponytailExtExists = true;
|
|
@@ -163,7 +185,7 @@ async function downloadRtkChecksums(release) {
|
|
|
163
185
|
return null;
|
|
164
186
|
}
|
|
165
187
|
}
|
|
166
|
-
// Fails closed:
|
|
188
|
+
// Fails closed: an unverifiable RTK binary runs with the user's privileges. `--allow-unverified` opts out.
|
|
167
189
|
async function verifyRtkArchive(archivePath, assetName, checksumsText) {
|
|
168
190
|
const unverified = (reason) => {
|
|
169
191
|
if (allowUnverified) {
|
|
@@ -229,6 +251,34 @@ async function extractRtkArchive(archivePath, extractDir) {
|
|
|
229
251
|
sayTagged(` [fail] Unknown archive format: ${archivePath}`);
|
|
230
252
|
return false;
|
|
231
253
|
}
|
|
254
|
+
// A rule body found in a retired copy still resolves and loads twice, so drop the agent tree's extras.
|
|
255
|
+
const RETIRED_OMP_AGENT_DIRS = [
|
|
256
|
+
'caveman-session',
|
|
257
|
+
'rtk-session',
|
|
258
|
+
'rtk-filter',
|
|
259
|
+
'combo-toggle',
|
|
260
|
+
'tersio-commands',
|
|
261
|
+
'ai-addons-updater',
|
|
262
|
+
'shared',
|
|
263
|
+
'lib',
|
|
264
|
+
];
|
|
265
|
+
/** Drop the copies an older OMP install left in the agent tree, keeping rtk's `rtk.ts`. */
|
|
266
|
+
async function stepRetireAgentCopies(extDir, options) {
|
|
267
|
+
const stale = RETIRED_OMP_AGENT_DIRS.filter((dir) => existsSync(path.join(extDir, dir)));
|
|
268
|
+
if (stale.length === 0) {
|
|
269
|
+
if (!options.quiet)
|
|
270
|
+
console.log(' Retired agent copies — none');
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
if (!options.quiet)
|
|
274
|
+
console.log(` Retired agent copies — remove ${stale.join(', ')}`);
|
|
275
|
+
if (options.dryRun) {
|
|
276
|
+
if (verbose)
|
|
277
|
+
sayTagged(` [dry-run] would remove ${stale.map((d) => path.join(extDir, d)).join(', ')}`);
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
await Promise.all(stale.map((dir) => fs.rm(path.join(extDir, dir), { recursive: true, force: true })));
|
|
281
|
+
}
|
|
232
282
|
// The binary is machine-wide; hosts without a hook only rebind. `tersio update` still refreshes it.
|
|
233
283
|
async function stepRtk(binDir, options, target = 'omp') {
|
|
234
284
|
const binDest = path.join(binDir, RTK_BINARY_NAME);
|
|
@@ -257,7 +307,7 @@ async function stepRtk(binDir, options, target = 'omp') {
|
|
|
257
307
|
}
|
|
258
308
|
if (!options.quiet)
|
|
259
309
|
console.log(` RTK — download binary and wire into ${hostName}`);
|
|
260
|
-
// A failed download must not skip wiring: a pre-existing binary serves the host hook
|
|
310
|
+
// A failed download must not skip wiring: a pre-existing binary serves the host hook. Dry runs stay offline.
|
|
261
311
|
if (options.dryRun) {
|
|
262
312
|
if (verbose && !options.quiet)
|
|
263
313
|
sayTagged(` [dry-run] would download rtk binary and install to ${binDest}`);
|
|
@@ -268,7 +318,7 @@ async function stepRtk(binDir, options, target = 'omp') {
|
|
|
268
318
|
const triple = resolveRtkTriple();
|
|
269
319
|
if (!triple)
|
|
270
320
|
return;
|
|
271
|
-
// One task with streamed sub-steps
|
|
321
|
+
// One task with streamed sub-steps; non-TTY keeps the plain lines each step already prints.
|
|
272
322
|
await withInteractiveTask('Installing RTK', async (update) => {
|
|
273
323
|
const release = await fetchJson(RTK_RELEASE_API);
|
|
274
324
|
const asset = findRtkAsset(release, triple);
|
|
@@ -327,7 +377,7 @@ async function stepRtk(binDir, options, target = 'omp') {
|
|
|
327
377
|
// Wire rtk's tool_call extension: the binary alone never meters a session.
|
|
328
378
|
await bind(binDest);
|
|
329
379
|
}
|
|
330
|
-
//
|
|
380
|
+
// First entry is required; the rest are optional companions.
|
|
331
381
|
async function copySources(extDir, files, skipLabel, options) {
|
|
332
382
|
const src = await readTextIfExists(files[0][0]);
|
|
333
383
|
if (!src) {
|
|
@@ -348,22 +398,20 @@ async function copySources(extDir, files, skipLabel, options) {
|
|
|
348
398
|
async function stepSharedSessionState(extDir, options) {
|
|
349
399
|
if (!options.quiet)
|
|
350
400
|
console.log(' Shared files — sync session bridge');
|
|
351
|
-
|
|
352
|
-
await copySources(extDir, shared, 'shared/session-state.js', options);
|
|
401
|
+
await copySources(extDir, filesUnder('shared'), 'shared/session-state.js', options);
|
|
353
402
|
}
|
|
354
403
|
async function stepRtkSession(extDir, options) {
|
|
355
404
|
if (!options.quiet)
|
|
356
405
|
console.log(' RTK session — install session mode');
|
|
357
|
-
await copySources(extDir, filesUnder('rtk
|
|
406
|
+
await copySources(extDir, filesUnder('rtk'), 'rtk/session.ts', options);
|
|
358
407
|
}
|
|
359
408
|
// pi's built-in grep and glob never reach the bash hook, so this one filters their output.
|
|
360
409
|
async function stepRtkFilter(extDir, options) {
|
|
361
410
|
if (!options.quiet)
|
|
362
411
|
console.log(' RTK filter — install built-in tool output filtering');
|
|
363
|
-
await copySources(extDir, filesUnder('rtk
|
|
412
|
+
await copySources(extDir, filesUnder('rtk'), 'rtk/filter.ts', options);
|
|
364
413
|
}
|
|
365
|
-
// rule.md
|
|
366
|
-
// rule.md is a deliberate fork of upstream skills/caveman/SKILL.md; a blind sync would drop the mode switch line and the STE100 rules.
|
|
414
|
+
// rule.md is a deliberate fork of skills/caveman/SKILL.md: a blind sync would drop the mode switch line.
|
|
367
415
|
const CAVEMAN_RULES = [
|
|
368
416
|
['rule.md', null],
|
|
369
417
|
['rule-ultra.md', CAVEMAN_REMOTE_ULTRA],
|
|
@@ -372,11 +420,11 @@ const CAVEMAN_RULES = [
|
|
|
372
420
|
async function stepCaveman(extDir, options) {
|
|
373
421
|
if (!options.quiet)
|
|
374
422
|
console.log(' Caveman — install session mode and rule bodies');
|
|
375
|
-
const cavemanDir = path.join(extDir, 'caveman
|
|
423
|
+
const cavemanDir = path.join(extDir, 'caveman');
|
|
376
424
|
if (!options.dryRun)
|
|
377
425
|
await fs.mkdir(cavemanDir, { recursive: true });
|
|
378
426
|
for (const [name, remote] of CAVEMAN_RULES) {
|
|
379
|
-
const bundled = await readTextIfExists(sourcePath(`caveman
|
|
427
|
+
const bundled = await readTextIfExists(sourcePath(`caveman/${name}`));
|
|
380
428
|
let rule = bundled;
|
|
381
429
|
if (remote && !options.dryRun) {
|
|
382
430
|
try {
|
|
@@ -405,7 +453,7 @@ async function stepCaveman(extDir, options) {
|
|
|
405
453
|
await writeIfChanged(path.join(cavemanDir, name), rule, options);
|
|
406
454
|
}
|
|
407
455
|
}
|
|
408
|
-
await copySources(extDir, filesUnder('caveman
|
|
456
|
+
await copySources(extDir, filesUnder('caveman').filter(([, to]) => to.endsWith('index.ts')), 'caveman/index.ts', options);
|
|
409
457
|
// Renamed body: drop the stale copy an older install left behind.
|
|
410
458
|
if (!options.dryRun)
|
|
411
459
|
await fs.rm(path.join(cavemanDir, 'rule-wenyan.md'), { force: true });
|
|
@@ -413,22 +461,31 @@ async function stepCaveman(extDir, options) {
|
|
|
413
461
|
async function stepTersioCommands(extDir, options) {
|
|
414
462
|
if (!options.quiet)
|
|
415
463
|
console.log(' Tersio commands — install /tersio root command');
|
|
416
|
-
await copySources(extDir, filesUnder('
|
|
464
|
+
await copySources(extDir, filesUnder('commands'), 'commands/index.ts', options);
|
|
417
465
|
}
|
|
418
466
|
async function stepUpdater(extDir, options) {
|
|
419
|
-
await copySources(extDir, filesUnder('
|
|
467
|
+
await copySources(extDir, filesUnder('updater'), 'updater/index.ts', options);
|
|
468
|
+
}
|
|
469
|
+
// omp loads no OpenCode entry, but its package ships one: the repair writes it, so the install does too.
|
|
470
|
+
async function stepPluginEntry(extDir, options) {
|
|
471
|
+
await copySources(extDir, rootFiles(), 'server.ts', options);
|
|
472
|
+
}
|
|
473
|
+
// pi records no first-token latency, so this stamps one into the row the dashboard reads.
|
|
474
|
+
async function stepTiming(extDir, options) {
|
|
475
|
+
await copySources(extDir, filesUnder('timing'), 'timing/index.ts', options);
|
|
420
476
|
}
|
|
421
477
|
async function stepCombo(extDir, options) {
|
|
422
478
|
if (!options.quiet)
|
|
423
479
|
console.log(' Combo — install preset switch');
|
|
424
|
-
await copySources(extDir, filesUnder('combo
|
|
480
|
+
await copySources(extDir, filesUnder('combo'), 'combo/index.ts', options);
|
|
425
481
|
}
|
|
426
|
-
// The
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
'
|
|
431
|
-
|
|
482
|
+
// The body is a Tersio fork of an upstream skill, so the install copies it and never fetches it.
|
|
483
|
+
async function stepPstack(extDir, options) {
|
|
484
|
+
if (!options.quiet)
|
|
485
|
+
console.log(' pstack — install the rigor mode');
|
|
486
|
+
await copySources(extDir, filesUnder('pstack'), 'pstack/index.ts', options);
|
|
487
|
+
}
|
|
488
|
+
// The tree ships no node_modules, so the ruleset rides along as files. Both hosts need the same six.
|
|
432
489
|
async function stepPonytailBundle(treeDir, options) {
|
|
433
490
|
const root = findHoistedPackage('@dietrichgebert/ponytail', findPackageRoot(path.dirname(fileURLToPath(import.meta.url))));
|
|
434
491
|
if (!root) {
|
|
@@ -456,19 +513,20 @@ async function stepOpencode(options) {
|
|
|
456
513
|
const dir = path.join(HOME, '.config', 'opencode', 'plugins', 'tersio');
|
|
457
514
|
if (!options.quiet)
|
|
458
515
|
console.log(` OpenCode — write the plugin tree (${dir})`);
|
|
516
|
+
// Retire the `-session`/`-toggle` leftovers first: a rule body found in one still resolves.
|
|
517
|
+
await stepRetireAgentCopies(dir, options);
|
|
459
518
|
await stepSharedSessionState(dir, options);
|
|
460
|
-
await copySources(dir, filesUnder('rtk
|
|
519
|
+
await copySources(dir, filesUnder('rtk'), 'rtk/session.ts', options);
|
|
461
520
|
await stepRtkFilter(dir, options);
|
|
462
|
-
await
|
|
521
|
+
await stepTiming(dir, options);
|
|
522
|
+
await copySources(dir, filesUnder('caveman'), 'caveman/index.ts', options);
|
|
463
523
|
await stepCombo(dir, options);
|
|
464
|
-
await
|
|
524
|
+
await stepPstack(dir, options);
|
|
525
|
+
await copySources(dir, filesUnder('commands'), 'commands/index.ts', options);
|
|
465
526
|
await stepUpdater(dir, options);
|
|
466
|
-
await copySources(dir, filesUnder('opencode'), 'opencode/server.ts', options);
|
|
467
527
|
await stepPonytailBundle(dir, options);
|
|
468
|
-
// The loader resolves <pluginDir>/server.ts,
|
|
469
|
-
await
|
|
470
|
-
if (!options.dryRun)
|
|
471
|
-
await fs.rm(path.join(dir, 'opencode', 'index.ts'), { force: true }).catch(() => { });
|
|
528
|
+
// The loader resolves <pluginDir>/server.ts, and the entry is that file: copy it in place.
|
|
529
|
+
await copySources(dir, rootFiles(), 'server.ts', options);
|
|
472
530
|
const configPath = path.join(HOME, '.config', 'opencode', 'opencode.json');
|
|
473
531
|
let config = {};
|
|
474
532
|
try {
|
|
@@ -519,6 +577,7 @@ async function resolveProfile(opts = {}) {
|
|
|
519
577
|
profile.cavemanDefault = preset.caveman;
|
|
520
578
|
profile.rtkDefault = preset.rtk;
|
|
521
579
|
profile.ponytailDefault = preset.ponytail;
|
|
580
|
+
profile.pstackDefault = preset.pstack;
|
|
522
581
|
}
|
|
523
582
|
if (cavemanDefaultFlag !== undefined)
|
|
524
583
|
profile.cavemanDefault = cavemanDefaultFlag;
|
|
@@ -526,8 +585,10 @@ async function resolveProfile(opts = {}) {
|
|
|
526
585
|
profile.rtkDefault = rtkDefaultFlag === 'on';
|
|
527
586
|
if (ponytailDefaultFlag !== undefined)
|
|
528
587
|
profile.ponytailDefault = ponytailDefaultFlag;
|
|
588
|
+
if (pstackDefaultFlag !== undefined)
|
|
589
|
+
profile.pstackDefault = pstackDefaultFlag;
|
|
529
590
|
if ((!opts.quiet && verbose) || dryRun)
|
|
530
|
-
console.log(` Defaults: combo=${profile.comboDefault} (caveman=${profile.cavemanDefault} · rtk=${profile.rtkDefault ? 'on' : 'off'} · ponytail=${profile.ponytailDefault})`);
|
|
591
|
+
console.log(` Defaults: combo=${profile.comboDefault} (caveman=${profile.cavemanDefault} · rtk=${profile.rtkDefault ? 'on' : 'off'} · ponytail=${profile.ponytailDefault} · pstack=${profile.pstackDefault})`);
|
|
531
592
|
return profile;
|
|
532
593
|
}
|
|
533
594
|
let updatePromptDone = false;
|
|
@@ -552,6 +613,7 @@ async function runCommandMenu() {
|
|
|
552
613
|
updatePromptDone = true;
|
|
553
614
|
const choice = await askInteractiveChoice(`Tersio — what next?\n${formatCliStatus(storedProfileSync())}`, [
|
|
554
615
|
{ value: 'install', label: 'Install add-ons', hint: 'user scope + combo defaults' },
|
|
616
|
+
{ value: 'reinstall', label: 'Reinstall', hint: 're-install Tersio for the ai agents, then verify' },
|
|
555
617
|
{ value: 'update', label: 'Update', hint: 'CLI version check, then refresh add-ons (RTK, Caveman rule, Ponytail)' },
|
|
556
618
|
{ value: 'doctor', label: 'Doctor', hint: 'verify the installation' },
|
|
557
619
|
{ value: 'settings', label: 'Settings', hint: 'default combo mode, currency, diagnosis schedule' },
|
|
@@ -568,8 +630,11 @@ async function runCommandMenu() {
|
|
|
568
630
|
case 'install':
|
|
569
631
|
await runInstall();
|
|
570
632
|
break;
|
|
633
|
+
case 'reinstall':
|
|
634
|
+
await runReinstall();
|
|
635
|
+
break;
|
|
571
636
|
case 'update': {
|
|
572
|
-
//
|
|
637
|
+
// Version check already ran above, so report it and offer the refresh here — no second "update" quiz.
|
|
573
638
|
if (typeof newer === 'string')
|
|
574
639
|
console.log(` tersio ${newer} available (installed ${PACKAGE_VERSION})`);
|
|
575
640
|
else
|
|
@@ -616,7 +681,7 @@ async function runCommandMenu() {
|
|
|
616
681
|
closeRL();
|
|
617
682
|
break;
|
|
618
683
|
case 'dashboard':
|
|
619
|
-
await runDashboard({ port: dashboardPort, open: true, exportFile: dashboardExport });
|
|
684
|
+
await runDashboard({ port: dashboardPort, open: true, exportFile: dashboardExport, currency });
|
|
620
685
|
closeRL();
|
|
621
686
|
break;
|
|
622
687
|
case 'reset':
|
|
@@ -629,9 +694,57 @@ async function runCommandMenu() {
|
|
|
629
694
|
break;
|
|
630
695
|
}
|
|
631
696
|
}
|
|
632
|
-
// Which host this run targets. --host pins it;
|
|
697
|
+
// Which host this run targets. --host pins it; scripts and --yes keep the default of Oh My Pi.
|
|
633
698
|
let targetHost = 'omp';
|
|
634
|
-
//
|
|
699
|
+
// A reinstall re-runs the npm fast path and the Ponytail probe instead of reusing what is on disk.
|
|
700
|
+
let forceReinstall = false;
|
|
701
|
+
/** Reinstall one agent: pick it from what is installed now, then run the full install with a refresh. */
|
|
702
|
+
async function runReinstall() {
|
|
703
|
+
let pinned;
|
|
704
|
+
try {
|
|
705
|
+
pinned = parseHostArg(args);
|
|
706
|
+
}
|
|
707
|
+
catch (e) {
|
|
708
|
+
console.error(`[fail] ${e.message}`);
|
|
709
|
+
process.exit(1);
|
|
710
|
+
}
|
|
711
|
+
const installed = detectHosts().filter((host) => host.installed);
|
|
712
|
+
let host;
|
|
713
|
+
if (pinned)
|
|
714
|
+
host = pinned;
|
|
715
|
+
else if (installed.length === 0) {
|
|
716
|
+
console.log(' Nothing to reinstall — no agent has Tersio installed.\n Run `tersio install`, or pick it from the menu.');
|
|
717
|
+
closeRL();
|
|
718
|
+
return;
|
|
719
|
+
}
|
|
720
|
+
else if (installed.length === 1 || !tty() || yes || dryRun) {
|
|
721
|
+
host = installed[0].id;
|
|
722
|
+
}
|
|
723
|
+
else {
|
|
724
|
+
const options = installed.map((entry) => ({
|
|
725
|
+
value: entry.id,
|
|
726
|
+
label: hostLabel(entry),
|
|
727
|
+
hint: hostHint(entry),
|
|
728
|
+
}));
|
|
729
|
+
const picked = await askInteractiveChoice('Reinstall Tersio into which agent?', options, installed[0].id);
|
|
730
|
+
if (picked.status !== 'selected') {
|
|
731
|
+
closeRL();
|
|
732
|
+
process.exit(130);
|
|
733
|
+
}
|
|
734
|
+
host = picked.value;
|
|
735
|
+
}
|
|
736
|
+
forceReinstall = true;
|
|
737
|
+
printWelcome();
|
|
738
|
+
console.log(`Reinstall — Tersio v${PACKAGE_VERSION} into ${hostLabel(hostEntry(host))}`);
|
|
739
|
+
console.log(` ${resolvedHostPath(host)}`);
|
|
740
|
+
await runInstall(host);
|
|
741
|
+
if (!dryRun) {
|
|
742
|
+
const summary = await runDoctor();
|
|
743
|
+
if (summary.missing + summary.warn === 0)
|
|
744
|
+
console.log(`\n Reinstall verified — ${hostLabel(hostEntry(host))} is healthy.`);
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
// Hosts the menu advertises but cannot install yet; listed disabled so the roadmap stays visible.
|
|
635
748
|
const SOON_HOSTS = [
|
|
636
749
|
{ value: 'claude', label: 'Claude Code', hint: 'Coming soon' },
|
|
637
750
|
{ value: 'codex', label: 'Codex', hint: 'Coming soon' },
|
|
@@ -656,7 +769,19 @@ async function chooseHost(title, only) {
|
|
|
656
769
|
function hostEntry(id) {
|
|
657
770
|
return detectHosts().find((host) => host.id === id);
|
|
658
771
|
}
|
|
659
|
-
|
|
772
|
+
/** pi discovers a subdirectory only through an index file or a package.json with `pi.extensions`, so rtk/ needs one or both files stay dead. */
|
|
773
|
+
async function stepPiRtkManifest(extDir, options) {
|
|
774
|
+
await syncTreeFiles(extDir, [PI_RTK_MANIFEST], options);
|
|
775
|
+
}
|
|
776
|
+
/** The directory a host's install writes, for the plan line a reinstall prints. */
|
|
777
|
+
function resolvedHostPath(host) {
|
|
778
|
+
if (host === 'pi')
|
|
779
|
+
return path.join(piAgentDir(), 'extensions');
|
|
780
|
+
if (host === 'opencode')
|
|
781
|
+
return path.join(HOME, '.config', 'opencode', 'plugins', 'tersio');
|
|
782
|
+
return path.join(OMP_PLUGINS_DIR, 'node_modules', ...PACKAGE_NAME.split('/'), 'extensions');
|
|
783
|
+
}
|
|
784
|
+
// pi auto-discovers `<agent-dir>/extensions/**/index.ts`, so this route writes the same tree and registers nothing.
|
|
660
785
|
async function stepPiLayer(options) {
|
|
661
786
|
const agentDir = piAgentDir();
|
|
662
787
|
const extDir = path.join(agentDir, 'extensions');
|
|
@@ -666,23 +791,19 @@ async function stepPiLayer(options) {
|
|
|
666
791
|
if (!options.quiet)
|
|
667
792
|
console.log(` Pi — write the extension tree (${extDir})`);
|
|
668
793
|
await stepSharedSessionState(extDir, options);
|
|
669
|
-
// OMP loads rtk
|
|
670
|
-
await copySources(extDir, filesUnder('rtk
|
|
794
|
+
// OMP loads rtk/session.ts from its plugin manifest, so only the pi tree copies it.
|
|
795
|
+
await copySources(extDir, filesUnder('rtk'), 'rtk/session.ts', options);
|
|
671
796
|
await stepRtkFilter(extDir, options);
|
|
797
|
+
await stepPiRtkManifest(extDir, options);
|
|
798
|
+
await stepTiming(extDir, options);
|
|
672
799
|
await stepCaveman(extDir, options);
|
|
673
800
|
await stepCombo(extDir, options);
|
|
801
|
+
await stepPstack(extDir, options);
|
|
674
802
|
await stepTersioCommands(extDir, options);
|
|
675
803
|
await stepUpdater(extDir, options);
|
|
676
804
|
await stepPonytailForPi(agentDir, options);
|
|
677
805
|
}
|
|
678
806
|
// A local dir, not an npm spec: pi only version-checks npm entries, so this avoids the update nag.
|
|
679
|
-
const PI_PONYTAIL_FILES = [
|
|
680
|
-
'pi-extension/index.js',
|
|
681
|
-
'hooks/ponytail-instructions.js',
|
|
682
|
-
'hooks/ponytail-config.js',
|
|
683
|
-
...['ponytail', 'ponytail-audit', 'ponytail-debt', 'ponytail-gain', 'ponytail-help', 'ponytail-review']
|
|
684
|
-
.map((skill) => `skills/${skill}/SKILL.md`),
|
|
685
|
-
];
|
|
686
807
|
const PI_PONYTAIL_PACKAGE = `${JSON.stringify({ name: 'ponytail', version: '0.0.0', private: true, pi: { extensions: ['./pi-extension/index.js'], skills: ['./skills'] } }, null, 2)}\n`;
|
|
687
808
|
async function stepPonytailForPi(agentDir, options) {
|
|
688
809
|
if (!options.quiet)
|
|
@@ -708,8 +829,7 @@ async function stepPonytailForPi(agentDir, options) {
|
|
|
708
829
|
}
|
|
709
830
|
if (options.dryRun)
|
|
710
831
|
return;
|
|
711
|
-
// Only when pi still declares the npm spec: with nothing to remove, `pi remove` still
|
|
712
|
-
// shells out to npm, which warns on stderr and exits non-zero.
|
|
832
|
+
// Only when pi still declares the npm spec: with nothing to remove, `pi remove` still warns and exits non-zero.
|
|
713
833
|
const declared = await piDeclaredPackages(agentDir);
|
|
714
834
|
if (declared.some(isNpmPonytailPackage)) {
|
|
715
835
|
try {
|
|
@@ -755,8 +875,7 @@ async function registerPiLocalPonytail(agentDir, dest, options) {
|
|
|
755
875
|
function comboHint(host, comboDefault) {
|
|
756
876
|
return comboDefault === 'off' ? `\nDone — restart ${host}.` : `\nDone — restart ${host}, then /combo ${comboDefault}.`;
|
|
757
877
|
}
|
|
758
|
-
async function runInstall() {
|
|
759
|
-
// Without this guard a picked command re-enters here with command === null and loops straight back into the picker.
|
|
878
|
+
async function runInstall(forcedHost) {
|
|
760
879
|
if (command === null && tty() && !yes && !updatePromptDone) {
|
|
761
880
|
await runCommandMenu();
|
|
762
881
|
return;
|
|
@@ -770,7 +889,9 @@ async function runInstall() {
|
|
|
770
889
|
console.error(`[fail] ${e.message}`);
|
|
771
890
|
process.exit(1);
|
|
772
891
|
}
|
|
773
|
-
if (
|
|
892
|
+
if (forcedHost)
|
|
893
|
+
targetHost = forcedHost;
|
|
894
|
+
else if (pinned)
|
|
774
895
|
targetHost = pinned;
|
|
775
896
|
else if (tty() && !yes && !dryRun && !applyUpdate && command !== 'uninstall') {
|
|
776
897
|
targetHost = await chooseHost('Install Tersio into which agent?');
|
|
@@ -788,7 +909,7 @@ async function runInstall() {
|
|
|
788
909
|
if (tty() && !applyUpdate && command !== 'uninstall') {
|
|
789
910
|
const newer = await checkForUpdate();
|
|
790
911
|
if (typeof newer === 'string') {
|
|
791
|
-
// Update pending: offer it here
|
|
912
|
+
// Update pending: offer it here; yes runs the full update and stops.
|
|
792
913
|
if (command === null && !dryRun && !yes && !updatePromptDone) {
|
|
793
914
|
const answer = await askInteractiveConfirm(`tersio ${newer} is available (installed ${PACKAGE_VERSION}). Install it now?`);
|
|
794
915
|
if (answer.status === 'confirmed' && answer.value) {
|
|
@@ -812,7 +933,7 @@ async function runInstall() {
|
|
|
812
933
|
const userDir = OMP_AGENT_DIR;
|
|
813
934
|
const userExtDir = path.join(userDir, 'extensions');
|
|
814
935
|
// apply-update refreshes every add-on like a reinstall.
|
|
815
|
-
const installOptions = { dryRun, verbose, yes, reinstall: applyUpdate, quiet };
|
|
936
|
+
const installOptions = { dryRun, verbose, yes, reinstall: applyUpdate || forceReinstall, quiet };
|
|
816
937
|
// Point of no return: disk writes start here; pipes stay unattended.
|
|
817
938
|
if (!yes && !dryRun && !applyUpdate && tty()) {
|
|
818
939
|
const target = hostEntry(targetHost);
|
|
@@ -873,17 +994,31 @@ async function runInstall() {
|
|
|
873
994
|
closeRL();
|
|
874
995
|
return;
|
|
875
996
|
}
|
|
876
|
-
await capture('shared', () => stepSharedSessionState(userExtDir, installOptions));
|
|
877
997
|
let selfPlugin = false;
|
|
878
998
|
await capture('self-plugin', async () => { selfPlugin = await stepSelfPlugin(OMP_PLUGINS_DIR, installOptions); });
|
|
879
999
|
await capture('ponytail', () => stepPonytail(OMP_PLUGINS_DIR, installOptions));
|
|
880
1000
|
await capture('rtk', () => stepRtk(BUN_BIN_DIR, installOptions));
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
1001
|
+
// The plugin manifest owns every mode extension; only a failed registration falls back to the agent tree.
|
|
1002
|
+
const pluginExtDir = path.join(OMP_PLUGINS_DIR, 'node_modules', '@krtclcdy', 'tersio', 'extensions');
|
|
1003
|
+
const ownsTree = selfPlugin || dryRun;
|
|
1004
|
+
const extDir = ownsTree ? pluginExtDir : userExtDir;
|
|
1005
|
+
if (!quiet)
|
|
1006
|
+
console.log(` Add-ons load from: ${ownsTree ? 'the OMP plugin manifest' : 'the agent tree (plugin registration failed)'}`);
|
|
1007
|
+
await capture('shared', () => stepSharedSessionState(extDir, installOptions));
|
|
1008
|
+
await capture('rtk session', () => stepRtkSession(extDir, installOptions));
|
|
1009
|
+
await capture('rtk filter', () => stepRtkFilter(extDir, installOptions));
|
|
1010
|
+
await capture('caveman', () => stepCaveman(extDir, installOptions));
|
|
1011
|
+
await capture('combo', () => stepCombo(extDir, installOptions));
|
|
1012
|
+
await capture('pstack', () => stepPstack(extDir, installOptions));
|
|
1013
|
+
await capture('commands', () => stepTersioCommands(extDir, installOptions));
|
|
1014
|
+
await capture('updater', () => stepUpdater(extDir, installOptions));
|
|
1015
|
+
// One package, one file set: the repair writes every tree file here, so the install does too.
|
|
1016
|
+
if (ownsTree)
|
|
1017
|
+
await capture('plugin entry', () => stepPluginEntry(extDir, installOptions));
|
|
1018
|
+
if (ownsTree)
|
|
1019
|
+
await capture('plugin manifest', () => syncPluginManifest(path.dirname(pluginExtDir), installOptions));
|
|
1020
|
+
if (ownsTree)
|
|
1021
|
+
await capture('retired agent copies', () => stepRetireAgentCopies(userExtDir, installOptions));
|
|
887
1022
|
if (selfPlugin)
|
|
888
1023
|
await capture('settings', () => writePluginSettings(profile, installOptions));
|
|
889
1024
|
if (quiet) {
|
|
@@ -895,4 +1030,4 @@ async function runInstall() {
|
|
|
895
1030
|
}
|
|
896
1031
|
closeRL();
|
|
897
1032
|
}
|
|
898
|
-
export { runInstall, verifyRtkArchive };
|
|
1033
|
+
export { runInstall, runReinstall, verifyRtkArchive };
|
package/dist/cli/interactive.js
CHANGED
|
@@ -30,7 +30,7 @@ let spinnerDepth = 0;
|
|
|
30
30
|
async function withInteractiveSpinner(message, work) {
|
|
31
31
|
if (!tty() || spinnerDepth > 0 || process.env.CI === 'true')
|
|
32
32
|
return work(() => { });
|
|
33
|
-
// Clack manages stdin itself. Close the legacy question interface
|
|
33
|
+
// Clack manages stdin itself. Close the legacy question interface first; non-interactive callers skip it.
|
|
34
34
|
closeRL();
|
|
35
35
|
const active = clackSpinner({ indicator: 'timer' });
|
|
36
36
|
active.start(message);
|
|
@@ -73,7 +73,7 @@ async function askInteractiveConfirm(message, initialValue = true) {
|
|
|
73
73
|
finally {
|
|
74
74
|
}
|
|
75
75
|
}
|
|
76
|
-
// Confirm a destructive run: a Clack dialog at a terminal, a plain prompt in a pipe
|
|
76
|
+
// Confirm a destructive run: a Clack dialog at a terminal, a plain prompt in a pipe. False means declined or aborted.
|
|
77
77
|
async function confirmDestructive(message) {
|
|
78
78
|
if (tty()) {
|
|
79
79
|
closeRL();
|
|
@@ -98,7 +98,7 @@ async function confirmDestructive(message) {
|
|
|
98
98
|
}
|
|
99
99
|
return true;
|
|
100
100
|
}
|
|
101
|
-
// Run collecting work under one TTY-only Clack task
|
|
101
|
+
// Run collecting work under one TTY-only Clack task; callers print after it, so output stays out of the spinner.
|
|
102
102
|
async function runInteractivePhase(title, collect) {
|
|
103
103
|
if (!tty())
|
|
104
104
|
return collect();
|