@krtclcdy/tersio 2.25.4 → 2.25.6
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 +27 -63
- package/dashboard/dist/index.html +18 -16
- package/dist/cli/backups.js +187 -0
- package/dist/cli/banner.js +4 -3
- package/dist/cli/common.js +29 -13
- package/dist/cli/currency.js +1 -1
- package/dist/cli/dashboard.js +168 -110
- package/dist/cli/doctor-fix.js +232 -50
- package/dist/cli/doctor.js +281 -32
- package/dist/cli/hosts.js +29 -9
- package/dist/cli/install.js +204 -69
- package/dist/cli/interactive.js +3 -3
- package/dist/cli/manifest.js +125 -17
- package/dist/cli/profile.js +14 -9
- package/dist/cli/rtk-wiring.js +1 -1
- package/dist/cli/settings.js +20 -5
- package/dist/cli/uninstall.js +24 -8
- package/dist/cli/update.js +9 -10
- package/dist/cli/usage.js +75 -7
- package/dist/extensions/{caveman-session → caveman}/index.js +4 -2
- package/{extensions/caveman-session → dist/extensions/caveman}/rule-megacave.md +5 -3
- package/{extensions/caveman-session → dist/extensions/caveman}/rule-ultra.md +4 -4
- package/dist/extensions/caveman/rule.md +89 -0
- package/dist/extensions/{combo-toggle → combo}/index.js +62 -21
- package/dist/extensions/{tersio-commands → commands}/index.js +2 -2
- package/dist/extensions/pstack/index.js +119 -0
- package/dist/extensions/pstack/rule.md +65 -0
- package/dist/extensions/{rtk-filter/index.js → rtk/filter.js} +1 -2
- package/dist/extensions/{rtk-session/index.js → rtk/session.js} +5 -5
- package/dist/extensions/{opencode/server.js → server.js} +98 -14
- package/dist/extensions/shared/agent-stats.js +1198 -0
- package/dist/extensions/shared/brand-marks.js +153 -0
- package/dist/extensions/shared/carbon.js +1 -1
- package/dist/extensions/shared/plugin-settings.js +12 -7
- package/dist/extensions/shared/pricing.js +86 -30
- package/dist/extensions/shared/rtk-gain.js +4 -9
- package/dist/extensions/shared/session-state.js +15 -10
- package/dist/extensions/shared/sqlite.js +168 -0
- package/dist/extensions/shared/status.js +8 -8
- package/dist/extensions/shared/types.js +1 -1
- package/dist/extensions/shared/usage-ledger.js +113 -39
- package/dist/extensions/shared/usage-store.js +191 -48
- package/dist/extensions/{lib → shared}/utils.js +6 -6
- package/dist/extensions/timing/index.js +31 -0
- package/dist/extensions/{ai-addons-updater → updater}/index.js +13 -6
- package/dist/probe-catalog.js +48 -0
- package/dist/probe-catalog188.js +44 -0
- package/dist/tersio.js +9 -4
- package/extensions/{caveman-session → caveman}/index.ts +4 -2
- package/{dist/extensions/caveman-session → extensions/caveman}/rule-megacave.md +5 -3
- package/{dist/extensions/caveman-session → extensions/caveman}/rule-ultra.md +4 -4
- package/extensions/caveman/rule.md +89 -0
- package/extensions/{combo-toggle → combo}/index.ts +62 -19
- package/extensions/{tersio-commands → commands}/index.ts +2 -2
- package/extensions/pstack/index.ts +128 -0
- package/extensions/pstack/rule.md +65 -0
- package/extensions/{rtk-filter/index.ts → rtk/filter.ts} +1 -2
- package/extensions/{rtk-session/index.ts → rtk/session.ts} +5 -5
- package/extensions/{opencode/server.ts → server.ts} +81 -16
- package/extensions/shared/agent-stats.ts +1605 -0
- package/extensions/shared/brand-marks.ts +163 -0
- package/extensions/shared/carbon.ts +1 -1
- package/extensions/shared/plugin-settings.ts +13 -7
- package/extensions/shared/pricing.ts +95 -32
- package/extensions/shared/rtk-gain.ts +4 -9
- package/extensions/shared/session-state.ts +18 -13
- package/extensions/shared/sqlite.ts +168 -0
- package/extensions/shared/status.ts +9 -8
- package/extensions/shared/types.ts +2 -1
- package/extensions/shared/usage-ledger.ts +134 -46
- package/extensions/shared/usage-store.ts +202 -50
- package/extensions/{lib → shared}/utils.ts +6 -6
- package/extensions/timing/index.ts +45 -0
- package/extensions/{ai-addons-updater → updater}/index.ts +15 -9
- package/package.json +27 -18
- package/dist/extensions/caveman-session/rule.md +0 -79
- package/extensions/caveman-session/rule.md +0 -79
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
// Data snapshots: a manifest directory, not one file; older lone `usage-<stamp>.db` installs still restore.
|
|
2
|
+
import { promises as fs } from 'node:fs';
|
|
3
|
+
import { readdirSync, readFileSync, statSync } from 'node:fs';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { sqliteExec } from '../extensions/shared/sqlite.js';
|
|
6
|
+
import { agentDbPath, statsDbPath } from '../extensions/shared/agent-stats.js';
|
|
7
|
+
import { rtkDbPath } from '../extensions/shared/rtk-gain.js';
|
|
8
|
+
import { pricesCachePath } from '../extensions/shared/pricing.js';
|
|
9
|
+
import { usageDbPath } from '../extensions/shared/usage-store.js';
|
|
10
|
+
import { resetMarkerPath } from '../extensions/shared/usage-ledger.js';
|
|
11
|
+
import { tersioHome } from '../extensions/shared/utils.js';
|
|
12
|
+
/** Snapshots kept on disk, oldest pruned first; six fills past one screen so the pane scrolls. */
|
|
13
|
+
export const BACKUP_KEEP = 6;
|
|
14
|
+
export function backupDir() {
|
|
15
|
+
return path.join(tersioHome(), 'backups');
|
|
16
|
+
}
|
|
17
|
+
function manifestPath(dir) {
|
|
18
|
+
return path.join(dir, 'manifest.json');
|
|
19
|
+
}
|
|
20
|
+
/** Every file this build knows how to snapshot, whether or not it exists yet. */
|
|
21
|
+
export function dataSources() {
|
|
22
|
+
const sources = [
|
|
23
|
+
['Usage ledger', usageDbPath()],
|
|
24
|
+
['Reset watermark', resetMarkerPath()],
|
|
25
|
+
['Settings', path.join(tersioHome(), 'settings.json')],
|
|
26
|
+
['Price cache', pricesCachePath()],
|
|
27
|
+
['Model price cache', path.join(tersioHome(), 'prices-modelsdev.json')],
|
|
28
|
+
['Agent stats', statsDbPath()],
|
|
29
|
+
['Agent quota windows', agentDbPath()],
|
|
30
|
+
['Command history', rtkDbPath()],
|
|
31
|
+
];
|
|
32
|
+
return sources.map(([label, source]) => ({ label, source, bytes: sizeOf(source) }));
|
|
33
|
+
}
|
|
34
|
+
/** Byte size of a file, zero when it is absent or unreadable. */
|
|
35
|
+
function sizeOf(file) {
|
|
36
|
+
try {
|
|
37
|
+
return statSync(file).size;
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
return 0;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
/** Snapshot label for a legacy single-file backup, so the list reads the same for both. */
|
|
44
|
+
function legacyLabel(name) {
|
|
45
|
+
return name.startsWith('usage-') ? 'Usage ledger' : 'Snapshot';
|
|
46
|
+
}
|
|
47
|
+
/** Newest first: the directories this build wrote, then the lone `usage-*.db` files. */
|
|
48
|
+
export function listDataBackups() {
|
|
49
|
+
let names = [];
|
|
50
|
+
try {
|
|
51
|
+
names = readdirSync(backupDir());
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
return [];
|
|
55
|
+
}
|
|
56
|
+
const dirs = [];
|
|
57
|
+
const legacy = [];
|
|
58
|
+
for (const name of names) {
|
|
59
|
+
const full = path.join(backupDir(), name);
|
|
60
|
+
let stat;
|
|
61
|
+
try {
|
|
62
|
+
stat = statSync(full);
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
if (stat.isDirectory()) {
|
|
68
|
+
const files = readManifest(full);
|
|
69
|
+
if (files.length === 0)
|
|
70
|
+
continue;
|
|
71
|
+
dirs.push({ name, path: full, mtime: stat.mtimeMs, bytes: totalOf(full), files, legacy: false });
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
if (/^usage-\d{14}\.db$/.test(name)) {
|
|
75
|
+
legacy.push({
|
|
76
|
+
name,
|
|
77
|
+
path: full,
|
|
78
|
+
mtime: stat.mtimeMs,
|
|
79
|
+
bytes: stat.size,
|
|
80
|
+
files: [{ label: legacyLabel(name), source: usageDbPath(), bytes: stat.size }],
|
|
81
|
+
legacy: true,
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return [...dirs, ...legacy].toSorted((a, b) => b.mtime - a.mtime);
|
|
86
|
+
}
|
|
87
|
+
function readManifest(dir) {
|
|
88
|
+
try {
|
|
89
|
+
const raw = JSON.parse(readFileSync(manifestPath(dir), 'utf8'));
|
|
90
|
+
if (!Array.isArray(raw))
|
|
91
|
+
return [];
|
|
92
|
+
return raw
|
|
93
|
+
.map((entry) => {
|
|
94
|
+
const record = (entry ?? {});
|
|
95
|
+
const source = String(record.source ?? '');
|
|
96
|
+
const name = path.basename(source);
|
|
97
|
+
const file = path.join(dir, name);
|
|
98
|
+
const bytes = sizeOf(file);
|
|
99
|
+
if (!source || bytes === 0)
|
|
100
|
+
return null;
|
|
101
|
+
return { label: String(record.label ?? name), source, bytes };
|
|
102
|
+
})
|
|
103
|
+
.filter((entry) => entry !== null);
|
|
104
|
+
}
|
|
105
|
+
catch {
|
|
106
|
+
return [];
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
/** Disk size of a snapshot, counting the manifest. */
|
|
110
|
+
function totalOf(dir) {
|
|
111
|
+
return sizeOf(manifestPath(dir)) + readdirSync(dir).reduce((sum, name) => sum + sizeOf(path.join(dir, name)), 0);
|
|
112
|
+
}
|
|
113
|
+
/** Put the sqlite ledger's write-ahead log back in the main file, so a copy is one whole database. */
|
|
114
|
+
function checkpointLedger() {
|
|
115
|
+
try {
|
|
116
|
+
sqliteExec(usageDbPath(), 'PRAGMA wal_checkpoint(TRUNCATE);');
|
|
117
|
+
}
|
|
118
|
+
catch {
|
|
119
|
+
/* no SQLite on this host, or no ledger: the plain file copy is the best available */
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
/** Take a snapshot: one copy of every existing file under a stamp dir, keeping the newest {@link BACKUP_KEEP}. */
|
|
123
|
+
export async function createDataBackup() {
|
|
124
|
+
// The stamp keeps milliseconds: two snapshots in the same second must not land in one directory.
|
|
125
|
+
const stamp = new Date().toISOString().replace(/[-:TZ.]/g, '');
|
|
126
|
+
const dir = path.join(backupDir(), `data-${stamp}`);
|
|
127
|
+
checkpointLedger();
|
|
128
|
+
await fs.mkdir(dir, { recursive: true });
|
|
129
|
+
const files = [];
|
|
130
|
+
for (const source of dataSources()) {
|
|
131
|
+
if (source.bytes === 0)
|
|
132
|
+
continue;
|
|
133
|
+
const target = path.join(dir, path.basename(source.source));
|
|
134
|
+
await fs.copyFile(source.source, target);
|
|
135
|
+
files.push({ ...source, bytes: sizeOf(target) });
|
|
136
|
+
}
|
|
137
|
+
await fs.writeFile(manifestPath(dir), JSON.stringify(files.map(({ label, source }) => ({ label, source }))), 'utf8');
|
|
138
|
+
await pruneBackups();
|
|
139
|
+
return { name: path.basename(dir), path: dir, mtime: Date.now(), bytes: files.reduce((sum, f) => sum + f.bytes, 0), files, legacy: false };
|
|
140
|
+
}
|
|
141
|
+
/** Keep the newest {@link BACKUP_KEEP} snapshots and drop the rest, legacy files included. */
|
|
142
|
+
export async function pruneBackups() {
|
|
143
|
+
const all = listDataBackups();
|
|
144
|
+
for (const old of all.slice(BACKUP_KEEP)) {
|
|
145
|
+
await fs.rm(old.path, { recursive: true, force: true }).catch(() => { });
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
/** A snapshot name resolves to a path inside the backup directory, or null when it does not. */
|
|
149
|
+
function resolveBackup(name) {
|
|
150
|
+
if (!name || path.basename(name) !== name || /[\\/]/.test(name))
|
|
151
|
+
return null;
|
|
152
|
+
const found = listDataBackups().find((entry) => entry.name === name);
|
|
153
|
+
return found ?? null;
|
|
154
|
+
}
|
|
155
|
+
/** Restore: files the snapshot holds overwrite their live source; files it lacks stay put, so nothing is lost. */
|
|
156
|
+
export async function restoreDataBackup(name) {
|
|
157
|
+
const found = resolveBackup(name);
|
|
158
|
+
if (!found)
|
|
159
|
+
return false;
|
|
160
|
+
if (found.legacy) {
|
|
161
|
+
await fs.mkdir(path.dirname(usageDbPath()), { recursive: true });
|
|
162
|
+
await fs.copyFile(found.path, usageDbPath());
|
|
163
|
+
return true;
|
|
164
|
+
}
|
|
165
|
+
for (const file of found.files) {
|
|
166
|
+
const from = path.join(found.path, path.basename(file.source));
|
|
167
|
+
await fs.mkdir(path.dirname(file.source), { recursive: true });
|
|
168
|
+
await fs.copyFile(from, file.source);
|
|
169
|
+
}
|
|
170
|
+
return true;
|
|
171
|
+
}
|
|
172
|
+
/** Remove one snapshot. Only ever touches a path inside the backup directory. */
|
|
173
|
+
export async function deleteDataBackup(name) {
|
|
174
|
+
const found = resolveBackup(name);
|
|
175
|
+
if (!found)
|
|
176
|
+
return false;
|
|
177
|
+
await fs.rm(found.path, { recursive: true, force: true });
|
|
178
|
+
return true;
|
|
179
|
+
}
|
|
180
|
+
/** Total size of every snapshot on disk, for the pane's footer. */
|
|
181
|
+
export function backupDiskBytes() {
|
|
182
|
+
return listDataBackups().reduce((sum, entry) => sum + entry.bytes, 0);
|
|
183
|
+
}
|
|
184
|
+
/** The home directory the snapshots live under, for the pane's path line. */
|
|
185
|
+
export function backupHome() {
|
|
186
|
+
return tersioHome();
|
|
187
|
+
}
|
package/dist/cli/banner.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// cli/banner.ts — mini pixel-scissor welcome. TTY-only
|
|
1
|
+
// cli/banner.ts — mini pixel-scissor welcome. TTY-only, skipped on --dry-run; piped output stays byte-identical.
|
|
2
2
|
import { intro as clackIntro, note as clackNote } from '@clack/prompts';
|
|
3
3
|
import { PACKAGE_VERSION, dryRun } from './common.js';
|
|
4
4
|
function tty() {
|
|
@@ -81,9 +81,10 @@ function printWelcome() {
|
|
|
81
81
|
'tersio update refresh CLI + add-ons',
|
|
82
82
|
'tersio doctor verify the install',
|
|
83
83
|
];
|
|
84
|
-
// Below 26 rows the note scrolls off the top and slices the art
|
|
84
|
+
// Below 26 rows the note scrolls off the top and slices the art, and the screen must also hold the
|
|
85
|
+
// update prompt and every option, so the tips take one line there.
|
|
85
86
|
if (process.stdout.rows < 26) {
|
|
86
|
-
clackNote(
|
|
87
|
+
clackNote(` ${tips.join(' · ')}`);
|
|
87
88
|
return;
|
|
88
89
|
}
|
|
89
90
|
const art = bannerLines();
|
package/dist/cli/common.js
CHANGED
|
@@ -5,7 +5,7 @@ import path from 'node:path';
|
|
|
5
5
|
import { promisify } from 'node:util';
|
|
6
6
|
import { createRequire } from 'node:module';
|
|
7
7
|
import { fileURLToPath } from 'node:url';
|
|
8
|
-
import { findPackageRoot, readTextIfExists } from '../extensions/
|
|
8
|
+
import { findPackageRoot, readTextIfExists } from '../extensions/shared/utils.js';
|
|
9
9
|
import { parseCurrencyFlag, readStoredCurrency } from './currency.js';
|
|
10
10
|
const IS_WINDOWS = process.platform === 'win32';
|
|
11
11
|
const RTK_BINARY_NAME = IS_WINDOWS ? 'rtk.exe' : 'rtk';
|
|
@@ -19,14 +19,15 @@ const PACKAGE_BIN = 'tersio';
|
|
|
19
19
|
const { version: PACKAGE_VERSION } = createRequire(import.meta.url)(path.join(findPackageRoot(path.dirname(fileURLToPath(import.meta.url))), 'package.json'));
|
|
20
20
|
const CAVEMAN_DEFAULTS = new Set(['off', 'lite', 'full', 'ultra', 'megacave-lite', 'megacave-full', 'megacave-ultra']);
|
|
21
21
|
const PONYTAIL_DEFAULTS = new Set(['off', 'lite', 'full', 'ultra']);
|
|
22
|
+
const PSTACK_DEFAULTS = new Set(['off', 'on']);
|
|
22
23
|
const RTK_DEFAULTS = new Set(['on', 'off']);
|
|
23
24
|
const DIAG_SCHEDULES = new Set(['manual', 'daily', 'weekly', 'monthly']);
|
|
24
|
-
// Mirrors COMBO_LEVELS in extensions/shared/session-state.ts
|
|
25
|
+
// Mirrors COMBO_LEVELS in extensions/shared/session-state.ts, so the combo preset implies all four modes.
|
|
25
26
|
const COMBO_PRESET_MODES = {
|
|
26
|
-
off: { caveman: 'off', rtk: false, ponytail: 'off' },
|
|
27
|
-
medium: { caveman: 'lite', rtk: true, ponytail: 'lite' },
|
|
28
|
-
balanced: { caveman: 'full', rtk: true, ponytail: 'full' },
|
|
29
|
-
max: { caveman: 'ultra', rtk: true, ponytail: 'ultra' },
|
|
27
|
+
off: { caveman: 'off', rtk: false, ponytail: 'off', pstack: 'off' },
|
|
28
|
+
medium: { caveman: 'lite', rtk: true, ponytail: 'lite', pstack: 'on' },
|
|
29
|
+
balanced: { caveman: 'full', rtk: true, ponytail: 'full', pstack: 'on' },
|
|
30
|
+
max: { caveman: 'ultra', rtk: true, ponytail: 'ultra', pstack: 'on' },
|
|
30
31
|
};
|
|
31
32
|
const COMBO_DEFAULTS = new Set(Object.keys(COMBO_PRESET_MODES));
|
|
32
33
|
function parseEnum(value, valid, flag) {
|
|
@@ -46,12 +47,11 @@ function flagValue(name) {
|
|
|
46
47
|
const prefix = `${name}=`;
|
|
47
48
|
return args.find((arg) => arg.startsWith(prefix))?.slice(prefix.length);
|
|
48
49
|
}
|
|
49
|
-
|
|
50
|
-
const COMMANDS = { install: true, update: true, doctor: true, uninstall: true, usage: true, dashboard: true, reset: true, settings: true, version: true, help: true };
|
|
50
|
+
const COMMANDS = { install: true, reinstall: true, update: true, doctor: true, uninstall: true, usage: true, dashboard: true, reset: true, settings: true, version: true, help: true };
|
|
51
51
|
const args = process.argv.slice(2);
|
|
52
52
|
const commandArg = args.find((arg) => !arg.startsWith('-'));
|
|
53
53
|
const command = commandArg?.toLowerCase() || null;
|
|
54
|
-
// `tersio settings diagnosis` jumps to one prompt
|
|
54
|
+
// `tersio settings diagnosis` jumps to one prompt. Index-based, so flag values never count as positionals.
|
|
55
55
|
const settingArg = (() => {
|
|
56
56
|
const at = args.indexOf(commandArg ?? '');
|
|
57
57
|
const next = at >= 0 ? args[at + 1] : undefined;
|
|
@@ -61,6 +61,7 @@ const settingArg = (() => {
|
|
|
61
61
|
})();
|
|
62
62
|
const unknownCommand = command !== null && !COMMANDS[command];
|
|
63
63
|
const install = command === 'install';
|
|
64
|
+
const reinstall = command === 'reinstall';
|
|
64
65
|
const update = command === 'update';
|
|
65
66
|
const showVersion = command === 'version' || args.includes('--version') || args.includes('-v');
|
|
66
67
|
const showHelp = command === 'help' || args.includes('--help') || args.includes('-h');
|
|
@@ -96,9 +97,10 @@ if (legacyScope !== null && legacyScope !== 'user') {
|
|
|
96
97
|
const comboDefaultFlag = parseEnum(flagValue('--combo-default'), COMBO_DEFAULTS, '--combo-default');
|
|
97
98
|
const cavemanDefaultFlag = parseEnum(flagValue('--caveman-default'), CAVEMAN_DEFAULTS, '--caveman-default');
|
|
98
99
|
const ponytailDefaultFlag = parseEnum(flagValue('--ponytail-default'), PONYTAIL_DEFAULTS, '--ponytail-default');
|
|
100
|
+
const pstackDefaultFlag = parseEnum(flagValue('--pstack-default'), PSTACK_DEFAULTS, '--pstack-default');
|
|
99
101
|
const rtkDefaultFlag = parseEnum(flagValue('--rtk-default'), RTK_DEFAULTS, '--rtk-default');
|
|
100
102
|
const diagScheduleFlag = parseEnum(flagValue('--diag-schedule'), DIAG_SCHEDULES, '--diag-schedule');
|
|
101
|
-
const profileFlagsGiven = [comboDefaultFlag, cavemanDefaultFlag, rtkDefaultFlag, ponytailDefaultFlag]
|
|
103
|
+
const profileFlagsGiven = [comboDefaultFlag, cavemanDefaultFlag, rtkDefaultFlag, ponytailDefaultFlag, pstackDefaultFlag]
|
|
102
104
|
.some((flag) => flag !== undefined);
|
|
103
105
|
// Repeatable `--subagent-marker`; overrides the built-in host prompt marker.
|
|
104
106
|
const subagentMarkerFlags = args
|
|
@@ -138,7 +140,7 @@ async function writeIfChanged(dest, content, options = {}) {
|
|
|
138
140
|
console.log(` [write] ${dest}`);
|
|
139
141
|
return true;
|
|
140
142
|
}
|
|
141
|
-
// omp ships 'extensions: null'
|
|
143
|
+
// omp ships 'extensions: null'; normalize it to a mapping key first, or YAML rejects the list items.
|
|
142
144
|
const EXTENSIONS_KEY_RE = /^\s*extensions\s*:/i;
|
|
143
145
|
const EXTENSIONS_NULL_RE = /^\s*extensions\s*:\s*(?:\[\s*\]|null|~)?\s*$/i;
|
|
144
146
|
function normalizeExtensionsKey(lines) {
|
|
@@ -160,7 +162,7 @@ async function ensureExtensionInConfig(configPath, extensionPath, label, options
|
|
|
160
162
|
const line = ` - ${normalizedPath}`;
|
|
161
163
|
const raw = await readTextIfExists(configPath);
|
|
162
164
|
let lines = (raw || '').split('\n');
|
|
163
|
-
// Drop legacy compiled twins (
|
|
165
|
+
// Drop legacy compiled twins (.js beside the name): OMP must never load both. Matched on the trailing path.
|
|
164
166
|
if (normalizedPath.endsWith('.ts')) {
|
|
165
167
|
const legacyTail = `${normalizedPath.slice(0, -3)}.js`.split('/').slice(-2).join('/');
|
|
166
168
|
lines = lines.filter((l) => !l.trim().replace(/^\.\//, '').endsWith(legacyTail));
|
|
@@ -239,6 +241,12 @@ function readPonytailConfig() {
|
|
|
239
241
|
: path.join(HOME, '.config', 'ponytail');
|
|
240
242
|
return { dir, path: path.join(dir, 'config.json') };
|
|
241
243
|
}
|
|
244
|
+
/** This package's ponytail range, or null; repair pins it, since npm installs the published range. */
|
|
245
|
+
async function bundledPonytailSpec() {
|
|
246
|
+
const root = findPackageRoot(path.dirname(fileURLToPath(import.meta.url)));
|
|
247
|
+
const text = await readTextIfExists(path.join(root, 'package.json'));
|
|
248
|
+
return parseJsonObject(text)?.dependencies?.['@dietrichgebert/ponytail'] ?? null;
|
|
249
|
+
}
|
|
242
250
|
function parseJsonObject(text) {
|
|
243
251
|
if (!text)
|
|
244
252
|
return null;
|
|
@@ -280,6 +288,14 @@ async function ensurePonytailConfigValue(key, value, options = {}) {
|
|
|
280
288
|
return true;
|
|
281
289
|
}, `would set Ponytail ${key}=${value}`, `Set Ponytail ${key}=${value}`, options);
|
|
282
290
|
}
|
|
291
|
+
// npm prefers a nested copy over the pin when the plugin's range excludes it; only `overrides` reaches it.
|
|
292
|
+
function ponytailOverride(pkg) {
|
|
293
|
+
const overrides = pkg.overrides;
|
|
294
|
+
if (typeof overrides !== 'object' || overrides === null)
|
|
295
|
+
return null;
|
|
296
|
+
const value = overrides['@dietrichgebert/ponytail'];
|
|
297
|
+
return typeof value === 'string' ? value : null;
|
|
298
|
+
}
|
|
283
299
|
// Read plugins/package.json tolerantly; a corrupt or missing file starts fresh with installer-managed defaults.
|
|
284
300
|
async function readPluginsPackage(pkgPath) {
|
|
285
301
|
const pkg = parseJsonObject(await readTextIfExists(pkgPath)) ?? {};
|
|
@@ -300,4 +316,4 @@ function relTime(ageMs) {
|
|
|
300
316
|
const weeks = Math.floor(days / 7);
|
|
301
317
|
return `${weeks} week${weeks === 1 ? '' : 's'} ago`;
|
|
302
318
|
}
|
|
303
|
-
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, DIAG_SCHEDULES, COMBO_PRESET_MODES, COMBO_DEFAULTS, parseEnum, flagValue, COMMANDS, commandArg, command, settingArg, unknownCommand, install, update, showVersion, showHelp, applyUpdate, args, dryRun, yes, verbose, doctor, fix, uninstall, usage, dashboard, reset, settings, allowUnverified, dashboardPort, dashboardOpen, dashboardExport, currency, currencyGiven, removePonytail, keepPonytail, removeRtk, comboDefaultFlag, cavemanDefaultFlag, ponytailDefaultFlag, rtkDefaultFlag, diagScheduleFlag, profileFlagsGiven, subagentMarkerFlags, subagentMarkersGiven, debug, execFileP, execP, writeIfChanged, normalizeExtensionsKey,
|
|
319
|
+
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, PSTACK_DEFAULTS, RTK_DEFAULTS, DIAG_SCHEDULES, COMBO_PRESET_MODES, COMBO_DEFAULTS, parseEnum, flagValue, COMMANDS, commandArg, command, settingArg, unknownCommand, install, reinstall, update, showVersion, showHelp, applyUpdate, args, dryRun, yes, verbose, doctor, fix, uninstall, usage, dashboard, reset, settings, allowUnverified, dashboardPort, dashboardOpen, dashboardExport, currency, currencyGiven, removePonytail, keepPonytail, removeRtk, comboDefaultFlag, cavemanDefaultFlag, ponytailDefaultFlag, pstackDefaultFlag, rtkDefaultFlag, diagScheduleFlag, profileFlagsGiven, subagentMarkerFlags, subagentMarkersGiven, debug, execFileP, execP, writeIfChanged, normalizeExtensionsKey, writeConfigLines, ensureExtensionInConfig, removeExtensionFromConfig, readPonytailConfig, parseJsonObject, parsePonytailConfig, patchPonytailConfig, ensurePonytailConfigValue, readPluginsPackage, relTime, bundledPonytailSpec, ponytailOverride, };
|
package/dist/cli/currency.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// cli/currency.ts — USD-base
|
|
1
|
+
// cli/currency.ts — USD-base helpers on offline snapshot rates. Same 10 currencies as the dashboard's live figures.
|
|
2
2
|
import { existsSync, readFileSync } from 'node:fs';
|
|
3
3
|
import { tersioSettingsFile } from '../extensions/shared/plugin-settings.js';
|
|
4
4
|
const CURRENCY_CODES = [
|