@krtclcdy/tersio 2.25.5 → 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 +17 -36
- package/dashboard/dist/index.html +15 -15
- package/dist/cli/backups.js +7 -14
- package/dist/cli/banner.js +4 -3
- package/dist/cli/common.js +28 -12
- package/dist/cli/currency.js +1 -1
- package/dist/cli/dashboard.js +66 -35
- package/dist/cli/doctor-fix.js +204 -19
- package/dist/cli/doctor.js +226 -39
- package/dist/cli/hosts.js +25 -5
- package/dist/cli/install.js +144 -48
- package/dist/cli/interactive.js +3 -3
- package/dist/cli/manifest.js +87 -7
- package/dist/cli/profile.js +13 -8
- package/dist/cli/rtk-wiring.js +1 -1
- package/dist/cli/settings.js +20 -5
- package/dist/cli/uninstall.js +3 -3
- package/dist/cli/update.js +6 -7
- package/dist/cli/usage.js +6 -9
- package/dist/extensions/caveman/index.js +4 -2
- package/dist/extensions/caveman/rule-megacave.md +5 -3
- package/dist/extensions/caveman/rule-ultra.md +4 -4
- package/dist/extensions/caveman/rule.md +55 -45
- package/dist/extensions/combo/index.js +62 -21
- package/dist/extensions/commands/index.js +1 -1
- package/dist/extensions/pstack/index.js +119 -0
- package/dist/extensions/pstack/rule.md +65 -0
- package/dist/extensions/rtk/filter.js +0 -1
- package/dist/extensions/rtk/session.js +4 -4
- package/dist/extensions/server.js +90 -6
- package/dist/extensions/shared/agent-stats.js +214 -278
- package/dist/extensions/shared/brand-marks.js +33 -34
- package/dist/extensions/shared/carbon.js +1 -1
- package/dist/extensions/shared/plugin-settings.js +11 -6
- package/dist/extensions/shared/pricing.js +8 -16
- 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 +53 -41
- package/dist/extensions/shared/usage-store.js +111 -52
- package/dist/extensions/shared/utils.js +5 -5
- package/dist/extensions/timing/index.js +31 -0
- package/dist/extensions/updater/index.js +3 -6
- package/dist/tersio.js +8 -3
- package/extensions/caveman/index.ts +4 -2
- package/extensions/caveman/rule-megacave.md +5 -3
- package/extensions/caveman/rule-ultra.md +4 -4
- package/extensions/caveman/rule.md +55 -45
- package/extensions/combo/index.ts +62 -19
- package/extensions/commands/index.ts +1 -1
- package/extensions/pstack/index.ts +128 -0
- package/extensions/pstack/rule.md +65 -0
- package/extensions/rtk/filter.ts +0 -1
- package/extensions/rtk/session.ts +4 -4
- package/extensions/server.ts +72 -7
- package/extensions/shared/agent-stats.ts +226 -278
- package/extensions/shared/brand-marks.ts +34 -34
- package/extensions/shared/carbon.ts +1 -1
- package/extensions/shared/plugin-settings.ts +12 -6
- package/extensions/shared/pricing.ts +10 -26
- 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 +60 -47
- package/extensions/shared/usage-store.ts +120 -52
- package/extensions/shared/utils.ts +5 -5
- package/extensions/timing/index.ts +45 -0
- package/extensions/updater/index.ts +3 -8
- package/package.json +17 -8
package/dist/cli/doctor-fix.js
CHANGED
|
@@ -1,20 +1,119 @@
|
|
|
1
1
|
import { existsSync, promises as fs } from 'node:fs';
|
|
2
|
+
import { spawnSync } from 'node:child_process';
|
|
2
3
|
import os from 'node:os';
|
|
3
4
|
import path from 'node:path';
|
|
4
|
-
import { BUN_BIN_DIR, OMP_AGENT_DIR, OMP_PLUGINS_DIR, PACKAGE_NAME, PACKAGE_VERSION, RTK_BINARY_NAME, removeExtensionFromConfig, dryRun, verbose, debug, execP, readPluginsPackage, } from './common.js';
|
|
5
|
+
import { BUN_BIN_DIR, HOME, OMP_AGENT_DIR, OMP_PLUGINS_DIR, PACKAGE_NAME, PACKAGE_VERSION, RTK_BINARY_NAME, removeExtensionFromConfig, dryRun, verbose, debug, bundledPonytailSpec, execP, ponytailOverride, readPluginsPackage, } from './common.js';
|
|
5
6
|
import { execNetwork, sayTagged } from './interactive.js';
|
|
6
7
|
import { wireRtkOmp, wireRtkOpencode, wireRtkPi, ensureRtkInConfig } from './rtk-wiring.js';
|
|
7
|
-
import { RTK_RELEASE_API, fetchJson, findFile, httpsGet, httpsDownload, parseChecksum, readTextIfExists, rtkPlatformSpec, sha256File, } from '../extensions/shared/utils.js';
|
|
8
|
+
import { RTK_RELEASE_API, fetchJson, findFile, httpsGet, httpsDownload, parseChecksum, readTextIfExists, resolveHostBinary, rtkPlatformSpec, sha256File, } from '../extensions/shared/utils.js';
|
|
8
9
|
import { runLatestUpdate } from './update.js';
|
|
9
|
-
import { OPENCODE_ENTRY, TREE_FILES, syncTreeFiles, treeFilesFor } from './manifest.js';
|
|
10
|
+
import { OPENCODE_ENTRY, TREE_FILES, isObsoleteTreeFile, syncPluginManifest, syncTreeFiles, treeFilesFor } from './manifest.js';
|
|
10
11
|
import { detectHosts, hostExtensionsDir } from './hosts.js';
|
|
11
|
-
|
|
12
|
+
/** Pids of `opencode serve --service` processes whose service manager no longer tracks them. The orphan holds the managed port, so the next start exits 1. */
|
|
13
|
+
export function serviceOrphanPids() {
|
|
14
|
+
const bin = resolveHostBinary('opencode', path.join(HOME, '.opencode', 'bin'));
|
|
15
|
+
if (!bin)
|
|
16
|
+
return [];
|
|
17
|
+
let status = '';
|
|
18
|
+
try {
|
|
19
|
+
status = spawnSync(bin, ['service', 'status'], { encoding: 'utf8' }).stdout.trim().toLowerCase();
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
return [];
|
|
23
|
+
}
|
|
24
|
+
if (serviceRunning(status))
|
|
25
|
+
return [];
|
|
26
|
+
let out = '';
|
|
27
|
+
try {
|
|
28
|
+
out = spawnSync('pgrep', ['-f', 'opencode serve --service'], { encoding: 'utf8' }).stdout ?? '';
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
return [];
|
|
32
|
+
}
|
|
33
|
+
// Only digits: the value comes from another process, and `process.kill` throws on anything else.
|
|
34
|
+
return out.split(/[\s,]+/).filter((line) => /^\d+$/.test(line.trim())).map((line) => line.trim());
|
|
35
|
+
}
|
|
36
|
+
/** Whether OpenCode reports its service running, and whether a serve process is alive. */
|
|
37
|
+
export function serviceState() {
|
|
38
|
+
const bin = resolveHostBinary('opencode', path.join(HOME, '.opencode', 'bin'));
|
|
39
|
+
if (!bin)
|
|
40
|
+
return null;
|
|
41
|
+
let status = '';
|
|
42
|
+
try {
|
|
43
|
+
status = spawnSync(bin, ['service', 'status'], { encoding: 'utf8' }).stdout.trim().toLowerCase();
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
if (status === '')
|
|
49
|
+
return null;
|
|
50
|
+
let live = false;
|
|
51
|
+
try {
|
|
52
|
+
const out = spawnSync('pgrep', ['-f', 'opencode serve --service'], { encoding: 'utf8' }).stdout ?? '';
|
|
53
|
+
live = out.split(/[\s,]+/).some((line) => /^\d+$/.test(line.trim()));
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
live = false;
|
|
57
|
+
}
|
|
58
|
+
return { status, live };
|
|
59
|
+
}
|
|
60
|
+
/** A running service reports its URL, so the word is only one of the answers. */
|
|
61
|
+
export function serviceRunning(status) {
|
|
62
|
+
return status === 'running' || status.startsWith('http://') || status.startsWith('https://');
|
|
63
|
+
}
|
|
64
|
+
/** Every obsolete file under `dest`, or an empty list. `treeFilesFor` is what the install writes. */
|
|
65
|
+
async function obsoleteFiles(dest, id) {
|
|
66
|
+
const keep = new Set(treeFilesFor(id));
|
|
67
|
+
const out = [];
|
|
68
|
+
const walk = async (dir, rel) => {
|
|
69
|
+
let entries;
|
|
70
|
+
try {
|
|
71
|
+
entries = await fs.readdir(dir, { withFileTypes: true });
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
for (const entry of entries) {
|
|
77
|
+
if (entry.name === 'node_modules' || entry.name === '.git')
|
|
78
|
+
continue;
|
|
79
|
+
const child = rel ? `${rel}/${entry.name}` : entry.name;
|
|
80
|
+
const at = path.join(dir, entry.name);
|
|
81
|
+
if (entry.isDirectory()) {
|
|
82
|
+
await walk(at, child);
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
if (keep.has(child))
|
|
86
|
+
continue;
|
|
87
|
+
if (isObsoleteTreeFile(child, id))
|
|
88
|
+
out.push(child);
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
await walk(dest, '');
|
|
92
|
+
return out;
|
|
93
|
+
}
|
|
94
|
+
/** Obsolete top-level directories, empty or not: a debris name is a whole directory, so a repair must drop the directory, not only the files inside it. */
|
|
95
|
+
async function obsoleteDirs(dest, id) {
|
|
96
|
+
const out = [];
|
|
97
|
+
let entries;
|
|
98
|
+
try {
|
|
99
|
+
entries = await fs.readdir(dest, { withFileTypes: true });
|
|
100
|
+
}
|
|
101
|
+
catch {
|
|
102
|
+
return out;
|
|
103
|
+
}
|
|
104
|
+
for (const entry of entries) {
|
|
105
|
+
if (!entry.isDirectory())
|
|
106
|
+
continue;
|
|
107
|
+
if (isObsoleteTreeFile(entry.name, id))
|
|
108
|
+
out.push(entry.name);
|
|
109
|
+
}
|
|
110
|
+
return out;
|
|
111
|
+
}
|
|
112
|
+
// A written tree is the install for both hosts, so --fix restores it for whichever hosts have it.
|
|
12
113
|
async function fixExtensionTrees() {
|
|
13
|
-
// The
|
|
14
|
-
// those hosts do not carry. One shared filter, so a repair cannot drop it into the wrong tree.
|
|
114
|
+
// The entry imports @opencode/plugin, which pi and omp lack, so one shared filter keeps repairs out of their trees.
|
|
15
115
|
const forHost = (id) => treeFilesFor(id);
|
|
16
|
-
//
|
|
17
|
-
// written against the old filter may have left the entry at the root. Remove both strays.
|
|
116
|
+
// Older trees nested the entry; a repair under the old filter may have left it at the root, so remove both strays.
|
|
18
117
|
const scrubOpencodeEntry = async (dest) => {
|
|
19
118
|
for (const target of [path.join(dest, OPENCODE_ENTRY), path.join(dest, 'opencode')]) {
|
|
20
119
|
if (!existsSync(target))
|
|
@@ -29,7 +128,7 @@ async function fixExtensionTrees() {
|
|
|
29
128
|
};
|
|
30
129
|
for (const host of detectHosts()) {
|
|
31
130
|
const dest = hostExtensionsDir(host.id);
|
|
32
|
-
// Repair a
|
|
131
|
+
// Repair a partly-present tree: a half-written one is this run's case, and it reads as "not installed".
|
|
33
132
|
const present = forHost(host.id).filter((file) => existsSync(path.join(dest, file))).length;
|
|
34
133
|
if (present === 0) {
|
|
35
134
|
if (host.id !== 'opencode')
|
|
@@ -40,21 +139,38 @@ async function fixExtensionTrees() {
|
|
|
40
139
|
const missing = await syncTreeFiles(dest, forHost(host.id), { dryRun, verbose });
|
|
41
140
|
for (const from of missing)
|
|
42
141
|
sayTagged(` [warn] bundled source missing: ${from}`);
|
|
142
|
+
// Writing the current tree repairs stale and missing at once; dropped debris needs removal, else it survives.
|
|
143
|
+
const debris = (await obsoleteFiles(dest, host.id)).sort();
|
|
144
|
+
for (const rel of debris) {
|
|
145
|
+
const at = path.join(dest, ...rel.split('/'));
|
|
146
|
+
if (dryRun) {
|
|
147
|
+
sayTagged(` [dry-run] would remove debris ${at}`);
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
150
|
+
await fs.rm(at, { force: true });
|
|
151
|
+
sayTagged(` [rm] debris ${at}`);
|
|
152
|
+
}
|
|
153
|
+
for (const rel of await obsoleteDirs(dest, host.id)) {
|
|
154
|
+
const at = path.join(dest, ...rel.split('/'));
|
|
155
|
+
if (dryRun) {
|
|
156
|
+
sayTagged(` [dry-run] would remove debris dir ${at}`);
|
|
157
|
+
continue;
|
|
158
|
+
}
|
|
159
|
+
await fs.rm(at, { recursive: true, force: true });
|
|
160
|
+
sayTagged(` [rm] debris dir ${at}`);
|
|
161
|
+
}
|
|
43
162
|
if (host.id !== 'opencode')
|
|
44
163
|
await scrubOpencodeEntry(dest);
|
|
45
164
|
if (!dryRun)
|
|
46
165
|
sayTagged(` [ok] ${host.label} extension tree: ${dest}`);
|
|
47
166
|
}
|
|
48
167
|
}
|
|
49
|
-
//
|
|
50
|
-
// The tersio tree ships the rtk rewrite, so the rtk plugin entry is redundant.
|
|
168
|
+
// On OpenCode the tree is inert without its entry in opencode.json, and the rtk plugin entry is redundant beside it.
|
|
51
169
|
async function fixOpencodeEntry() {
|
|
52
170
|
const { HOME } = await import('./common.js');
|
|
53
171
|
const dir = path.join(HOME, '.config', 'opencode', 'plugins', 'tersio');
|
|
54
172
|
const configPath = path.join(HOME, '.config', 'opencode', 'opencode.json');
|
|
55
|
-
// The loader resolves <pluginDir>/server.ts
|
|
56
|
-
// entry moved to the extensions root held a root shim that re-exported a nested copy, so the
|
|
57
|
-
// real entry is written in place and the stale nested tree is dropped.
|
|
173
|
+
// The loader resolves <pluginDir>/server.ts: an entry at the extensions root leaves a shim to a nested copy.
|
|
58
174
|
const nested = path.join(dir, 'opencode');
|
|
59
175
|
const staleNested = (await readTextIfExists(path.join(nested, 'server.ts'))) !== null;
|
|
60
176
|
await syncTreeFiles(dir, TREE_FILES.filter((file) => !file.includes('/')), { dryRun, verbose });
|
|
@@ -97,6 +213,28 @@ async function fixExtensions(pluginsDir) {
|
|
|
97
213
|
const missing = await syncTreeFiles(pluginExtDir, TREE_FILES, { dryRun, verbose }, (file) => ruleFile(file) && existsSync(path.join(pluginExtDir, ...file.split('/'))));
|
|
98
214
|
for (const from of missing)
|
|
99
215
|
sayTagged(` [warn] bundled source missing: ${from}`);
|
|
216
|
+
// omp loads this tree, so its debris is what doctor reports: the package tree, compared against every entry.
|
|
217
|
+
const debris = (await obsoleteFiles(pluginExtDir, 'opencode')).sort();
|
|
218
|
+
for (const rel of debris) {
|
|
219
|
+
const at = path.join(pluginExtDir, ...rel.split('/'));
|
|
220
|
+
if (dryRun) {
|
|
221
|
+
sayTagged(` [dry-run] would remove debris ${at}`);
|
|
222
|
+
continue;
|
|
223
|
+
}
|
|
224
|
+
await fs.rm(at, { force: true });
|
|
225
|
+
sayTagged(` [rm] debris ${at}`);
|
|
226
|
+
}
|
|
227
|
+
for (const rel of await obsoleteDirs(pluginExtDir, 'opencode')) {
|
|
228
|
+
const at = path.join(pluginExtDir, ...rel.split('/'));
|
|
229
|
+
if (dryRun) {
|
|
230
|
+
sayTagged(` [dry-run] would remove debris dir ${at}`);
|
|
231
|
+
continue;
|
|
232
|
+
}
|
|
233
|
+
await fs.rm(at, { recursive: true, force: true });
|
|
234
|
+
sayTagged(` [rm] debris dir ${at}`);
|
|
235
|
+
}
|
|
236
|
+
// A synced tree is not enough: omp loads only the entries its manifest names.
|
|
237
|
+
await syncPluginManifest(path.dirname(pluginExtDir), { dryRun, verbose });
|
|
100
238
|
// Writing is not atomic, so re-read rather than assume the tree is whole.
|
|
101
239
|
if (dryRun)
|
|
102
240
|
return;
|
|
@@ -206,13 +344,22 @@ async function fixPonytail(pluginsDir) {
|
|
|
206
344
|
await fs.mkdir(pluginsDir, { recursive: true });
|
|
207
345
|
const pkgPath = path.join(pluginsDir, 'package.json');
|
|
208
346
|
const pkg = await readPluginsPackage(pkgPath);
|
|
209
|
-
// Drop the legacy separate entry: ponytail is a tersio dependency now.
|
|
210
|
-
if ('@dietrichgebert/ponytail' in pkg.dependencies) {
|
|
211
|
-
delete pkg.dependencies['@dietrichgebert/ponytail'];
|
|
212
|
-
console.log(' [migrate] dropped separate @dietrichgebert/ponytail dependency (now bundled with tersio)');
|
|
213
|
-
}
|
|
214
347
|
if (!(PACKAGE_NAME in pkg.dependencies))
|
|
215
348
|
pkg.dependencies[PACKAGE_NAME] = `^${PACKAGE_VERSION}`;
|
|
349
|
+
// npm resolves the *published* tersio, whose range can lag this tree; the override is what reaches the loaded copy.
|
|
350
|
+
const localSpec = await bundledPonytailSpec();
|
|
351
|
+
if (localSpec !== null) {
|
|
352
|
+
if (pkg.dependencies['@dietrichgebert/ponytail'] !== localSpec)
|
|
353
|
+
pkg.dependencies['@dietrichgebert/ponytail'] = localSpec;
|
|
354
|
+
if (ponytailOverride(pkg) !== localSpec) {
|
|
355
|
+
pkg.overrides = { ...(typeof pkg.overrides === 'object' && pkg.overrides !== null ? pkg.overrides : {}), '@dietrichgebert/ponytail': localSpec };
|
|
356
|
+
const note = 'forced with overrides, so the nested copy is dropped';
|
|
357
|
+
if (dryRun)
|
|
358
|
+
sayTagged(` [dry-run] would pin @dietrichgebert/ponytail → ${localSpec} (${note})`);
|
|
359
|
+
else
|
|
360
|
+
console.log(` [pin] @dietrichgebert/ponytail → ${localSpec} (${note})`);
|
|
361
|
+
}
|
|
362
|
+
}
|
|
216
363
|
if (dryRun) {
|
|
217
364
|
console.log(' [dry-run] would run: npm install --no-audit --no-fund (in plugins dir)');
|
|
218
365
|
return;
|
|
@@ -222,10 +369,46 @@ async function fixPonytail(pluginsDir) {
|
|
|
222
369
|
throw new Error(`bundled ponytail restore failed: ${e.message}`);
|
|
223
370
|
});
|
|
224
371
|
}
|
|
372
|
+
/** The decisive line of a failed command, so one [warn] line carries the reason. */
|
|
373
|
+
function shortError(e) {
|
|
374
|
+
return ((e.stderr) || e.message || '').trim().slice(0, 200);
|
|
375
|
+
}
|
|
225
376
|
async function fixCli() {
|
|
226
377
|
console.log(' Doctor --fix: updating CLI + add-ons');
|
|
227
378
|
await runLatestUpdate();
|
|
228
379
|
}
|
|
380
|
+
/** OpenCode's manager can lose its daemonized serve process, so `stop` and `restart` cannot reach it: kill the orphan by pid, then start the service again. */
|
|
381
|
+
async function fixOpencodeService() {
|
|
382
|
+
const orphans = serviceOrphanPids();
|
|
383
|
+
if (orphans.length === 0) {
|
|
384
|
+
sayTagged(' [skip] no orphaned OpenCode serve process; the service answers');
|
|
385
|
+
return;
|
|
386
|
+
}
|
|
387
|
+
for (const pid of orphans) {
|
|
388
|
+
if (dryRun) {
|
|
389
|
+
sayTagged(` [dry-run] would stop the orphaned serve process (pid ${pid})`);
|
|
390
|
+
continue;
|
|
391
|
+
}
|
|
392
|
+
try {
|
|
393
|
+
process.kill(Number(pid), 'SIGTERM');
|
|
394
|
+
sayTagged(` [ok] stopped the orphaned serve process (pid ${pid})`);
|
|
395
|
+
}
|
|
396
|
+
catch (e) {
|
|
397
|
+
sayTagged(` [warn] pid ${pid} already gone: ${shortError(e)}`);
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
if (dryRun)
|
|
401
|
+
return;
|
|
402
|
+
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
403
|
+
const bin = resolveHostBinary('opencode', path.join(HOME, '.opencode', 'bin'));
|
|
404
|
+
if (!bin) {
|
|
405
|
+
sayTagged(' [warn] opencode not found on PATH; start it with `opencode service start`');
|
|
406
|
+
return;
|
|
407
|
+
}
|
|
408
|
+
await execNetwork('Starting the OpenCode service', bin, ['service', 'start'], { timeout: 60000 }).catch((e) => {
|
|
409
|
+
throw new Error(`service start failed: ${shortError(e)}`);
|
|
410
|
+
});
|
|
411
|
+
}
|
|
229
412
|
async function runDoctorRepairs(targets) {
|
|
230
413
|
const all = targets.includes('all');
|
|
231
414
|
const want = (t) => all || targets.includes(t);
|
|
@@ -253,6 +436,8 @@ async function runDoctorRepairs(targets) {
|
|
|
253
436
|
await attempt('ponytail', () => fixPonytail(OMP_PLUGINS_DIR));
|
|
254
437
|
if (want('cli'))
|
|
255
438
|
await attempt('cli', fixCli);
|
|
439
|
+
if (want('service'))
|
|
440
|
+
await attempt('opencode service', fixOpencodeService);
|
|
256
441
|
return failed;
|
|
257
442
|
}
|
|
258
443
|
export { runDoctorRepairs };
|