@krtclcdy/tersio 2.15.0 → 2.17.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 +0 -18
- package/cli/common.js +17 -10
- package/cli/dashboard.js +4 -0
- package/cli/doctor.js +3 -0
- package/cli/install.js +109 -111
- package/cli/rtk-wiring.js +66 -8
- package/cli/update.js +54 -42
- package/cli/usage.js +3 -0
- package/dashboard/app.js +220 -55
- package/dashboard/styles.css +15 -4
- package/dashboard/template.html +29 -9
- package/extensions/shared/usage-ledger.js +17 -5
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -10,24 +10,6 @@
|
|
|
10
10
|
|
|
11
11
|
Terse replies, compact shell output, and minimal code decisions for [Oh My Pi (OMP)](https://github.com/can1357/oh-my-pi) — one-command combo presets.
|
|
12
12
|
|
|
13
|
-
## ✨ Features
|
|
14
|
-
|
|
15
|
-
- **Caveman** — Shortens replies, keeps technical substance. Modes: `lite`, `full`, `ultra`, `wenyan`.
|
|
16
|
-
- **RTK** — Routes noisy shell commands through the RTK binary for compact output; exact bytes bypass it by policy. The installer wires rtk into OMP automatically (`rtk init -g --agent omp`), so bash tool calls rewrite to `rtk` before execution and meter into the gain dashboard.
|
|
17
|
-
- **Ponytail** — Minimal, YAGNI-oriented code decisions, from nudge (`lite`) to requirement-challenger (`ultra`).
|
|
18
|
-
- **Combo** — Toggles all three at once. Presets: `off`, `medium`, `balanced`, `max`.
|
|
19
|
-
- **Updater** — `/tersio check` and `/tersio update` cover Ponytail, RTK, and Caveman in-session, with dry-run and backups.
|
|
20
|
-
- **Gain dashboard** — `tersio gain` serves a local-only savings dashboard: cost with currency conversion, activity graph, command tools, and model breakdowns.
|
|
21
|
-
- **Usage ledger** — `tersio usage` prints a ledger-backed savings report for the machine, local only.
|
|
22
|
-
|
|
23
|
-
## 🧱 Tech Stack
|
|
24
|
-
|
|
25
|
-
- [TypeScript](https://www.typescriptlang.org/) + [Node.js](https://nodejs.org/): CLI and OMP extensions, compiled to dependency-free JS for install.
|
|
26
|
-
- Vanilla dashboard: dependency-free HTML/CSS/JS export — no build step, no runtime deps.
|
|
27
|
-
- [js-tiktoken](https://github.com/dqbd/tiktoken): Real `o200k_base` BPE token counts for benchmarks (dev only).
|
|
28
|
-
- [tokscale](https://github.com/junhoyeo/tokscale): Token-bucket convention (input / output / cache read / cache write) for parsing OMP session transcripts.
|
|
29
|
-
- [EcoLogits](https://github.com/genai-impact/ecologits): CO2 model (v0.8.2 port — per-model params, provider grids, served ÷32 amortization).
|
|
30
|
-
|
|
31
13
|
## ⚡ Getting Started
|
|
32
14
|
|
|
33
15
|
**Default** (macOS/Linux/WSL) — one line. Installs the CLI via npm, then runs the main installer (scope + Combo preset menus) in the same pass:
|
package/cli/common.js
CHANGED
|
@@ -104,7 +104,8 @@ async function writeIfChanged(dest, content, options = {}) {
|
|
|
104
104
|
return false;
|
|
105
105
|
}
|
|
106
106
|
if (options.dryRun) {
|
|
107
|
-
|
|
107
|
+
if (verbose)
|
|
108
|
+
console.log(` [dry-run] would write ${dest}`);
|
|
108
109
|
return true;
|
|
109
110
|
}
|
|
110
111
|
await fs.mkdir(path.dirname(dest), { recursive: true });
|
|
@@ -113,7 +114,8 @@ async function writeIfChanged(dest, content, options = {}) {
|
|
|
113
114
|
debug(`${path.basename(dest)} → ${path.basename(dest)}.bak`);
|
|
114
115
|
}
|
|
115
116
|
await fs.writeFile(dest, content, 'utf8');
|
|
116
|
-
|
|
117
|
+
if (verbose && !options.quiet)
|
|
118
|
+
console.log(` [write] ${dest}`);
|
|
117
119
|
return true;
|
|
118
120
|
}
|
|
119
121
|
// omp ships 'extensions: null'; appending list items under a null scalar breaks
|
|
@@ -127,11 +129,12 @@ function normalizeExtensionsKey(lines) {
|
|
|
127
129
|
lines[idx] = 'extensions:';
|
|
128
130
|
return true;
|
|
129
131
|
}
|
|
130
|
-
async function writeConfigLines(configPath, lines, logMsg) {
|
|
132
|
+
async function writeConfigLines(configPath, lines, logMsg, options = {}) {
|
|
131
133
|
await fs.mkdir(path.dirname(configPath), { recursive: true });
|
|
132
134
|
await fs.copyFile(configPath, `${configPath}.bak`).catch(() => { });
|
|
133
135
|
await fs.writeFile(configPath, lines.join('\n'), 'utf8');
|
|
134
|
-
|
|
136
|
+
if (verbose && !options.quiet)
|
|
137
|
+
console.log(logMsg);
|
|
135
138
|
}
|
|
136
139
|
async function ensureExtensionInConfig(configPath, extensionPath, label, options = {}) {
|
|
137
140
|
const normalizedPath = extensionPath.replace(/\\/g, '/');
|
|
@@ -144,7 +147,8 @@ async function ensureExtensionInConfig(configPath, extensionPath, label, options
|
|
|
144
147
|
}
|
|
145
148
|
const extLineIdx = lines.findIndex((l) => EXTENSIONS_KEY_RE.test(l));
|
|
146
149
|
if (options.dryRun) {
|
|
147
|
-
|
|
150
|
+
if (verbose && !options.quiet)
|
|
151
|
+
console.log(` [dry-run] would add ${label} to config.yml`);
|
|
148
152
|
return true;
|
|
149
153
|
}
|
|
150
154
|
if (normalizeExtensionsKey(lines)) {
|
|
@@ -158,7 +162,7 @@ async function ensureExtensionInConfig(configPath, extensionPath, label, options
|
|
|
158
162
|
else {
|
|
159
163
|
lines.splice(extLineIdx + 1, 0, line);
|
|
160
164
|
}
|
|
161
|
-
await writeConfigLines(configPath, lines, ` [write] Added ${label} to config.yml
|
|
165
|
+
await writeConfigLines(configPath, lines, ` [write] Added ${label} to config.yml`, options);
|
|
162
166
|
return true;
|
|
163
167
|
}
|
|
164
168
|
async function ensureExtensionAfterConfigEntry(configPath, extensionPath, afterPath, label, options = {}) {
|
|
@@ -172,7 +176,8 @@ async function ensureExtensionAfterConfigEntry(configPath, extensionPath, afterP
|
|
|
172
176
|
if (existingIndex !== -1 && afterIndex !== -1 && existingIndex === afterIndex + 1)
|
|
173
177
|
return false;
|
|
174
178
|
if (options.dryRun) {
|
|
175
|
-
|
|
179
|
+
if (verbose && !options.quiet)
|
|
180
|
+
console.log(` [dry-run] would place ${label} after Ponytail in config.yml`);
|
|
176
181
|
return true;
|
|
177
182
|
}
|
|
178
183
|
if (existingIndex !== -1)
|
|
@@ -191,7 +196,7 @@ async function ensureExtensionAfterConfigEntry(configPath, extensionPath, afterP
|
|
|
191
196
|
lines.splice(extensionsIndex + 1, 0, line);
|
|
192
197
|
}
|
|
193
198
|
}
|
|
194
|
-
await writeConfigLines(configPath, lines, ` [write] Placed ${label} after Ponytail in config.yml
|
|
199
|
+
await writeConfigLines(configPath, lines, ` [write] Placed ${label} after Ponytail in config.yml`, options);
|
|
195
200
|
return true;
|
|
196
201
|
}
|
|
197
202
|
function readPonytailConfig() {
|
|
@@ -219,7 +224,8 @@ function parsePonytailConfig(existing) {
|
|
|
219
224
|
async function patchPonytailConfig(apply, dryRunNote, writeNote, options = {}) {
|
|
220
225
|
const config = readPonytailConfig();
|
|
221
226
|
if (options.dryRun) {
|
|
222
|
-
|
|
227
|
+
if (verbose && !options.quiet)
|
|
228
|
+
console.log(` [dry-run] ${dryRunNote}`);
|
|
223
229
|
return;
|
|
224
230
|
}
|
|
225
231
|
const cfg = parsePonytailConfig(await readTextIfExists(config.path));
|
|
@@ -229,7 +235,8 @@ async function patchPonytailConfig(apply, dryRunNote, writeNote, options = {}) {
|
|
|
229
235
|
}
|
|
230
236
|
await fs.mkdir(config.dir, { recursive: true });
|
|
231
237
|
await fs.writeFile(config.path, JSON.stringify(cfg, null, 2) + '\n', 'utf8');
|
|
232
|
-
|
|
238
|
+
if (verbose && !options.quiet)
|
|
239
|
+
console.log(` [write] ${writeNote}`);
|
|
233
240
|
}
|
|
234
241
|
async function ensurePonytailConfigValue(key, value, options = {}) {
|
|
235
242
|
await patchPonytailConfig((cfg) => {
|
package/cli/dashboard.js
CHANGED
|
@@ -88,6 +88,10 @@ async function runDashboard(options) {
|
|
|
88
88
|
res.end(html);
|
|
89
89
|
});
|
|
90
90
|
server.listen(options.port, '127.0.0.1', () => {
|
|
91
|
+
try {
|
|
92
|
+
process.title = 'tersio gain';
|
|
93
|
+
}
|
|
94
|
+
catch { /* non-POSIX shells keep node */ }
|
|
91
95
|
const address = server.address();
|
|
92
96
|
const port = typeof address === 'object' && address ? address.port : options.port;
|
|
93
97
|
const url = `http://127.0.0.1:${port}`;
|
package/cli/doctor.js
CHANGED
|
@@ -146,7 +146,10 @@ async function runDoctor() {
|
|
|
146
146
|
check('RTK binary', rtkBinText !== null, rtkBinText === null ? rtkBin : [rtkVersion, rtkAge].filter(Boolean).join(', '));
|
|
147
147
|
if (rtkBinText !== null && !rtkVersion)
|
|
148
148
|
warnLine('RTK version', 'unavailable — binary may not be executable');
|
|
149
|
+
const rtkRegistered = rtkOmpText !== null && (configText ?? '').includes('extensions/rtk.ts');
|
|
149
150
|
check('RTK OMP wiring (rtk.ts)', rtkOmpText !== null, rtkOmpText === null ? 'run: rtk init -g --agent omp' : '');
|
|
151
|
+
if (rtkOmpText !== null && !rtkRegistered)
|
|
152
|
+
warnLine('RTK in config.yml', 'rtk.ts not listed — OMP will not load it; rerun install');
|
|
150
153
|
const ponytailAge = ponytailMtime ? `updated ${relTime(Date.now() - ponytailMtime.mtimeMs)}` : '';
|
|
151
154
|
check('Ponytail', ponytailPkgText !== null, [parseJsonObject(ponytailPkgText)?.version ?? '', ponytailAge].filter(Boolean).join(', '));
|
|
152
155
|
const total = tally.ok + tally.missing + tally.warn;
|
package/cli/install.js
CHANGED
|
@@ -3,7 +3,7 @@ import { promises as fs } from 'node:fs';
|
|
|
3
3
|
import os from 'node:os';
|
|
4
4
|
import path from 'node:path';
|
|
5
5
|
import { fileURLToPath } from 'node:url';
|
|
6
|
-
import { BUN_BIN_DIR, CAVEMAN_DEFAULTS, COMBO_PRESET_MODES,
|
|
6
|
+
import { BUN_BIN_DIR, CAVEMAN_DEFAULTS, COMBO_PRESET_MODES, IS_WINDOWS, OMP_AGENT_DIR, OMP_PLUGINS_DIR, OMP_BIN, PACKAGE_NAME, PACKAGE_VERSION, PONYTAIL_DEFAULTS, RTK_BINARY_NAME, applyUpdate, cavemanDefaultFlag, comboDefaultFlag, command, dryRun, install, ponytailDefaultFlag, profileFlagsGiven, reinstall, rtkDefaultFlag, verbose, yes, dashboardExport, dashboardPort, debug, ensureExtensionAfterConfigEntry, ensureExtensionInConfig, ensurePonytailConfigValue, execP, parseJsonObject, readPluginsPackage, writeIfChanged, } from './common.js';
|
|
7
7
|
import { askInteractiveChoice, askInteractiveConfirm, closeRL, execNetwork, tty, withInteractiveSpinner, } from './interactive.js';
|
|
8
8
|
import { printWelcome } from './banner.js';
|
|
9
9
|
import { checkForUpdate, runLatestUpdate } from './update.js';
|
|
@@ -32,17 +32,18 @@ const SHARED_CARBON = path.join(EXT_DIR, 'shared', 'carbon.js');
|
|
|
32
32
|
const PONYTAIL_GITHUB_SPEC = 'github:DietrichGebert/ponytail';
|
|
33
33
|
const PONYTAIL_NPM_SPEC = '@dietrichgebert/ponytail@latest';
|
|
34
34
|
async function stepPonytail(pluginsDir, userDir, options) {
|
|
35
|
-
|
|
35
|
+
if (!options.quiet)
|
|
36
|
+
console.log(' Ponytail — refresh plugin');
|
|
36
37
|
await fs.mkdir(pluginsDir, { recursive: true });
|
|
37
38
|
const pkgPath = path.join(pluginsDir, 'package.json');
|
|
38
39
|
const pkg = await readPluginsPackage(pkgPath);
|
|
39
40
|
pkg.dependencies['@dietrichgebert/ponytail'] = PONYTAIL_GITHUB_SPEC;
|
|
40
41
|
if (options.dryRun) {
|
|
41
|
-
|
|
42
|
+
if (verbose && !options.quiet)
|
|
43
|
+
console.log(` [dry-run] would write ${pkgPath}`);
|
|
42
44
|
}
|
|
43
45
|
else {
|
|
44
46
|
await fs.writeFile(pkgPath, JSON.stringify(pkg, null, 2) + '\n');
|
|
45
|
-
console.log(' [write] package.json');
|
|
46
47
|
}
|
|
47
48
|
const ponytailExtPath = path.join(pluginsDir, 'node_modules', '@dietrichgebert', 'ponytail', 'pi-extension', 'index.js');
|
|
48
49
|
const probeExt = async () => (await readTextIfExists(ponytailExtPath)) !== null;
|
|
@@ -53,8 +54,9 @@ async function stepPonytail(pluginsDir, userDir, options) {
|
|
|
53
54
|
}
|
|
54
55
|
else if (options.dryRun) {
|
|
55
56
|
// Dry runs preview the wiring below without touching the network.
|
|
56
|
-
|
|
57
|
-
|
|
57
|
+
if (verbose && !options.quiet)
|
|
58
|
+
console.log(` [dry-run] would run: omp plugin install ${PONYTAIL_GITHUB_SPEC}`);
|
|
59
|
+
if (options.reinstall && verbose && !options.quiet) {
|
|
58
60
|
console.log(` [dry-run] would run: npm install ${PONYTAIL_NPM_SPEC} --save --no-audit --no-fund`);
|
|
59
61
|
}
|
|
60
62
|
ponytailExtExists = true;
|
|
@@ -63,7 +65,6 @@ async function stepPonytail(pluginsDir, userDir, options) {
|
|
|
63
65
|
// Try omp plugin install first
|
|
64
66
|
try {
|
|
65
67
|
await execNetwork('Installing Ponytail plugin', OMP_BIN, ['plugin', 'install', PONYTAIL_GITHUB_SPEC], { cwd: pluginsDir });
|
|
66
|
-
console.log(' [ok] omp plugin install ran');
|
|
67
68
|
}
|
|
68
69
|
catch (e) {
|
|
69
70
|
console.log(` [warn] omp plugin install failed: ${e.message}`);
|
|
@@ -71,7 +72,6 @@ async function stepPonytail(pluginsDir, userDir, options) {
|
|
|
71
72
|
if (options.reinstall) {
|
|
72
73
|
try {
|
|
73
74
|
await execNetwork('Refreshing Ponytail package', 'npm', ['install', PONYTAIL_NPM_SPEC, '--save', '--no-audit', '--no-fund'], { cwd: pluginsDir, timeout: 120000 });
|
|
74
|
-
console.log(' [ok] Ponytail refreshed');
|
|
75
75
|
}
|
|
76
76
|
catch (e) {
|
|
77
77
|
console.log(` [fail] Could not refresh ponytail: ${e.message}`);
|
|
@@ -82,15 +82,14 @@ async function stepPonytail(pluginsDir, userDir, options) {
|
|
|
82
82
|
ponytailExtExists = await probeExt();
|
|
83
83
|
// Fallback: try bun install or npm install
|
|
84
84
|
if (!ponytailExtExists) {
|
|
85
|
-
|
|
85
|
+
if (!options.quiet)
|
|
86
|
+
console.log(' [info] pi-extension/index.js not found after omp plugin install — trying npm/bun install...');
|
|
86
87
|
try {
|
|
87
88
|
await execNetwork('Installing Ponytail dependencies', 'npm', ['install'], { cwd: pluginsDir, timeout: 120000 });
|
|
88
|
-
console.log(' [ok] npm install completed');
|
|
89
89
|
}
|
|
90
90
|
catch {
|
|
91
91
|
try {
|
|
92
92
|
await execNetwork('Installing Ponytail dependencies', 'bun', ['install'], { cwd: pluginsDir, timeout: 120000 });
|
|
93
|
-
console.log(' [ok] bun install completed');
|
|
94
93
|
}
|
|
95
94
|
catch (e2) {
|
|
96
95
|
console.log(` [fail] Could not install ponytail: ${e2.message}`);
|
|
@@ -101,12 +100,12 @@ async function stepPonytail(pluginsDir, userDir, options) {
|
|
|
101
100
|
}
|
|
102
101
|
// Last-resort fallback: git clone the repo into node_modules
|
|
103
102
|
if (!ponytailExtExists) {
|
|
104
|
-
|
|
103
|
+
if (!options.quiet)
|
|
104
|
+
console.log(' [info] npm/bun did not produce pi-extension — trying git clone...');
|
|
105
105
|
try {
|
|
106
106
|
const dest = path.join(pluginsDir, 'node_modules', '@dietrichgebert', 'ponytail');
|
|
107
107
|
await fs.mkdir(path.dirname(dest), { recursive: true });
|
|
108
108
|
await execNetwork('Cloning Ponytail repository', 'git', ['clone', '--depth', '1', 'https://github.com/DietrichGebert/ponytail.git', dest], { timeout: 180000 });
|
|
109
|
-
console.log(' [ok] git clone completed');
|
|
110
109
|
ponytailExtExists = await probeExt();
|
|
111
110
|
}
|
|
112
111
|
catch (e3) {
|
|
@@ -119,9 +118,9 @@ async function stepPonytail(pluginsDir, userDir, options) {
|
|
|
119
118
|
console.log(' [skip] Ponytail pi-extension/index.js still not found — skill-only mode');
|
|
120
119
|
console.log(' [hint] The /ponytail command won\'t work, but ponytail skills will still load');
|
|
121
120
|
}
|
|
122
|
-
else if (!options.dryRun) {
|
|
121
|
+
else if (!options.dryRun && !options.quiet) {
|
|
123
122
|
// Wire extension into config.yml so /ponytail command loads
|
|
124
|
-
|
|
123
|
+
debug('Ponytail pi-extension found');
|
|
125
124
|
}
|
|
126
125
|
// Still set Ponytail config defaults even without the extension command
|
|
127
126
|
const configPath = path.join(userDir, 'config.yml');
|
|
@@ -134,20 +133,23 @@ async function stepPonytail(pluginsDir, userDir, options) {
|
|
|
134
133
|
// Settings → Plugins page (OMP enumerates plugins/package.json dependencies).
|
|
135
134
|
// Returns true when the package is verified in plugins/node_modules.
|
|
136
135
|
async function stepSelfPlugin(pluginsDir, options) {
|
|
137
|
-
|
|
136
|
+
if (!options.quiet)
|
|
137
|
+
console.log(' Tersio — register plugin');
|
|
138
138
|
const pkgPath = path.join(pluginsDir, 'package.json');
|
|
139
139
|
const pkg = await readPluginsPackage(pkgPath);
|
|
140
140
|
pkg.dependencies[PACKAGE_NAME] = `^${PACKAGE_VERSION}`;
|
|
141
141
|
for (const legacy of ['oh-my-pi-token-saver', 'tersio-omp']) {
|
|
142
142
|
if (legacy in pkg.dependencies) {
|
|
143
143
|
delete pkg.dependencies[legacy];
|
|
144
|
-
if (!options.dryRun)
|
|
144
|
+
if (!options.dryRun && !options.quiet)
|
|
145
145
|
console.log(` [migrate] dropped legacy ${legacy} dependency`);
|
|
146
146
|
}
|
|
147
147
|
}
|
|
148
148
|
if (options.dryRun) {
|
|
149
|
-
|
|
150
|
-
|
|
149
|
+
if (verbose && !options.quiet)
|
|
150
|
+
console.log(` [dry-run] would add ${PACKAGE_NAME}@^${PACKAGE_VERSION} to ${pkgPath}`);
|
|
151
|
+
if (verbose && !options.quiet)
|
|
152
|
+
console.log(` [dry-run] would run: npm install --no-audit --no-fund (in ${pluginsDir})`);
|
|
151
153
|
return false;
|
|
152
154
|
}
|
|
153
155
|
// pkg.dependencies[PACKAGE_NAME] was assigned above, so only reinstall skips this fast path.
|
|
@@ -155,7 +157,6 @@ async function stepSelfPlugin(pluginsDir, options) {
|
|
|
155
157
|
const installedPkgRaw = await readTextIfExists(path.join(pluginsDir, 'node_modules', PACKAGE_NAME, 'package.json'));
|
|
156
158
|
if (parseJsonObject(installedPkgRaw)?.version === PACKAGE_VERSION) {
|
|
157
159
|
debug(`${PACKAGE_NAME} already installed at v${PACKAGE_VERSION}; skipping npm install`);
|
|
158
|
-
console.log(' [ok] Listed in OMP Settings → Plugins as tersio');
|
|
159
160
|
return true;
|
|
160
161
|
}
|
|
161
162
|
}
|
|
@@ -179,7 +180,7 @@ async function stepSelfPlugin(pluginsDir, options) {
|
|
|
179
180
|
console.log(` [warn] ${PACKAGE_NAME} not found in plugins/node_modules after install`);
|
|
180
181
|
return false;
|
|
181
182
|
}
|
|
182
|
-
|
|
183
|
+
debug('tersio listed in OMP plugins');
|
|
183
184
|
return true;
|
|
184
185
|
}
|
|
185
186
|
function resolveRtkTriple() {
|
|
@@ -211,19 +212,21 @@ async function downloadRtkChecksums(release) {
|
|
|
211
212
|
return null;
|
|
212
213
|
}
|
|
213
214
|
}
|
|
214
|
-
async function verifyRtkArchive(archivePath, assetName, checksumsText) {
|
|
215
|
+
async function verifyRtkArchive(archivePath, assetName, checksumsText, options = {}) {
|
|
215
216
|
if (!checksumsText) {
|
|
216
|
-
|
|
217
|
+
if (!options.quiet)
|
|
218
|
+
console.log(' [warn] No checksums.txt available — skipping verification');
|
|
217
219
|
return true;
|
|
218
220
|
}
|
|
219
221
|
const expected = parseChecksum(checksumsText, assetName);
|
|
220
222
|
const actual = await sha256File(archivePath);
|
|
221
223
|
if (!expected) {
|
|
222
|
-
|
|
224
|
+
if (!options.quiet)
|
|
225
|
+
console.log(` [warn] checksums.txt missing entry for ${assetName} — skipping verification`);
|
|
223
226
|
return true;
|
|
224
227
|
}
|
|
225
228
|
if (actual === expected) {
|
|
226
|
-
|
|
229
|
+
debug(`Checksum verified for ${assetName}`);
|
|
227
230
|
return true;
|
|
228
231
|
}
|
|
229
232
|
console.log(` [fail] Checksum mismatch for ${assetName}`);
|
|
@@ -272,10 +275,18 @@ async function extractRtkArchive(archivePath, extractDir) {
|
|
|
272
275
|
return false;
|
|
273
276
|
}
|
|
274
277
|
async function stepRtk(binDir, options) {
|
|
275
|
-
|
|
278
|
+
if (!options.quiet)
|
|
279
|
+
console.log(' RTK — download binary and wire into OMP');
|
|
276
280
|
const binDest = path.join(binDir, RTK_BINARY_NAME);
|
|
277
281
|
// Download failure must not skip wiring: a pre-existing rtk binary is
|
|
278
282
|
// exactly as good for the OMP hook, so wire whatever ends up at binDest.
|
|
283
|
+
// Dry runs stay offline: no registry probe, just the plan line above.
|
|
284
|
+
if (options.dryRun) {
|
|
285
|
+
if (verbose && !options.quiet)
|
|
286
|
+
console.log(` [dry-run] would download rtk binary and install to ${binDest}`);
|
|
287
|
+
await wireRtkOmp(binDest, options);
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
279
290
|
try {
|
|
280
291
|
const release = await withInteractiveSpinner('Finding latest RTK release', () => fetchJson(RTK_RELEASE_API));
|
|
281
292
|
const triple = resolveRtkTriple();
|
|
@@ -284,12 +295,6 @@ async function stepRtk(binDir, options) {
|
|
|
284
295
|
const asset = findRtkAsset(release, triple);
|
|
285
296
|
if (!asset)
|
|
286
297
|
return;
|
|
287
|
-
if (options.dryRun) {
|
|
288
|
-
console.log(` [dry-run] would download ${asset.name} from release ${release.tag_name}`);
|
|
289
|
-
console.log(' [dry-run] would verify checksum against checksums.txt');
|
|
290
|
-
console.log(` [dry-run] would extract and install to ${binDest}`);
|
|
291
|
-
return;
|
|
292
|
-
}
|
|
293
298
|
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'omp-rtk-'));
|
|
294
299
|
try {
|
|
295
300
|
const archivePath = path.join(tmpDir, asset.name);
|
|
@@ -301,7 +306,7 @@ async function stepRtk(binDir, options) {
|
|
|
301
306
|
update('Verifying RTK checksum');
|
|
302
307
|
return downloadedChecksums;
|
|
303
308
|
});
|
|
304
|
-
if (!await verifyRtkArchive(archivePath, asset.name, checksumsText))
|
|
309
|
+
if (!await verifyRtkArchive(archivePath, asset.name, checksumsText, options))
|
|
305
310
|
return;
|
|
306
311
|
const extractDir = path.join(tmpDir, 'extracted');
|
|
307
312
|
if (!await extractRtkArchive(archivePath, extractDir))
|
|
@@ -314,14 +319,12 @@ async function stepRtk(binDir, options) {
|
|
|
314
319
|
await fs.mkdir(path.dirname(binDest), { recursive: true });
|
|
315
320
|
await fs.copyFile(binDest, `${binDest}.bak`).catch(() => { });
|
|
316
321
|
await fs.copyFile(found, binDest);
|
|
317
|
-
console.log(` [write] ${binDest}`);
|
|
318
322
|
if (!IS_WINDOWS) {
|
|
319
323
|
await fs.chmod(binDest, 0o755);
|
|
320
324
|
debug(`chmod 755 ${binDest}`);
|
|
321
325
|
}
|
|
322
326
|
try {
|
|
323
|
-
|
|
324
|
-
console.log(` [ok] ${binDest} → ${v}`);
|
|
327
|
+
await execP(binDest, ['--version'], { timeout: 30000, shell: false });
|
|
325
328
|
}
|
|
326
329
|
catch {
|
|
327
330
|
console.log(` [hint] Verify manually: ${binDest} --version`);
|
|
@@ -352,7 +355,8 @@ async function stepRtk(binDir, options) {
|
|
|
352
355
|
async function copySources(extDir, files, skipLabel, options) {
|
|
353
356
|
const src = await readTextIfExists(files[0][0]);
|
|
354
357
|
if (!src) {
|
|
355
|
-
|
|
358
|
+
if (!options.quiet && options.dryRun)
|
|
359
|
+
console.log(` [skip] ${skipLabel} not found in repo`);
|
|
356
360
|
return false;
|
|
357
361
|
}
|
|
358
362
|
await writeIfChanged(path.join(extDir, files[0][1]), src, options);
|
|
@@ -366,6 +370,8 @@ async function copySources(extDir, files, skipLabel, options) {
|
|
|
366
370
|
return true;
|
|
367
371
|
}
|
|
368
372
|
async function stepSharedSessionState(extDir, options) {
|
|
373
|
+
if (!options.quiet)
|
|
374
|
+
console.log(' Shared files — sync session bridge');
|
|
369
375
|
await copySources(extDir, [
|
|
370
376
|
[SHARED_SESSION_STATE, path.join('shared', 'session-state.js')],
|
|
371
377
|
[SHARED_TYPES, path.join('shared', 'types.js')],
|
|
@@ -377,14 +383,16 @@ async function stepSharedSessionState(extDir, options) {
|
|
|
377
383
|
], 'shared/session-state.js', options);
|
|
378
384
|
}
|
|
379
385
|
async function stepModeReinforcement(extDir, ponytailExtPath, options) {
|
|
380
|
-
|
|
386
|
+
if (!options.quiet)
|
|
387
|
+
console.log(' Session helpers — sync shared files');
|
|
381
388
|
const dest = path.join(extDir, 'shared', 'mode-reinforcement.js');
|
|
382
389
|
if (!await copySources(extDir, [[MODE_REINFORCEMENT_INDEX, path.join('shared', 'mode-reinforcement.js')]], 'shared/mode-reinforcement.js', options))
|
|
383
390
|
return;
|
|
384
391
|
await ensureExtensionAfterConfigEntry(path.join(path.dirname(extDir), 'config.yml'), dest, ponytailExtPath, 'mode reinforcement', options);
|
|
385
392
|
}
|
|
386
393
|
async function stepRtkSession(extDir, options) {
|
|
387
|
-
|
|
394
|
+
if (!options.quiet)
|
|
395
|
+
console.log(' RTK session — install session mode');
|
|
388
396
|
await copySources(extDir, [[RTK_SESSION_INDEX, path.join('rtk-session', 'index.js')]], 'rtk-session/index.js', options);
|
|
389
397
|
}
|
|
390
398
|
// One rule fetch serves the install; dry runs stay offline
|
|
@@ -401,13 +409,14 @@ async function fetchCavemanRule(options) {
|
|
|
401
409
|
}
|
|
402
410
|
}
|
|
403
411
|
async function stepCaveman(extDir, rule, options) {
|
|
404
|
-
|
|
412
|
+
if (!options.quiet)
|
|
413
|
+
console.log(' Caveman — fetch rule and install session mode');
|
|
405
414
|
const cavemanDir = path.join(extDir, 'caveman-session');
|
|
406
415
|
if (!options.dryRun)
|
|
407
416
|
await fs.mkdir(cavemanDir, { recursive: true });
|
|
408
417
|
const ruleDest = path.join(cavemanDir, 'rule.md');
|
|
409
418
|
if (rule === null && (await readTextIfExists(ruleDest)) !== null) {
|
|
410
|
-
|
|
419
|
+
debug('Keeping existing rule.md');
|
|
411
420
|
}
|
|
412
421
|
else if (rule === null) {
|
|
413
422
|
console.log(' [skip] Caveman rule.md unavailable');
|
|
@@ -419,14 +428,16 @@ async function stepCaveman(extDir, rule, options) {
|
|
|
419
428
|
await copySources(extDir, [[CAVEMAN_INDEX, path.join('caveman-session', 'index.js')]], 'caveman-session/index.js', options);
|
|
420
429
|
}
|
|
421
430
|
async function stepTersioCommands(extDir, options) {
|
|
422
|
-
|
|
431
|
+
if (!options.quiet)
|
|
432
|
+
console.log(' Tersio commands — install /tersio root command');
|
|
423
433
|
await copySources(extDir, [[TERSIO_COMMANDS_INDEX, path.join('tersio-commands', 'index.js')]], 'tersio-commands/index.js', options);
|
|
424
434
|
}
|
|
425
435
|
async function stepUpdater(extDir, options) {
|
|
426
436
|
await copySources(extDir, [[UPDATER_INDEX, path.join('ai-addons-updater', 'index.js')]], 'ai-addons-updater/index.js', options);
|
|
427
437
|
}
|
|
428
438
|
async function stepCombo(extDir, options) {
|
|
429
|
-
|
|
439
|
+
if (!options.quiet)
|
|
440
|
+
console.log(' Combo — install preset switch');
|
|
430
441
|
const dest = path.join(extDir, 'combo-toggle', 'index.js');
|
|
431
442
|
if (!await copySources(extDir, [[COMBO_TOGGLE_INDEX, path.join('combo-toggle', 'index.js')]], 'combo-toggle/index.js', options))
|
|
432
443
|
return;
|
|
@@ -473,7 +484,7 @@ async function storedProfile() {
|
|
|
473
484
|
base.ponytailDefault = stored.ponytailDefault;
|
|
474
485
|
return base;
|
|
475
486
|
}
|
|
476
|
-
async function resolveProfile(forceReinstall = false) {
|
|
487
|
+
async function resolveProfile(forceReinstall = false, opts = {}) {
|
|
477
488
|
// Seed from the lock file so flag-less update/reinstall runs keep the
|
|
478
489
|
// user's configured defaults instead of resetting them to off.
|
|
479
490
|
const profile = await storedProfile();
|
|
@@ -508,32 +519,10 @@ async function resolveProfile(forceReinstall = false) {
|
|
|
508
519
|
profile.rtkDefault = rtkDefaultFlag === 'on';
|
|
509
520
|
if (ponytailDefaultFlag !== undefined)
|
|
510
521
|
profile.ponytailDefault = ponytailDefaultFlag;
|
|
511
|
-
|
|
522
|
+
if ((!opts.quiet && verbose) || dryRun)
|
|
523
|
+
console.log(` Defaults: combo=${profile.comboDefault} (caveman=${profile.cavemanDefault} · rtk=${profile.rtkDefault ? 'on' : 'off'} · ponytail=${profile.ponytailDefault})`);
|
|
512
524
|
return profile;
|
|
513
525
|
}
|
|
514
|
-
// ponytail: post-install guard. List entries under `extensions: null` break
|
|
515
|
-
// OMP launch, so fail loudly here instead of at the next OMP start.
|
|
516
|
-
async function validateConfigExtensions(agentDir, options) {
|
|
517
|
-
if (options.dryRun)
|
|
518
|
-
return;
|
|
519
|
-
const raw = await readTextIfExists(path.join(agentDir, 'config.yml'));
|
|
520
|
-
if (!raw)
|
|
521
|
-
return;
|
|
522
|
-
const lines = raw.split('\n');
|
|
523
|
-
const keyIdx = lines.findIndex((l) => EXTENSIONS_KEY_RE.test(l));
|
|
524
|
-
if (keyIdx === -1)
|
|
525
|
-
return;
|
|
526
|
-
const scalarNull = /^\s*extensions\s*:\s*(null|~|\[\s*\])\s*$/i.test(lines[keyIdx]);
|
|
527
|
-
const hasEntries = lines.slice(keyIdx + 1).some((l) => /^\s*-\s+\S/.test(l));
|
|
528
|
-
if (scalarNull && hasEntries) {
|
|
529
|
-
console.log(' [fail] config.yml lists extensions under `extensions: null` — OMP will fail to launch.');
|
|
530
|
-
console.log(' [hint] Replace the `extensions: null` line with `extensions:`, then reinstall.');
|
|
531
|
-
process.exitCode = 1;
|
|
532
|
-
}
|
|
533
|
-
else {
|
|
534
|
-
console.log(' [ok] config.yml extensions key valid');
|
|
535
|
-
}
|
|
536
|
-
}
|
|
537
526
|
// Persist the profile as omp plugin settings so `omp plugin config get`
|
|
538
527
|
// reflects the choice and the extensions pick it up on session start.
|
|
539
528
|
async function writePluginSettings(profile, options) {
|
|
@@ -559,7 +548,8 @@ async function writePluginSettings(profile, options) {
|
|
|
559
548
|
ponytailDefault: profile.ponytailDefault,
|
|
560
549
|
};
|
|
561
550
|
if (options.dryRun) {
|
|
562
|
-
|
|
551
|
+
if (verbose && !options.quiet)
|
|
552
|
+
console.log(` [dry-run] would write plugin settings (${PACKAGE_NAME}) to ${lockPath}`);
|
|
563
553
|
return;
|
|
564
554
|
}
|
|
565
555
|
await fs.mkdir(pluginsDir, { recursive: true });
|
|
@@ -606,16 +596,13 @@ async function runCommandMenu() {
|
|
|
606
596
|
await runInstall();
|
|
607
597
|
break;
|
|
608
598
|
case 'update': {
|
|
609
|
-
// One bound flow:
|
|
610
|
-
// refresh in the same
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
console.log(` [update] tersio ${latest} available (installed ${PACKAGE_VERSION})`);
|
|
614
|
-
else if (latest === 'unknown')
|
|
615
|
-
console.log(` [warn] could not reach the npm registry — run \`tersio update\` to retry`);
|
|
599
|
+
// One bound flow: the version check already ran above, so report it
|
|
600
|
+
// and offer the refresh in the same breath — no second "update" quiz.
|
|
601
|
+
if (typeof newer === 'string')
|
|
602
|
+
console.log(` tersio ${newer} available (installed ${PACKAGE_VERSION})`);
|
|
616
603
|
else
|
|
617
|
-
console.log(`
|
|
618
|
-
const go = await askInteractiveConfirm('Update
|
|
604
|
+
console.log(` tersio ${PACKAGE_VERSION} is the latest`);
|
|
605
|
+
const go = await askInteractiveConfirm('Update now (CLI + RTK, Caveman rule, Ponytail)?');
|
|
619
606
|
if (go.status === 'confirmed' && go.value) {
|
|
620
607
|
await runLatestUpdate();
|
|
621
608
|
}
|
|
@@ -664,19 +651,23 @@ async function runInstall(overrides = {}) {
|
|
|
664
651
|
await runCommandMenu();
|
|
665
652
|
return;
|
|
666
653
|
}
|
|
667
|
-
|
|
654
|
+
// apply-update is the delegated payload of `tersio update`: stay silent —
|
|
655
|
+
// the parent already printed the plan and owns the closing summary. Banner
|
|
656
|
+
// repeats here otherwise (the reported double-print), burying real output.
|
|
657
|
+
const quiet = applyUpdate;
|
|
658
|
+
if (!quiet)
|
|
659
|
+
printWelcome();
|
|
668
660
|
if (isReinstall) {
|
|
669
661
|
// removeRtk stays false: reinstall is about to replace the binary, and
|
|
670
662
|
// deleting it first would leave nothing to wire if the fresh download
|
|
671
663
|
// fails (rate limit, offline). rtk.ts is removed here and re-wired below.
|
|
672
664
|
await runUninstall({ yes: true, removePonytail: false, removeRtk: false });
|
|
673
665
|
}
|
|
674
|
-
if (dryRun)
|
|
675
|
-
console.log('
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
console.log(` Home: ${HOME}`);
|
|
666
|
+
if (dryRun && !quiet)
|
|
667
|
+
console.log('Preview — no changes will be written.\n');
|
|
668
|
+
if (!quiet) {
|
|
669
|
+
console.log(`Installing Tersio v${PACKAGE_VERSION}`);
|
|
670
|
+
}
|
|
680
671
|
// Remind humans a newer release exists; silent for scripts (no TTY) and for
|
|
681
672
|
// the apply-update payload, which is itself an update run.
|
|
682
673
|
if (tty() && !applyUpdate && command !== 'uninstall') {
|
|
@@ -703,47 +694,54 @@ async function runInstall(overrides = {}) {
|
|
|
703
694
|
}
|
|
704
695
|
}
|
|
705
696
|
}
|
|
706
|
-
console.log(' Scope: user (all OMP sessions)');
|
|
707
697
|
// Resolve session defaults: flags > interactive prompt > defaults.
|
|
708
|
-
const profile = await resolveProfile(isReinstall);
|
|
698
|
+
const profile = await resolveProfile(isReinstall, { quiet });
|
|
709
699
|
const userDir = OMP_AGENT_DIR;
|
|
710
700
|
const userExtDir = path.join(userDir, 'extensions');
|
|
711
701
|
// apply-update is `tersio update`'s payload run: treat it like reinstall so
|
|
712
702
|
// the add-ons refresh too — Ponytail package via npm, self plugin, RTK
|
|
713
703
|
// binary (always re-downloaded), and the Caveman rule (always re-fetched).
|
|
714
|
-
const options = { dryRun, verbose, yes, reinstall: isReinstall || applyUpdate };
|
|
715
|
-
// Check prerequisites
|
|
716
|
-
console.log('\nPrerequisites:');
|
|
704
|
+
const options = { dryRun, verbose, yes, reinstall: isReinstall || applyUpdate, quiet };
|
|
717
705
|
try {
|
|
718
706
|
const v = (await execP(OMP_BIN, ['--version'])).stdout.trim();
|
|
719
|
-
|
|
707
|
+
if (verbose && !quiet)
|
|
708
|
+
console.log(` omp ${v}`);
|
|
720
709
|
}
|
|
721
710
|
catch {
|
|
722
711
|
console.log(' [fail] omp not found — ensure it\'s installed');
|
|
723
712
|
}
|
|
724
713
|
const cavemanRule = await fetchCavemanRule(options);
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
714
|
+
const failures = [];
|
|
715
|
+
const capture = async (label, work) => {
|
|
716
|
+
try {
|
|
717
|
+
await work();
|
|
718
|
+
}
|
|
719
|
+
catch (e) {
|
|
720
|
+
failures.push(label);
|
|
721
|
+
console.log(` [fail] ${label}: ${shortError(e)}`);
|
|
722
|
+
}
|
|
723
|
+
};
|
|
724
|
+
await capture('shared', () => stepSharedSessionState(userExtDir, options));
|
|
725
|
+
await capture('ponytail', () => stepPonytail(OMP_PLUGINS_DIR, userDir, options));
|
|
726
|
+
let selfPlugin = false;
|
|
727
|
+
await capture('self-plugin', async () => { selfPlugin = await stepSelfPlugin(OMP_PLUGINS_DIR, options); });
|
|
729
728
|
const ponytailExtPath = path.join(OMP_PLUGINS_DIR, 'node_modules', '@dietrichgebert', 'ponytail', 'pi-extension', 'index.js');
|
|
730
|
-
await stepRtk(BUN_BIN_DIR, options);
|
|
731
|
-
await stepRtkSession(userExtDir, options);
|
|
732
|
-
await stepCaveman(userExtDir, cavemanRule, options);
|
|
733
|
-
await stepCombo(userExtDir, options);
|
|
734
|
-
await stepTersioCommands(userExtDir, options);
|
|
735
|
-
await stepModeReinforcement(userExtDir, ponytailExtPath, options);
|
|
736
|
-
await stepUpdater(userExtDir, options);
|
|
729
|
+
await capture('rtk', () => stepRtk(BUN_BIN_DIR, options));
|
|
730
|
+
await capture('rtk session', () => stepRtkSession(userExtDir, options));
|
|
731
|
+
await capture('caveman', () => stepCaveman(userExtDir, cavemanRule, options));
|
|
732
|
+
await capture('combo', () => stepCombo(userExtDir, options));
|
|
733
|
+
await capture('commands', () => stepTersioCommands(userExtDir, options));
|
|
734
|
+
await capture('reinforcement', () => stepModeReinforcement(userExtDir, ponytailExtPath, options));
|
|
735
|
+
await capture('updater', () => stepUpdater(userExtDir, options));
|
|
737
736
|
if (selfPlugin)
|
|
738
|
-
await writePluginSettings(profile, options);
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
console.log(' 6. /combo medium (toggle all 3 at once — off by default)');
|
|
737
|
+
await capture('settings', () => writePluginSettings(profile, options));
|
|
738
|
+
if (quiet) {
|
|
739
|
+
if (failures.length > 0)
|
|
740
|
+
console.log(` add-ons: ${failures.length} failed (${failures.join(', ')}) — see [fail] lines above`);
|
|
741
|
+
}
|
|
742
|
+
else {
|
|
743
|
+
console.log(failures.length === 0 ? '\nDone — restart OMP, then /combo medium.' : `\nDone with ${failures.length} failure(s) — see [fail] lines above.`);
|
|
744
|
+
}
|
|
747
745
|
closeRL();
|
|
748
746
|
}
|
|
749
747
|
export { runInstall };
|