@krtclcdy/tersio 2.23.0 → 2.24.1
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 +56 -45
- package/cli/agent-hosts.js +42 -0
- package/cli/agents.js +266 -0
- package/cli/banner.js +2 -3
- package/cli/common.js +16 -20
- package/cli/currency.js +11 -19
- package/cli/dashboard.js +216 -40
- package/cli/doctor-fix.js +40 -26
- package/cli/doctor.js +41 -41
- package/cli/hosts.js +114 -0
- package/cli/install.js +284 -179
- package/cli/interactive.js +62 -2
- package/cli/layer.js +22 -0
- package/cli/manifest.js +33 -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 +48 -48
- package/cli/reset.js +5 -32
- package/cli/rtk-wiring.js +7 -17
- package/cli/settings.js +3 -8
- package/cli/uninstall.js +162 -52
- package/cli/update.js +19 -27
- package/cli/usage.js +15 -5
- package/dashboard/dist/index.html +18 -18
- package/extensions/ai-addons-updater/index.js +37 -29
- package/extensions/ai-addons-updater/index.ts +39 -38
- package/extensions/caveman-session/index.js +19 -34
- package/extensions/caveman-session/index.ts +20 -35
- package/extensions/combo-toggle/index.js +47 -49
- package/extensions/combo-toggle/index.ts +42 -58
- package/extensions/lib/utils.js +40 -9
- package/extensions/lib/utils.ts +40 -9
- package/extensions/rtk-session/index.js +34 -38
- package/extensions/rtk-session/index.ts +32 -41
- package/extensions/shared/carbon.js +3 -11
- package/extensions/shared/carbon.ts +4 -11
- package/extensions/shared/host.js +58 -0
- package/extensions/shared/host.ts +94 -0
- package/extensions/shared/omp-prompt.js +12 -0
- package/extensions/shared/plugin-settings.js +33 -30
- package/extensions/shared/plugin-settings.ts +35 -26
- package/extensions/shared/pricing.js +25 -16
- package/extensions/shared/pricing.ts +27 -17
- package/extensions/shared/rtk-gain.js +6 -22
- package/extensions/shared/rtk-gain.ts +6 -22
- package/extensions/shared/session-state.js +44 -36
- package/extensions/shared/session-state.ts +48 -35
- package/extensions/shared/types.ts +10 -3
- package/extensions/shared/usage-ledger.js +160 -51
- package/extensions/shared/usage-ledger.ts +170 -67
- package/extensions/shared/usage-store.js +178 -24
- package/extensions/shared/usage-store.ts +176 -25
- package/extensions/tersio-commands/index.js +3 -6
- package/extensions/tersio-commands/index.ts +3 -6
- package/package.json +6 -5
- package/tersio.js +4 -6
- package/extensions/shared/mode-reinforcement.js +0 -26
package/cli/uninstall.js
CHANGED
|
@@ -1,12 +1,98 @@
|
|
|
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, sayTagged } 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
|
+
sayTagged(` [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
|
+
sayTagged(` [ok] removed the pi package ${spec}`);
|
|
39
|
+
}
|
|
40
|
+
catch (e) {
|
|
41
|
+
sayTagged(` [fail] pi remove: ${e.message}`);
|
|
42
|
+
sayTagged(` [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 removal delegates to pi remove; always list and confirm first.
|
|
56
|
+
async function removePiLayer(host, shouldDryRun, shouldRemovePonytail, shouldRemoveRtk, confirmed) {
|
|
57
|
+
const targets = PI_TREE_DIRS.map((dir) => path.join(piAgentDir(), 'extensions', dir));
|
|
58
|
+
const rtkWiring = path.join(piAgentDir(), 'extensions', 'rtk.ts');
|
|
59
|
+
if (!host.installed && !host.declared) {
|
|
60
|
+
sayTagged(` [skip] ${host.label} — nothing installed (${host.installCmd})`);
|
|
61
|
+
return false;
|
|
62
|
+
}
|
|
63
|
+
console.log('Will remove:');
|
|
64
|
+
for (const t of targets)
|
|
65
|
+
console.log(` ${t}`);
|
|
66
|
+
if (shouldRemovePonytail)
|
|
67
|
+
console.log(` npm:${PONYTAIL_PKG} (pi package)`);
|
|
68
|
+
if (shouldRemoveRtk)
|
|
69
|
+
console.log(` ${rtkWiring} (rtk wiring)`);
|
|
70
|
+
console.log(` ${tersioSettingsFile()} (session defaults)`);
|
|
71
|
+
if (!confirmed && !(await confirmDestructive(`Remove Tersio from ${host.label}?`))) {
|
|
72
|
+
closeRL();
|
|
73
|
+
return false;
|
|
74
|
+
}
|
|
75
|
+
if (host.declared)
|
|
76
|
+
await piRemove(`npm:${PACKAGE_NAME}`, shouldDryRun);
|
|
77
|
+
if (shouldRemovePonytail) {
|
|
78
|
+
await piRemove(`npm:${PONYTAIL_PKG}`, shouldDryRun);
|
|
79
|
+
await removeCopiedPonytailSkills(shouldDryRun);
|
|
80
|
+
}
|
|
81
|
+
await Promise.all(targets.map((t) => removeUninstallTarget(t, shouldDryRun)));
|
|
82
|
+
// rtk's wiring is a loose file, not one of the tree dirs, so it survived
|
|
83
|
+
// --remove-rtk on pi while OMP removed its copy.
|
|
84
|
+
if (shouldRemoveRtk) {
|
|
85
|
+
await removeUninstallTarget(rtkWiring, shouldDryRun, false);
|
|
86
|
+
await removeUninstallTarget(path.join(BUN_BIN_DIR, RTK_BINARY_NAME), shouldDryRun, false);
|
|
87
|
+
}
|
|
88
|
+
await clearSessionDefaults(shouldDryRun);
|
|
89
|
+
return true;
|
|
90
|
+
}
|
|
91
|
+
// ~/.tersio/settings.json holds nothing but Tersio's values, so it goes whole.
|
|
92
|
+
async function clearSessionDefaults(shouldDryRun) {
|
|
93
|
+
await removeUninstallTarget(tersioSettingsFile(), shouldDryRun, false);
|
|
94
|
+
}
|
|
95
|
+
// Read-modify-write a JSON file; mutate returning false prints nothing.
|
|
10
96
|
async function updateJsonFile(filePath, mutate, dryRunNote, writeNote, dryRun) {
|
|
11
97
|
const raw = await readTextIfExists(filePath);
|
|
12
98
|
if (!raw)
|
|
@@ -22,11 +108,11 @@ async function updateJsonFile(filePath, mutate, dryRunNote, writeNote, dryRun) {
|
|
|
22
108
|
if (!mutate(data))
|
|
23
109
|
return;
|
|
24
110
|
if (dryRun) {
|
|
25
|
-
|
|
111
|
+
sayTagged(` [dry-run] ${dryRunNote}`);
|
|
26
112
|
return;
|
|
27
113
|
}
|
|
28
114
|
await fs.writeFile(filePath, JSON.stringify(data, null, 2) + '\n', 'utf8');
|
|
29
|
-
|
|
115
|
+
sayTagged(` [write] ${writeNote}`);
|
|
30
116
|
}
|
|
31
117
|
function dropKey(section, key) {
|
|
32
118
|
if (!section || typeof section !== 'object')
|
|
@@ -40,14 +126,14 @@ function dropKey(section, key) {
|
|
|
40
126
|
async function removeUninstallTarget(target, shouldDryRun, recursive = true) {
|
|
41
127
|
try {
|
|
42
128
|
if (shouldDryRun) {
|
|
43
|
-
|
|
129
|
+
sayTagged(` [dry-run] would remove ${target}`);
|
|
44
130
|
return;
|
|
45
131
|
}
|
|
46
132
|
if (recursive)
|
|
47
133
|
await fs.rm(target, { recursive: true, force: true });
|
|
48
134
|
else
|
|
49
135
|
await fs.unlink(target);
|
|
50
|
-
|
|
136
|
+
sayTagged(` [rm] ${target}`);
|
|
51
137
|
}
|
|
52
138
|
catch {
|
|
53
139
|
debug(`Could not remove ${target}`);
|
|
@@ -56,11 +142,52 @@ async function removeUninstallTarget(target, shouldDryRun, recursive = true) {
|
|
|
56
142
|
async function runUninstall(options = {}) {
|
|
57
143
|
const shouldDryRun = options.dryRun ?? dryRun;
|
|
58
144
|
const confirmed = (options.yes ?? yes) || shouldDryRun;
|
|
59
|
-
// Ponytail
|
|
60
|
-
// its copy too; --keep-ponytail opts out and reinstall always preserves it.
|
|
145
|
+
// Ponytail ships with Tersio, so a full uninstall removes it.
|
|
61
146
|
const shouldRemovePonytail = options.removePonytail ?? (removePonytail || !keepPonytail);
|
|
62
147
|
const shouldRemoveRtk = options.removeRtk ?? removeRtk;
|
|
63
148
|
console.log('\n=== Tersio Uninstall ===\n');
|
|
149
|
+
// A TTY user picks the host; only hosts with tersio on them are offered.
|
|
150
|
+
const installed = detectHosts().filter((host) => host.installed);
|
|
151
|
+
let host = options.host ?? 'omp';
|
|
152
|
+
try {
|
|
153
|
+
host = parseHostArg(args) ?? host;
|
|
154
|
+
}
|
|
155
|
+
catch (e) {
|
|
156
|
+
console.error(`[fail] ${e.message}`);
|
|
157
|
+
process.exit(1);
|
|
158
|
+
}
|
|
159
|
+
if (!options.host && tty() && !confirmed) {
|
|
160
|
+
if (installed.length === 0) {
|
|
161
|
+
console.log('Tersio is not installed for any agent on this machine. Nothing to remove.');
|
|
162
|
+
closeRL();
|
|
163
|
+
return false;
|
|
164
|
+
}
|
|
165
|
+
if (installed.length > 1) {
|
|
166
|
+
const choice = await askInteractiveChoice('Uninstall Tersio from which agent?', installed.map((h) => ({
|
|
167
|
+
value: h.id,
|
|
168
|
+
label: hostLabel(h),
|
|
169
|
+
hint: hostHint(h),
|
|
170
|
+
})), host);
|
|
171
|
+
if (choice.status !== 'selected') {
|
|
172
|
+
closeRL();
|
|
173
|
+
return false;
|
|
174
|
+
}
|
|
175
|
+
host = choice.value;
|
|
176
|
+
}
|
|
177
|
+
else {
|
|
178
|
+
host = installed[0].id;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
const selected = detectHosts().find((h) => h.id === host);
|
|
182
|
+
if (host === 'pi') {
|
|
183
|
+
const removed = await removePiLayer(selected, shouldDryRun, shouldRemovePonytail, shouldRemoveRtk, confirmed);
|
|
184
|
+
if (removed)
|
|
185
|
+
console.log('\nDone. Restart pi, then /combo medium.');
|
|
186
|
+
closeRL();
|
|
187
|
+
return removed;
|
|
188
|
+
}
|
|
189
|
+
if (!selected.installed)
|
|
190
|
+
sayTagged(` [note] ${selected.label} has no tersio install; removing what is left behind.`);
|
|
64
191
|
const extDir = path.join(OMP_AGENT_DIR, 'extensions');
|
|
65
192
|
const configPath = path.join(OMP_AGENT_DIR, 'config.yml');
|
|
66
193
|
const rtkBin = path.join(BUN_BIN_DIR, RTK_BINARY_NAME);
|
|
@@ -73,10 +200,9 @@ async function runUninstall(options = {}) {
|
|
|
73
200
|
'combo-toggle',
|
|
74
201
|
'tersio-commands',
|
|
75
202
|
'shared',
|
|
76
|
-
// Only
|
|
203
|
+
// Only the updater reads it, and it runs before this.
|
|
77
204
|
'lib',
|
|
78
|
-
// Legacy
|
|
79
|
-
// breaks with a module-not-found warning once the shared dir is removed.
|
|
205
|
+
// Legacy: imports session-state.js, so it warns once that module is gone.
|
|
80
206
|
'aaa-combo-boot',
|
|
81
207
|
].map((dir) => path.join(extDir, dir));
|
|
82
208
|
console.log('Will remove:');
|
|
@@ -90,33 +216,11 @@ async function runUninstall(options = {}) {
|
|
|
90
216
|
console.log(` ${rtkBin}`);
|
|
91
217
|
console.log(` ${path.join(extDir, 'rtk.ts')} (rtk OMP wiring)`);
|
|
92
218
|
}
|
|
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
|
-
}
|
|
219
|
+
if (!confirmed && !(await confirmDestructive('Remove the listed Tersio files?')))
|
|
220
|
+
return false;
|
|
117
221
|
// Remove extension directories (independent paths, so concurrently).
|
|
118
222
|
await Promise.all(targets.map((t) => removeUninstallTarget(t, shouldDryRun)));
|
|
119
|
-
// Remove Combo
|
|
223
|
+
// Remove Combo, mode-reinforcement, and rtk registrations; Ponytail on request.
|
|
120
224
|
const configRaw = await readTextIfExists(configPath);
|
|
121
225
|
if (configRaw) {
|
|
122
226
|
let lines = configRaw.split('\n');
|
|
@@ -126,28 +230,36 @@ async function runUninstall(options = {}) {
|
|
|
126
230
|
return false;
|
|
127
231
|
if (shouldRemovePonytail && l.includes('ponytail') && l.includes('pi-extension'))
|
|
128
232
|
return false;
|
|
233
|
+
// Our rtk wiring, absolute or relative. Leaving it listed would keep
|
|
234
|
+
// OMP loading a hook for a package that is no longer installed.
|
|
235
|
+
const entry = l.trim().replace(/^-\s*/, '').replace(/^['"]|['"]$/g, '');
|
|
236
|
+
if (entry.endsWith(`extensions${path.sep}rtk.ts`) || entry.endsWith('extensions/rtk.ts'))
|
|
237
|
+
return false;
|
|
129
238
|
return true;
|
|
130
239
|
});
|
|
131
240
|
if (lines.length !== before) {
|
|
132
241
|
if (shouldDryRun)
|
|
133
|
-
|
|
242
|
+
sayTagged(` [dry-run] would remove ${before - lines.length} config.yml entries`);
|
|
134
243
|
else
|
|
135
244
|
await writeConfigLines(configPath, lines, ` [write] Updated config.yml (removed ${before - lines.length} entries)`);
|
|
136
245
|
}
|
|
137
246
|
}
|
|
138
|
-
//
|
|
139
|
-
|
|
247
|
+
// Cleared last: writeConfigLines above would just make a fresh one.
|
|
248
|
+
for (const backup of [`${configPath}.bak`, path.join(pluginsDir, 'node_modules', PACKAGE_NAME, 'extensions', 'caveman-session', 'rule.md.bak')]) {
|
|
249
|
+
await removeUninstallTarget(backup, shouldDryRun, false);
|
|
250
|
+
}
|
|
251
|
+
// Remove the bundled Ponytail copy; its config.yml entry is already filtered.
|
|
140
252
|
if (shouldRemovePonytail) {
|
|
141
253
|
const pluginsPkgPath = path.join(pluginsDir, 'package.json');
|
|
142
254
|
await updateJsonFile(pluginsPkgPath, (data) => dropKey(data.dependencies, '@dietrichgebert/ponytail'), `would remove @dietrichgebert/ponytail from ${pluginsPkgPath}`, 'Removed @dietrichgebert/ponytail from plugins/package.json', shouldDryRun);
|
|
143
255
|
try {
|
|
144
256
|
if (shouldDryRun)
|
|
145
|
-
|
|
257
|
+
sayTagged(` [dry-run] would remove ${ponytailPkgDir}`);
|
|
146
258
|
else {
|
|
147
259
|
await fs.rm(ponytailPkgDir, { recursive: true, force: true });
|
|
148
|
-
|
|
260
|
+
sayTagged(` [rm] ${ponytailPkgDir}`);
|
|
149
261
|
await fs.rm(path.dirname(ponytailPkgDir));
|
|
150
|
-
|
|
262
|
+
sayTagged(` [rm] ${path.dirname(ponytailPkgDir)} (empty scope)`);
|
|
151
263
|
}
|
|
152
264
|
}
|
|
153
265
|
catch {
|
|
@@ -163,13 +275,11 @@ async function runUninstall(options = {}) {
|
|
|
163
275
|
if ((await readTextIfExists(path.join(selfPluginDir, 'package.json'))) !== null) {
|
|
164
276
|
await removeUninstallTarget(selfPluginDir, shouldDryRun);
|
|
165
277
|
}
|
|
166
|
-
//
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
if (shouldRemoveRtk) {
|
|
278
|
+
// Our rtk wiring goes too, or OMP keeps rewriting for a removed package.
|
|
279
|
+
await removeUninstallTarget(path.join(extDir, 'rtk.ts'), shouldDryRun);
|
|
280
|
+
if (shouldRemoveRtk)
|
|
170
281
|
await removeUninstallTarget(rtkBin, shouldDryRun, false);
|
|
171
|
-
|
|
172
|
-
}
|
|
282
|
+
await clearSessionDefaults(shouldDryRun);
|
|
173
283
|
console.log('\nDone. Restart OMP for changes to take effect.');
|
|
174
284
|
return true;
|
|
175
285
|
}
|
package/cli/update.js
CHANGED
|
@@ -3,7 +3,7 @@ import { promises as fs } from 'node:fs';
|
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import { spawn } from 'node:child_process';
|
|
5
5
|
import { IS_WINDOWS, OMP_PLUGINS_DIR, PACKAGE_BIN, PACKAGE_NAME, PACKAGE_VERSION, dryRun, execP, parseJsonObject, verbose, } from './common.js';
|
|
6
|
-
import { execNetwork } from './interactive.js';
|
|
6
|
+
import { execNetwork, sayTagged } from './interactive.js';
|
|
7
7
|
import { readTextIfExists, resolveRtkBinary, tersioDataPath } from '../extensions/lib/utils.js';
|
|
8
8
|
import { refreshPrices } from '../extensions/shared/pricing.js';
|
|
9
9
|
import { CAVEMAN_REMOTE_RULE, RTK_RELEASE_API, httpsGet, normalizeRtkVersion, sha256Hex, } from '../extensions/lib/utils.js';
|
|
@@ -34,10 +34,8 @@ async function latestPublishedVersion() {
|
|
|
34
34
|
return null;
|
|
35
35
|
}
|
|
36
36
|
}
|
|
37
|
-
//
|
|
38
|
-
//
|
|
39
|
-
// (cached under OMP_PLUGINS_DIR) unless force skips the cache — an explicit
|
|
40
|
-
// "check for updates" must never trust a stale cache.
|
|
37
|
+
// A newer published version, null when up to date, 'unknown' when the registry
|
|
38
|
+
// is unreachable. Cached for a TTL unless force, which must not trust a cache.
|
|
41
39
|
async function checkForUpdate(force = false) {
|
|
42
40
|
const cachePath = tersioDataPath('update-check.json', 'tersio-update-check.json');
|
|
43
41
|
const cached = parseJsonObject(await readTextIfExists(cachePath));
|
|
@@ -50,12 +48,11 @@ async function checkForUpdate(force = false) {
|
|
|
50
48
|
await fs.writeFile(cachePath, JSON.stringify({ latest, lastCheck: Date.now() }) + '\n', 'utf8').catch(() => { });
|
|
51
49
|
return newerThan(latest, PACKAGE_VERSION) ? latest : null;
|
|
52
50
|
}
|
|
53
|
-
//
|
|
54
|
-
// actionable
|
|
51
|
+
// Unreachable registry: a stale cache naming a newer release is still
|
|
52
|
+
// actionable, else report unknown so nobody claims "latest".
|
|
55
53
|
return cached?.latest && newerThan(cached.latest, PACKAGE_VERSION) ? cached.latest : 'unknown';
|
|
56
54
|
}
|
|
57
|
-
// Race a probe against a timeout;
|
|
58
|
-
// update plan stays best-effort and never blocks the update itself.
|
|
55
|
+
// Race a probe against a timeout; failures resolve null so nothing blocks.
|
|
59
56
|
async function settle(work, ms) {
|
|
60
57
|
let timer;
|
|
61
58
|
try {
|
|
@@ -68,9 +65,7 @@ async function settle(work, ms) {
|
|
|
68
65
|
clearTimeout(timer);
|
|
69
66
|
}
|
|
70
67
|
}
|
|
71
|
-
//
|
|
72
|
-
// renders its own interactive UI (the delegated installer runs Clack
|
|
73
|
-
// spinners), which a concurrent outer spinner would corrupt into stray bars.
|
|
68
|
+
// Inherited stdio, so a child spinner cannot corrupt this parent's.
|
|
74
69
|
async function execInherit(cmd, args) {
|
|
75
70
|
const code = await new Promise((resolve, reject) => {
|
|
76
71
|
const child = spawn(cmd, args, { stdio: 'inherit' });
|
|
@@ -80,8 +75,8 @@ async function execInherit(cmd, args) {
|
|
|
80
75
|
if (code !== 0)
|
|
81
76
|
throw new Error(`${cmd} exited with code ${code}`);
|
|
82
77
|
}
|
|
83
|
-
// Current → latest per add-on. Every probe is capped and nullable
|
|
84
|
-
// prints `unknown` for anything unreachable
|
|
78
|
+
// Current → latest per add-on. Every probe is capped and nullable, and the
|
|
79
|
+
// plan prints `unknown` for anything unreachable.
|
|
85
80
|
async function probeUpdatePlan(cliLatest) {
|
|
86
81
|
const rtkBin = resolveRtkBinary();
|
|
87
82
|
const ponytailPkg = path.join(OMP_PLUGINS_DIR, 'node_modules', '@dietrichgebert', 'ponytail', 'package.json');
|
|
@@ -102,8 +97,8 @@ async function probeUpdatePlan(cliLatest) {
|
|
|
102
97
|
ponytail: ponytailLocalText ? parseJsonObject(ponytailLocalText)?.version ?? null : null,
|
|
103
98
|
};
|
|
104
99
|
}
|
|
105
|
-
// Every add-on prints
|
|
106
|
-
//
|
|
100
|
+
// Every add-on prints a version jump when stale, "up to date" otherwise, and
|
|
101
|
+
// "unknown" when its source cannot be reached.
|
|
107
102
|
function planLines(plan) {
|
|
108
103
|
const stale = [];
|
|
109
104
|
const status = [];
|
|
@@ -137,18 +132,16 @@ async function runLatestUpdate() {
|
|
|
137
132
|
if (verbose)
|
|
138
133
|
forwardedArgs.push('--verbose');
|
|
139
134
|
const npmCommand = IS_WINDOWS ? process.env.ComSpec || 'cmd.exe' : 'npm';
|
|
140
|
-
//
|
|
141
|
-
// metadata cache, so a stale `@latest` can never pin an older release.
|
|
135
|
+
// `--prefer-online` beats the local cache, so a stale @latest cannot pin down.
|
|
142
136
|
const cliLatest = await latestPublishedVersion();
|
|
143
137
|
const target = cliLatest && /^\d+\.\d+\.\d+$/.test(cliLatest) ? `@${cliLatest}` : '@latest';
|
|
144
|
-
//
|
|
145
|
-
// unknown and never block the update.
|
|
138
|
+
// Best-effort: unreachable probes print as unknown.
|
|
146
139
|
const plan = await probeUpdatePlan(cliLatest);
|
|
147
140
|
const { stale, status } = planLines(plan);
|
|
148
141
|
if (dryRun) {
|
|
149
142
|
console.log(`tersio update (dry-run):\n ${status.join('\n ')}`);
|
|
150
|
-
|
|
151
|
-
|
|
143
|
+
sayTagged(` [dry-run] would run: npm install -g ${PACKAGE_NAME}${target} --no-audit --no-fund --prefer-online`);
|
|
144
|
+
sayTagged(` [dry-run] would delegate: npm exec --yes --prefer-online --package=${PACKAGE_NAME}${target} -- tersio --apply-update ${forwardedArgs.join(' ')}`);
|
|
152
145
|
return;
|
|
153
146
|
}
|
|
154
147
|
console.log(`Checking for updates:\n ${status.join('\n ')}`);
|
|
@@ -193,8 +186,8 @@ async function runLatestUpdate() {
|
|
|
193
186
|
process.stderr.write(globalResult.stderr);
|
|
194
187
|
}
|
|
195
188
|
catch (e) {
|
|
196
|
-
|
|
197
|
-
|
|
189
|
+
sayTagged(` [warn] Global CLI update skipped: ${e.message}`);
|
|
190
|
+
sayTagged(` [hint] Manual: npm install -g ${PACKAGE_NAME}${target} --no-audit --no-fund --prefer-online`);
|
|
198
191
|
}
|
|
199
192
|
const npmArgs = [
|
|
200
193
|
'exec',
|
|
@@ -207,9 +200,8 @@ async function runLatestUpdate() {
|
|
|
207
200
|
...forwardedArgs,
|
|
208
201
|
];
|
|
209
202
|
const npmCommandArgs = IS_WINDOWS ? ['/d', '/s', '/c', 'npm', ...npmArgs] : npmArgs;
|
|
210
|
-
// Inherited stdio, no outer spinner
|
|
211
|
-
//
|
|
212
|
-
// line above and the closing summary below — one voice, no repeats.
|
|
203
|
+
// Inherited stdio, no outer spinner; the delegated installer runs quiet and
|
|
204
|
+
// this parent owns both the plan line and the closing summary.
|
|
213
205
|
try {
|
|
214
206
|
await execInherit(npmCommand, npmCommandArgs);
|
|
215
207
|
console.log(`Done — tersio ${plan.cli ?? PACKAGE_VERSION}. Restart OMP.`);
|
package/cli/usage.js
CHANGED
|
@@ -113,6 +113,7 @@ function bar(frac, width = 12) {
|
|
|
113
113
|
const filled = Math.max(0, Math.min(width, Math.round(frac * width)));
|
|
114
114
|
return '█'.repeat(filled) + '░'.repeat(width - filled);
|
|
115
115
|
}
|
|
116
|
+
const ROW_CAP = 15;
|
|
116
117
|
function pad(s, n) {
|
|
117
118
|
return s.length >= n ? s : s + ' '.repeat(n - s.length);
|
|
118
119
|
}
|
|
@@ -136,10 +137,15 @@ function printReport(report) {
|
|
|
136
137
|
return;
|
|
137
138
|
}
|
|
138
139
|
const t = report.tokens;
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
140
|
+
const a = report.rtkAdoption;
|
|
141
|
+
for (const l of textTable(['Metric', 'Value'], [
|
|
142
|
+
['Tokens', `${fmt(t.input)} in · ${fmt(t.output)} out · ${fmt(t.cacheRead)} cache read · ${fmt(t.cacheWrite)} cache write`],
|
|
143
|
+
['Cost', `${formatCurrency(report.usd, report.currency)}${report.priced ? '' : ' (includes default pricing)'} · ~${formatCurrency(report.savedUsd, report.currency)} cache-saved (est.) · ~${report.co2g.toFixed(1)}g CO2 (est.)`],
|
|
144
|
+
['RTK adoption', `${fmt(a.rtkCalls)}/${fmt(a.eligibleCalls)} eligible Bash calls use RTK (${a.adoptionPct.toFixed(1)}%) · ${fmt(a.missedCalls)} missed · recall ${report.rtkRecall.available ? `${report.rtkRecall.mode} (${report.rtkRecall.entries})` : 'unavailable'}`],
|
|
145
|
+
], [false, false], 118))
|
|
146
|
+
console.log(l);
|
|
147
|
+
const allModels = Object.entries(report.byModel).filter(([, b]) => b.input + b.output + b.cacheRead + b.cacheWrite > 0).sort((a, b) => (b[1].input + b[1].output) - (a[1].input + a[1].output));
|
|
148
|
+
const models = allModels.slice(0, ROW_CAP);
|
|
143
149
|
if (models.length) {
|
|
144
150
|
console.log(' BY MODEL');
|
|
145
151
|
const top = models.reduce((m, [, b]) => Math.max(m, b.input + b.output + b.cacheRead + b.cacheWrite), 1);
|
|
@@ -152,6 +158,8 @@ function printReport(report) {
|
|
|
152
158
|
for (const l of textTable(['Model', 'Input', 'Output', 'Cache r/w', report.currency, 'Hit', 'Share'], mrows, [false, true, true, true, true, true, false])) {
|
|
153
159
|
console.log(l);
|
|
154
160
|
}
|
|
161
|
+
if (allModels.length > ROW_CAP)
|
|
162
|
+
console.log(` showing top ${ROW_CAP} of ${fmt(allModels.length)} models — full set in the dashboard`);
|
|
155
163
|
}
|
|
156
164
|
const cmdRows = report.byTool.map(([tool, n]) => ({ name: tool, count: n, saved: null, avgPct: null, avgMs: null }));
|
|
157
165
|
for (const r of report.rtkGain.byCommand) {
|
|
@@ -165,7 +173,7 @@ function printReport(report) {
|
|
|
165
173
|
: ' COMMAND TOOLS';
|
|
166
174
|
console.log(scope);
|
|
167
175
|
const top = cmdRows.reduce((m, r) => Math.max(m, r.count), 1);
|
|
168
|
-
const crows = cmdRows.slice(0,
|
|
176
|
+
const crows = cmdRows.slice(0, ROW_CAP).map((r, idx) => [
|
|
169
177
|
String(idx + 1),
|
|
170
178
|
r.name,
|
|
171
179
|
fmt(r.count),
|
|
@@ -177,6 +185,8 @@ function printReport(report) {
|
|
|
177
185
|
for (const l of textTable(['#', 'Tool/Command', 'Count', 'Saved', 'Avg%', 'Time', 'Impact'], crows, [true, false, true, true, true, true, false], 30)) {
|
|
178
186
|
console.log(l);
|
|
179
187
|
}
|
|
188
|
+
if (cmdRows.length > ROW_CAP)
|
|
189
|
+
console.log(` showing top ${ROW_CAP} of ${fmt(cmdRows.length)} commands — full set in the dashboard`);
|
|
180
190
|
}
|
|
181
191
|
}
|
|
182
192
|
async function runUsage() {
|