@krtclcdy/tersio 2.20.1 → 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 +1 -1
- package/cli/common.js +2 -1
- package/cli/doctor-fix.js +185 -0
- package/cli/doctor.js +101 -3
- package/cli/install.js +17 -2
- package/cli/rtk-wiring.js +1 -1
- package/cli/update.js +19 -0
- package/package.json +1 -1
- package/tersio.js +2 -1
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
|
package/package.json
CHANGED
package/tersio.js
CHANGED
|
@@ -19,7 +19,7 @@ Commands:
|
|
|
19
19
|
install Install the add-ons (user scope: all OMP sessions)
|
|
20
20
|
update Refresh the CLI and add-ons (RTK binary, Caveman rule, Ponytail)
|
|
21
21
|
reinstall Clean and reinstall the user-scope add-ons
|
|
22
|
-
doctor Check the current installation
|
|
22
|
+
doctor Check the current installation (--fix repairs, --dry-run previews)
|
|
23
23
|
usage Ledger-backed usage + savings report
|
|
24
24
|
gain Open the gain dashboard (localhost only)
|
|
25
25
|
reset Clear tersio statistics (usage ledger)
|
|
@@ -29,6 +29,7 @@ Commands:
|
|
|
29
29
|
help Show this help
|
|
30
30
|
|
|
31
31
|
Options:
|
|
32
|
+
--fix (doctor: repairs all; --fix=<scope> repairs one of extensions, registrations, rtk, ponytail, cli)
|
|
32
33
|
--scope user (legacy; accepted and ignored — user scope is the only scope)
|
|
33
34
|
--keep-ponytail (uninstall: keep the Ponytail plugin — removed by default)
|
|
34
35
|
--remove-rtk (uninstall: also remove the RTK binary)
|