@krtclcdy/tersio 2.20.0 → 2.21.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 CHANGED
@@ -103,7 +103,7 @@ Break-even math, balanced preset (caveman full + rtk + ponytail): a mixed turn c
103
103
  | `tersio install` | Install (user scope: all OMP sessions) |
104
104
  | `tersio update` | Refresh the CLI, extensions, and add-ons (RTK binary, Caveman rule, Ponytail) |
105
105
  | `tersio reinstall` | Fresh install, preserving the Ponytail package |
106
- | `tersio doctor` | Check OMP, extension, Ponytail, and RTK health |
106
+ | `tersio doctor` | Check OMP, extension, Ponytail, and RTK health (`--fix` repairs, `--fix=<scope>` one scope, `--dry-run` previews) |
107
107
  | `tersio usage` | Ledger-backed usage + savings report |
108
108
  | `tersio gain` | Open the gain dashboard (`--open`, `--export <file>`, `--port <n>`, `--currency <code>`; serves localhost only) |
109
109
  | `tersio reset` | Clear tersio statistics: usage ledger + a watermark that hides pre-reset rows from every derived view (Y/N confirm, `--dry-run`, `--yes`) — session transcripts and RTK history stay intact on disk |
package/cli/common.js CHANGED
@@ -62,6 +62,7 @@ const dryRun = args.includes('--dry-run');
62
62
  const yes = args.includes('--yes') || args.includes('-y') || install || update || reinstall || applyUpdate;
63
63
  const verbose = args.includes('--verbose');
64
64
  const doctor = command === 'doctor' || args.includes('--doctor');
65
+ const fix = args.includes('--fix') || args.some((a) => a.startsWith('--fix='));
65
66
  const usage = command === 'usage' || args.includes('--usage');
66
67
  const gain = command === 'gain';
67
68
  const reset = command === 'reset';
@@ -285,4 +286,4 @@ function relTime(ageMs) {
285
286
  const weeks = Math.floor(days / 7);
286
287
  return `${weeks} week${weeks === 1 ? '' : 's'} ago`;
287
288
  }
288
- export { IS_WINDOWS, RTK_BINARY_NAME, HOME, OMP_AGENT_DIR, OMP_PLUGINS_DIR, BUN_BIN_DIR, OMP_BIN, PACKAGE_NAME, PACKAGE_BIN, PACKAGE_VERSION, CAVEMAN_DEFAULTS, PONYTAIL_DEFAULTS, RTK_DEFAULTS, COMBO_PRESET_MODES, COMBO_DEFAULTS, parseEnum, flagValue, COMMANDS, commandArg, command, unknownCommand, install, update, reinstall, showVersion, showHelp, applyUpdate, dryRun, yes, verbose, doctor, uninstall, usage, gain, reset, settings, dashboardPort, dashboardOpen, dashboardExport, currency, currencyGiven, removePonytail, keepPonytail, removeRtk, comboDefaultFlag, cavemanDefaultFlag, ponytailDefaultFlag, rtkDefaultFlag, profileFlagsGiven, debug, execFileP, execP, writeIfChanged, normalizeExtensionsKey, EXTENSIONS_KEY_RE, writeConfigLines, ensureExtensionInConfig, ensureExtensionAfterConfigEntry, readPonytailConfig, parseJsonObject, parsePonytailConfig, patchPonytailConfig, ensurePonytailConfigValue, readPluginsPackage, relTime, };
289
+ export { IS_WINDOWS, RTK_BINARY_NAME, HOME, OMP_AGENT_DIR, OMP_PLUGINS_DIR, BUN_BIN_DIR, OMP_BIN, PACKAGE_NAME, PACKAGE_BIN, PACKAGE_VERSION, CAVEMAN_DEFAULTS, PONYTAIL_DEFAULTS, RTK_DEFAULTS, COMBO_PRESET_MODES, COMBO_DEFAULTS, parseEnum, flagValue, COMMANDS, commandArg, command, unknownCommand, install, update, reinstall, showVersion, showHelp, applyUpdate, args, dryRun, yes, verbose, doctor, fix, uninstall, usage, gain, reset, settings, dashboardPort, dashboardOpen, dashboardExport, currency, currencyGiven, removePonytail, keepPonytail, removeRtk, comboDefaultFlag, cavemanDefaultFlag, ponytailDefaultFlag, rtkDefaultFlag, profileFlagsGiven, debug, execFileP, execP, writeIfChanged, normalizeExtensionsKey, EXTENSIONS_KEY_RE, writeConfigLines, ensureExtensionInConfig, ensureExtensionAfterConfigEntry, readPonytailConfig, parseJsonObject, parsePonytailConfig, patchPonytailConfig, ensurePonytailConfigValue, readPluginsPackage, relTime, };
@@ -0,0 +1,185 @@
1
+ import { promises as fs } from 'node:fs';
2
+ import os from 'node:os';
3
+ import path from 'node:path';
4
+ import { fileURLToPath } from 'node:url';
5
+ import { BUN_BIN_DIR, OMP_AGENT_DIR, OMP_PLUGINS_DIR, OMP_BIN, PACKAGE_NAME, PACKAGE_VERSION, RTK_BINARY_NAME, dryRun, verbose, debug, ensureExtensionAfterConfigEntry, ensureExtensionInConfig, execP, readPluginsPackage, writeIfChanged, } from './common.js';
6
+ import { execNetwork } from './interactive.js';
7
+ import { wireRtkOmp, ensureRtkInConfig } from './rtk-wiring.js';
8
+ import { CAVEMAN_REMOTE_RULE, RTK_RELEASE_API, fetchJson, findFile, httpsGet, httpsDownload, parseChecksum, readTextIfExists, rtkPlatformSpec, sha256File, } from '../extensions/lib/utils.js';
9
+ import { runLatestUpdate } from './update.js';
10
+ const EXT_DIR = path.join(path.dirname(fileURLToPath(import.meta.url)), '..', 'extensions');
11
+ const SOURCES = [
12
+ [path.join(EXT_DIR, 'shared', 'session-state.ts'), path.join('shared', 'session-state.ts')],
13
+ [path.join(EXT_DIR, 'shared', 'types.ts'), path.join('shared', 'types.ts')],
14
+ [path.join(EXT_DIR, 'lib', 'utils.ts'), path.join('lib', 'utils.ts')],
15
+ [path.join(EXT_DIR, 'shared', 'plugin-settings.ts'), path.join('shared', 'plugin-settings.ts')],
16
+ [path.join(EXT_DIR, 'shared', 'usage-ledger.ts'), path.join('shared', 'usage-ledger.ts')],
17
+ [path.join(EXT_DIR, 'shared', 'pricing.ts'), path.join('shared', 'pricing.ts')],
18
+ [path.join(EXT_DIR, 'shared', 'carbon.ts'), path.join('shared', 'carbon.ts')],
19
+ [path.join(EXT_DIR, 'caveman-session', 'index.ts'), path.join('caveman-session', 'index.ts')],
20
+ [path.join(EXT_DIR, 'rtk-session', 'index.ts'), path.join('rtk-session', 'index.ts')],
21
+ [path.join(EXT_DIR, 'ai-addons-updater', 'index.ts'), path.join('ai-addons-updater', 'index.ts')],
22
+ [path.join(EXT_DIR, 'combo-toggle', 'index.ts'), path.join('combo-toggle', 'index.ts')],
23
+ [path.join(EXT_DIR, 'tersio-commands', 'index.ts'), path.join('tersio-commands', 'index.ts')],
24
+ [path.join(EXT_DIR, 'shared', 'mode-reinforcement.ts'), path.join('shared', 'mode-reinforcement.ts')],
25
+ ];
26
+ const PONYTAIL_GITHUB_SPEC = 'github:DietrichGebert/ponytail';
27
+ const PONYTAIL_NPM_SPEC = '@dietrichgebert/ponytail@latest';
28
+ async function fixExtensions(extDir) {
29
+ console.log(' Doctor --fix: restoring extension files');
30
+ await fs.mkdir(extDir, { recursive: true });
31
+ const texts = await Promise.all(SOURCES.map(([from]) => readTextIfExists(from)));
32
+ const writes = [];
33
+ for (let i = 0; i < SOURCES.length; i++) {
34
+ if (texts[i] !== null)
35
+ writes.push(writeIfChanged(path.join(extDir, SOURCES[i][1]), texts[i], { dryRun, verbose }));
36
+ else
37
+ console.log(` [warn] bundled source missing: ${SOURCES[i][0]}`);
38
+ }
39
+ await Promise.all(writes);
40
+ if ((await readTextIfExists(path.join(extDir, 'caveman-session', 'rule.md'))) === null) {
41
+ const rule = await Promise.race([
42
+ httpsGet(CAVEMAN_REMOTE_RULE).catch(() => null),
43
+ new Promise((resolve) => setTimeout(() => resolve(null), 8000)),
44
+ ]);
45
+ if (rule !== null)
46
+ await writeIfChanged(path.join(extDir, 'caveman-session', 'rule.md'), rule, { dryRun, verbose });
47
+ else
48
+ console.log(' [warn] Caveman rule unreachable — kept existing rule.md');
49
+ }
50
+ }
51
+ async function fixRegistrations(agentDir, pluginsDir) {
52
+ console.log(' Doctor --fix: repairing config.yml registrations');
53
+ const configPath = path.join(agentDir, 'config.yml');
54
+ if ((await readTextIfExists(configPath)) === null) {
55
+ if (dryRun)
56
+ console.log(` [dry-run] would create ${configPath}`);
57
+ else {
58
+ await fs.mkdir(agentDir, { recursive: true });
59
+ await fs.writeFile(configPath, 'extensions:\n', 'utf8');
60
+ }
61
+ }
62
+ const extDir = path.join(agentDir, 'extensions');
63
+ const ponytailExtPath = path.join(pluginsDir, 'node_modules', '@dietrichgebert', 'ponytail', 'pi-extension', 'index.js');
64
+ await ensureExtensionInConfig(configPath, ponytailExtPath, 'ponytail', { dryRun, verbose });
65
+ await ensureExtensionInConfig(configPath, path.join(extDir, 'combo-toggle', 'index.ts'), 'combo', { dryRun, verbose });
66
+ const rtkTs = path.join(extDir, 'rtk.ts');
67
+ if ((await readTextIfExists(rtkTs)) !== null)
68
+ await ensureRtkInConfig({ dryRun, verbose });
69
+ await ensureExtensionAfterConfigEntry(configPath, path.join(extDir, 'shared', 'mode-reinforcement.ts'), ponytailExtPath, 'mode reinforcement', { dryRun, verbose });
70
+ const pkgPath = path.join(pluginsDir, 'package.json');
71
+ const pkg = await readPluginsPackage(pkgPath);
72
+ if (!(PACKAGE_NAME in pkg.dependencies)) {
73
+ pkg.dependencies[PACKAGE_NAME] = `^${PACKAGE_VERSION}`;
74
+ if (dryRun)
75
+ console.log(` [dry-run] would register ${PACKAGE_NAME} in ${pkgPath}`);
76
+ else {
77
+ await fs.mkdir(pluginsDir, { recursive: true });
78
+ await fs.writeFile(pkgPath, JSON.stringify(pkg, null, 2) + '\n', 'utf8');
79
+ }
80
+ }
81
+ }
82
+ async function fixRtk(binDir) {
83
+ console.log(' Doctor --fix: repairing RTK binary + wiring');
84
+ const binDest = path.join(binDir, RTK_BINARY_NAME);
85
+ if (dryRun) {
86
+ console.log(` [dry-run] would download a checksum-verified rtk to ${binDest} and wire it into OMP`);
87
+ return;
88
+ }
89
+ const spec = rtkPlatformSpec();
90
+ if (!spec)
91
+ throw new Error(`unsupported platform ${process.platform}/${process.arch}`);
92
+ const release = await fetchJson(RTK_RELEASE_API);
93
+ const asset = (release.assets || []).find((a) => a.name === `rtk-${spec.triple}${spec.ext}`);
94
+ const checksAsset = (release.assets || []).find((a) => a.name === 'checksums.txt');
95
+ if (!asset || !checksAsset)
96
+ throw new Error(`required assets missing in release ${release.tag_name}`);
97
+ const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'tersio-doctor-rtk-'));
98
+ try {
99
+ const archivePath = path.join(tmpDir, asset.name);
100
+ const [checks] = await Promise.all([
101
+ httpsGet(checksAsset.browser_download_url).catch(() => null),
102
+ httpsDownload(asset.browser_download_url, archivePath),
103
+ ]);
104
+ if (checks) {
105
+ const expected = parseChecksum(checks, asset.name);
106
+ if (expected && await sha256File(archivePath) !== expected)
107
+ throw new Error(`checksum mismatch for ${asset.name}`);
108
+ debug('RTK checksum verified');
109
+ }
110
+ const extractDir = path.join(tmpDir, 'extracted');
111
+ await fs.mkdir(extractDir, { recursive: true });
112
+ if (asset.name.endsWith('.zip'))
113
+ await execP('powershell', ['Expand-Archive', '-Path', archivePath, '-DestinationPath', extractDir, '-Force'], { timeout: 60000 });
114
+ else
115
+ await execP('tar', ['xzf', archivePath, '-C', extractDir], { timeout: 60000 });
116
+ const found = await findFile(extractDir, RTK_BINARY_NAME);
117
+ if (!found)
118
+ throw new Error(`${RTK_BINARY_NAME} not found in ${asset.name}`);
119
+ await fs.mkdir(path.dirname(binDest), { recursive: true });
120
+ await fs.copyFile(binDest, `${binDest}.bak`).catch(() => { });
121
+ await fs.copyFile(found, binDest);
122
+ if (process.platform !== 'win32')
123
+ await fs.chmod(binDest, 0o755);
124
+ await execP(binDest, ['--version'], { timeout: 30000, shell: false });
125
+ }
126
+ finally {
127
+ await fs.rm(tmpDir, { recursive: true, force: true }).catch(() => { });
128
+ }
129
+ if (!(await wireRtkOmp(binDest, { dryRun, verbose })))
130
+ throw new Error('rtk wiring failed');
131
+ }
132
+ async function fixPonytail(pluginsDir, agentDir) {
133
+ console.log(' Doctor --fix: refreshing Ponytail package');
134
+ await fs.mkdir(pluginsDir, { recursive: true });
135
+ const pkgPath = path.join(pluginsDir, 'package.json');
136
+ const pkg = await readPluginsPackage(pkgPath);
137
+ pkg.dependencies['@dietrichgebert/ponytail'] = PONYTAIL_GITHUB_SPEC;
138
+ if (dryRun) {
139
+ console.log(` [dry-run] would run: omp plugin install ${PONYTAIL_GITHUB_SPEC}`);
140
+ console.log(` [dry-run] would run: npm install ${PONYTAIL_NPM_SPEC} --save --no-audit --no-fund`);
141
+ return;
142
+ }
143
+ await fs.writeFile(pkgPath, JSON.stringify(pkg, null, 2) + '\n', 'utf8');
144
+ await execNetwork('Installing Ponytail plugin', OMP_BIN, ['plugin', 'install', PONYTAIL_GITHUB_SPEC], { cwd: pluginsDir }).catch((e) => {
145
+ console.log(` [warn] omp plugin install failed: ${e.message}`);
146
+ });
147
+ await execNetwork('Refreshing Ponytail package', 'npm', ['install', PONYTAIL_NPM_SPEC, '--save', '--no-audit', '--no-fund'], { cwd: pluginsDir, timeout: 120000 }).catch((e) => {
148
+ throw new Error(`ponytail refresh failed: ${e.message}`);
149
+ });
150
+ await ensureExtensionInConfig(path.join(agentDir, 'config.yml'), path.join(pluginsDir, 'node_modules', '@dietrichgebert', 'ponytail', 'pi-extension', 'index.js'), 'ponytail', { dryRun, verbose });
151
+ }
152
+ async function fixCli() {
153
+ console.log(' Doctor --fix: updating CLI + add-ons');
154
+ await runLatestUpdate();
155
+ }
156
+ async function runDoctorRepairs(targets) {
157
+ const all = targets.includes('all');
158
+ const want = (t) => all || targets.includes(t);
159
+ if (dryRun)
160
+ console.log('Preview — no changes will be written.\n');
161
+ const failed = [];
162
+ const attempt = async (label, work) => {
163
+ try {
164
+ await work();
165
+ if (!dryRun)
166
+ console.log(` [ok] ${label}`);
167
+ }
168
+ catch (e) {
169
+ failed.push(label);
170
+ console.log(` [fail] ${label}: ${(e.message || '').slice(0, 200)}`);
171
+ }
172
+ };
173
+ if (want('extensions'))
174
+ await attempt('extensions', () => fixExtensions(path.join(OMP_AGENT_DIR, 'extensions')));
175
+ if (want('registrations'))
176
+ await attempt('registrations', () => fixRegistrations(OMP_AGENT_DIR, OMP_PLUGINS_DIR));
177
+ if (want('rtk'))
178
+ await attempt('rtk', () => fixRtk(BUN_BIN_DIR));
179
+ if (want('ponytail'))
180
+ await attempt('ponytail', () => fixPonytail(OMP_PLUGINS_DIR, OMP_AGENT_DIR));
181
+ if (want('cli'))
182
+ await attempt('cli', fixCli);
183
+ return failed;
184
+ }
185
+ export { runDoctorRepairs };
package/cli/doctor.js CHANGED
@@ -1,14 +1,14 @@
1
1
  // cli/doctor.ts — installation health checks.
2
2
  import { promises as fs } from 'node:fs';
3
3
  import path from 'node:path';
4
- import { BUN_BIN_DIR, OMP_AGENT_DIR, OMP_PLUGINS_DIR, OMP_BIN, PACKAGE_NAME, PACKAGE_VERSION, RTK_BINARY_NAME, execP, parseJsonObject, relTime, } from './common.js';
5
- import { runInteractivePhase } from './interactive.js';
4
+ import { BUN_BIN_DIR, OMP_AGENT_DIR, OMP_PLUGINS_DIR, OMP_BIN, PACKAGE_NAME, PACKAGE_VERSION, RTK_BINARY_NAME, args, dryRun, fix, yes, execP, parseJsonObject, relTime, } from './common.js';
5
+ import { askInteractiveChoice, askInteractiveConfirm, runInteractivePhase } from './interactive.js';
6
6
  import { ledgerPath, readUsage, sessionsDir } from '../extensions/shared/usage-ledger.js';
7
7
  import { usageDbPath } from '../extensions/shared/usage-store.js';
8
8
  import { readRtkGain, rtkDbPath } from '../extensions/shared/rtk-gain.js';
9
9
  import { checkForUpdate } from './update.js';
10
10
  import { readTextIfExists } from '../extensions/lib/utils.js';
11
- async function runDoctor() {
11
+ async function runDoctor(recheck = false) {
12
12
  console.log('\n=== Tersio Doctor ===');
13
13
  // Directories
14
14
  const agentDir = OMP_AGENT_DIR;
@@ -156,6 +156,104 @@ async function runDoctor() {
156
156
  check('Ponytail', ponytailPkgText !== null, [parseJsonObject(ponytailPkgText)?.version ?? '', ponytailAge].filter(Boolean).join(', '));
157
157
  const total = tally.ok + tally.missing + tally.warn;
158
158
  console.log(`\n Summary: ${total} checks — ✅ ${tally.ok} ok, ⚠️ ${tally.warn} warn, ❌ ${tally.missing} missing`);
159
+ if (!recheck && tally.missing + tally.warn > 0 && (fix || dryRun)) {
160
+ await runDoctorFix(tally);
161
+ }
162
+ else if (!recheck && tally.missing + tally.warn > 0) {
163
+ console.log('\n Run `tersio doctor --fix` to repair the rows above.');
164
+ }
165
+ return tally;
166
+ }
167
+ function fixScope() {
168
+ const eq = args.find((a) => a.startsWith('--fix='));
169
+ if (eq !== undefined) {
170
+ const raw = eq.slice('--fix='.length).trim().toLowerCase();
171
+ if (raw === '')
172
+ return null;
173
+ return checkFixScope(raw);
174
+ }
175
+ const i = args.indexOf('--fix');
176
+ if (i === -1)
177
+ return null;
178
+ const next = args[i + 1];
179
+ if (next === undefined || next.startsWith('-'))
180
+ return null;
181
+ return checkFixScope(next.trim().toLowerCase());
182
+ }
183
+ function checkFixScope(raw) {
184
+ const valid = ['extensions', 'registrations', 'rtk', 'ponytail', 'cli'];
185
+ if (!valid.includes(raw)) {
186
+ console.error(`[fail] Invalid --fix scope: ${raw}. Valid: ${valid.join(', ')}`);
187
+ process.exit(1);
188
+ }
189
+ return raw;
190
+ }
191
+ async function runDoctorFix(tally) {
192
+ const problems = [];
193
+ if (tally.missing > 0)
194
+ problems.push(`${tally.missing} missing`);
195
+ if (tally.warn > 0)
196
+ problems.push(`${tally.warn} warn`);
197
+ if (dryRun) {
198
+ console.log(`\n [dry-run] would repair ${problems.join(' + ')} (extensions, config.yml registrations, rtk wiring, ponytail refresh, CLI update)`);
199
+ return;
200
+ }
201
+ const scoped = fixScope();
202
+ let targets;
203
+ if (scoped) {
204
+ targets = [scoped];
205
+ if (!yes) {
206
+ const go = await askInteractiveConfirm(`Repair "${scoped}" now?`);
207
+ if (go.status !== 'confirmed' || !go.value) {
208
+ process.exitCode = 130;
209
+ return;
210
+ }
211
+ }
212
+ }
213
+ else if (yes) {
214
+ targets = ['all'];
215
+ }
216
+ else {
217
+ const picked = await askInteractiveChoice('Doctor — what should I repair?', [
218
+ { value: 'all', label: 'Fix everything', hint: `${tally.missing + tally.warn} rows` },
219
+ { value: 'extensions', label: 'Missing extension files', hint: 'copy sources from this CLI' },
220
+ { value: 'registrations', label: 'config.yml registrations', hint: 'combo, ponytail, rtk.ts entries' },
221
+ { value: 'rtk', label: 'RTK binary + wiring', hint: 'download checksum-verified binary, rtk init' },
222
+ { value: 'ponytail', label: 'Ponytail package', hint: 'omp plugin install + npm refresh' },
223
+ { value: 'cli', label: 'CLI update', hint: 'latest tersio + delegated refresh' },
224
+ ], 'all');
225
+ if (picked.status !== 'selected') {
226
+ process.exitCode = 130;
227
+ return;
228
+ }
229
+ if (picked.value === 'all')
230
+ targets = ['all'];
231
+ else {
232
+ const go = await askInteractiveConfirm(`Repair "${picked.value}" now?`);
233
+ if (go.status !== 'confirmed' || !go.value) {
234
+ process.exitCode = 130;
235
+ return;
236
+ }
237
+ targets = [picked.value];
238
+ }
239
+ }
240
+ const { runDoctorRepairs } = await import('./doctor-fix.js');
241
+ const failed = await runDoctorRepairs(targets);
242
+ if (failed.length > 0) {
243
+ console.log(`\n Doctor --fix: ${failed.length} repair(s) failed (${failed.join(', ')}) — rerun or see [fail] lines above.`);
244
+ process.exitCode = 1;
245
+ return;
246
+ }
247
+ if (targets.includes('cli')) {
248
+ console.log('\n Doctor --fix: CLI refresh delegated — rerun `tersio doctor` after it lands.');
249
+ return;
250
+ }
251
+ console.log('\n Doctor --fix: repairs done — rechecking.');
252
+ const again = await runDoctor(true);
253
+ if (again.missing + again.warn === 0)
254
+ console.log(' Doctor --fix: all checks pass. Restart OMP.');
255
+ else
256
+ console.log(` Doctor --fix: still ${again.missing} missing + ${again.warn} warn — rerun or repair manually.`);
159
257
  }
160
258
  function section(name) {
161
259
  console.log(`\n${name}`);
package/cli/install.js CHANGED
@@ -551,10 +551,25 @@ async function runCommandMenu() {
551
551
  await runInstall({ reinstall: true });
552
552
  closeRL();
553
553
  break;
554
- case 'doctor':
555
- await runDoctor();
554
+ case 'doctor': {
555
+ const summary = await runDoctor();
556
+ if (!dryRun && summary.missing + summary.warn > 0) {
557
+ const fixIt = await askInteractiveConfirm('Doctor found problems — repair them now?');
558
+ if (fixIt.status === 'confirmed' && fixIt.value) {
559
+ const { runDoctorRepairs } = await import('./doctor-fix.js');
560
+ const failed = await runDoctorRepairs(['all']);
561
+ if (failed.length === 0) {
562
+ console.log('\n Repairs done — rechecking.');
563
+ await runDoctor();
564
+ }
565
+ else {
566
+ console.log(`\n ${failed.length} repair(s) failed (${failed.join(', ')}) — see [fail] lines above.`);
567
+ }
568
+ }
569
+ }
556
570
  closeRL();
557
571
  break;
572
+ }
558
573
  case 'usage':
559
574
  await runUsage();
560
575
  closeRL();
package/cli/rtk-wiring.js CHANGED
@@ -55,7 +55,7 @@ function rtkExtensionPath() {
55
55
  }
56
56
  // OMP loads only listed extensions: append rtk.ts after the existing entries
57
57
  // (or create the key) so a fresh wire takes effect on next OMP start.
58
- async function ensureRtkInConfig(options) {
58
+ export async function ensureRtkInConfig(options) {
59
59
  const home = process.env.HOME || process.env.USERPROFILE || os.homedir();
60
60
  const configPath = path.join(home, '.omp', 'agent', 'config.yml');
61
61
  const extPath = rtkExtensionPath().replace(/\\/g, '/');
package/cli/update.js CHANGED
@@ -166,6 +166,25 @@ async function runLatestUpdate() {
166
166
  return;
167
167
  }
168
168
  console.log(`Updating ${stale.join(', ')}`);
169
+ // Broken payloads (v2.20.0) crash on boot — smoke-check the target before touching anything installed.
170
+ const smokeArgs = IS_WINDOWS
171
+ ? ['/d', '/s', '/c', 'npm', 'exec', '--yes', '--prefer-online', `--package=${PACKAGE_NAME}${target}`, '--', PACKAGE_BIN, '--version']
172
+ : ['exec', '--yes', '--prefer-online', `--package=${PACKAGE_NAME}${target}`, '--', PACKAGE_BIN, '--version'];
173
+ try {
174
+ await execP(npmCommand, smokeArgs, {
175
+ timeout: 120000,
176
+ maxBuffer: 1024 * 1024,
177
+ windowsHide: true,
178
+ shell: false,
179
+ });
180
+ }
181
+ catch (e) {
182
+ console.error(`[fail] Update payload ${PACKAGE_NAME}${target} failed its smoke check — staying on ${PACKAGE_VERSION}.`);
183
+ console.error(`[fail] ${e.message.split('\n')[0]}`);
184
+ console.error(`[hint] Retry once released fixed; manual: npm install -g ${PACKAGE_NAME}@latest --no-audit --no-fund --prefer-online`);
185
+ process.exitCode = 1;
186
+ return;
187
+ }
169
188
  // The npx delegation below only refreshes the OMP-side files; the globally
170
189
  // installed CLI keeps its old version until npm -g runs. Refresh both.
171
190
  const globalArgs = IS_WINDOWS