@krtclcdy/tersio 2.18.0 → 2.20.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 +2 -2
- package/cli/common.js +21 -3
- package/cli/currency.js +102 -0
- package/cli/dashboard.js +54 -2
- package/cli/doctor.js +9 -7
- package/cli/install.js +35 -101
- package/cli/profile.js +92 -0
- package/cli/reset.js +4 -2
- package/cli/settings.js +122 -0
- package/cli/usage.js +38 -24
- package/dashboard/app.js +297 -70
- package/dashboard/styles.css +58 -1
- package/dashboard/template.html +75 -3
- package/extensions/ai-addons-updater/index.ts +377 -0
- package/extensions/caveman-session/index.ts +161 -0
- package/extensions/combo-toggle/index.ts +217 -0
- package/extensions/lib/utils.ts +162 -0
- package/extensions/rtk-session/index.ts +164 -0
- package/extensions/shared/carbon.ts +123 -0
- package/extensions/shared/mode-reinforcement.ts +29 -0
- package/extensions/shared/plugin-settings.ts +57 -0
- package/extensions/shared/pricing.ts +168 -0
- package/extensions/shared/rtk-gain.ts +105 -0
- package/extensions/shared/session-state.ts +201 -0
- package/extensions/shared/types.ts +85 -0
- package/extensions/shared/usage-ledger.ts +406 -0
- package/extensions/shared/usage-store.ts +325 -0
- package/extensions/tersio-commands/index.ts +110 -0
- package/package.json +36 -24
- package/tersio.js +8 -1
- package/extensions/ai-addons-updater/index.js +0 -331
- package/extensions/caveman-session/index.js +0 -152
- package/extensions/combo-toggle/index.js +0 -165
- package/extensions/lib/utils.js +0 -154
- package/extensions/rtk-session/index.js +0 -148
- package/extensions/shared/carbon.js +0 -88
- package/extensions/shared/mode-reinforcement.js +0 -26
- package/extensions/shared/plugin-settings.js +0 -51
- package/extensions/shared/pricing.js +0 -163
- package/extensions/shared/rtk-gain.js +0 -82
- package/extensions/shared/session-state.js +0 -177
- package/extensions/shared/types.js +0 -3
- package/extensions/shared/usage-ledger.js +0 -333
- package/extensions/tersio-commands/index.js +0 -102
package/README.md
CHANGED
|
@@ -105,12 +105,12 @@ Break-even math, balanced preset (caveman full + rtk + ponytail): a mixed turn c
|
|
|
105
105
|
| `tersio reinstall` | Fresh install, preserving the Ponytail package |
|
|
106
106
|
| `tersio doctor` | Check OMP, extension, Ponytail, and RTK health |
|
|
107
107
|
| `tersio usage` | Ledger-backed usage + savings report |
|
|
108
|
-
| `tersio gain` | Open the gain dashboard (`--open`, `--export <file>`, `--port <n>`; serves localhost only) |
|
|
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 |
|
|
110
110
|
| `tersio uninstall` | Remove extensions, registration, and the Ponytail plugin (`--keep-ponytail` keeps Ponytail; `--remove-rtk` also removes the RTK binary and its `rtk.ts` OMP wiring) |
|
|
111
111
|
| `tersio version` | Print version |
|
|
112
112
|
|
|
113
|
-
Flags: `--dry-run`, `--yes`/`-y`, `--verbose`, `--combo-default`/`--caveman-default`/`--rtk-default`/`--ponytail-default
|
|
113
|
+
Flags: `--dry-run`, `--yes`/`-y`, `--verbose`, `--combo-default`/`--caveman-default`/`--rtk-default`/`--ponytail-default`, `--currency <code>` (usage|gain display currency; flag wins, then the `tersio settings` default, then USD). Legacy `--doctor` / `--uninstall` forms still work.
|
|
114
114
|
|
|
115
115
|
## ⌨️ Commands reference
|
|
116
116
|
|
package/cli/common.js
CHANGED
|
@@ -5,6 +5,7 @@ import path from 'node:path';
|
|
|
5
5
|
import { promisify } from 'node:util';
|
|
6
6
|
import { createRequire } from 'node:module';
|
|
7
7
|
import { readTextIfExists } from '../extensions/lib/utils.js';
|
|
8
|
+
import { parseCurrencyFlag, readStoredCurrency } from './currency.js';
|
|
8
9
|
const IS_WINDOWS = process.platform === 'win32';
|
|
9
10
|
const RTK_BINARY_NAME = IS_WINDOWS ? 'rtk.exe' : 'rtk';
|
|
10
11
|
const HOME = process.env.HOME || process.env.USERPROFILE || '';
|
|
@@ -46,7 +47,7 @@ function flagValue(name) {
|
|
|
46
47
|
return args.find((arg) => arg.startsWith(prefix))?.slice(prefix.length);
|
|
47
48
|
}
|
|
48
49
|
// --- CLI flags ---
|
|
49
|
-
const COMMANDS = { install: true, update: true, reinstall: true, doctor: true, uninstall: true, usage: true, gain: true, reset: true, version: true, help: true };
|
|
50
|
+
const COMMANDS = { install: true, update: true, reinstall: true, doctor: true, uninstall: true, usage: true, gain: true, reset: true, settings: true, version: true, help: true };
|
|
50
51
|
const args = process.argv.slice(2);
|
|
51
52
|
const commandArg = args.find((arg) => !arg.startsWith('-'));
|
|
52
53
|
const command = commandArg?.toLowerCase() || null;
|
|
@@ -64,9 +65,14 @@ const doctor = command === 'doctor' || args.includes('--doctor');
|
|
|
64
65
|
const usage = command === 'usage' || args.includes('--usage');
|
|
65
66
|
const gain = command === 'gain';
|
|
66
67
|
const reset = command === 'reset';
|
|
68
|
+
const settings = command === 'settings';
|
|
67
69
|
const dashboardPort = Number.parseInt(flagValue('--port') ?? '', 10) || 0;
|
|
68
70
|
const dashboardOpen = args.includes('--open');
|
|
69
71
|
const dashboardExport = flagValue('--export') ?? null;
|
|
72
|
+
// Display currency for usage/gain: --currency flag wins, then the stored
|
|
73
|
+
// plugin default (tersio settings), then USD.
|
|
74
|
+
const currency = parseCurrencyFlag(flagValue('--currency')) ?? readStoredCurrency();
|
|
75
|
+
const currencyGiven = flagValue('--currency') !== undefined;
|
|
70
76
|
const uninstall = command === 'uninstall' || args.includes('--uninstall');
|
|
71
77
|
const removePonytail = args.includes('--remove-ponytail');
|
|
72
78
|
const keepPonytail = args.includes('--keep-ponytail');
|
|
@@ -141,6 +147,13 @@ async function ensureExtensionInConfig(configPath, extensionPath, label, options
|
|
|
141
147
|
const line = ` - ${normalizedPath}`;
|
|
142
148
|
const raw = await readTextIfExists(configPath);
|
|
143
149
|
let lines = (raw || '').split('\n');
|
|
150
|
+
// Drop legacy compiled twins (same dir/name, .js): previous installs
|
|
151
|
+
// registered them, and OMP must never load both copies. Match on the
|
|
152
|
+
// trailing path so absolute and relative entries are both caught.
|
|
153
|
+
if (normalizedPath.endsWith('.ts')) {
|
|
154
|
+
const legacyTail = `${normalizedPath.slice(0, -3)}.js`.split('/').slice(-2).join('/');
|
|
155
|
+
lines = lines.filter((l) => !l.trim().replace(/^\.\//, '').endsWith(legacyTail));
|
|
156
|
+
}
|
|
144
157
|
if (lines.some((l) => l.includes(normalizedPath))) {
|
|
145
158
|
debug(`${label} already in config.yml`);
|
|
146
159
|
return false;
|
|
@@ -170,7 +183,12 @@ async function ensureExtensionAfterConfigEntry(configPath, extensionPath, afterP
|
|
|
170
183
|
const normalizedAfterPath = afterPath.replace(/\\/g, '/');
|
|
171
184
|
const line = ` - ${normalizedPath}`;
|
|
172
185
|
const raw = await readTextIfExists(configPath);
|
|
173
|
-
|
|
186
|
+
let lines = (raw || '').split('\n');
|
|
187
|
+
// Same legacy-twin drop as ensureExtensionInConfig above.
|
|
188
|
+
if (normalizedPath.endsWith('.ts')) {
|
|
189
|
+
const legacyTail = `${normalizedPath.slice(0, -3)}.js`.split('/').slice(-2).join('/');
|
|
190
|
+
lines = lines.filter((l) => !l.trim().replace(/^\.\//, '').endsWith(legacyTail));
|
|
191
|
+
}
|
|
174
192
|
const existingIndex = lines.findIndex((entry) => entry.includes(normalizedPath));
|
|
175
193
|
const afterIndex = lines.findIndex((entry) => entry.includes(normalizedAfterPath));
|
|
176
194
|
if (existingIndex !== -1 && afterIndex !== -1 && existingIndex === afterIndex + 1)
|
|
@@ -267,4 +285,4 @@ function relTime(ageMs) {
|
|
|
267
285
|
const weeks = Math.floor(days / 7);
|
|
268
286
|
return `${weeks} week${weeks === 1 ? '' : 's'} ago`;
|
|
269
287
|
}
|
|
270
|
-
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, dashboardPort, dashboardOpen, dashboardExport, 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, };
|
|
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, };
|
package/cli/currency.js
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
// cli/currency.ts — display-currency helpers (USD-base, offline snapshot rates).
|
|
2
|
+
// Same 10 currencies, symbols, and snapshot rates as the gain dashboard's
|
|
3
|
+
// converter (dashboard/app.js); the dashboard replaces rates with live
|
|
4
|
+
// frankfurter figures when reachable, the CLI always uses the snapshot.
|
|
5
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
6
|
+
import os from 'node:os';
|
|
7
|
+
import path from 'node:path';
|
|
8
|
+
const CURRENCY_CODES = [
|
|
9
|
+
'USD',
|
|
10
|
+
'PHP',
|
|
11
|
+
'EUR',
|
|
12
|
+
'GBP',
|
|
13
|
+
'JPY',
|
|
14
|
+
'KRW',
|
|
15
|
+
'SGD',
|
|
16
|
+
'AUD',
|
|
17
|
+
'CAD',
|
|
18
|
+
'INR',
|
|
19
|
+
];
|
|
20
|
+
const DEFAULT_CURRENCY = 'USD';
|
|
21
|
+
function isCurrencyCode(value) {
|
|
22
|
+
return CURRENCY_CODES.includes(value);
|
|
23
|
+
}
|
|
24
|
+
// --currency flag parsing: undefined when absent, exits on invalid input.
|
|
25
|
+
function parseCurrencyFlag(raw) {
|
|
26
|
+
if (raw === undefined)
|
|
27
|
+
return undefined;
|
|
28
|
+
const v = raw.trim().toUpperCase();
|
|
29
|
+
if (!isCurrencyCode(v)) {
|
|
30
|
+
console.error(`[fail] Invalid --currency: ${raw}. Valid: ${CURRENCY_CODES.join(', ')}`);
|
|
31
|
+
process.exit(1);
|
|
32
|
+
}
|
|
33
|
+
return v;
|
|
34
|
+
}
|
|
35
|
+
// Stored default from the omp plugin lock file (written by tersio settings);
|
|
36
|
+
// USD when missing, corrupt, or holding an unknown code. Sync so flag
|
|
37
|
+
// parsing in cli/common.ts can fall back to it at startup.
|
|
38
|
+
function readStoredCurrency() {
|
|
39
|
+
const home = process.env.HOME || process.env.USERPROFILE || os.homedir();
|
|
40
|
+
const lock = path.join(home, '.omp', 'plugins', 'omp-plugins.lock.json');
|
|
41
|
+
if (!existsSync(lock))
|
|
42
|
+
return DEFAULT_CURRENCY;
|
|
43
|
+
try {
|
|
44
|
+
const parsed = JSON.parse(readFileSync(lock, 'utf8'));
|
|
45
|
+
if (!parsed || typeof parsed !== 'object' || !('settings' in parsed))
|
|
46
|
+
return DEFAULT_CURRENCY;
|
|
47
|
+
const entry = parsed.settings?.['@krtclcdy/tersio'];
|
|
48
|
+
if (!entry || typeof entry !== 'object')
|
|
49
|
+
return DEFAULT_CURRENCY;
|
|
50
|
+
const raw = entry.currency;
|
|
51
|
+
return typeof raw === 'string' && isCurrencyCode(raw.trim().toUpperCase())
|
|
52
|
+
? raw.trim().toUpperCase()
|
|
53
|
+
: DEFAULT_CURRENCY;
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
return DEFAULT_CURRENCY;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
const CURRENCY_META = {
|
|
60
|
+
USD: { symbol: '$', decimals: 2 },
|
|
61
|
+
PHP: { symbol: '₱', decimals: 2 },
|
|
62
|
+
EUR: { symbol: '€', decimals: 2 },
|
|
63
|
+
GBP: { symbol: '£', decimals: 2 },
|
|
64
|
+
JPY: { symbol: '¥', decimals: 0 },
|
|
65
|
+
KRW: { symbol: '₩', decimals: 0 },
|
|
66
|
+
SGD: { symbol: 'S$', decimals: 2 },
|
|
67
|
+
AUD: { symbol: 'A$', decimals: 2 },
|
|
68
|
+
CAD: { symbol: 'C$', decimals: 2 },
|
|
69
|
+
INR: { symbol: '₹', decimals: 2 },
|
|
70
|
+
};
|
|
71
|
+
const FX_SNAPSHOT_RATES = {
|
|
72
|
+
USD: 1,
|
|
73
|
+
PHP: 58.7,
|
|
74
|
+
EUR: 0.92,
|
|
75
|
+
GBP: 0.79,
|
|
76
|
+
JPY: 149.8,
|
|
77
|
+
KRW: 1385,
|
|
78
|
+
SGD: 1.34,
|
|
79
|
+
AUD: 1.52,
|
|
80
|
+
CAD: 1.37,
|
|
81
|
+
INR: 88.2,
|
|
82
|
+
};
|
|
83
|
+
function decimalsFor(amount, base) {
|
|
84
|
+
const a = Math.abs(amount);
|
|
85
|
+
if (a >= 0.1 || a === 0)
|
|
86
|
+
return base;
|
|
87
|
+
if (a >= 0.001)
|
|
88
|
+
return Math.max(base, 4);
|
|
89
|
+
if (a >= 0.00001)
|
|
90
|
+
return Math.max(base, 6);
|
|
91
|
+
return Math.max(base, 8);
|
|
92
|
+
}
|
|
93
|
+
function convertUsd(usd, currency) {
|
|
94
|
+
return usd * FX_SNAPSHOT_RATES[currency];
|
|
95
|
+
}
|
|
96
|
+
function formatCurrency(usd, currency) {
|
|
97
|
+
const meta = CURRENCY_META[currency];
|
|
98
|
+
const amount = convertUsd(usd, currency);
|
|
99
|
+
const d = decimalsFor(amount, meta.decimals);
|
|
100
|
+
return meta.symbol + amount.toLocaleString('en-US', { minimumFractionDigits: d, maximumFractionDigits: d });
|
|
101
|
+
}
|
|
102
|
+
export { CURRENCY_CODES, DEFAULT_CURRENCY, convertUsd, formatCurrency, isCurrencyCode, parseCurrencyFlag, readStoredCurrency, };
|
package/cli/dashboard.js
CHANGED
|
@@ -10,6 +10,8 @@ import path from 'node:path';
|
|
|
10
10
|
import { fileURLToPath } from 'node:url';
|
|
11
11
|
import { clearUsageLedger, markReset, readUsage } from '../extensions/shared/usage-ledger.js';
|
|
12
12
|
import { summarizeUsage } from './usage.js';
|
|
13
|
+
import { isCurrencyCode } from './currency.js';
|
|
14
|
+
import { storedProfile, writePluginSettings } from './profile.js';
|
|
13
15
|
import { withInteractiveSpinner } from './interactive.js';
|
|
14
16
|
const DASH_DIR = path.join(path.dirname(fileURLToPath(import.meta.url)), '..', 'dashboard');
|
|
15
17
|
const TEMPLATE = path.join(DASH_DIR, 'template.html');
|
|
@@ -26,6 +28,29 @@ async function faviconDataUri() {
|
|
|
26
28
|
function dataJson() {
|
|
27
29
|
return JSON.stringify(summarizeUsage(readUsage()));
|
|
28
30
|
}
|
|
31
|
+
// Persist the dashboard's picker choice so close → reopen keeps it: each
|
|
32
|
+
// `tersio gain` run serves a fresh ephemeral port (a new origin), so the
|
|
33
|
+
// browser's localStorage alone cannot survive a restart. The stored default
|
|
34
|
+
// feeds data.json and `tersio usage` on the next run.
|
|
35
|
+
async function saveDashboardCurrency(raw) {
|
|
36
|
+
if (typeof raw !== 'string')
|
|
37
|
+
return null;
|
|
38
|
+
const code = raw.trim().toUpperCase();
|
|
39
|
+
if (!isCurrencyCode(code))
|
|
40
|
+
return null;
|
|
41
|
+
const profile = await storedProfile();
|
|
42
|
+
profile.currency = code;
|
|
43
|
+
await writePluginSettings(profile, {});
|
|
44
|
+
return code;
|
|
45
|
+
}
|
|
46
|
+
function readBody(req) {
|
|
47
|
+
return new Promise((resolve) => {
|
|
48
|
+
let body = '';
|
|
49
|
+
req.on('data', (chunk) => { body += String(chunk); });
|
|
50
|
+
req.on('end', () => { resolve(body); });
|
|
51
|
+
req.on('error', () => { resolve(''); });
|
|
52
|
+
});
|
|
53
|
+
}
|
|
29
54
|
function openBrowser(url) {
|
|
30
55
|
const cmd = process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'start' : 'xdg-open';
|
|
31
56
|
spawn(cmd, [url], { detached: true, stdio: 'ignore' }).unref();
|
|
@@ -38,10 +63,18 @@ async function runDashboard(options) {
|
|
|
38
63
|
// Replacer functions throughout: session data routinely contains `$'`
|
|
39
64
|
// sequences (shell quoting in tool details), which String.replace would
|
|
40
65
|
// expand as match-suffix patterns and corrupt the file.
|
|
66
|
+
// RTK-metered command lines routinely contain literal `</script>` (Vue
|
|
67
|
+
// SFC probes), which would close the inlined <script> early and dump the
|
|
68
|
+
// rest of the JSON as page text. Escape it; JSON.parse never sees the
|
|
69
|
+
// backslash form inside a string literal, the browser decodes it first.
|
|
41
70
|
const inline = template
|
|
42
71
|
.replace('<link rel="stylesheet" href="styles.css">', () => `<style>\n${css}</style>`)
|
|
43
72
|
.replace('<script src="app.js" defer></script>', () => `<script>\n${js}</script>`)
|
|
44
|
-
.replace("fetch('data.json')", () => `Promise.resolve({ json: function () { return ${dataJson()}; } })`)
|
|
73
|
+
.replace("fetch('data.json')", () => `Promise.resolve({ json: function () { return ${dataJson().replace(/<\/(script)/gi, '<\\/$1')}; } })`)
|
|
74
|
+
// Exported file runs on file:// where load() early-returns before the
|
|
75
|
+
// stub above ever runs, so nothing ever renders. Drop that guard in
|
|
76
|
+
// the export only; template.html keeps it to avoid 404 polling loops.
|
|
77
|
+
.replace("if (window.location.protocol === 'file:') return;", () => `if (false) return;`)
|
|
45
78
|
.replace('href="brand.webp"', () => `href="${icon}"`)
|
|
46
79
|
.replace('src="brand.webp"', () => `src="${icon}"`);
|
|
47
80
|
await fs.writeFile(options.exportFile, inline, 'utf8');
|
|
@@ -57,6 +90,25 @@ async function runDashboard(options) {
|
|
|
57
90
|
res.end(JSON.stringify({ ok: true, rows }));
|
|
58
91
|
return;
|
|
59
92
|
}
|
|
93
|
+
if (req.url === '/currency' && req.method === 'POST') {
|
|
94
|
+
let code = null;
|
|
95
|
+
try {
|
|
96
|
+
code = JSON.parse(await readBody(req)).currency ?? null;
|
|
97
|
+
}
|
|
98
|
+
catch {
|
|
99
|
+
code = null;
|
|
100
|
+
}
|
|
101
|
+
const saved = await saveDashboardCurrency(code);
|
|
102
|
+
if (saved === null) {
|
|
103
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
104
|
+
res.end(JSON.stringify({ ok: false, error: 'unknown currency' }));
|
|
105
|
+
}
|
|
106
|
+
else {
|
|
107
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
108
|
+
res.end(JSON.stringify({ ok: true, currency: saved }));
|
|
109
|
+
}
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
60
112
|
if (req.url === '/data.json') {
|
|
61
113
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
62
114
|
res.end(dataJson());
|
|
@@ -100,4 +152,4 @@ async function runDashboard(options) {
|
|
|
100
152
|
openBrowser(url);
|
|
101
153
|
});
|
|
102
154
|
}
|
|
103
|
-
export { runDashboard };
|
|
155
|
+
export { runDashboard, saveDashboardCurrency };
|
package/cli/doctor.js
CHANGED
|
@@ -4,6 +4,7 @@ import path from 'node:path';
|
|
|
4
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
5
|
import { runInteractivePhase } from './interactive.js';
|
|
6
6
|
import { ledgerPath, readUsage, sessionsDir } from '../extensions/shared/usage-ledger.js';
|
|
7
|
+
import { usageDbPath } from '../extensions/shared/usage-store.js';
|
|
7
8
|
import { readRtkGain, rtkDbPath } from '../extensions/shared/rtk-gain.js';
|
|
8
9
|
import { checkForUpdate } from './update.js';
|
|
9
10
|
import { readTextIfExists } from '../extensions/lib/utils.js';
|
|
@@ -18,13 +19,13 @@ async function runDoctor() {
|
|
|
18
19
|
// Ponytail
|
|
19
20
|
const ponytailPkg = path.join(pluginsDir, 'node_modules', '@dietrichgebert', 'ponytail', 'package.json');
|
|
20
21
|
const ponytailExt = path.join(pluginsDir, 'node_modules', '@dietrichgebert', 'ponytail', 'pi-extension', 'index.js');
|
|
21
|
-
const cavemanIndex = path.join(extDir, 'caveman-session', 'index.
|
|
22
|
+
const cavemanIndex = path.join(extDir, 'caveman-session', 'index.ts');
|
|
22
23
|
const cavemanRule = path.join(extDir, 'caveman-session', 'rule.md');
|
|
23
|
-
const rtkIndex = path.join(extDir, 'rtk-session', 'index.
|
|
24
|
-
const updaterIndex = path.join(extDir, 'ai-addons-updater', 'index.
|
|
25
|
-
const comboIndex = path.join(extDir, 'combo-toggle', 'index.
|
|
26
|
-
const tersioIndex = path.join(extDir, 'tersio-commands', 'index.
|
|
27
|
-
const modeReinforcement = path.join(extDir, 'shared', 'mode-reinforcement.
|
|
24
|
+
const rtkIndex = path.join(extDir, 'rtk-session', 'index.ts');
|
|
25
|
+
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');
|
|
28
29
|
const selfPkg = path.join(pluginsDir, 'node_modules', PACKAGE_NAME, 'package.json');
|
|
29
30
|
// Independent probes start concurrently; sections report in fixed order as
|
|
30
31
|
// their data settles. Every probe resolves instead of rejecting.
|
|
@@ -32,7 +33,7 @@ async function runDoctor() {
|
|
|
32
33
|
ompVersion: execP(OMP_BIN, ['--version']).then((r) => r.stdout.trim(), () => null),
|
|
33
34
|
agentEntries: fs.readdir(agentDir).catch(() => null),
|
|
34
35
|
extEntries: fs.readdir(extDir).catch(() => null),
|
|
35
|
-
sharedStateText: readTextIfExists(path.join(extDir, 'shared', 'session-state.
|
|
36
|
+
sharedStateText: readTextIfExists(path.join(extDir, 'shared', 'session-state.ts')),
|
|
36
37
|
configText: readTextIfExists(configPath),
|
|
37
38
|
ponytailPkgText: readTextIfExists(ponytailPkg),
|
|
38
39
|
ponytailExtText: readTextIfExists(ponytailExt),
|
|
@@ -136,6 +137,7 @@ async function runDoctor() {
|
|
|
136
137
|
check('Usage ledger', true, usageRows.length ? `${usageRows.length} rows` : 'empty — no records yet');
|
|
137
138
|
console.log(` Usage ledger (tersio-owned, tersio reset clears): ${ledgerPath()} · ${usageRows.length} rows`);
|
|
138
139
|
console.log(` Session transcripts (host-owned, never touched): ${sessionsDir()}`);
|
|
140
|
+
console.log(` Usage DB (tersio-owned, tersio reset clears): ${usageDbPath()}`);
|
|
139
141
|
const rtkDb = rtkDbPath();
|
|
140
142
|
const rtk = readRtkGain();
|
|
141
143
|
console.log(` RTK history (rtk-owned, never touched): ${rtkDb}${rtk.commands ? ` · ${rtk.commands} commands` : ''}`);
|
package/cli/install.js
CHANGED
|
@@ -3,32 +3,34 @@ 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,
|
|
6
|
+
import { BUN_BIN_DIR, COMBO_PRESET_MODES, IS_WINDOWS, OMP_AGENT_DIR, OMP_PLUGINS_DIR, OMP_BIN, PACKAGE_NAME, PACKAGE_VERSION, 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';
|
|
10
10
|
import { runUninstall } from './uninstall.js';
|
|
11
11
|
import { runDoctor } from './doctor.js';
|
|
12
12
|
import { runReset } from './reset.js';
|
|
13
|
+
import { runSettings } from './settings.js';
|
|
13
14
|
import { runUsage } from './usage.js';
|
|
14
15
|
import { runDashboard } from './dashboard.js';
|
|
15
16
|
import { wireRtkOmp } from './rtk-wiring.js';
|
|
16
17
|
import { CAVEMAN_REMOTE_RULE, RTK_RELEASE_API, fetchJson, findFile, httpsGet, httpsDownload, parseChecksum, readTextIfExists, rtkPlatformSpec, sha256File, } from '../extensions/lib/utils.js';
|
|
18
|
+
import { storedProfile, writePluginSettings } from './profile.js';
|
|
17
19
|
// Paths to extension source files (relative to this script)
|
|
18
20
|
const EXT_DIR = path.join(path.dirname(fileURLToPath(import.meta.url)), '..', 'extensions');
|
|
19
|
-
const SHARED_SESSION_STATE = path.join(EXT_DIR, 'shared', 'session-state.
|
|
20
|
-
const CAVEMAN_INDEX = path.join(EXT_DIR, 'caveman-session', 'index.
|
|
21
|
-
const RTK_SESSION_INDEX = path.join(EXT_DIR, 'rtk-session', 'index.
|
|
22
|
-
const UPDATER_INDEX = path.join(EXT_DIR, 'ai-addons-updater', 'index.
|
|
23
|
-
const COMBO_TOGGLE_INDEX = path.join(EXT_DIR, 'combo-toggle', 'index.
|
|
24
|
-
const TERSIO_COMMANDS_INDEX = path.join(EXT_DIR, 'tersio-commands', 'index.
|
|
25
|
-
const MODE_REINFORCEMENT_INDEX = path.join(EXT_DIR, 'shared', 'mode-reinforcement.
|
|
26
|
-
const SHARED_TYPES = path.join(EXT_DIR, 'shared', 'types.
|
|
27
|
-
const LIB_UTILS = path.join(EXT_DIR, 'lib', 'utils.
|
|
28
|
-
const SHARED_PLUGIN_SETTINGS = path.join(EXT_DIR, 'shared', 'plugin-settings.
|
|
29
|
-
const SHARED_USAGE_LEDGER = path.join(EXT_DIR, 'shared', 'usage-ledger.
|
|
30
|
-
const SHARED_PRICING = path.join(EXT_DIR, 'shared', 'pricing.
|
|
31
|
-
const SHARED_CARBON = path.join(EXT_DIR, 'shared', 'carbon.
|
|
21
|
+
const SHARED_SESSION_STATE = path.join(EXT_DIR, 'shared', 'session-state.ts');
|
|
22
|
+
const CAVEMAN_INDEX = path.join(EXT_DIR, 'caveman-session', 'index.ts');
|
|
23
|
+
const RTK_SESSION_INDEX = path.join(EXT_DIR, 'rtk-session', 'index.ts');
|
|
24
|
+
const UPDATER_INDEX = path.join(EXT_DIR, 'ai-addons-updater', 'index.ts');
|
|
25
|
+
const COMBO_TOGGLE_INDEX = path.join(EXT_DIR, 'combo-toggle', 'index.ts');
|
|
26
|
+
const TERSIO_COMMANDS_INDEX = path.join(EXT_DIR, 'tersio-commands', 'index.ts');
|
|
27
|
+
const MODE_REINFORCEMENT_INDEX = path.join(EXT_DIR, 'shared', 'mode-reinforcement.ts');
|
|
28
|
+
const SHARED_TYPES = path.join(EXT_DIR, 'shared', 'types.ts');
|
|
29
|
+
const LIB_UTILS = path.join(EXT_DIR, 'lib', 'utils.ts');
|
|
30
|
+
const SHARED_PLUGIN_SETTINGS = path.join(EXT_DIR, 'shared', 'plugin-settings.ts');
|
|
31
|
+
const SHARED_USAGE_LEDGER = path.join(EXT_DIR, 'shared', 'usage-ledger.ts');
|
|
32
|
+
const SHARED_PRICING = path.join(EXT_DIR, 'shared', 'pricing.ts');
|
|
33
|
+
const SHARED_CARBON = path.join(EXT_DIR, 'shared', 'carbon.ts');
|
|
32
34
|
const PONYTAIL_GITHUB_SPEC = 'github:DietrichGebert/ponytail';
|
|
33
35
|
const PONYTAIL_NPM_SPEC = '@dietrichgebert/ponytail@latest';
|
|
34
36
|
async function stepPonytail(pluginsDir, userDir, options) {
|
|
@@ -373,27 +375,27 @@ async function stepSharedSessionState(extDir, options) {
|
|
|
373
375
|
if (!options.quiet)
|
|
374
376
|
console.log(' Shared files — sync session bridge');
|
|
375
377
|
await copySources(extDir, [
|
|
376
|
-
[SHARED_SESSION_STATE, path.join('shared', 'session-state.
|
|
377
|
-
[SHARED_TYPES, path.join('shared', 'types.
|
|
378
|
-
[LIB_UTILS, path.join('lib', 'utils.
|
|
379
|
-
[SHARED_PLUGIN_SETTINGS, path.join('shared', 'plugin-settings.
|
|
380
|
-
[SHARED_USAGE_LEDGER, path.join('shared', 'usage-ledger.
|
|
381
|
-
[SHARED_PRICING, path.join('shared', 'pricing.
|
|
382
|
-
[SHARED_CARBON, path.join('shared', 'carbon.
|
|
378
|
+
[SHARED_SESSION_STATE, path.join('shared', 'session-state.ts')],
|
|
379
|
+
[SHARED_TYPES, path.join('shared', 'types.ts')],
|
|
380
|
+
[LIB_UTILS, path.join('lib', 'utils.ts')],
|
|
381
|
+
[SHARED_PLUGIN_SETTINGS, path.join('shared', 'plugin-settings.ts')],
|
|
382
|
+
[SHARED_USAGE_LEDGER, path.join('shared', 'usage-ledger.ts')],
|
|
383
|
+
[SHARED_PRICING, path.join('shared', 'pricing.ts')],
|
|
384
|
+
[SHARED_CARBON, path.join('shared', 'carbon.ts')],
|
|
383
385
|
], 'shared/session-state.js', options);
|
|
384
386
|
}
|
|
385
387
|
async function stepModeReinforcement(extDir, ponytailExtPath, options) {
|
|
386
388
|
if (!options.quiet)
|
|
387
389
|
console.log(' Session helpers — sync shared files');
|
|
388
|
-
const dest = path.join(extDir, 'shared', 'mode-reinforcement.
|
|
389
|
-
if (!await copySources(extDir, [[MODE_REINFORCEMENT_INDEX, path.join('shared', 'mode-reinforcement.
|
|
390
|
+
const dest = path.join(extDir, 'shared', 'mode-reinforcement.ts');
|
|
391
|
+
if (!await copySources(extDir, [[MODE_REINFORCEMENT_INDEX, path.join('shared', 'mode-reinforcement.ts')]], 'shared/mode-reinforcement.ts', options))
|
|
390
392
|
return;
|
|
391
393
|
await ensureExtensionAfterConfigEntry(path.join(path.dirname(extDir), 'config.yml'), dest, ponytailExtPath, 'mode reinforcement', options);
|
|
392
394
|
}
|
|
393
395
|
async function stepRtkSession(extDir, options) {
|
|
394
396
|
if (!options.quiet)
|
|
395
397
|
console.log(' RTK session — install session mode');
|
|
396
|
-
await copySources(extDir, [[RTK_SESSION_INDEX, path.join('rtk-session', 'index.
|
|
398
|
+
await copySources(extDir, [[RTK_SESSION_INDEX, path.join('rtk-session', 'index.ts')]], 'rtk-session/index.ts', options);
|
|
397
399
|
}
|
|
398
400
|
// One rule fetch serves the install; dry runs stay offline
|
|
399
401
|
// and fall back to the bundled rule to preview its destination.
|
|
@@ -425,65 +427,26 @@ async function stepCaveman(extDir, rule, options) {
|
|
|
425
427
|
else {
|
|
426
428
|
await writeIfChanged(ruleDest, rule, options);
|
|
427
429
|
}
|
|
428
|
-
await copySources(extDir, [[CAVEMAN_INDEX, path.join('caveman-session', 'index.
|
|
430
|
+
await copySources(extDir, [[CAVEMAN_INDEX, path.join('caveman-session', 'index.ts')]], 'caveman-session/index.ts', options);
|
|
429
431
|
}
|
|
430
432
|
async function stepTersioCommands(extDir, options) {
|
|
431
433
|
if (!options.quiet)
|
|
432
434
|
console.log(' Tersio commands — install /tersio root command');
|
|
433
|
-
await copySources(extDir, [[TERSIO_COMMANDS_INDEX, path.join('tersio-commands', 'index.
|
|
435
|
+
await copySources(extDir, [[TERSIO_COMMANDS_INDEX, path.join('tersio-commands', 'index.ts')]], 'tersio-commands/index.ts', options);
|
|
434
436
|
}
|
|
435
437
|
async function stepUpdater(extDir, options) {
|
|
436
|
-
await copySources(extDir, [[UPDATER_INDEX, path.join('ai-addons-updater', 'index.
|
|
438
|
+
await copySources(extDir, [[UPDATER_INDEX, path.join('ai-addons-updater', 'index.ts')]], 'ai-addons-updater/index.ts', options);
|
|
437
439
|
}
|
|
438
440
|
async function stepCombo(extDir, options) {
|
|
439
441
|
if (!options.quiet)
|
|
440
442
|
console.log(' Combo — install preset switch');
|
|
441
|
-
const dest = path.join(extDir, 'combo-toggle', 'index.
|
|
442
|
-
if (!await copySources(extDir, [[COMBO_TOGGLE_INDEX, path.join('combo-toggle', 'index.
|
|
443
|
+
const dest = path.join(extDir, 'combo-toggle', 'index.ts');
|
|
444
|
+
if (!await copySources(extDir, [[COMBO_TOGGLE_INDEX, path.join('combo-toggle', 'index.ts')]], 'combo-toggle/index.ts', options))
|
|
443
445
|
return;
|
|
444
446
|
// Auto-register combo in config.yml
|
|
445
447
|
const configPath = path.join(path.dirname(extDir), 'config.yml');
|
|
446
448
|
await ensureExtensionInConfig(configPath, dest, 'combo', options);
|
|
447
449
|
}
|
|
448
|
-
function defaultProfile() {
|
|
449
|
-
return {
|
|
450
|
-
comboDefault: 'off',
|
|
451
|
-
cavemanDefault: 'off',
|
|
452
|
-
rtkDefault: false,
|
|
453
|
-
ponytailDefault: 'off',
|
|
454
|
-
};
|
|
455
|
-
}
|
|
456
|
-
async function storedProfile() {
|
|
457
|
-
const base = defaultProfile();
|
|
458
|
-
const raw = await readTextIfExists(path.join(OMP_PLUGINS_DIR, 'omp-plugins.lock.json'));
|
|
459
|
-
if (!raw)
|
|
460
|
-
return base;
|
|
461
|
-
let stored;
|
|
462
|
-
try {
|
|
463
|
-
const parsed = JSON.parse(raw);
|
|
464
|
-
if (!parsed || typeof parsed !== 'object' || !('settings' in parsed))
|
|
465
|
-
return base;
|
|
466
|
-
const settings = parsed.settings;
|
|
467
|
-
if (!settings || typeof settings !== 'object' || !(PACKAGE_NAME in settings))
|
|
468
|
-
return base;
|
|
469
|
-
const entry = settings[PACKAGE_NAME];
|
|
470
|
-
if (!entry || typeof entry !== 'object')
|
|
471
|
-
return base;
|
|
472
|
-
stored = entry;
|
|
473
|
-
}
|
|
474
|
-
catch {
|
|
475
|
-
return base;
|
|
476
|
-
}
|
|
477
|
-
if (typeof stored.comboDefault === 'string' && stored.comboDefault in COMBO_PRESET_MODES)
|
|
478
|
-
base.comboDefault = stored.comboDefault;
|
|
479
|
-
if (typeof stored.cavemanDefault === 'string' && CAVEMAN_DEFAULTS.has(stored.cavemanDefault))
|
|
480
|
-
base.cavemanDefault = stored.cavemanDefault;
|
|
481
|
-
if (typeof stored.rtkDefault === 'boolean')
|
|
482
|
-
base.rtkDefault = stored.rtkDefault;
|
|
483
|
-
if (typeof stored.ponytailDefault === 'string' && PONYTAIL_DEFAULTS.has(stored.ponytailDefault))
|
|
484
|
-
base.ponytailDefault = stored.ponytailDefault;
|
|
485
|
-
return base;
|
|
486
|
-
}
|
|
487
450
|
async function resolveProfile(forceReinstall = false, opts = {}) {
|
|
488
451
|
// Seed from the lock file so flag-less update/reinstall runs keep the
|
|
489
452
|
// user's configured defaults instead of resetting them to off.
|
|
@@ -523,39 +486,6 @@ async function resolveProfile(forceReinstall = false, opts = {}) {
|
|
|
523
486
|
console.log(` Defaults: combo=${profile.comboDefault} (caveman=${profile.cavemanDefault} · rtk=${profile.rtkDefault ? 'on' : 'off'} · ponytail=${profile.ponytailDefault})`);
|
|
524
487
|
return profile;
|
|
525
488
|
}
|
|
526
|
-
// Persist the profile as omp plugin settings so `omp plugin config get`
|
|
527
|
-
// reflects the choice and the extensions pick it up on session start.
|
|
528
|
-
async function writePluginSettings(profile, options) {
|
|
529
|
-
const pluginsDir = OMP_PLUGINS_DIR;
|
|
530
|
-
const lockPath = path.join(pluginsDir, 'omp-plugins.lock.json');
|
|
531
|
-
let config = {};
|
|
532
|
-
const existing = await readTextIfExists(lockPath);
|
|
533
|
-
if (existing) {
|
|
534
|
-
try {
|
|
535
|
-
config = JSON.parse(existing);
|
|
536
|
-
}
|
|
537
|
-
catch {
|
|
538
|
-
config = {};
|
|
539
|
-
}
|
|
540
|
-
}
|
|
541
|
-
config.plugins = config.plugins || {};
|
|
542
|
-
config.settings = config.settings || {};
|
|
543
|
-
config.settings[PACKAGE_NAME] = {
|
|
544
|
-
...(config.settings[PACKAGE_NAME] || {}),
|
|
545
|
-
comboDefault: profile.comboDefault,
|
|
546
|
-
cavemanDefault: profile.cavemanDefault,
|
|
547
|
-
rtkDefault: profile.rtkDefault,
|
|
548
|
-
ponytailDefault: profile.ponytailDefault,
|
|
549
|
-
};
|
|
550
|
-
if (options.dryRun) {
|
|
551
|
-
if (verbose && !options.quiet)
|
|
552
|
-
console.log(` [dry-run] would write plugin settings (${PACKAGE_NAME}) to ${lockPath}`);
|
|
553
|
-
return;
|
|
554
|
-
}
|
|
555
|
-
await fs.mkdir(pluginsDir, { recursive: true });
|
|
556
|
-
await fs.writeFile(lockPath, JSON.stringify(config, null, 2) + '\n', 'utf8');
|
|
557
|
-
console.log(` [write] Plugin settings in ${lockPath}`);
|
|
558
|
-
}
|
|
559
489
|
let updatePromptDone = false;
|
|
560
490
|
// Bare `tersio` at a terminal is a command picker, not an install run:
|
|
561
491
|
// update offer first (when pending), then a Clack menu over every command.
|
|
@@ -585,6 +515,7 @@ async function runCommandMenu() {
|
|
|
585
515
|
{ value: 'usage', label: 'Usage', hint: 'token usage and savings report' },
|
|
586
516
|
{ value: 'gain', label: 'Gain dashboard', hint: 'open the report in your browser' },
|
|
587
517
|
{ value: 'reset', label: 'Reset statistics', hint: 'clear statistics; transcripts and RTK history stay' },
|
|
518
|
+
{ value: 'settings', label: 'Settings', hint: 'defaults: combo, caveman, rtk, ponytail, currency' },
|
|
588
519
|
{ value: 'uninstall', label: 'Uninstall', hint: 'remove tersio' },
|
|
589
520
|
], 'install');
|
|
590
521
|
if (choice.status !== 'selected') {
|
|
@@ -636,6 +567,9 @@ async function runCommandMenu() {
|
|
|
636
567
|
await runReset();
|
|
637
568
|
closeRL();
|
|
638
569
|
break;
|
|
570
|
+
case 'settings':
|
|
571
|
+
await runSettings();
|
|
572
|
+
break;
|
|
639
573
|
case 'uninstall':
|
|
640
574
|
await runUninstall();
|
|
641
575
|
closeRL();
|
package/cli/profile.js
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
// cli/profile.ts — session-start defaults profile (combo/caveman/rtk/ponytail)
|
|
2
|
+
// plus the display-currency default for usage/gain reports.
|
|
3
|
+
// Single source for reading/writing the omp plugin settings lock entry.
|
|
4
|
+
// Extracted from cli/install.ts so both install and settings share it.
|
|
5
|
+
import { promises as fs } from 'node:fs';
|
|
6
|
+
import path from 'node:path';
|
|
7
|
+
import { CAVEMAN_DEFAULTS, COMBO_PRESET_MODES, OMP_PLUGINS_DIR, PACKAGE_NAME, PONYTAIL_DEFAULTS, verbose, } from './common.js';
|
|
8
|
+
import { DEFAULT_CURRENCY, isCurrencyCode } from './currency.js';
|
|
9
|
+
import { readTextIfExists } from '../extensions/lib/utils.js';
|
|
10
|
+
function defaultProfile() {
|
|
11
|
+
return {
|
|
12
|
+
comboDefault: 'off',
|
|
13
|
+
cavemanDefault: 'off',
|
|
14
|
+
rtkDefault: false,
|
|
15
|
+
ponytailDefault: 'off',
|
|
16
|
+
currency: DEFAULT_CURRENCY,
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
// Stored profile from the live lock file: update/reinstall runs without flags
|
|
20
|
+
// or prompts must preserve the user's choice, never reset it to off.
|
|
21
|
+
async function storedProfile() {
|
|
22
|
+
const base = defaultProfile();
|
|
23
|
+
const raw = await readTextIfExists(path.join(OMP_PLUGINS_DIR, 'omp-plugins.lock.json'));
|
|
24
|
+
if (!raw)
|
|
25
|
+
return base;
|
|
26
|
+
let stored;
|
|
27
|
+
try {
|
|
28
|
+
const parsed = JSON.parse(raw);
|
|
29
|
+
if (!parsed || typeof parsed !== 'object' || !('settings' in parsed))
|
|
30
|
+
return base;
|
|
31
|
+
const settings = parsed.settings;
|
|
32
|
+
if (!settings || typeof settings !== 'object' || !(PACKAGE_NAME in settings))
|
|
33
|
+
return base;
|
|
34
|
+
const entry = settings[PACKAGE_NAME];
|
|
35
|
+
if (!entry || typeof entry !== 'object')
|
|
36
|
+
return base;
|
|
37
|
+
stored = entry;
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
return base;
|
|
41
|
+
}
|
|
42
|
+
if (typeof stored.comboDefault === 'string' && stored.comboDefault in COMBO_PRESET_MODES)
|
|
43
|
+
base.comboDefault = stored.comboDefault;
|
|
44
|
+
if (typeof stored.cavemanDefault === 'string' && CAVEMAN_DEFAULTS.has(stored.cavemanDefault))
|
|
45
|
+
base.cavemanDefault = stored.cavemanDefault;
|
|
46
|
+
if (typeof stored.rtkDefault === 'boolean')
|
|
47
|
+
base.rtkDefault = stored.rtkDefault;
|
|
48
|
+
if (typeof stored.ponytailDefault === 'string' && PONYTAIL_DEFAULTS.has(stored.ponytailDefault))
|
|
49
|
+
base.ponytailDefault = stored.ponytailDefault;
|
|
50
|
+
if (typeof stored.currency === 'string' && isCurrencyCode(stored.currency.trim().toUpperCase())) {
|
|
51
|
+
base.currency = stored.currency.trim().toUpperCase();
|
|
52
|
+
}
|
|
53
|
+
return base;
|
|
54
|
+
}
|
|
55
|
+
// Persist the profile as omp plugin settings so `omp plugin config get`
|
|
56
|
+
// reflects the choice and the extensions pick it up on session start.
|
|
57
|
+
async function writePluginSettings(profile, options) {
|
|
58
|
+
const pluginsDir = OMP_PLUGINS_DIR;
|
|
59
|
+
const lockPath = path.join(pluginsDir, 'omp-plugins.lock.json');
|
|
60
|
+
let config = {};
|
|
61
|
+
const existing = await readTextIfExists(lockPath);
|
|
62
|
+
if (existing) {
|
|
63
|
+
try {
|
|
64
|
+
config = JSON.parse(existing);
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
config = {};
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
config.plugins = config.plugins || {};
|
|
71
|
+
config.settings = config.settings || {};
|
|
72
|
+
config.settings[PACKAGE_NAME] = {
|
|
73
|
+
...(config.settings[PACKAGE_NAME] || {}),
|
|
74
|
+
comboDefault: profile.comboDefault,
|
|
75
|
+
cavemanDefault: profile.cavemanDefault,
|
|
76
|
+
rtkDefault: profile.rtkDefault,
|
|
77
|
+
ponytailDefault: profile.ponytailDefault,
|
|
78
|
+
currency: profile.currency,
|
|
79
|
+
};
|
|
80
|
+
if (options.dryRun) {
|
|
81
|
+
if (verbose && !options.quiet)
|
|
82
|
+
console.log(` [dry-run] would write plugin settings (${PACKAGE_NAME}) to ${lockPath}`);
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
await fs.mkdir(pluginsDir, { recursive: true });
|
|
86
|
+
await fs.writeFile(lockPath, JSON.stringify(config, null, 2) + '\n', 'utf8');
|
|
87
|
+
console.log(` [write] Plugin settings in ${lockPath}`);
|
|
88
|
+
}
|
|
89
|
+
function formatProfile(profile) {
|
|
90
|
+
return `combo=${profile.comboDefault} (caveman=${profile.cavemanDefault} · rtk=${profile.rtkDefault ? 'on' : 'off'} · ponytail=${profile.ponytailDefault}) · currency=${profile.currency}`;
|
|
91
|
+
}
|
|
92
|
+
export { defaultProfile, formatProfile, storedProfile, writePluginSettings };
|