@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
package/dist/cli/doctor-fix.js
CHANGED
|
@@ -1,63 +1,181 @@
|
|
|
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,
|
|
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/
|
|
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 {
|
|
10
|
+
import { OPENCODE_ENTRY, TREE_FILES, isObsoleteTreeFile, syncPluginManifest, syncTreeFiles, treeFilesFor } from './manifest.js';
|
|
10
11
|
import { detectHosts, hostExtensionsDir } from './hosts.js';
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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 {
|
|
21
74
|
return;
|
|
22
75
|
}
|
|
23
|
-
|
|
24
|
-
|
|
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.
|
|
113
|
+
async function fixExtensionTrees() {
|
|
114
|
+
// The entry imports @opencode/plugin, which pi and omp lack, so one shared filter keeps repairs out of their trees.
|
|
115
|
+
const forHost = (id) => treeFilesFor(id);
|
|
116
|
+
// Older trees nested the entry; a repair under the old filter may have left it at the root, so remove both strays.
|
|
117
|
+
const scrubOpencodeEntry = async (dest) => {
|
|
118
|
+
for (const target of [path.join(dest, OPENCODE_ENTRY), path.join(dest, 'opencode')]) {
|
|
119
|
+
if (!existsSync(target))
|
|
120
|
+
continue;
|
|
121
|
+
if (dryRun) {
|
|
122
|
+
sayTagged(` [dry-run] would remove foreign ${target}`);
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
await fs.rm(target, { recursive: true, force: true });
|
|
126
|
+
sayTagged(` [rm] foreign ${target}`);
|
|
127
|
+
}
|
|
25
128
|
};
|
|
26
129
|
for (const host of detectHosts()) {
|
|
27
130
|
const dest = hostExtensionsDir(host.id);
|
|
28
|
-
// Repair a
|
|
131
|
+
// Repair a partly-present tree: a half-written one is this run's case, and it reads as "not installed".
|
|
29
132
|
const present = forHost(host.id).filter((file) => existsSync(path.join(dest, file))).length;
|
|
30
133
|
if (present === 0) {
|
|
31
134
|
if (host.id !== 'opencode')
|
|
32
|
-
await
|
|
135
|
+
await scrubOpencodeEntry(dest);
|
|
33
136
|
continue;
|
|
34
137
|
}
|
|
35
138
|
await fs.mkdir(dest, { recursive: true });
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
139
|
+
const missing = await syncTreeFiles(dest, forHost(host.id), { dryRun, verbose });
|
|
140
|
+
for (const from of missing)
|
|
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}`);
|
|
43
161
|
}
|
|
44
162
|
if (host.id !== 'opencode')
|
|
45
|
-
await
|
|
163
|
+
await scrubOpencodeEntry(dest);
|
|
46
164
|
if (!dryRun)
|
|
47
165
|
sayTagged(` [ok] ${host.label} extension tree: ${dest}`);
|
|
48
166
|
}
|
|
49
167
|
}
|
|
50
|
-
//
|
|
51
|
-
// 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.
|
|
52
169
|
async function fixOpencodeEntry() {
|
|
53
170
|
const { HOME } = await import('./common.js');
|
|
54
171
|
const dir = path.join(HOME, '.config', 'opencode', 'plugins', 'tersio');
|
|
55
172
|
const configPath = path.join(HOME, '.config', 'opencode', 'opencode.json');
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
173
|
+
// The loader resolves <pluginDir>/server.ts: an entry at the extensions root leaves a shim to a nested copy.
|
|
174
|
+
const nested = path.join(dir, 'opencode');
|
|
175
|
+
const staleNested = (await readTextIfExists(path.join(nested, 'server.ts'))) !== null;
|
|
176
|
+
await syncTreeFiles(dir, TREE_FILES.filter((file) => !file.includes('/')), { dryRun, verbose });
|
|
177
|
+
if (staleNested && !dryRun)
|
|
178
|
+
await fs.rm(nested, { recursive: true, force: true }).catch(() => { });
|
|
61
179
|
if (dryRun)
|
|
62
180
|
sayTagged(` [dry-run] would remove the redundant rtk plugin entry`);
|
|
63
181
|
else
|
|
@@ -91,23 +209,38 @@ async function fixExtensions(pluginsDir) {
|
|
|
91
209
|
const pluginExtDir = path.join(pluginsDir, 'node_modules', '@krtclcdy', 'tersio', 'extensions');
|
|
92
210
|
await fs.mkdir(pluginExtDir, { recursive: true });
|
|
93
211
|
// Rule bodies are never overwritten: they can be newer upstream fetches.
|
|
94
|
-
const ruleFile = (f) => /^caveman
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
212
|
+
const ruleFile = (f) => /^caveman\/rule.*\.md$/.test(f);
|
|
213
|
+
const missing = await syncTreeFiles(pluginExtDir, TREE_FILES, { dryRun, verbose }, (file) => ruleFile(file) && existsSync(path.join(pluginExtDir, ...file.split('/'))));
|
|
214
|
+
for (const from of missing)
|
|
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}`);
|
|
98
222
|
continue;
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
else
|
|
103
|
-
await writeIfChanged(dest, text, { dryRun, verbose });
|
|
223
|
+
}
|
|
224
|
+
await fs.rm(at, { force: true });
|
|
225
|
+
sayTagged(` [rm] debris ${at}`);
|
|
104
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 });
|
|
105
238
|
// Writing is not atomic, so re-read rather than assume the tree is whole.
|
|
106
239
|
if (dryRun)
|
|
107
240
|
return;
|
|
108
|
-
const
|
|
109
|
-
if (
|
|
110
|
-
sayTagged(` [fail] tree incomplete: ${
|
|
241
|
+
const stillMissing = TREE_FILES.filter((file) => !existsSync(path.join(pluginExtDir, ...file.split('/'))));
|
|
242
|
+
if (stillMissing.length > 0)
|
|
243
|
+
sayTagged(` [fail] tree incomplete: ${stillMissing.join(', ')}`);
|
|
111
244
|
else
|
|
112
245
|
sayTagged(` [ok] extension tree: ${TREE_FILES.length} files`);
|
|
113
246
|
}
|
|
@@ -127,7 +260,9 @@ async function fixRegistrations(agentDir, pluginsDir) {
|
|
|
127
260
|
if ((await readTextIfExists(rtkTs)) !== null)
|
|
128
261
|
await ensureRtkInConfig({ dryRun, verbose });
|
|
129
262
|
const ponytailExtPath = path.join(pluginsDir, 'node_modules', '@dietrichgebert', 'ponytail', 'pi-extension', 'index.js');
|
|
130
|
-
|
|
263
|
+
// A tree from before the fold still names `combo-toggle`; both lines name the same module.
|
|
264
|
+
await removeExtensionFromConfig(configPath, path.join(extDir, 'combo', 'index.ts'), 'manifest-loaded combo', { dryRun, verbose });
|
|
265
|
+
await removeExtensionFromConfig(configPath, path.join(extDir, 'combo-toggle', 'index.ts'), 'retired manifest-loaded combo', { dryRun, verbose });
|
|
131
266
|
await removeExtensionFromConfig(configPath, ponytailExtPath, 'manifest-loaded ponytail', { dryRun, verbose });
|
|
132
267
|
await removeExtensionFromConfig(configPath, path.join(extDir, 'shared', 'mode-reinforcement.ts'), 'retired mode reinforcement', { dryRun, verbose });
|
|
133
268
|
const pkgPath = path.join(pluginsDir, 'package.json');
|
|
@@ -209,13 +344,22 @@ async function fixPonytail(pluginsDir) {
|
|
|
209
344
|
await fs.mkdir(pluginsDir, { recursive: true });
|
|
210
345
|
const pkgPath = path.join(pluginsDir, 'package.json');
|
|
211
346
|
const pkg = await readPluginsPackage(pkgPath);
|
|
212
|
-
// Drop the legacy separate entry: ponytail is a tersio dependency now.
|
|
213
|
-
if ('@dietrichgebert/ponytail' in pkg.dependencies) {
|
|
214
|
-
delete pkg.dependencies['@dietrichgebert/ponytail'];
|
|
215
|
-
console.log(' [migrate] dropped separate @dietrichgebert/ponytail dependency (now bundled with tersio)');
|
|
216
|
-
}
|
|
217
347
|
if (!(PACKAGE_NAME in pkg.dependencies))
|
|
218
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
|
+
}
|
|
219
363
|
if (dryRun) {
|
|
220
364
|
console.log(' [dry-run] would run: npm install --no-audit --no-fund (in plugins dir)');
|
|
221
365
|
return;
|
|
@@ -225,10 +369,46 @@ async function fixPonytail(pluginsDir) {
|
|
|
225
369
|
throw new Error(`bundled ponytail restore failed: ${e.message}`);
|
|
226
370
|
});
|
|
227
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
|
+
}
|
|
228
376
|
async function fixCli() {
|
|
229
377
|
console.log(' Doctor --fix: updating CLI + add-ons');
|
|
230
378
|
await runLatestUpdate();
|
|
231
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
|
+
}
|
|
232
412
|
async function runDoctorRepairs(targets) {
|
|
233
413
|
const all = targets.includes('all');
|
|
234
414
|
const want = (t) => all || targets.includes(t);
|
|
@@ -256,6 +436,8 @@ async function runDoctorRepairs(targets) {
|
|
|
256
436
|
await attempt('ponytail', () => fixPonytail(OMP_PLUGINS_DIR));
|
|
257
437
|
if (want('cli'))
|
|
258
438
|
await attempt('cli', fixCli);
|
|
439
|
+
if (want('service'))
|
|
440
|
+
await attempt('opencode service', fixOpencodeService);
|
|
259
441
|
return failed;
|
|
260
442
|
}
|
|
261
443
|
export { runDoctorRepairs };
|