@krtclcdy/tersio 2.20.1 → 2.22.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 +8 -9
- package/cli/common.js +14 -1
- package/cli/dashboard.js +367 -35
- package/cli/doctor-fix.js +185 -0
- package/cli/doctor.js +120 -53
- package/cli/install.js +46 -60
- package/cli/rtk-wiring.js +1 -1
- package/cli/settings.js +128 -5
- package/cli/uninstall.js +4 -4
- package/cli/update.js +29 -20
- package/cli/usage.js +1 -1
- package/dashboard/{app.js → charts.js} +74 -361
- package/dashboard/core.js +357 -0
- package/dashboard/settings.js +166 -0
- package/dashboard/share.js +218 -0
- package/dashboard/styles.css +87 -12
- package/dashboard/template.html +162 -57
- package/extensions/ai-addons-updater/index.js +13 -30
- package/extensions/ai-addons-updater/index.ts +11 -29
- package/extensions/lib/utils.js +26 -1
- package/extensions/lib/utils.ts +28 -1
- package/extensions/shared/pricing.js +2 -2
- package/extensions/shared/pricing.ts +2 -1
- package/extensions/shared/usage-ledger.js +3 -2
- package/extensions/shared/usage-ledger.ts +3 -2
- package/extensions/shared/usage-store.js +7 -7
- package/extensions/shared/usage-store.ts +7 -6
- package/gain/dist/brand.webp +0 -0
- package/gain/dist/index.html +65 -0
- package/gain/dist/vite.svg +1 -0
- package/package.json +17 -34
- package/tersio.js +3 -2
|
@@ -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, 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
|
+
async function fixExtensions(extDir) {
|
|
27
|
+
console.log(' Doctor --fix: restoring extension files');
|
|
28
|
+
await fs.mkdir(extDir, { recursive: true });
|
|
29
|
+
const texts = await Promise.all(SOURCES.map(([from]) => readTextIfExists(from)));
|
|
30
|
+
const writes = [];
|
|
31
|
+
for (let i = 0; i < SOURCES.length; i++) {
|
|
32
|
+
if (texts[i] !== null)
|
|
33
|
+
writes.push(writeIfChanged(path.join(extDir, SOURCES[i][1]), texts[i], { dryRun, verbose }));
|
|
34
|
+
else
|
|
35
|
+
console.log(` [warn] bundled source missing: ${SOURCES[i][0]}`);
|
|
36
|
+
}
|
|
37
|
+
await Promise.all(writes);
|
|
38
|
+
if ((await readTextIfExists(path.join(extDir, 'caveman-session', 'rule.md'))) === null) {
|
|
39
|
+
const rule = await Promise.race([
|
|
40
|
+
httpsGet(CAVEMAN_REMOTE_RULE).catch(() => null),
|
|
41
|
+
new Promise((resolve) => setTimeout(() => resolve(null), 8000)),
|
|
42
|
+
]);
|
|
43
|
+
if (rule !== null)
|
|
44
|
+
await writeIfChanged(path.join(extDir, 'caveman-session', 'rule.md'), rule, { dryRun, verbose });
|
|
45
|
+
else
|
|
46
|
+
console.log(' [warn] Caveman rule unreachable — kept existing rule.md');
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
async function fixRegistrations(agentDir, pluginsDir) {
|
|
50
|
+
console.log(' Doctor --fix: repairing config.yml registrations');
|
|
51
|
+
const configPath = path.join(agentDir, 'config.yml');
|
|
52
|
+
if ((await readTextIfExists(configPath)) === null) {
|
|
53
|
+
if (dryRun)
|
|
54
|
+
console.log(` [dry-run] would create ${configPath}`);
|
|
55
|
+
else {
|
|
56
|
+
await fs.mkdir(agentDir, { recursive: true });
|
|
57
|
+
await fs.writeFile(configPath, 'extensions:\n', 'utf8');
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
const extDir = path.join(agentDir, 'extensions');
|
|
61
|
+
const ponytailExtPath = path.join(pluginsDir, 'node_modules', '@dietrichgebert', 'ponytail', 'pi-extension', 'index.js');
|
|
62
|
+
await ensureExtensionInConfig(configPath, ponytailExtPath, 'ponytail', { dryRun, verbose });
|
|
63
|
+
await ensureExtensionInConfig(configPath, path.join(extDir, 'combo-toggle', 'index.ts'), 'combo', { dryRun, verbose });
|
|
64
|
+
const rtkTs = path.join(extDir, 'rtk.ts');
|
|
65
|
+
if ((await readTextIfExists(rtkTs)) !== null)
|
|
66
|
+
await ensureRtkInConfig({ dryRun, verbose });
|
|
67
|
+
await ensureExtensionAfterConfigEntry(configPath, path.join(extDir, 'shared', 'mode-reinforcement.ts'), ponytailExtPath, 'mode reinforcement', { dryRun, verbose });
|
|
68
|
+
const pkgPath = path.join(pluginsDir, 'package.json');
|
|
69
|
+
const pkg = await readPluginsPackage(pkgPath);
|
|
70
|
+
if (!(PACKAGE_NAME in pkg.dependencies)) {
|
|
71
|
+
pkg.dependencies[PACKAGE_NAME] = `^${PACKAGE_VERSION}`;
|
|
72
|
+
if (dryRun)
|
|
73
|
+
console.log(` [dry-run] would register ${PACKAGE_NAME} in ${pkgPath}`);
|
|
74
|
+
else {
|
|
75
|
+
await fs.mkdir(pluginsDir, { recursive: true });
|
|
76
|
+
await fs.writeFile(pkgPath, JSON.stringify(pkg, null, 2) + '\n', 'utf8');
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
async function fixRtk(binDir) {
|
|
81
|
+
console.log(' Doctor --fix: repairing RTK binary + wiring');
|
|
82
|
+
const binDest = path.join(binDir, RTK_BINARY_NAME);
|
|
83
|
+
if (dryRun) {
|
|
84
|
+
console.log(` [dry-run] would download a checksum-verified rtk to ${binDest} and wire it into OMP`);
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
const spec = rtkPlatformSpec();
|
|
88
|
+
if (!spec)
|
|
89
|
+
throw new Error(`unsupported platform ${process.platform}/${process.arch}`);
|
|
90
|
+
const release = await fetchJson(RTK_RELEASE_API);
|
|
91
|
+
const asset = (release.assets || []).find((a) => a.name === `rtk-${spec.triple}${spec.ext}`);
|
|
92
|
+
const checksAsset = (release.assets || []).find((a) => a.name === 'checksums.txt');
|
|
93
|
+
if (!asset || !checksAsset)
|
|
94
|
+
throw new Error(`required assets missing in release ${release.tag_name}`);
|
|
95
|
+
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'tersio-doctor-rtk-'));
|
|
96
|
+
try {
|
|
97
|
+
const archivePath = path.join(tmpDir, asset.name);
|
|
98
|
+
const [checks] = await Promise.all([
|
|
99
|
+
httpsGet(checksAsset.browser_download_url).catch(() => null),
|
|
100
|
+
httpsDownload(asset.browser_download_url, archivePath),
|
|
101
|
+
]);
|
|
102
|
+
if (checks) {
|
|
103
|
+
const expected = parseChecksum(checks, asset.name);
|
|
104
|
+
if (expected && await sha256File(archivePath) !== expected)
|
|
105
|
+
throw new Error(`checksum mismatch for ${asset.name}`);
|
|
106
|
+
debug('RTK checksum verified');
|
|
107
|
+
}
|
|
108
|
+
const extractDir = path.join(tmpDir, 'extracted');
|
|
109
|
+
await fs.mkdir(extractDir, { recursive: true });
|
|
110
|
+
if (asset.name.endsWith('.zip'))
|
|
111
|
+
await execP('powershell', ['Expand-Archive', '-Path', archivePath, '-DestinationPath', extractDir, '-Force'], { timeout: 60000 });
|
|
112
|
+
else
|
|
113
|
+
await execP('tar', ['xzf', archivePath, '-C', extractDir], { timeout: 60000 });
|
|
114
|
+
const found = await findFile(extractDir, RTK_BINARY_NAME);
|
|
115
|
+
if (!found)
|
|
116
|
+
throw new Error(`${RTK_BINARY_NAME} not found in ${asset.name}`);
|
|
117
|
+
await fs.mkdir(path.dirname(binDest), { recursive: true });
|
|
118
|
+
await fs.copyFile(binDest, `${binDest}.bak`).catch(() => { });
|
|
119
|
+
await fs.copyFile(found, binDest);
|
|
120
|
+
if (process.platform !== 'win32')
|
|
121
|
+
await fs.chmod(binDest, 0o755);
|
|
122
|
+
await execP(binDest, ['--version'], { timeout: 30000, shell: false });
|
|
123
|
+
}
|
|
124
|
+
finally {
|
|
125
|
+
await fs.rm(tmpDir, { recursive: true, force: true }).catch(() => { });
|
|
126
|
+
}
|
|
127
|
+
if (!(await wireRtkOmp(binDest, { dryRun, verbose })))
|
|
128
|
+
throw new Error('rtk wiring failed');
|
|
129
|
+
}
|
|
130
|
+
async function fixPonytail(pluginsDir, agentDir) {
|
|
131
|
+
console.log(' Doctor --fix: restoring bundled Ponytail');
|
|
132
|
+
await fs.mkdir(pluginsDir, { recursive: true });
|
|
133
|
+
const pkgPath = path.join(pluginsDir, 'package.json');
|
|
134
|
+
const pkg = await readPluginsPackage(pkgPath);
|
|
135
|
+
// Drop the legacy separate entry: ponytail is a tersio dependency now.
|
|
136
|
+
if ('@dietrichgebert/ponytail' in pkg.dependencies) {
|
|
137
|
+
delete pkg.dependencies['@dietrichgebert/ponytail'];
|
|
138
|
+
console.log(' [migrate] dropped separate @dietrichgebert/ponytail dependency (now bundled with tersio)');
|
|
139
|
+
}
|
|
140
|
+
if (!(PACKAGE_NAME in pkg.dependencies))
|
|
141
|
+
pkg.dependencies[PACKAGE_NAME] = `^${PACKAGE_VERSION}`;
|
|
142
|
+
if (dryRun) {
|
|
143
|
+
console.log(' [dry-run] would run: npm install --no-audit --no-fund (in plugins dir)');
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
await fs.writeFile(pkgPath, JSON.stringify(pkg, null, 2) + '\n', 'utf8');
|
|
147
|
+
await execNetwork('Restoring bundled Ponytail', 'npm', ['install', '--no-audit', '--no-fund'], { cwd: pluginsDir, timeout: 180000 }).catch((e) => {
|
|
148
|
+
throw new Error(`bundled ponytail restore 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,12 @@
|
|
|
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,
|
|
5
|
-
import { runInteractivePhase } from './interactive.js';
|
|
6
|
-
import { ledgerPath, readUsage, sessionsDir } from '../extensions/shared/usage-ledger.js';
|
|
4
|
+
import { BUN_BIN_DIR, OMP_AGENT_DIR, OMP_PLUGINS_DIR, RTK_BINARY_NAME, args, dryRun, fix, yes, execP, parseJsonObject, relTime, } from './common.js';
|
|
5
|
+
import { askInteractiveChoice, askInteractiveConfirm, runInteractivePhase } from './interactive.js';
|
|
7
6
|
import { usageDbPath } from '../extensions/shared/usage-store.js';
|
|
8
|
-
import {
|
|
9
|
-
import { checkForUpdate } from './update.js';
|
|
7
|
+
import { pricesCachePath } from '../extensions/shared/pricing.js';
|
|
10
8
|
import { readTextIfExists } from '../extensions/lib/utils.js';
|
|
11
|
-
async function runDoctor() {
|
|
9
|
+
async function runDoctor(recheck = false) {
|
|
12
10
|
console.log('\n=== Tersio Doctor ===');
|
|
13
11
|
// Directories
|
|
14
12
|
const agentDir = OMP_AGENT_DIR;
|
|
@@ -23,60 +21,43 @@ async function runDoctor() {
|
|
|
23
21
|
const cavemanRule = path.join(extDir, 'caveman-session', 'rule.md');
|
|
24
22
|
const rtkIndex = path.join(extDir, 'rtk-session', 'index.ts');
|
|
25
23
|
const updaterIndex = path.join(extDir, 'ai-addons-updater', 'index.ts');
|
|
26
|
-
const comboIndex = path.join(extDir, 'combo-toggle', 'index.ts');
|
|
27
|
-
const tersioIndex = path.join(extDir, 'tersio-commands', 'index.ts');
|
|
28
|
-
const modeReinforcement = path.join(extDir, 'shared', 'mode-reinforcement.ts');
|
|
29
|
-
const selfPkg = path.join(pluginsDir, 'node_modules', PACKAGE_NAME, 'package.json');
|
|
30
24
|
// Independent probes start concurrently; sections report in fixed order as
|
|
31
25
|
// their data settles. Every probe resolves instead of rejecting.
|
|
32
26
|
const probes = {
|
|
33
|
-
ompVersion: execP(OMP_BIN, ['--version']).then((r) => r.stdout.trim(), () => null),
|
|
34
27
|
agentEntries: fs.readdir(agentDir).catch(() => null),
|
|
35
28
|
extEntries: fs.readdir(extDir).catch(() => null),
|
|
36
29
|
sharedStateText: readTextIfExists(path.join(extDir, 'shared', 'session-state.ts')),
|
|
37
30
|
configText: readTextIfExists(configPath),
|
|
38
31
|
ponytailPkgText: readTextIfExists(ponytailPkg),
|
|
39
32
|
ponytailExtText: readTextIfExists(ponytailExt),
|
|
40
|
-
pluginsPkgRaw: readTextIfExists(path.join(pluginsDir, 'package.json')),
|
|
41
|
-
selfPkgText: readTextIfExists(selfPkg),
|
|
42
33
|
rtkBinText: readTextIfExists(rtkBin),
|
|
43
34
|
rtkOmpText: readTextIfExists(path.join(extDir, 'rtk.ts')),
|
|
44
35
|
cavemanIndexText: readTextIfExists(cavemanIndex),
|
|
45
36
|
cavemanRuleText: readTextIfExists(cavemanRule),
|
|
46
37
|
rtkIndexText: readTextIfExists(rtkIndex),
|
|
47
38
|
updaterIndexText: readTextIfExists(updaterIndex),
|
|
48
|
-
comboIndexText: readTextIfExists(comboIndex),
|
|
49
|
-
tersioIndexText: readTextIfExists(tersioIndex),
|
|
50
|
-
modeReinforcementText: readTextIfExists(modeReinforcement),
|
|
51
39
|
ruleMtime: fs.stat(cavemanRule).catch(() => null),
|
|
52
40
|
rtkMtime: fs.stat(rtkBin).catch(() => null),
|
|
53
41
|
ponytailMtime: fs.stat(ponytailPkg).catch(() => null),
|
|
54
|
-
|
|
42
|
+
pricesMtime: fs.stat(pricesCachePath()).catch(() => null),
|
|
55
43
|
};
|
|
56
44
|
const rtkVersionProbe = probes.rtkBinText.then((text) => text === null ? null : execP(rtkBin, ['--version'], { timeout: 5000 }).then((r) => r.stdout.trim() || r.stderr.trim() || null, (e) => {
|
|
57
45
|
const err = e;
|
|
58
46
|
return err.stdout?.trim() || err.stderr?.trim() || null;
|
|
59
47
|
}));
|
|
60
|
-
const [[
|
|
48
|
+
const [[agentEntries, extEntries, sharedStateText, configText], [cavemanIndexText, rtkIndexText, updaterIndexText, ponytailPkgText, ponytailExtText], [cavemanRuleText, ruleMtime, rtkBinText, rtkMtime, rtkVersion, rtkOmpText, ponytailMtime, pricesMtime]] = await runInteractivePhase('Checking installation', () => Promise.all([
|
|
61
49
|
Promise.all([
|
|
62
|
-
probes.ompVersion,
|
|
63
50
|
probes.agentEntries,
|
|
64
51
|
probes.extEntries,
|
|
65
52
|
probes.sharedStateText,
|
|
66
53
|
probes.configText,
|
|
67
|
-
probes.updateVersion,
|
|
68
54
|
]),
|
|
69
55
|
Promise.all([
|
|
70
56
|
probes.cavemanIndexText,
|
|
71
57
|
probes.rtkIndexText,
|
|
72
58
|
probes.updaterIndexText,
|
|
73
|
-
probes.comboIndexText,
|
|
74
|
-
probes.tersioIndexText,
|
|
75
|
-
probes.modeReinforcementText,
|
|
76
59
|
probes.ponytailPkgText,
|
|
77
60
|
probes.ponytailExtText,
|
|
78
|
-
probes.pluginsPkgRaw,
|
|
79
|
-
probes.selfPkgText,
|
|
80
61
|
]),
|
|
81
62
|
Promise.all([
|
|
82
63
|
probes.cavemanRuleText,
|
|
@@ -86,11 +67,18 @@ async function runDoctor() {
|
|
|
86
67
|
rtkVersionProbe,
|
|
87
68
|
probes.rtkOmpText,
|
|
88
69
|
probes.ponytailMtime,
|
|
70
|
+
probes.pricesMtime,
|
|
89
71
|
]),
|
|
90
72
|
]));
|
|
91
73
|
// Categorized output with a tally; success rows stay quiet (no path echoes)
|
|
92
74
|
// while failures print the expected path or fix so they stay actionable.
|
|
93
75
|
const tally = { ok: 0, missing: 0, warn: 0 };
|
|
76
|
+
function absDate(ms) {
|
|
77
|
+
const d = new Date(ms);
|
|
78
|
+
const q = (n) => String(n).padStart(2, '0');
|
|
79
|
+
const h24 = d.getHours();
|
|
80
|
+
return `${q(d.getMonth() + 1)}-${q(d.getDate())}-${d.getFullYear()}, ${q(h24 % 12 || 12)}:${q(d.getMinutes())} ${h24 >= 12 ? 'PM' : 'AM'}`;
|
|
81
|
+
}
|
|
94
82
|
function check(label, ok, detail = '') {
|
|
95
83
|
if (ok) {
|
|
96
84
|
tally.ok++;
|
|
@@ -107,13 +95,6 @@ async function runDoctor() {
|
|
|
107
95
|
}
|
|
108
96
|
section('Environment');
|
|
109
97
|
check('Node', true, process.version);
|
|
110
|
-
check('OMP CLI', ompVersion !== null, ompVersion ?? '');
|
|
111
|
-
if (typeof updateVersion === 'string')
|
|
112
|
-
warnLine('Tersio CLI', `${updateVersion} available — run tersio update`);
|
|
113
|
-
else if (updateVersion === 'unknown')
|
|
114
|
-
warnLine('Tersio CLI', `${PACKAGE_VERSION} (version check unreachable)`);
|
|
115
|
-
else
|
|
116
|
-
check('Tersio CLI', true, PACKAGE_VERSION);
|
|
117
98
|
section('Installation');
|
|
118
99
|
check('OMP agent dir', agentEntries !== null, agentEntries === null ? agentDir : '');
|
|
119
100
|
check('OMP extensions dir', extEntries !== null, extEntries === null ? extDir : '');
|
|
@@ -123,39 +104,125 @@ async function runDoctor() {
|
|
|
123
104
|
check('Caveman extension', cavemanIndexText !== null);
|
|
124
105
|
check('RTK extension', rtkIndexText !== null);
|
|
125
106
|
check('Updater extension', updaterIndexText !== null);
|
|
126
|
-
check('Combo extension', comboIndexText !== null);
|
|
127
|
-
check('Tersio commands extension', tersioIndexText !== null);
|
|
128
|
-
check('Mode reinforcement extension', modeReinforcementText !== null);
|
|
129
107
|
check('Ponytail extension', ponytailExtText !== null);
|
|
130
|
-
check('Ponytail in config.yml', (configText ?? '').includes('ponytail') && (configText ?? '').includes('pi-extension'));
|
|
131
|
-
check('Combo in config.yml', (configText ?? '').includes('combo-toggle'));
|
|
132
|
-
check('Self plugin package', selfPkgText !== null, parseJsonObject(selfPkgText)?.version ?? '');
|
|
133
|
-
const selfDep = PACKAGE_NAME in (parseJsonObject(pluginsPkgRaw)?.dependencies || {});
|
|
134
|
-
check('Self plugin in plugins/package.json', selfDep);
|
|
135
108
|
section('Usage & records');
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
console.log(` Session transcripts (host-owned, never touched): ${sessionsDir()}`);
|
|
140
|
-
console.log(` Usage DB (tersio-owned, tersio reset clears): ${usageDbPath()}`);
|
|
141
|
-
const rtkDb = rtkDbPath();
|
|
142
|
-
const rtk = readRtkGain();
|
|
143
|
-
console.log(` RTK history (rtk-owned, never touched): ${rtkDb}${rtk.commands ? ` · ${rtk.commands} commands` : ''}`);
|
|
109
|
+
console.log(` Usage DB (tersio-owned · local hosted): ${usageDbPath()}`);
|
|
110
|
+
const pricesAge = pricesMtime ? `(pulled ${relTime(Date.now() - pricesMtime.mtimeMs)} · ${absDate(pricesMtime.mtimeMs)})` : '';
|
|
111
|
+
check('Prices feed', pricesMtime !== null, pricesAge || pricesCachePath());
|
|
144
112
|
section('Add-ons');
|
|
145
|
-
const ruleAge = ruleMtime ? `updated ${relTime(Date.now() - ruleMtime.mtimeMs)}` : '';
|
|
113
|
+
const ruleAge = ruleMtime ? `(updated ${relTime(Date.now() - ruleMtime.mtimeMs)} · ${absDate(ruleMtime.mtimeMs)})` : '';
|
|
146
114
|
check('Caveman rule', cavemanRuleText !== null, ruleAge);
|
|
147
|
-
const rtkAge = rtkMtime ? `updated ${relTime(Date.now() - rtkMtime.mtimeMs)}` : '';
|
|
148
|
-
check('RTK binary', rtkBinText !== null, rtkBinText === null ? rtkBin : [rtkVersion, rtkAge].filter(Boolean).join('
|
|
115
|
+
const rtkAge = rtkMtime ? `(updated ${relTime(Date.now() - rtkMtime.mtimeMs)} · ${absDate(rtkMtime.mtimeMs)})` : '';
|
|
116
|
+
check('RTK binary', rtkBinText !== null, rtkBinText === null ? rtkBin : [rtkVersion, rtkAge].filter(Boolean).join(' '));
|
|
149
117
|
if (rtkBinText !== null && !rtkVersion)
|
|
150
118
|
warnLine('RTK version', 'unavailable — binary may not be executable');
|
|
151
119
|
const rtkRegistered = rtkOmpText !== null && (configText ?? '').includes('extensions/rtk.ts');
|
|
152
120
|
check('RTK OMP wiring (rtk.ts)', rtkOmpText !== null, rtkOmpText === null ? 'run: rtk init -g --agent omp' : '');
|
|
153
121
|
if (rtkOmpText !== null && !rtkRegistered)
|
|
154
122
|
warnLine('RTK in config.yml', 'rtk.ts not listed — OMP will not load it; rerun install');
|
|
155
|
-
const ponytailAge = ponytailMtime ? `updated ${relTime(Date.now() - ponytailMtime.mtimeMs)}` : '';
|
|
156
|
-
|
|
123
|
+
const ponytailAge = ponytailMtime ? `(updated ${relTime(Date.now() - ponytailMtime.mtimeMs)} · ${absDate(ponytailMtime.mtimeMs)})` : '';
|
|
124
|
+
const ponytailVer = parseJsonObject(ponytailPkgText)?.version ?? '';
|
|
125
|
+
check('Ponytail', ponytailPkgText !== null, [ponytailVer, ponytailAge].filter(Boolean).join(' '));
|
|
157
126
|
const total = tally.ok + tally.missing + tally.warn;
|
|
158
127
|
console.log(`\n Summary: ${total} checks — ✅ ${tally.ok} ok, ⚠️ ${tally.warn} warn, ❌ ${tally.missing} missing`);
|
|
128
|
+
if (!recheck && tally.missing + tally.warn > 0 && (fix || dryRun)) {
|
|
129
|
+
await runDoctorFix(tally);
|
|
130
|
+
}
|
|
131
|
+
else if (!recheck && tally.missing + tally.warn > 0) {
|
|
132
|
+
console.log('\n Run `tersio doctor --fix` to repair the rows above.');
|
|
133
|
+
}
|
|
134
|
+
return tally;
|
|
135
|
+
}
|
|
136
|
+
function fixScope() {
|
|
137
|
+
const eq = args.find((a) => a.startsWith('--fix='));
|
|
138
|
+
if (eq !== undefined) {
|
|
139
|
+
const raw = eq.slice('--fix='.length).trim().toLowerCase();
|
|
140
|
+
if (raw === '')
|
|
141
|
+
return null;
|
|
142
|
+
return checkFixScope(raw);
|
|
143
|
+
}
|
|
144
|
+
const i = args.indexOf('--fix');
|
|
145
|
+
if (i === -1)
|
|
146
|
+
return null;
|
|
147
|
+
const next = args[i + 1];
|
|
148
|
+
if (next === undefined || next.startsWith('-'))
|
|
149
|
+
return null;
|
|
150
|
+
return checkFixScope(next.trim().toLowerCase());
|
|
151
|
+
}
|
|
152
|
+
function checkFixScope(raw) {
|
|
153
|
+
const valid = ['extensions', 'registrations', 'rtk', 'ponytail', 'cli'];
|
|
154
|
+
if (!valid.includes(raw)) {
|
|
155
|
+
console.error(`[fail] Invalid --fix scope: ${raw}. Valid: ${valid.join(', ')}`);
|
|
156
|
+
process.exit(1);
|
|
157
|
+
}
|
|
158
|
+
return raw;
|
|
159
|
+
}
|
|
160
|
+
async function runDoctorFix(tally) {
|
|
161
|
+
const problems = [];
|
|
162
|
+
if (tally.missing > 0)
|
|
163
|
+
problems.push(`${tally.missing} missing`);
|
|
164
|
+
if (tally.warn > 0)
|
|
165
|
+
problems.push(`${tally.warn} warn`);
|
|
166
|
+
if (dryRun) {
|
|
167
|
+
console.log(`\n [dry-run] would repair ${problems.join(' + ')} (extensions, config.yml registrations, rtk wiring, ponytail refresh, CLI update)`);
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
const scoped = fixScope();
|
|
171
|
+
let targets;
|
|
172
|
+
if (scoped) {
|
|
173
|
+
targets = [scoped];
|
|
174
|
+
if (!yes) {
|
|
175
|
+
const go = await askInteractiveConfirm(`Repair "${scoped}" now?`);
|
|
176
|
+
if (go.status !== 'confirmed' || !go.value) {
|
|
177
|
+
process.exitCode = 130;
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
else if (yes) {
|
|
183
|
+
targets = ['all'];
|
|
184
|
+
}
|
|
185
|
+
else {
|
|
186
|
+
const picked = await askInteractiveChoice('Doctor — what should I repair?', [
|
|
187
|
+
{ value: 'all', label: 'Fix everything', hint: `${tally.missing + tally.warn} rows` },
|
|
188
|
+
{ value: 'extensions', label: 'Missing extension files', hint: 'copy sources from this CLI' },
|
|
189
|
+
{ value: 'registrations', label: 'config.yml registrations', hint: 'combo, ponytail, rtk.ts entries' },
|
|
190
|
+
{ value: 'rtk', label: 'RTK binary + wiring', hint: 'download checksum-verified binary, rtk init' },
|
|
191
|
+
{ value: 'ponytail', label: 'Ponytail package', hint: 'bundled reinstall via tersio dep' },
|
|
192
|
+
{ value: 'cli', label: 'CLI update', hint: 'latest tersio + delegated refresh' },
|
|
193
|
+
], 'all');
|
|
194
|
+
if (picked.status !== 'selected') {
|
|
195
|
+
process.exitCode = 130;
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
if (picked.value === 'all')
|
|
199
|
+
targets = ['all'];
|
|
200
|
+
else {
|
|
201
|
+
const go = await askInteractiveConfirm(`Repair "${picked.value}" now?`);
|
|
202
|
+
if (go.status !== 'confirmed' || !go.value) {
|
|
203
|
+
process.exitCode = 130;
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
targets = [picked.value];
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
const { runDoctorRepairs } = await import('./doctor-fix.js');
|
|
210
|
+
const failed = await runDoctorRepairs(targets);
|
|
211
|
+
if (failed.length > 0) {
|
|
212
|
+
console.log(`\n Doctor --fix: ${failed.length} repair(s) failed (${failed.join(', ')}) — rerun or see [fail] lines above.`);
|
|
213
|
+
process.exitCode = 1;
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
if (targets.includes('cli')) {
|
|
217
|
+
console.log('\n Doctor --fix: CLI refresh delegated — rerun `tersio doctor` after it lands.');
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
console.log('\n Doctor --fix: repairs done — rechecking.');
|
|
221
|
+
const again = await runDoctor(true);
|
|
222
|
+
if (again.missing + again.warn === 0)
|
|
223
|
+
console.log(' Doctor --fix: all checks pass. Restart OMP.');
|
|
224
|
+
else
|
|
225
|
+
console.log(` Doctor --fix: still ${again.missing} missing + ${again.warn} warn — rerun or repair manually.`);
|
|
159
226
|
}
|
|
160
227
|
function section(name) {
|
|
161
228
|
console.log(`\n${name}`);
|
package/cli/install.js
CHANGED
|
@@ -31,98 +31,69 @@ const SHARED_PLUGIN_SETTINGS = path.join(EXT_DIR, 'shared', 'plugin-settings.ts'
|
|
|
31
31
|
const SHARED_USAGE_LEDGER = path.join(EXT_DIR, 'shared', 'usage-ledger.ts');
|
|
32
32
|
const SHARED_PRICING = path.join(EXT_DIR, 'shared', 'pricing.ts');
|
|
33
33
|
const SHARED_CARBON = path.join(EXT_DIR, 'shared', 'carbon.ts');
|
|
34
|
-
const PONYTAIL_GITHUB_SPEC = 'github:DietrichGebert/ponytail';
|
|
35
|
-
const PONYTAIL_NPM_SPEC = '@dietrichgebert/ponytail@latest';
|
|
36
34
|
async function stepPonytail(pluginsDir, userDir, options) {
|
|
37
35
|
if (!options.quiet)
|
|
38
|
-
console.log(' Ponytail —
|
|
36
|
+
console.log(' Ponytail — ensure bundled plugin');
|
|
39
37
|
await fs.mkdir(pluginsDir, { recursive: true });
|
|
40
38
|
const pkgPath = path.join(pluginsDir, 'package.json');
|
|
41
39
|
const pkg = await readPluginsPackage(pkgPath);
|
|
42
|
-
|
|
40
|
+
// Migration: ponytail was a separate plugin row. It is now a tersio
|
|
41
|
+
// dependency, so drop the legacy row. Runs after stepSelfPlugin.
|
|
42
|
+
let migrated = false;
|
|
43
|
+
if ('@dietrichgebert/ponytail' in pkg.dependencies) {
|
|
44
|
+
delete pkg.dependencies['@dietrichgebert/ponytail'];
|
|
45
|
+
migrated = true;
|
|
46
|
+
}
|
|
43
47
|
if (options.dryRun) {
|
|
44
48
|
if (verbose && !options.quiet)
|
|
45
49
|
console.log(` [dry-run] would write ${pkgPath}`);
|
|
50
|
+
if (migrated && verbose && !options.quiet)
|
|
51
|
+
console.log(` [dry-run] would drop the separate @dietrichgebert/ponytail dependency (bundled with tersio)`);
|
|
46
52
|
}
|
|
47
|
-
else {
|
|
53
|
+
else if (migrated) {
|
|
48
54
|
await fs.writeFile(pkgPath, JSON.stringify(pkg, null, 2) + '\n');
|
|
55
|
+
if (!options.quiet)
|
|
56
|
+
console.log(' [migrate] dropped separate @dietrichgebert/ponytail dependency (now bundled with tersio)');
|
|
49
57
|
}
|
|
50
58
|
const ponytailExtPath = path.join(pluginsDir, 'node_modules', '@dietrichgebert', 'ponytail', 'pi-extension', 'index.js');
|
|
51
59
|
const probeExt = async () => (await readTextIfExists(ponytailExtPath)) !== null;
|
|
52
|
-
// Fast path:
|
|
53
|
-
|
|
60
|
+
// Fast path: bundled copy already present and no refresh asked — skip
|
|
61
|
+
// network. Migration always reinstalls: the probe may hit the stale legacy
|
|
62
|
+
// copy, and only npm install prunes it into the tersio-owned one.
|
|
63
|
+
let ponytailExtExists = !options.reinstall && !options.dryRun && !migrated ? await probeExt() : false;
|
|
54
64
|
if (ponytailExtExists) {
|
|
55
|
-
debug('Ponytail pi-extension already installed; skipping network refresh');
|
|
65
|
+
debug('Bundled Ponytail pi-extension already installed; skipping network refresh');
|
|
56
66
|
}
|
|
57
67
|
else if (options.dryRun) {
|
|
58
68
|
// Dry runs preview the wiring below without touching the network.
|
|
59
69
|
if (verbose && !options.quiet)
|
|
60
|
-
console.log(
|
|
61
|
-
if (options.reinstall && verbose && !options.quiet) {
|
|
62
|
-
console.log(` [dry-run] would run: npm install ${PONYTAIL_NPM_SPEC} --save --no-audit --no-fund`);
|
|
63
|
-
}
|
|
70
|
+
console.log(' [dry-run] would run: npm install --no-audit --no-fund (in plugins dir)');
|
|
64
71
|
ponytailExtExists = true;
|
|
65
72
|
}
|
|
66
73
|
else {
|
|
67
|
-
//
|
|
74
|
+
// Self-plugin npm install already materialized the bundled copy.
|
|
75
|
+
// Reinstall here only when the probe still misses.
|
|
68
76
|
try {
|
|
69
|
-
await execNetwork('Installing Ponytail
|
|
70
|
-
}
|
|
71
|
-
catch (e) {
|
|
72
|
-
console.log(` [warn] omp plugin install failed: ${e.message}`);
|
|
77
|
+
await execNetwork('Installing bundled Ponytail', 'npm', ['install', '--no-audit', '--no-fund'], { cwd: pluginsDir, timeout: 180000 });
|
|
73
78
|
}
|
|
74
|
-
|
|
79
|
+
catch {
|
|
75
80
|
try {
|
|
76
|
-
await execNetwork('
|
|
81
|
+
await execNetwork('Installing bundled Ponytail', 'bun', ['install'], { cwd: pluginsDir, timeout: 180000 });
|
|
77
82
|
}
|
|
78
83
|
catch (e) {
|
|
79
|
-
console.log(` [fail] Could not
|
|
80
|
-
console.log(
|
|
84
|
+
console.log(` [fail] Could not install bundled ponytail: ${e.message}`);
|
|
85
|
+
console.log(' [hint] Manual: cd ~/.omp/plugins && npm install --no-audit --no-fund');
|
|
81
86
|
}
|
|
82
87
|
}
|
|
83
|
-
// Verify the pi-extension/index.js actually exists
|
|
84
88
|
ponytailExtExists = await probeExt();
|
|
85
|
-
// Fallback: try bun install or npm install
|
|
86
|
-
if (!ponytailExtExists) {
|
|
87
|
-
if (!options.quiet)
|
|
88
|
-
console.log(' [info] pi-extension/index.js not found after omp plugin install — trying npm/bun install...');
|
|
89
|
-
try {
|
|
90
|
-
await execNetwork('Installing Ponytail dependencies', 'npm', ['install'], { cwd: pluginsDir, timeout: 120000 });
|
|
91
|
-
}
|
|
92
|
-
catch {
|
|
93
|
-
try {
|
|
94
|
-
await execNetwork('Installing Ponytail dependencies', 'bun', ['install'], { cwd: pluginsDir, timeout: 120000 });
|
|
95
|
-
}
|
|
96
|
-
catch (e2) {
|
|
97
|
-
console.log(` [fail] Could not install ponytail: ${e2.message}`);
|
|
98
|
-
console.log(' [hint] Manual: cd ~/.omp/plugins && npm install');
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
ponytailExtExists = await probeExt();
|
|
102
|
-
}
|
|
103
|
-
// Last-resort fallback: git clone the repo into node_modules
|
|
104
|
-
if (!ponytailExtExists) {
|
|
105
|
-
if (!options.quiet)
|
|
106
|
-
console.log(' [info] npm/bun did not produce pi-extension — trying git clone...');
|
|
107
|
-
try {
|
|
108
|
-
const dest = path.join(pluginsDir, 'node_modules', '@dietrichgebert', 'ponytail');
|
|
109
|
-
await fs.mkdir(path.dirname(dest), { recursive: true });
|
|
110
|
-
await execNetwork('Cloning Ponytail repository', 'git', ['clone', '--depth', '1', 'https://github.com/DietrichGebert/ponytail.git', dest], { timeout: 180000 });
|
|
111
|
-
ponytailExtExists = await probeExt();
|
|
112
|
-
}
|
|
113
|
-
catch (e3) {
|
|
114
|
-
console.log(` [fail] git clone failed: ${e3.message}`);
|
|
115
|
-
console.log(' [hint] Install git or check network: https://github.com/DietrichGebert/ponytail');
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
89
|
}
|
|
119
90
|
if (!ponytailExtExists) {
|
|
120
|
-
console.log(' [skip] Ponytail pi-extension/index.js still not found — skill-only mode');
|
|
91
|
+
console.log(' [skip] Bundled Ponytail pi-extension/index.js still not found — skill-only mode');
|
|
121
92
|
console.log(' [hint] The /ponytail command won\'t work, but ponytail skills will still load');
|
|
122
93
|
}
|
|
123
94
|
else if (!options.dryRun && !options.quiet) {
|
|
124
95
|
// Wire extension into config.yml so /ponytail command loads
|
|
125
|
-
debug('Ponytail pi-extension found');
|
|
96
|
+
debug('Bundled Ponytail pi-extension found');
|
|
126
97
|
}
|
|
127
98
|
// Still set Ponytail config defaults even without the extension command
|
|
128
99
|
const configPath = path.join(userDir, 'config.yml');
|
|
@@ -551,10 +522,25 @@ async function runCommandMenu() {
|
|
|
551
522
|
await runInstall({ reinstall: true });
|
|
552
523
|
closeRL();
|
|
553
524
|
break;
|
|
554
|
-
case 'doctor':
|
|
555
|
-
await runDoctor();
|
|
525
|
+
case 'doctor': {
|
|
526
|
+
const summary = await runDoctor();
|
|
527
|
+
if (!dryRun && summary.missing + summary.warn > 0) {
|
|
528
|
+
const fixIt = await askInteractiveConfirm('Doctor found problems — repair them now?');
|
|
529
|
+
if (fixIt.status === 'confirmed' && fixIt.value) {
|
|
530
|
+
const { runDoctorRepairs } = await import('./doctor-fix.js');
|
|
531
|
+
const failed = await runDoctorRepairs(['all']);
|
|
532
|
+
if (failed.length === 0) {
|
|
533
|
+
console.log('\n Repairs done — rechecking.');
|
|
534
|
+
await runDoctor();
|
|
535
|
+
}
|
|
536
|
+
else {
|
|
537
|
+
console.log(`\n ${failed.length} repair(s) failed (${failed.join(', ')}) — see [fail] lines above.`);
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
}
|
|
556
541
|
closeRL();
|
|
557
542
|
break;
|
|
543
|
+
}
|
|
558
544
|
case 'usage':
|
|
559
545
|
await runUsage();
|
|
560
546
|
closeRL();
|
|
@@ -656,9 +642,9 @@ async function runInstall(overrides = {}) {
|
|
|
656
642
|
}
|
|
657
643
|
};
|
|
658
644
|
await capture('shared', () => stepSharedSessionState(userExtDir, options));
|
|
659
|
-
await capture('ponytail', () => stepPonytail(OMP_PLUGINS_DIR, userDir, options));
|
|
660
645
|
let selfPlugin = false;
|
|
661
646
|
await capture('self-plugin', async () => { selfPlugin = await stepSelfPlugin(OMP_PLUGINS_DIR, options); });
|
|
647
|
+
await capture('ponytail', () => stepPonytail(OMP_PLUGINS_DIR, userDir, options));
|
|
662
648
|
const ponytailExtPath = path.join(OMP_PLUGINS_DIR, 'node_modules', '@dietrichgebert', 'ponytail', 'pi-extension', 'index.js');
|
|
663
649
|
await capture('rtk', () => stepRtk(BUN_BIN_DIR, options));
|
|
664
650
|
await capture('rtk session', () => stepRtkSession(userExtDir, options));
|
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, '/');
|