@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/pi-layer.js
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
// cli/pi-layer.ts — the Pi extension layer as data: the five extension directories plus shared/ and lib/.
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { layerDirs, 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
|
+
];
|
|
11
|
+
// Imported by the extension directories but never loaded by Pi itself, so they stay out of the install list and only...
|
|
12
|
+
const MODULE_DIRECTORIES = [
|
|
13
|
+
['shared', 'Pi types, session bridge, usage and pricing store'],
|
|
14
|
+
['lib', 'shared installer and updater helpers'],
|
|
15
|
+
];
|
|
16
|
+
export const PI_EXTENSION_DIRS = layerDirs(INSTALLED_EXTENSIONS);
|
|
17
|
+
export const PI_MODULE_DIRS = layerDirs(MODULE_DIRECTORIES);
|
|
18
|
+
export function piLayer(home) {
|
|
19
|
+
const relocated = process.env.PI_CODING_AGENT_DIR;
|
|
20
|
+
const base = relocated && relocated.trim() !== '' ? relocated : path.join(home, '.pi', 'agent');
|
|
21
|
+
const extDir = path.join(base, 'extensions');
|
|
22
|
+
return {
|
|
23
|
+
extDir,
|
|
24
|
+
rtkExtension: path.join(extDir, 'rtk.ts'),
|
|
25
|
+
installed: layerEntries(extDir, INSTALLED_EXTENSIONS),
|
|
26
|
+
modules: layerEntries(extDir, MODULE_DIRECTORIES),
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
// Every directory the layer owns. Removal walks this list and nothing else.
|
|
30
|
+
export function piExtensionTargets(layer) {
|
|
31
|
+
return [...layer.installed, ...layer.modules].map((entry) => entry.path);
|
|
32
|
+
}
|
|
33
|
+
// Reports the layer without needing a platform rtk binary name.
|
|
34
|
+
export function reportPiLayer(home, exists) {
|
|
35
|
+
const { present, missing } = reportLayer(piLayer(home).extDir, [INSTALLED_EXTENSIONS, MODULE_DIRECTORIES], exists);
|
|
36
|
+
return { extensions: present, missing, installed: present.length > 0 };
|
|
37
|
+
}
|
package/cli/pi-wiring.js
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
// Install and remove Tersio's Pi extension tree: Pi loads ~/.pi/agent/extensions/<dir>/index.ts through jiti, no build...
|
|
2
|
+
import { promises as fs } from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { readTextIfExists } from '../extensions/lib/utils.js';
|
|
5
|
+
import { rewritePortImports } from './layer.js';
|
|
6
|
+
import { piExtensionTargets, piLayer } from './pi-layer.js';
|
|
7
|
+
// `$HOME`-relative form of a path, for output that must stay readable.
|
|
8
|
+
function rel(target, home) {
|
|
9
|
+
if (!home)
|
|
10
|
+
return target;
|
|
11
|
+
const prefix = home.endsWith(path.sep) ? home : `${home}${path.sep}`;
|
|
12
|
+
return target.startsWith(prefix) ? `~/${target.slice(prefix.length).split(path.sep).join('/')}` : target;
|
|
13
|
+
}
|
|
14
|
+
export function piExtensionDir(home) {
|
|
15
|
+
return piLayer(home).extDir;
|
|
16
|
+
}
|
|
17
|
+
// Writes only when the content differs, leaving a `.bak` of what it replaced.
|
|
18
|
+
async function writeIfChanged(target, content, options) {
|
|
19
|
+
const current = await readTextIfExists(target);
|
|
20
|
+
if (current === content)
|
|
21
|
+
return false;
|
|
22
|
+
if (options.dryRun)
|
|
23
|
+
return true;
|
|
24
|
+
await fs.mkdir(path.dirname(target), { recursive: true });
|
|
25
|
+
if (current !== null)
|
|
26
|
+
await fs.copyFile(target, `${target}.bak`).catch(() => { });
|
|
27
|
+
await fs.writeFile(target, content, 'utf8');
|
|
28
|
+
return true;
|
|
29
|
+
}
|
|
30
|
+
async function exists(target) {
|
|
31
|
+
try {
|
|
32
|
+
await fs.stat(target);
|
|
33
|
+
return true;
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
// Returns the paths it changed, so a no-op re-run is not counted as work.
|
|
40
|
+
export async function installPiTersio(home, tree, options = {}) {
|
|
41
|
+
const extDir = piExtensionDir(home);
|
|
42
|
+
const written = [];
|
|
43
|
+
for (const [from, to] of tree.sources) {
|
|
44
|
+
const content = await readTextIfExists(from);
|
|
45
|
+
if (content === null) {
|
|
46
|
+
// Named by its path under the extension dir, never by the repo source.
|
|
47
|
+
if (!options.quiet) {
|
|
48
|
+
console.log(` [fail] Pi extension source missing: ${to}`);
|
|
49
|
+
console.log(' [hint] Reinstall tersio — extensions/pi ships with the CLI');
|
|
50
|
+
}
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
if (await writeIfChanged(path.join(extDir, to), rewritePortImports(content), options))
|
|
54
|
+
written.push(to);
|
|
55
|
+
}
|
|
56
|
+
for (const [dir, rule] of tree.rules) {
|
|
57
|
+
if (rule === null)
|
|
58
|
+
continue;
|
|
59
|
+
const target = path.join(dir, 'rule.md');
|
|
60
|
+
if (await writeIfChanged(path.join(extDir, target), rule, options))
|
|
61
|
+
written.push(target);
|
|
62
|
+
}
|
|
63
|
+
if (written.length > 0 && !options.quiet) {
|
|
64
|
+
console.log(` ${options.dryRun ? '[dry-run] would write' : '[write]'} Pi — ${written.length} file(s) under ${rel(extDir, home)}`);
|
|
65
|
+
}
|
|
66
|
+
return written;
|
|
67
|
+
}
|
|
68
|
+
// Removes the Pi tree: every directory the layer owns, the `.bak` copies, and the flat module a pre-tree install wrote at...
|
|
69
|
+
export async function removePiTersio(home, options = {}) {
|
|
70
|
+
let removed = false;
|
|
71
|
+
for (const target of [...piExtensionTargets(piLayer(home)), path.join(piExtensionDir(home), 'tersio.ts')]) {
|
|
72
|
+
if (!(await exists(target)))
|
|
73
|
+
continue;
|
|
74
|
+
if (!options.quiet) {
|
|
75
|
+
console.log(` ${options.dryRun ? '[dry-run] would remove' : '[rm]'} ${rel(target, home)}`);
|
|
76
|
+
}
|
|
77
|
+
if (!options.dryRun) {
|
|
78
|
+
await fs.rm(target, { recursive: true, force: true });
|
|
79
|
+
await fs.rm(`${target}.bak`, { force: true, recursive: true });
|
|
80
|
+
}
|
|
81
|
+
removed = true;
|
|
82
|
+
}
|
|
83
|
+
return removed;
|
|
84
|
+
}
|
|
85
|
+
// True when any module of the Pi tree is present.
|
|
86
|
+
export async function piTersioInstalled(home) {
|
|
87
|
+
for (const entry of piLayer(home).installed) {
|
|
88
|
+
if (await exists(path.join(entry.path, 'index.ts')))
|
|
89
|
+
return true;
|
|
90
|
+
}
|
|
91
|
+
return false;
|
|
92
|
+
}
|
package/cli/profile.js
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
// cli/profile.ts — session-start defaults profile (combo/caveman/rtk/ponytail)
|
|
2
2
|
// plus the display-currency default for usage/dashboard reports.
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
import { promises as fs } from 'node:fs';
|
|
3
|
+
// Stored in ~/.tersio/settings.json, which every host reads. Extracted from
|
|
4
|
+
// cli/install.ts so both install and settings share it.
|
|
5
|
+
import { existsSync, promises as fs } from 'node:fs';
|
|
6
6
|
import path from 'node:path';
|
|
7
|
-
import { CAVEMAN_DEFAULTS, COMBO_PRESET_MODES,
|
|
7
|
+
import { CAVEMAN_DEFAULTS, COMBO_PRESET_MODES, PACKAGE_NAME, PONYTAIL_DEFAULTS, verbose, } from './common.js';
|
|
8
|
+
import os from 'node:os';
|
|
8
9
|
import { DEFAULT_CURRENCY, isCurrencyCode } from './currency.js';
|
|
9
10
|
import { readTextIfExists } from '../extensions/lib/utils.js';
|
|
11
|
+
import { tersioSettingsFile } from '../extensions/shared/plugin-settings.js';
|
|
10
12
|
function defaultProfile() {
|
|
11
13
|
return {
|
|
12
14
|
comboDefault: 'off',
|
|
@@ -16,29 +18,16 @@ function defaultProfile() {
|
|
|
16
18
|
currency: DEFAULT_CURRENCY,
|
|
17
19
|
};
|
|
18
20
|
}
|
|
19
|
-
// Stored profile
|
|
20
|
-
//
|
|
21
|
+
// Stored profile: ~/.tersio/settings.json. An install that predates that file
|
|
22
|
+
// kept the same values as OMP plugin settings, so seed from there once and
|
|
23
|
+
// never lose a user's choice.
|
|
21
24
|
async function storedProfile() {
|
|
22
25
|
const base = defaultProfile();
|
|
23
|
-
const
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
try {
|
|
28
|
-
const parsed = JSON.parse(raw);
|
|
29
|
-
if (!parsed || typeof parsed !== 'object' || !('settings' in parsed))
|
|
30
|
-
return base;
|
|
31
|
-
const settings = parsed.settings;
|
|
32
|
-
if (!settings || typeof settings !== 'object' || !(PACKAGE_NAME in settings))
|
|
33
|
-
return base;
|
|
34
|
-
const entry = settings[PACKAGE_NAME];
|
|
35
|
-
if (!entry || typeof entry !== 'object')
|
|
36
|
-
return base;
|
|
37
|
-
stored = entry;
|
|
38
|
-
}
|
|
39
|
-
catch {
|
|
26
|
+
const stored = (await readTextIfExists(tersioSettingsFile())) !== null
|
|
27
|
+
? parseStored(await readTextIfExists(tersioSettingsFile()))
|
|
28
|
+
: parseStored(await readTextIfExists(legacyOmpLockPath()));
|
|
29
|
+
if (!stored)
|
|
40
30
|
return base;
|
|
41
|
-
}
|
|
42
31
|
if (typeof stored.comboDefault === 'string' && stored.comboDefault in COMBO_PRESET_MODES)
|
|
43
32
|
base.comboDefault = stored.comboDefault;
|
|
44
33
|
if (typeof stored.cavemanDefault === 'string' && CAVEMAN_DEFAULTS.has(stored.cavemanDefault))
|
|
@@ -52,25 +41,34 @@ async function storedProfile() {
|
|
|
52
41
|
}
|
|
53
42
|
return base;
|
|
54
43
|
}
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
44
|
+
function legacyOmpLockPath() {
|
|
45
|
+
const configHome = process.env.XDG_CONFIG_HOME || path.join(os.homedir(), '.config');
|
|
46
|
+
return [
|
|
47
|
+
path.join(os.homedir(), '.omp', 'plugins', 'omp-plugins.lock.json'),
|
|
48
|
+
path.join(configHome, 'omp', 'plugins', 'omp-plugins.lock.json'),
|
|
49
|
+
].find((p) => existsSync(p)) ?? path.join(os.homedir(), '.omp', 'plugins', 'omp-plugins.lock.json');
|
|
50
|
+
}
|
|
51
|
+
function parseStored(raw) {
|
|
52
|
+
if (!raw)
|
|
53
|
+
return null;
|
|
54
|
+
try {
|
|
55
|
+
const parsed = JSON.parse(raw);
|
|
56
|
+
if (!parsed || typeof parsed !== 'object')
|
|
57
|
+
return null;
|
|
58
|
+
const direct = parsed;
|
|
59
|
+
const nested = direct.settings?.[PACKAGE_NAME];
|
|
60
|
+
return (nested ?? direct);
|
|
69
61
|
}
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
62
|
+
catch {
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
// Persist the profile where every host reads it. `omp plugin config get` no
|
|
67
|
+
// longer reports it; `tersio settings` does.
|
|
68
|
+
async function writePluginSettings(profile, options) {
|
|
69
|
+
const file = tersioSettingsFile();
|
|
70
|
+
const values = {
|
|
71
|
+
...(parseStored(await readTextIfExists(file)) ?? {}),
|
|
74
72
|
comboDefault: profile.comboDefault,
|
|
75
73
|
comboSetupComplete: true,
|
|
76
74
|
cavemanDefault: profile.cavemanDefault,
|
|
@@ -80,12 +78,12 @@ async function writePluginSettings(profile, options) {
|
|
|
80
78
|
};
|
|
81
79
|
if (options.dryRun) {
|
|
82
80
|
if (verbose && !options.quiet)
|
|
83
|
-
console.log(` [dry-run] would write
|
|
81
|
+
console.log(` [dry-run] would write session defaults to ${file}`);
|
|
84
82
|
return;
|
|
85
83
|
}
|
|
86
|
-
await fs.mkdir(
|
|
87
|
-
await fs.writeFile(
|
|
88
|
-
console.log(` [write]
|
|
84
|
+
await fs.mkdir(path.dirname(file), { recursive: true });
|
|
85
|
+
await fs.writeFile(file, `${JSON.stringify(values, null, 2)}\n`, 'utf8');
|
|
86
|
+
console.log(` [write] Session defaults in ${file}`);
|
|
89
87
|
}
|
|
90
88
|
function formatProfile(profile) {
|
|
91
89
|
return `combo=${profile.comboDefault} (caveman=${profile.cavemanDefault} · rtk=${profile.rtkDefault ? 'on' : 'off'} · ponytail=${profile.ponytailDefault}) · currency=${profile.currency}`;
|
package/cli/reset.js
CHANGED
|
@@ -3,9 +3,8 @@
|
|
|
3
3
|
// derived view. Session transcripts (~/.omp/agent/sessions) and the RTK
|
|
4
4
|
// database are host/tool-owned and are never touched; the watermark only
|
|
5
5
|
// changes what tersio shows.
|
|
6
|
-
import { cancel as clackCancel, confirm as clackConfirm } from '@clack/prompts';
|
|
7
6
|
import { dryRun, yes } from './common.js';
|
|
8
|
-
import {
|
|
7
|
+
import { confirmDestructive } from './interactive.js';
|
|
9
8
|
import { clearUsageLedger, importSessionTokens, ledgerPath, markReset, readUsage, sessionsDir } from '../extensions/shared/usage-ledger.js';
|
|
10
9
|
import { clearUsageDb, usageDbPath } from '../extensions/shared/usage-store.js';
|
|
11
10
|
import { readRtkGain, rtkDbPath } from '../extensions/shared/rtk-gain.js';
|
|
@@ -25,30 +24,8 @@ async function runReset() {
|
|
|
25
24
|
console.log('Nothing to reset — no statistics recorded.');
|
|
26
25
|
return true;
|
|
27
26
|
}
|
|
28
|
-
if (!yes)
|
|
29
|
-
|
|
30
|
-
closeRL();
|
|
31
|
-
const confirmedChoice = await clackConfirm({ message: 'Clear tersio statistics?', initialValue: false });
|
|
32
|
-
if (typeof confirmedChoice !== 'boolean') {
|
|
33
|
-
clackCancel('Aborted.');
|
|
34
|
-
closeRL();
|
|
35
|
-
return false;
|
|
36
|
-
}
|
|
37
|
-
if (!confirmedChoice) {
|
|
38
|
-
console.log('Aborted.');
|
|
39
|
-
closeRL();
|
|
40
|
-
return false;
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
else {
|
|
44
|
-
const answer = await ask('\nProceed? [y/N]: ');
|
|
45
|
-
if (!answer.toLowerCase().startsWith('y')) {
|
|
46
|
-
console.log('Aborted.');
|
|
47
|
-
closeRL();
|
|
48
|
-
return false;
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
}
|
|
27
|
+
if (!yes && !(await confirmDestructive('Clear tersio statistics?')))
|
|
28
|
+
return false;
|
|
52
29
|
const cleared = clearUsageLedger();
|
|
53
30
|
const dbCleared = clearUsageDb();
|
|
54
31
|
const ts = markReset();
|
package/cli/rtk-wiring.js
CHANGED
|
@@ -1,12 +1,9 @@
|
|
|
1
1
|
// cli/rtk-wiring.ts — wire the installed rtk binary into OMP.
|
|
2
2
|
// `rtk init -g --agent omp` writes rtk's tool_call extension to
|
|
3
|
-
// ~/.omp/agent/extensions/rtk.ts
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
// Fail-open: a failed wire only downgrades to manual `rtk` prefixing.
|
|
8
|
-
// Deliberately free of cli/common.ts imports (argv side effects) so tests
|
|
9
|
-
// can load it directly.
|
|
3
|
+
// ~/.omp/agent/extensions/rtk.ts but does not register it, and OMP loads only
|
|
4
|
+
// listed extensions, so we add the path to config.yml. Every rewritten bash
|
|
5
|
+
// call then lands in rtk's history.db. Fail-open: a failed wire only downgrades
|
|
6
|
+
// to manual `rtk` prefixing. No cli/common.ts import (argv side effects).
|
|
10
7
|
import { promises as fs } from 'node:fs';
|
|
11
8
|
import os from 'node:os';
|
|
12
9
|
import path from 'node:path';
|
|
@@ -34,8 +31,7 @@ function execFileP(cmd, args, timeout) {
|
|
|
34
31
|
});
|
|
35
32
|
});
|
|
36
33
|
}
|
|
37
|
-
// Idempotent: rtk rewrites its
|
|
38
|
-
// reinstalling tersio refreshes the wiring for free.
|
|
34
|
+
// Idempotent: rtk rewrites its file on every init run.
|
|
39
35
|
export async function wireRtkOmp(rtkBin, options = {}) {
|
|
40
36
|
if (!options.dryRun)
|
|
41
37
|
debugWire(options, 'Wiring rtk → OMP (bash tool_call rewrite)…');
|
|
@@ -47,14 +43,13 @@ export async function wireRtkOmp(rtkBin, options = {}) {
|
|
|
47
43
|
await ensureRtkInConfig(options);
|
|
48
44
|
return true;
|
|
49
45
|
}
|
|
50
|
-
// The rtk.ts path rtk init writes (
|
|
51
|
-
// without importing it — see header note on argv side effects).
|
|
46
|
+
// The rtk.ts path rtk init writes (OMP_AGENT_DIR inlined; see header note).
|
|
52
47
|
function rtkExtensionPath() {
|
|
53
48
|
const home = process.env.HOME || process.env.USERPROFILE || os.homedir();
|
|
54
49
|
return path.join(home, '.omp', 'agent', 'extensions', 'rtk.ts');
|
|
55
50
|
}
|
|
56
|
-
//
|
|
57
|
-
//
|
|
51
|
+
// Append rtk.ts after the existing entries (or create the key) so the wire
|
|
52
|
+
// takes effect on the next OMP start.
|
|
58
53
|
export async function ensureRtkInConfig(options) {
|
|
59
54
|
const home = process.env.HOME || process.env.USERPROFILE || os.homedir();
|
|
60
55
|
const configPath = path.join(home, '.omp', 'agent', 'config.yml');
|
|
@@ -98,8 +93,8 @@ async function runRtkInit(rtkBin, options) {
|
|
|
98
93
|
return true;
|
|
99
94
|
}
|
|
100
95
|
catch (first) {
|
|
101
|
-
// A
|
|
102
|
-
//
|
|
96
|
+
// A fresh binary can lose its first exec to macOS Gatekeeper, so settle and
|
|
97
|
+
// retry once before reporting failure.
|
|
103
98
|
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
104
99
|
try {
|
|
105
100
|
await execFileP(rtkBin, ['init', '-g', '--agent', 'omp'], 30000);
|
package/cli/uninstall.js
CHANGED
|
@@ -1,12 +1,81 @@
|
|
|
1
1
|
// cli/uninstall.ts — remove managed extensions, plugins, and binaries.
|
|
2
2
|
import { promises as fs } from 'node:fs';
|
|
3
3
|
import path from 'node:path';
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import { readTextIfExists } from '../extensions/lib/utils.js';
|
|
8
|
-
|
|
9
|
-
|
|
4
|
+
import { BUN_BIN_DIR, OMP_AGENT_DIR, OMP_PLUGINS_DIR, PACKAGE_NAME, RTK_BINARY_NAME, args, dryRun, keepPonytail, removePonytail, removeRtk, yes, debug, writeConfigLines, } from './common.js';
|
|
5
|
+
import { askInteractiveChoice, closeRL, confirmDestructive, tty } from './interactive.js';
|
|
6
|
+
import { detectHosts, hostHint, hostLabel, parseHostArg } from './hosts.js';
|
|
7
|
+
import { findHoistedPackage, piAgentDir, readTextIfExists } from '../extensions/lib/utils.js';
|
|
8
|
+
import { fileURLToPath } from 'node:url';
|
|
9
|
+
import { tersioSettingsFile } from '../extensions/shared/plugin-settings.js';
|
|
10
|
+
// The pi tree mirrors the OMP one. Ponytail and rtk stay: a package and a
|
|
11
|
+
// shared binary are not ours to delete.
|
|
12
|
+
const PONYTAIL_PKG = '@dietrichgebert/ponytail';
|
|
13
|
+
const PI_TREE_DIRS = [
|
|
14
|
+
'caveman-session',
|
|
15
|
+
'rtk-session',
|
|
16
|
+
'ai-addons-updater',
|
|
17
|
+
'combo-toggle',
|
|
18
|
+
'tersio-commands',
|
|
19
|
+
'shared',
|
|
20
|
+
'lib',
|
|
21
|
+
];
|
|
22
|
+
// One `pi remove`, for both packages the installer adds there.
|
|
23
|
+
async function piRemove(spec, shouldDryRun) {
|
|
24
|
+
if (shouldDryRun) {
|
|
25
|
+
console.log(` [dry-run] would run: pi remove ${spec}`);
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
try {
|
|
29
|
+
const { execFile } = await import('node:child_process');
|
|
30
|
+
await new Promise((resolve, reject) => {
|
|
31
|
+
execFile('pi', ['remove', spec], { timeout: 120000 }, (err, _out, errOut) => {
|
|
32
|
+
if (err)
|
|
33
|
+
reject(new Error(String(errOut).trim() || err.message));
|
|
34
|
+
else
|
|
35
|
+
resolve();
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
console.log(` [ok] removed the pi package ${spec}`);
|
|
39
|
+
}
|
|
40
|
+
catch (e) {
|
|
41
|
+
console.log(` [fail] pi remove: ${e.message}`);
|
|
42
|
+
console.log(` [hint] Manual: pi remove ${spec}`);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
// Only the skill directories the installer copied, never pi's own skills dir.
|
|
46
|
+
async function removeCopiedPonytailSkills(shouldDryRun) {
|
|
47
|
+
const source = findHoistedPackage(PONYTAIL_PKG, path.dirname(fileURLToPath(import.meta.url)), 'skills');
|
|
48
|
+
if (!source)
|
|
49
|
+
return;
|
|
50
|
+
for (const entry of await fs.readdir(source, { withFileTypes: true })) {
|
|
51
|
+
if (entry.isDirectory())
|
|
52
|
+
await removeUninstallTarget(path.join(piAgentDir(), 'skills', entry.name), shouldDryRun);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
// pi owns its package dir, so removal is `pi remove`; the tree and the defaults
|
|
56
|
+
// file are ours. Ponytail goes too unless --keep-ponytail, matching the OMP
|
|
57
|
+
// route, so neither host keeps a package we installed on its behalf.
|
|
58
|
+
async function removePiLayer(host, shouldDryRun, shouldRemovePonytail) {
|
|
59
|
+
const targets = PI_TREE_DIRS.map((dir) => path.join(piAgentDir(), 'extensions', dir));
|
|
60
|
+
if (!host.installed && !host.declared) {
|
|
61
|
+
console.log(` [skip] ${host.label} — nothing installed (${host.installCmd})`);
|
|
62
|
+
return false;
|
|
63
|
+
}
|
|
64
|
+
if (host.declared)
|
|
65
|
+
await piRemove(`npm:${PACKAGE_NAME}`, shouldDryRun);
|
|
66
|
+
if (shouldRemovePonytail) {
|
|
67
|
+
await piRemove(`npm:${PONYTAIL_PKG}`, shouldDryRun);
|
|
68
|
+
await removeCopiedPonytailSkills(shouldDryRun);
|
|
69
|
+
}
|
|
70
|
+
await Promise.all(targets.map((t) => removeUninstallTarget(t, shouldDryRun)));
|
|
71
|
+
await clearSessionDefaults(shouldDryRun);
|
|
72
|
+
return true;
|
|
73
|
+
}
|
|
74
|
+
// ~/.tersio/settings.json holds nothing but Tersio's values, so it goes whole.
|
|
75
|
+
async function clearSessionDefaults(shouldDryRun) {
|
|
76
|
+
await removeUninstallTarget(tersioSettingsFile(), shouldDryRun, false);
|
|
77
|
+
}
|
|
78
|
+
// Read-modify-write a JSON file; mutate returning false prints nothing.
|
|
10
79
|
async function updateJsonFile(filePath, mutate, dryRunNote, writeNote, dryRun) {
|
|
11
80
|
const raw = await readTextIfExists(filePath);
|
|
12
81
|
if (!raw)
|
|
@@ -56,11 +125,52 @@ async function removeUninstallTarget(target, shouldDryRun, recursive = true) {
|
|
|
56
125
|
async function runUninstall(options = {}) {
|
|
57
126
|
const shouldDryRun = options.dryRun ?? dryRun;
|
|
58
127
|
const confirmed = (options.yes ?? yes) || shouldDryRun;
|
|
59
|
-
// Ponytail
|
|
60
|
-
// its copy too; --keep-ponytail opts out and reinstall always preserves it.
|
|
128
|
+
// Ponytail ships with Tersio, so a full uninstall removes it.
|
|
61
129
|
const shouldRemovePonytail = options.removePonytail ?? (removePonytail || !keepPonytail);
|
|
62
130
|
const shouldRemoveRtk = options.removeRtk ?? removeRtk;
|
|
63
131
|
console.log('\n=== Tersio Uninstall ===\n');
|
|
132
|
+
// A TTY user picks the host; only hosts with tersio on them are offered.
|
|
133
|
+
const installed = detectHosts().filter((host) => host.installed);
|
|
134
|
+
let host = options.host ?? 'omp';
|
|
135
|
+
try {
|
|
136
|
+
host = parseHostArg(args) ?? host;
|
|
137
|
+
}
|
|
138
|
+
catch (e) {
|
|
139
|
+
console.error(`[fail] ${e.message}`);
|
|
140
|
+
process.exit(1);
|
|
141
|
+
}
|
|
142
|
+
if (!options.host && tty() && !confirmed) {
|
|
143
|
+
if (installed.length === 0) {
|
|
144
|
+
console.log('Tersio is not installed for any agent on this machine. Nothing to remove.');
|
|
145
|
+
closeRL();
|
|
146
|
+
return false;
|
|
147
|
+
}
|
|
148
|
+
if (installed.length > 1) {
|
|
149
|
+
const choice = await askInteractiveChoice('Uninstall Tersio from which agent?', installed.map((h) => ({
|
|
150
|
+
value: h.id,
|
|
151
|
+
label: hostLabel(h),
|
|
152
|
+
hint: hostHint(h),
|
|
153
|
+
})), host);
|
|
154
|
+
if (choice.status !== 'selected') {
|
|
155
|
+
closeRL();
|
|
156
|
+
return false;
|
|
157
|
+
}
|
|
158
|
+
host = choice.value;
|
|
159
|
+
}
|
|
160
|
+
else {
|
|
161
|
+
host = installed[0].id;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
const selected = detectHosts().find((h) => h.id === host);
|
|
165
|
+
if (host === 'pi') {
|
|
166
|
+
const removed = await removePiLayer(selected, shouldDryRun, shouldRemovePonytail);
|
|
167
|
+
if (removed)
|
|
168
|
+
console.log('\nDone. Restart pi for changes to take effect.');
|
|
169
|
+
closeRL();
|
|
170
|
+
return removed;
|
|
171
|
+
}
|
|
172
|
+
if (!selected.installed)
|
|
173
|
+
console.log(` [note] ${selected.label} has no tersio install; removing what is left behind.`);
|
|
64
174
|
const extDir = path.join(OMP_AGENT_DIR, 'extensions');
|
|
65
175
|
const configPath = path.join(OMP_AGENT_DIR, 'config.yml');
|
|
66
176
|
const rtkBin = path.join(BUN_BIN_DIR, RTK_BINARY_NAME);
|
|
@@ -73,10 +183,9 @@ async function runUninstall(options = {}) {
|
|
|
73
183
|
'combo-toggle',
|
|
74
184
|
'tersio-commands',
|
|
75
185
|
'shared',
|
|
76
|
-
// Only
|
|
186
|
+
// Only the updater reads it, and it runs before this.
|
|
77
187
|
'lib',
|
|
78
|
-
// Legacy
|
|
79
|
-
// breaks with a module-not-found warning once the shared dir is removed.
|
|
188
|
+
// Legacy: imports session-state.js, so it warns once that module is gone.
|
|
80
189
|
'aaa-combo-boot',
|
|
81
190
|
].map((dir) => path.join(extDir, dir));
|
|
82
191
|
console.log('Will remove:');
|
|
@@ -90,33 +199,11 @@ async function runUninstall(options = {}) {
|
|
|
90
199
|
console.log(` ${rtkBin}`);
|
|
91
200
|
console.log(` ${path.join(extDir, 'rtk.ts')} (rtk OMP wiring)`);
|
|
92
201
|
}
|
|
93
|
-
if (!confirmed)
|
|
94
|
-
|
|
95
|
-
closeRL();
|
|
96
|
-
const confirmedChoice = await clackConfirm({ message: 'Remove the listed Tersio files?', initialValue: false });
|
|
97
|
-
if (typeof confirmedChoice !== 'boolean') {
|
|
98
|
-
clackCancel('Aborted.');
|
|
99
|
-
closeRL();
|
|
100
|
-
return false;
|
|
101
|
-
}
|
|
102
|
-
if (!confirmedChoice) {
|
|
103
|
-
console.log('Aborted.');
|
|
104
|
-
closeRL();
|
|
105
|
-
return false;
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
else {
|
|
109
|
-
const answer = await ask('\nProceed? [y/N]: ');
|
|
110
|
-
if (!answer.toLowerCase().startsWith('y')) {
|
|
111
|
-
console.log('Aborted.');
|
|
112
|
-
closeRL();
|
|
113
|
-
return false;
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
}
|
|
202
|
+
if (!confirmed && !(await confirmDestructive('Remove the listed Tersio files?')))
|
|
203
|
+
return false;
|
|
117
204
|
// Remove extension directories (independent paths, so concurrently).
|
|
118
205
|
await Promise.all(targets.map((t) => removeUninstallTarget(t, shouldDryRun)));
|
|
119
|
-
// Remove Combo
|
|
206
|
+
// Remove Combo, mode-reinforcement, and rtk registrations; Ponytail on request.
|
|
120
207
|
const configRaw = await readTextIfExists(configPath);
|
|
121
208
|
if (configRaw) {
|
|
122
209
|
let lines = configRaw.split('\n');
|
|
@@ -126,6 +213,11 @@ async function runUninstall(options = {}) {
|
|
|
126
213
|
return false;
|
|
127
214
|
if (shouldRemovePonytail && l.includes('ponytail') && l.includes('pi-extension'))
|
|
128
215
|
return false;
|
|
216
|
+
// Our rtk wiring, absolute or relative. Leaving it listed would keep
|
|
217
|
+
// OMP loading a hook for a package that is no longer installed.
|
|
218
|
+
const entry = l.trim().replace(/^-\s*/, '').replace(/^['"]|['"]$/g, '');
|
|
219
|
+
if (entry.endsWith(`extensions${path.sep}rtk.ts`) || entry.endsWith('extensions/rtk.ts'))
|
|
220
|
+
return false;
|
|
129
221
|
return true;
|
|
130
222
|
});
|
|
131
223
|
if (lines.length !== before) {
|
|
@@ -135,8 +227,7 @@ async function runUninstall(options = {}) {
|
|
|
135
227
|
await writeConfigLines(configPath, lines, ` [write] Updated config.yml (removed ${before - lines.length} entries)`);
|
|
136
228
|
}
|
|
137
229
|
}
|
|
138
|
-
// Remove the bundled Ponytail copy
|
|
139
|
-
// pre-bundle installs); the config.yml entry was filtered above.
|
|
230
|
+
// Remove the bundled Ponytail copy; its config.yml entry is already filtered.
|
|
140
231
|
if (shouldRemovePonytail) {
|
|
141
232
|
const pluginsPkgPath = path.join(pluginsDir, 'package.json');
|
|
142
233
|
await updateJsonFile(pluginsPkgPath, (data) => dropKey(data.dependencies, '@dietrichgebert/ponytail'), `would remove @dietrichgebert/ponytail from ${pluginsPkgPath}`, 'Removed @dietrichgebert/ponytail from plugins/package.json', shouldDryRun);
|
|
@@ -163,13 +254,13 @@ async function runUninstall(options = {}) {
|
|
|
163
254
|
if ((await readTextIfExists(path.join(selfPluginDir, 'package.json'))) !== null) {
|
|
164
255
|
await removeUninstallTarget(selfPluginDir, shouldDryRun);
|
|
165
256
|
}
|
|
166
|
-
//
|
|
167
|
-
//
|
|
168
|
-
//
|
|
169
|
-
|
|
257
|
+
// Our rtk wiring goes with the rest: leaving the file would keep OMP
|
|
258
|
+
// rewriting bash calls to a package this uninstall just removed. The binary
|
|
259
|
+
// stays unless --remove-rtk, since it is a shared tool on PATH.
|
|
260
|
+
await removeUninstallTarget(path.join(extDir, 'rtk.ts'), shouldDryRun);
|
|
261
|
+
if (shouldRemoveRtk)
|
|
170
262
|
await removeUninstallTarget(rtkBin, shouldDryRun, false);
|
|
171
|
-
|
|
172
|
-
}
|
|
263
|
+
await clearSessionDefaults(shouldDryRun);
|
|
173
264
|
console.log('\nDone. Restart OMP for changes to take effect.');
|
|
174
265
|
return true;
|
|
175
266
|
}
|