@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/doctor.js
CHANGED
|
@@ -1,17 +1,16 @@
|
|
|
1
1
|
// cli/doctor.ts — installation health checks.
|
|
2
|
-
import { promises as fs } from 'node:fs';
|
|
2
|
+
import { existsSync, promises as fs } from 'node:fs';
|
|
3
3
|
import path from 'node:path';
|
|
4
|
-
import { OMP_AGENT_DIR, OMP_PLUGINS_DIR, args, dryRun, fix, yes, execP, parseJsonObject, relTime, } from './common.js';
|
|
4
|
+
import { OMP_AGENT_DIR, OMP_PLUGINS_DIR, PACKAGE_NAME, args, dryRun, fix, yes, execP, parseJsonObject, relTime, } from './common.js';
|
|
5
|
+
import { detectHosts, ompPackageDir } from './hosts.js';
|
|
5
6
|
import { askInteractiveChoice, askInteractiveConfirm, runInteractivePhase } from './interactive.js';
|
|
6
7
|
import { usageDbPath } from '../extensions/shared/usage-store.js';
|
|
7
8
|
import { pricesCachePath } from '../extensions/shared/pricing.js';
|
|
8
9
|
import { readTextIfExists, resolveRtkBinary } from '../extensions/lib/utils.js';
|
|
9
10
|
async function runDoctor(recheck = false) {
|
|
10
11
|
console.log('\n=== Tersio Doctor ===');
|
|
11
|
-
//
|
|
12
|
-
|
|
13
|
-
const extDir = path.join(agentDir, 'extensions');
|
|
14
|
-
const configPath = path.join(agentDir, 'config.yml');
|
|
12
|
+
// OMP loads the plugin from its plugins dir; pi loads the same package as a
|
|
13
|
+
// pi package, which its own settings.json declares and its npm dir holds.
|
|
15
14
|
const pluginsDir = OMP_PLUGINS_DIR;
|
|
16
15
|
const rtkBin = resolveRtkBinary();
|
|
17
16
|
// Ponytail
|
|
@@ -24,15 +23,13 @@ async function runDoctor(recheck = false) {
|
|
|
24
23
|
const updaterIndex = path.join(tersioPluginDir, 'extensions', 'ai-addons-updater', 'index.ts');
|
|
25
24
|
// Independent probes start concurrently; sections report in fixed order as
|
|
26
25
|
// their data settles. Every probe resolves instead of rejecting.
|
|
26
|
+
const hosts = detectHosts();
|
|
27
27
|
const probes = {
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
sharedStateText: readTextIfExists(path.join(tersioPluginDir, 'extensions', 'shared', 'session-state.ts')),
|
|
31
|
-
configText: readTextIfExists(configPath),
|
|
28
|
+
ompPkgText: readTextIfExists(path.join(ompPackageDir(), 'package.json')),
|
|
29
|
+
configText: readTextIfExists(path.join(OMP_AGENT_DIR, 'config.yml')),
|
|
32
30
|
ponytailPkgText: readTextIfExists(ponytailPkg),
|
|
33
31
|
ponytailExtText: readTextIfExists(ponytailExt),
|
|
34
32
|
rtkBinText: rtkBin ? readTextIfExists(rtkBin) : Promise.resolve(null),
|
|
35
|
-
rtkOmpText: readTextIfExists(path.join(extDir, 'rtk.ts')),
|
|
36
33
|
cavemanIndexText: readTextIfExists(cavemanIndex),
|
|
37
34
|
cavemanRuleText: readTextIfExists(cavemanRule),
|
|
38
35
|
rtkIndexText: readTextIfExists(rtkIndex),
|
|
@@ -46,11 +43,9 @@ async function runDoctor(recheck = false) {
|
|
|
46
43
|
const err = e;
|
|
47
44
|
return err.stdout?.trim() || err.stderr?.trim() || null;
|
|
48
45
|
}) : Promise.resolve(null);
|
|
49
|
-
const [[
|
|
46
|
+
const [[ompPkgText, configText], [cavemanIndexText, rtkIndexText, updaterIndexText, ponytailPkgText, ponytailExtText], [cavemanRuleText, ruleMtime, rtkBinText, rtkMtime, rtkVersion, ponytailMtime, pricesMtime]] = await runInteractivePhase('Checking installation', () => Promise.all([
|
|
50
47
|
Promise.all([
|
|
51
|
-
probes.
|
|
52
|
-
probes.extEntries,
|
|
53
|
-
probes.sharedStateText,
|
|
48
|
+
probes.ompPkgText,
|
|
54
49
|
probes.configText,
|
|
55
50
|
]),
|
|
56
51
|
Promise.all([
|
|
@@ -66,7 +61,6 @@ async function runDoctor(recheck = false) {
|
|
|
66
61
|
probes.rtkBinText,
|
|
67
62
|
probes.rtkMtime,
|
|
68
63
|
rtkVersionProbe,
|
|
69
|
-
probes.rtkOmpText,
|
|
70
64
|
probes.ponytailMtime,
|
|
71
65
|
probes.pricesMtime,
|
|
72
66
|
]),
|
|
@@ -94,28 +88,38 @@ async function runDoctor(recheck = false) {
|
|
|
94
88
|
tally.warn++;
|
|
95
89
|
console.log(` ⚠️ ${label}: warn ${detail}`);
|
|
96
90
|
}
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
91
|
+
// A host you do not use is not a fault, so it stays out of the tally and only
|
|
92
|
+
// carries the one line that installs it.
|
|
93
|
+
function unused(label, detail) {
|
|
94
|
+
console.log(` — ${label}: not installed ${detail}`);
|
|
95
|
+
}
|
|
96
|
+
section('Hosts');
|
|
97
|
+
for (const host of hosts) {
|
|
98
|
+
if (!host.installed) {
|
|
99
|
+
// A pi declaration without the tree or the package is a broken install,
|
|
100
|
+
// so it stays a counted row rather than reading as "not installed".
|
|
101
|
+
if (host.declared)
|
|
102
|
+
check(host.label, false, `declared (${host.declared}) but not installed`);
|
|
103
|
+
else
|
|
104
|
+
unused(host.label, `· ${host.installCmd}`);
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
const where = host.via === 'tree' ? `extensions in ${host.dir}` : `package ${PACKAGE_NAME}`;
|
|
108
|
+
check(host.label, true, `${where}${host.version ? ` ${host.version}` : ''}`);
|
|
109
|
+
}
|
|
104
110
|
section('Extensions & plugins');
|
|
105
|
-
const
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
check('
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
check('Updater extension', updaterIndexText !== null);
|
|
118
|
-
check('Ponytail extension', ponytailExtText !== null);
|
|
111
|
+
const ompEntry = hosts.find((host) => host.id === 'omp');
|
|
112
|
+
if (ompEntry?.via === 'package') {
|
|
113
|
+
const explicitEntries = (configText ?? '').split('\n')
|
|
114
|
+
.map((line) => line.trim().replace(/^-\s*/, '').replace(/^['"]|['"]$/g, ''))
|
|
115
|
+
.filter((line) => line.startsWith('/') || line.startsWith('.'));
|
|
116
|
+
const duplicateExtensions = [...new Set(explicitEntries.filter((entry, index) => explicitEntries.indexOf(entry) !== index))];
|
|
117
|
+
check('Unique config registrations', duplicateExtensions.length === 0, duplicateExtensions.length ? duplicateExtensions.join(', ') : '');
|
|
118
|
+
check('Caveman extension', existsSync(path.join(tersioPluginDir, 'extensions', 'caveman-session', 'index.ts')), existsSync(path.join(tersioPluginDir, 'extensions', 'caveman-session', 'index.ts')) ? '' : path.join(tersioPluginDir, 'extensions', 'caveman-session', 'index.ts'));
|
|
119
|
+
check('RTK extension', existsSync(path.join(tersioPluginDir, 'extensions', 'rtk-session', 'index.ts')), existsSync(path.join(tersioPluginDir, 'extensions', 'rtk-session', 'index.ts')) ? '' : path.join(tersioPluginDir, 'extensions', 'rtk-session', 'index.ts'));
|
|
120
|
+
check('Updater extension', existsSync(path.join(tersioPluginDir, 'extensions', 'ai-addons-updater', 'index.ts')), existsSync(path.join(tersioPluginDir, 'extensions', 'ai-addons-updater', 'index.ts')) ? '' : path.join(tersioPluginDir, 'extensions', 'ai-addons-updater', 'index.ts'));
|
|
121
|
+
check('Ponytail extension', existsSync(path.join(OMP_PLUGINS_DIR, 'node_modules', '@dietrichgebert', 'ponytail', 'pi-extension', 'index.js')), existsSync(path.join(OMP_PLUGINS_DIR, 'node_modules', '@dietrichgebert', 'ponytail', 'pi-extension', 'index.js')) ? '' : path.join(OMP_PLUGINS_DIR, 'node_modules', '@dietrichgebert', 'ponytail', 'pi-extension', 'index.js'));
|
|
122
|
+
}
|
|
119
123
|
section('Usage & records');
|
|
120
124
|
console.log(` Usage DB (tersio-owned · local hosted): ${usageDbPath()}`);
|
|
121
125
|
const pricesAge = pricesMtime ? `(pulled ${relTime(Date.now() - pricesMtime.mtimeMs)} · ${absDate(pricesMtime.mtimeMs)})` : '';
|
|
@@ -127,10 +131,6 @@ async function runDoctor(recheck = false) {
|
|
|
127
131
|
check('RTK binary', rtkBin !== null, rtkBinText === null ? 'not found in PATH' : [rtkVersion, rtkAge].filter(Boolean).join(' '));
|
|
128
132
|
if (rtkBinText !== null && !rtkVersion)
|
|
129
133
|
warnLine('RTK version', 'unavailable — binary may not be executable');
|
|
130
|
-
const rtkRegistered = rtkOmpText !== null && (configText ?? '').includes('extensions/rtk.ts');
|
|
131
|
-
check('RTK OMP wiring (rtk.ts)', rtkOmpText !== null, rtkOmpText === null ? 'run: rtk init -g --agent omp' : '');
|
|
132
|
-
if (rtkOmpText !== null && !rtkRegistered)
|
|
133
|
-
warnLine('RTK in config.yml', 'rtk.ts not listed — OMP will not load it; rerun install');
|
|
134
134
|
const ponytailAge = ponytailMtime ? `(updated ${relTime(Date.now() - ponytailMtime.mtimeMs)} · ${absDate(ponytailMtime.mtimeMs)})` : '';
|
|
135
135
|
const ponytailVer = parseJsonObject(ponytailPkgText)?.version ?? '';
|
|
136
136
|
check('Ponytail', ponytailPkgText !== null, [ponytailVer, ponytailAge].filter(Boolean).join(' '));
|
package/cli/hosts.js
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
// cli/hosts.ts — the agent hosts Tersio installs into, and what is on disk.
|
|
2
|
+
// One detector, shared by the doctor and both menus, so they cannot disagree.
|
|
3
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { HOME, OMP_PLUGINS_DIR, PACKAGE_NAME, PACKAGE_VERSION } from './common.js';
|
|
6
|
+
import { TREE_FILES } from './manifest.js';
|
|
7
|
+
import { piAgentDir } from '../extensions/lib/utils.js';
|
|
8
|
+
export function hostExtensionsDir(id, agentDir = piAgentDir()) {
|
|
9
|
+
return id === 'omp'
|
|
10
|
+
? path.join(HOME, '.omp', 'agent', 'extensions')
|
|
11
|
+
: path.join(agentDir, 'extensions');
|
|
12
|
+
}
|
|
13
|
+
export function missingTreeFiles(id, agentDir = piAgentDir()) {
|
|
14
|
+
const dir = hostExtensionsDir(id, agentDir);
|
|
15
|
+
return TREE_FILES.filter((file) => !existsSync(path.join(dir, file)));
|
|
16
|
+
}
|
|
17
|
+
export function ompPackageDir() {
|
|
18
|
+
return path.join(OMP_PLUGINS_DIR, 'node_modules', ...PACKAGE_NAME.split('/'));
|
|
19
|
+
}
|
|
20
|
+
function piPackageDir(agentDir = piAgentDir()) {
|
|
21
|
+
return path.join(agentDir, 'npm', 'node_modules', ...PACKAGE_NAME.split('/'));
|
|
22
|
+
}
|
|
23
|
+
function piSettingsFile(agentDir = piAgentDir()) {
|
|
24
|
+
return path.join(agentDir, 'settings.json');
|
|
25
|
+
}
|
|
26
|
+
function packageVersion(dir) {
|
|
27
|
+
try {
|
|
28
|
+
const parsed = JSON.parse(readFileSync(path.join(dir, 'package.json'), 'utf8'));
|
|
29
|
+
return typeof parsed.version === 'string' ? parsed.version : null;
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
// The tersio package pi declared in its own settings.json, or null. pi
|
|
36
|
+
// identifies a source by name, so an npm spec, git URL, or path all count.
|
|
37
|
+
export function piTersioSource(agentDir = piAgentDir()) {
|
|
38
|
+
let settings;
|
|
39
|
+
try {
|
|
40
|
+
const parsed = JSON.parse(readFileSync(piSettingsFile(agentDir), 'utf8'));
|
|
41
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed))
|
|
42
|
+
return null;
|
|
43
|
+
settings = parsed;
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
if (!Array.isArray(settings.packages))
|
|
49
|
+
return null;
|
|
50
|
+
for (const entry of settings.packages) {
|
|
51
|
+
const source = typeof entry === 'string' ? entry : entry?.source;
|
|
52
|
+
if (typeof source !== 'string')
|
|
53
|
+
continue;
|
|
54
|
+
const trimmed = source.trim().replace(/[/\\]+$/, '');
|
|
55
|
+
if (trimmed.includes(PACKAGE_NAME) || /(^|[/\\])tersio(\.git)?$/i.test(trimmed))
|
|
56
|
+
return source.trim();
|
|
57
|
+
}
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
// The hosts we install into, and the commands that add and remove each.
|
|
61
|
+
const HOSTS = [
|
|
62
|
+
{ id: 'omp', label: 'Oh My Pi', bin: 'omp', installCmd: `omp plugin install ${PACKAGE_NAME}`, removeCmd: `omp plugin remove ${PACKAGE_NAME}` },
|
|
63
|
+
{ id: 'pi', label: 'Pi', bin: 'pi', installCmd: `pi install npm:${PACKAGE_NAME}`, removeCmd: `pi remove npm:${PACKAGE_NAME}` },
|
|
64
|
+
];
|
|
65
|
+
function detect(agentDir = piAgentDir()) {
|
|
66
|
+
return HOSTS.map((host) => {
|
|
67
|
+
const pkgDir = host.id === 'omp' ? ompPackageDir() : piPackageDir(agentDir);
|
|
68
|
+
const tree = hostExtensionsDir(host.id, agentDir);
|
|
69
|
+
// A tree wins: it is what the installer writes, and it carries no
|
|
70
|
+
// package.json, so its version is ours.
|
|
71
|
+
const via = missingTreeFiles(host.id, agentDir).length === 0
|
|
72
|
+
? 'tree'
|
|
73
|
+
: existsSync(pkgDir) ? 'package' : null;
|
|
74
|
+
return {
|
|
75
|
+
...host,
|
|
76
|
+
installed: via !== null,
|
|
77
|
+
via,
|
|
78
|
+
declared: host.id === 'omp'
|
|
79
|
+
? (existsSync(pkgDir) ? `plugin ${PACKAGE_NAME}` : null)
|
|
80
|
+
: piTersioSource(agentDir),
|
|
81
|
+
version: via === 'tree' ? PACKAGE_VERSION : via === 'package' ? packageVersion(pkgDir) : null,
|
|
82
|
+
dir: via === 'tree' ? tree : via === 'package' ? pkgDir : null,
|
|
83
|
+
};
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
/** Menu label with current state in brackets, so a rerun shows what it replaces. */
|
|
87
|
+
export function hostLabel(host) {
|
|
88
|
+
if (host.installed && host.version)
|
|
89
|
+
return `${host.label} (installed ${host.version})`;
|
|
90
|
+
if (host.installed)
|
|
91
|
+
return `${host.label} (installed)`;
|
|
92
|
+
if (host.declared)
|
|
93
|
+
return `${host.label} (package declared, not on disk)`;
|
|
94
|
+
return `${host.label} (not installed)`;
|
|
95
|
+
}
|
|
96
|
+
// Hints render inline, so menus name the install and doctor keeps the path.
|
|
97
|
+
export function hostHint(host) {
|
|
98
|
+
if (host.installed && host.via)
|
|
99
|
+
return host.via === 'tree' ? 'extension tree' : 'plugin package';
|
|
100
|
+
return host.installCmd;
|
|
101
|
+
}
|
|
102
|
+
// `--host omp|pi` from raw argv; undefined when absent. Throws on an unknown
|
|
103
|
+
// value so the caller can exit(1) with the message.
|
|
104
|
+
export function parseHostArg(argv) {
|
|
105
|
+
const i = argv.indexOf('--host');
|
|
106
|
+
const raw = (i === -1 ? undefined : argv[i + 1]) ?? argv.find((a) => a.startsWith('--host='))?.slice('--host='.length);
|
|
107
|
+
if (raw === undefined)
|
|
108
|
+
return undefined;
|
|
109
|
+
const value = raw.trim().toLowerCase();
|
|
110
|
+
if (value !== 'omp' && value !== 'pi')
|
|
111
|
+
throw new Error(`Invalid --host: ${raw}. Valid: omp, pi`);
|
|
112
|
+
return value;
|
|
113
|
+
}
|
|
114
|
+
export { HOME, detect as detectHosts };
|