@krtclcdy/tersio 2.25.5 → 2.25.7
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 +42 -38
- package/dashboard/dist/index.html +15 -15
- package/dist/bench/code-out/pfull-0/debounce.js +13 -0
- package/dist/bench/code-out/pfull-0/deepClone.js +18 -0
- package/dist/bench/code-out/pfull-0/groupBy.js +15 -0
- package/dist/bench/code-out/pfull-1/debounce.js +13 -0
- package/dist/bench/code-out/pfull-1/deepClone.js +17 -0
- package/dist/bench/code-out/pfull-1/groupBy.js +14 -0
- package/dist/bench/code-out/pfull-2/debounce.js +13 -0
- package/dist/bench/code-out/pfull-2/deepClone.js +18 -0
- package/dist/bench/code-out/pfull-2/groupBy.js +15 -0
- package/dist/bench/code-out/pfull-3/debounce.js +12 -0
- package/dist/bench/code-out/pfull-3/deepClone.js +17 -0
- package/dist/bench/code-out/pfull-3/groupBy.js +7 -0
- package/dist/bench/code-out/pfull-4/debounce.js +10 -0
- package/dist/bench/code-out/pfull-4/deepClone.js +17 -0
- package/dist/bench/code-out/pfull-4/groupBy.js +14 -0
- package/dist/bench/code-out/plite-0/debounce.js +13 -0
- package/dist/bench/code-out/plite-0/deepClone.js +18 -0
- package/dist/bench/code-out/plite-0/groupBy.js +15 -0
- package/dist/bench/code-out/plite-1/debounce.js +13 -0
- package/dist/bench/code-out/plite-1/deepClone.js +17 -0
- package/dist/bench/code-out/plite-1/groupBy.js +11 -0
- package/dist/bench/code-out/plite-2/debounce.js +12 -0
- package/dist/bench/code-out/plite-2/deepClone.js +17 -0
- package/dist/bench/code-out/plite-2/groupBy.js +14 -0
- package/dist/bench/code-out/plite-3/debounce.js +12 -0
- package/dist/bench/code-out/plite-3/deepClone.js +17 -0
- package/dist/bench/code-out/plite-3/groupBy.js +14 -0
- package/dist/bench/code-out/plite-4/debounce.js +12 -0
- package/dist/bench/code-out/plite-4/deepClone.js +17 -0
- package/dist/bench/code-out/plite-4/groupBy.js +14 -0
- package/dist/bench/code-out/pultra-0/debounce.js +12 -0
- package/dist/bench/code-out/pultra-0/deepClone.js +20 -0
- package/dist/bench/code-out/pultra-0/groupBy.js +14 -0
- package/dist/bench/code-out/pultra-1/debounce.js +10 -0
- package/dist/bench/code-out/pultra-1/deepClone.js +17 -0
- package/dist/bench/code-out/pultra-1/groupBy.js +7 -0
- package/dist/bench/code-out/pultra-2/debounce.js +13 -0
- package/dist/bench/code-out/pultra-2/deepClone.js +18 -0
- package/dist/bench/code-out/pultra-2/groupBy.js +15 -0
- package/dist/bench/code-out/pultra-3/debounce.js +13 -0
- package/dist/bench/code-out/pultra-3/deepClone.js +18 -0
- package/dist/bench/code-out/pultra-3/groupBy.js +15 -0
- package/dist/bench/code-out/pultra-4/debounce.js +13 -0
- package/dist/bench/code-out/pultra-4/deepClone.js +18 -0
- package/dist/bench/code-out/pultra-4/groupBy.js +15 -0
- package/dist/bench/code-out/run-0/debounce.js +12 -0
- package/dist/bench/code-out/run-0/deepClone.js +17 -0
- package/dist/bench/code-out/run-0/groupBy.js +14 -0
- package/dist/bench/code-out/run-1/debounce.js +13 -0
- package/dist/bench/code-out/run-1/deepClone.js +15 -0
- package/dist/bench/code-out/run-1/groupBy.js +13 -0
- package/dist/bench/code-out/run-2/debounce.js +12 -0
- package/dist/bench/code-out/run-2/deepClone.js +18 -0
- package/dist/bench/code-out/run-2/groupBy.js +13 -0
- package/dist/bench/code-out/run-3/debounce.js +12 -0
- package/dist/bench/code-out/run-3/deepClone.js +18 -0
- package/dist/bench/code-out/run-3/groupBy.js +14 -0
- package/dist/bench/code-out/run-4/debounce.js +12 -0
- package/dist/bench/code-out/run-4/deepClone.js +16 -0
- package/dist/bench/code-out/run-4/groupBy.js +13 -0
- package/dist/cli/backups.js +7 -14
- package/dist/cli/banner.js +3 -3
- package/dist/cli/common.js +29 -13
- 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 +263 -40
- package/dist/cli/hosts.js +25 -6
- package/dist/cli/install.js +150 -49
- package/dist/cli/interactive.js +3 -3
- package/dist/cli/manifest.js +109 -9
- 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 +4 -4
- package/dist/cli/update.js +6 -7
- package/dist/cli/usage.js +6 -9
- package/dist/extensions/caveman/index.js +10 -4
- package/dist/extensions/caveman/rule-lite.md +77 -0
- package/dist/extensions/caveman/rule-megacave-lite.md +59 -0
- package/dist/extensions/caveman/rule-megacave-ultra.md +59 -0
- 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 +85 -25
- package/dist/extensions/commands/index.js +1 -1
- package/dist/extensions/pstack/index.js +121 -0
- package/dist/extensions/pstack/rule.md +141 -0
- package/dist/extensions/rtk/filter.js +0 -1
- package/dist/extensions/rtk/session.js +20 -12
- 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-eligible.js +84 -0
- package/dist/extensions/shared/rtk-gain.js +5 -10
- package/dist/extensions/shared/session-state.js +15 -10
- package/dist/extensions/shared/sqlite.js +162 -0
- package/dist/extensions/shared/status.js +8 -9
- package/dist/extensions/shared/types.js +1 -1
- package/dist/extensions/shared/usage-ledger.js +155 -61
- 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 +10 -4
- package/extensions/caveman/rule-lite.md +77 -0
- package/extensions/caveman/rule-megacave-lite.md +59 -0
- package/extensions/caveman/rule-megacave-ultra.md +59 -0
- 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 +86 -23
- package/extensions/commands/index.ts +1 -1
- package/extensions/pstack/index.ts +130 -0
- package/extensions/pstack/playbooks/authoring-a-skill.md +12 -0
- package/extensions/pstack/playbooks/autonomous-run.md +13 -0
- package/extensions/pstack/playbooks/autopilot-full.md +13 -0
- package/extensions/pstack/playbooks/autopilot-stack.md +16 -0
- package/extensions/pstack/playbooks/babysit.md +23 -0
- package/extensions/pstack/playbooks/bug-fix.md +15 -0
- package/extensions/pstack/playbooks/eval.md +25 -0
- package/extensions/pstack/playbooks/feature.md +21 -0
- package/extensions/pstack/playbooks/hillclimb.md +21 -0
- package/extensions/pstack/playbooks/investigation.md +14 -0
- package/extensions/pstack/playbooks/multi-phase-plan.md +155 -0
- package/extensions/pstack/playbooks/opening-a-pr.md +34 -0
- package/extensions/pstack/playbooks/orchestrate.md +111 -0
- package/extensions/pstack/playbooks/pause-safely.md +10 -0
- package/extensions/pstack/playbooks/perf-issue.md +25 -0
- package/extensions/pstack/playbooks/prototype.md +14 -0
- package/extensions/pstack/playbooks/refactoring.md +16 -0
- package/extensions/pstack/playbooks/runtime-forensics.md +11 -0
- package/extensions/pstack/playbooks/session-pickup.md +11 -0
- package/extensions/pstack/playbooks/shipping.md +17 -0
- package/extensions/pstack/playbooks/trace-forensics.md +14 -0
- package/extensions/pstack/playbooks/visual-parity.md +11 -0
- package/extensions/pstack/playbooks/worktree-cleanup.md +12 -0
- package/extensions/pstack/principles/attack-the-premise.md +22 -0
- package/extensions/pstack/principles/boundary-discipline.md +33 -0
- package/extensions/pstack/principles/build-the-lever.md +22 -0
- package/extensions/pstack/principles/encode-lessons-in-structure.md +10 -0
- package/extensions/pstack/principles/exhaust-the-design-space.md +8 -0
- package/extensions/pstack/principles/experience-first.md +18 -0
- package/extensions/pstack/principles/explain-the-number.md +16 -0
- package/extensions/pstack/principles/fix-root-causes.md +23 -0
- package/extensions/pstack/principles/foundational-thinking.md +20 -0
- package/extensions/pstack/principles/guard-the-context-window.md +11 -0
- package/extensions/pstack/principles/laziness-protocol.md +9 -0
- package/extensions/pstack/principles/make-operations-idempotent.md +23 -0
- package/extensions/pstack/principles/migrate-callers-then-delete-legacy-apis.md +21 -0
- package/extensions/pstack/principles/minimize-reader-load.md +17 -0
- package/extensions/pstack/principles/model-the-domain.md +23 -0
- package/extensions/pstack/principles/never-block-on-the-human.md +14 -0
- package/extensions/pstack/principles/outcome-oriented-execution.md +20 -0
- package/extensions/pstack/principles/prove-it-works.md +21 -0
- package/extensions/pstack/principles/redesign-from-first-principles.md +15 -0
- package/extensions/pstack/principles/separate-before-serializing-shared-state.md +15 -0
- package/extensions/pstack/principles/sequence-verifiable-units.md +16 -0
- package/extensions/pstack/principles/subtract-before-you-add.md +17 -0
- package/extensions/pstack/principles/test-behavior-not-implementation.md +20 -0
- package/extensions/pstack/principles/type-system-discipline.md +17 -0
- package/extensions/pstack/references/bugbot-triage.md +142 -0
- package/extensions/pstack/rule.md +141 -0
- package/extensions/pstack/skills/architect/SKILL.md +78 -0
- package/extensions/pstack/skills/architect/references/design-red-flags.md +57 -0
- package/extensions/pstack/skills/architect/references/rationale-template.md +35 -0
- package/extensions/pstack/skills/architect/references/runner-prompt.md +20 -0
- package/extensions/pstack/skills/arena/SKILL.md +47 -0
- package/extensions/pstack/skills/benchmark-checklist/SKILL.md +38 -0
- package/extensions/pstack/skills/blast-radius/SKILL.md +49 -0
- package/extensions/pstack/skills/bro/SKILL.md +6 -0
- package/extensions/pstack/skills/correct/SKILL.md +31 -0
- package/extensions/pstack/skills/figure-it-out/SKILL.md +51 -0
- package/extensions/pstack/skills/how/SKILL.md +35 -0
- package/extensions/pstack/skills/how/references/explainer-prompt.md +55 -0
- package/extensions/pstack/skills/how/references/explorer-prompt.md +35 -0
- package/extensions/pstack/skills/interrogate/SKILL.md +73 -0
- package/extensions/pstack/skills/interrogate/references/code-quality-review.md +47 -0
- package/extensions/pstack/skills/interrogate/references/lead-judgment.md +58 -0
- package/extensions/pstack/skills/interrogate/references/reviewer-prompt.md +62 -0
- package/extensions/pstack/skills/interrogate/references/rubric.md +77 -0
- package/extensions/pstack/skills/no-comments/SKILL.md +23 -0
- package/extensions/pstack/skills/reflect/SKILL.md +66 -0
- package/extensions/pstack/skills/reflect/references/divergent-reviewer.md +38 -0
- package/extensions/pstack/skills/reflect/references/judgment-reviewer.md +37 -0
- package/extensions/pstack/skills/reflect/references/synthesizer.md +56 -0
- package/extensions/pstack/skills/reflect/references/tooling-reviewer.md +48 -0
- package/extensions/pstack/skills/show-me-your-work/SKILL.md +81 -0
- package/extensions/pstack/skills/swarm/SKILL.md +45 -0
- package/extensions/pstack/skills/tdd/SKILL.md +41 -0
- package/extensions/pstack/skills/technical-writing/SKILL.md +105 -0
- package/extensions/pstack/skills/unslop/SKILL.md +61 -0
- package/extensions/pstack/skills/why/SKILL.md +97 -0
- package/extensions/pstack/skills/why/references/epistemics.md +71 -0
- package/extensions/pstack/skills/why/references/investigator-prompt.md +84 -0
- package/extensions/pstack/skills/why/references/source-playbook.md +17 -0
- package/extensions/pstack/skills/why/references/sources/code-archaeology.md +51 -0
- package/extensions/pstack/skills/why/references/sources/databricks.md +70 -0
- package/extensions/pstack/skills/why/references/sources/datadog.md +97 -0
- package/extensions/pstack/skills/why/references/sources/incident-postmortem.md +5 -0
- package/extensions/pstack/skills/why/references/sources/linear.md +48 -0
- package/extensions/pstack/skills/why/references/sources/notion.md +55 -0
- package/extensions/pstack/skills/why/references/sources/sentry.md +66 -0
- package/extensions/pstack/skills/why/references/sources/slack.md +17 -0
- package/extensions/pstack/skills/why/references/synthesizer-prompt.md +129 -0
- package/extensions/rtk/filter.ts +0 -1
- package/extensions/rtk/session.ts +19 -11
- 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-eligible.ts +83 -0
- package/extensions/shared/rtk-gain.ts +5 -10
- package/extensions/shared/session-state.ts +18 -13
- package/extensions/shared/sqlite.ts +162 -0
- package/extensions/shared/status.ts +9 -9
- package/extensions/shared/types.ts +2 -1
- package/extensions/shared/usage-ledger.ts +157 -66
- package/extensions/shared/usage-store.ts +120 -52
- package/extensions/shared/utils.ts +5 -5
- package/extensions/timing/index.ts +44 -0
- package/extensions/updater/index.ts +3 -8
- package/package.json +21 -9
package/dist/cli/dashboard.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
//
|
|
1
|
+
// Dashboard server + usage APIs. Binds 127.0.0.1; --export writes a file. No Promise.withResolvers (Node 20.12).
|
|
2
2
|
import { spawn } from 'node:child_process';
|
|
3
3
|
import { execFileSync } from 'node:child_process';
|
|
4
4
|
import { existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from 'node:fs';
|
|
@@ -11,11 +11,11 @@ import { isCurrencyCode } from './currency.js';
|
|
|
11
11
|
import { OMP_AGENT_DIR, OMP_PLUGINS_DIR, PACKAGE_VERSION, } from './common.js';
|
|
12
12
|
import { BACKUP_SCHEDULES, formatCliStatus, storedProfile, writePluginSettings } from './profile.js';
|
|
13
13
|
import { normalizeComboLevel } from '../extensions/shared/session-state.js';
|
|
14
|
-
import { CAVEMAN_DEFAULTS, PONYTAIL_DEFAULTS } from './common.js';
|
|
14
|
+
import { CAVEMAN_DEFAULTS, PONYTAIL_DEFAULTS, PSTACK_DEFAULTS } from './common.js';
|
|
15
15
|
import { PACKAGE_NAME } from './common.js';
|
|
16
|
-
import { findPackageRoot, resolveRtkBinary } from '../extensions/shared/utils.js';
|
|
16
|
+
import { findPackageRoot, resolveRtkBinary, ruleBodyProblem } from '../extensions/shared/utils.js';
|
|
17
17
|
import { BACKUP_KEEP, backupDir, backupDiskBytes, createDataBackup, dataSources, deleteDataBackup, listDataBackups, restoreDataBackup } from './backups.js';
|
|
18
|
-
import { readBackupSchedule } from '../extensions/shared/usage-store.js';
|
|
18
|
+
import { readBackupSchedule, syncUsageDb } from '../extensions/shared/usage-store.js';
|
|
19
19
|
import { agentDbPath, emptyRequestEntry, isAgentView, isRangeKey, readAgentRequestEntry, readAgentStats, readSessionTrace, statsDbPath } from '../extensions/shared/agent-stats.js';
|
|
20
20
|
const DASHBOARD_DIST = path.join(findPackageRoot(path.dirname(fileURLToPath(import.meta.url))), 'dashboard', 'dist');
|
|
21
21
|
const DASHBOARD_INDEX = path.join(DASHBOARD_DIST, 'index.html');
|
|
@@ -51,9 +51,12 @@ async function brandDataUri() {
|
|
|
51
51
|
}
|
|
52
52
|
/** The range a page opens on, matching the reference dashboard. */
|
|
53
53
|
const DEFAULT_AGENT_RANGE = '24h';
|
|
54
|
-
/**
|
|
54
|
+
/** Poll memo TTL: every poll re-reads the aggregate, so a repeat poll reuses the last answer. */
|
|
55
55
|
const AGENT_REFRESH_MS = 5000;
|
|
56
|
+
/** A sync walks every transcript, so it runs at most this often even while a poll outruns it. */
|
|
57
|
+
const AGENT_SYNC_MS = 15_000;
|
|
56
58
|
let agentMemo = null;
|
|
59
|
+
let lastSyncAt = 0;
|
|
57
60
|
function fileStamp(file) {
|
|
58
61
|
try {
|
|
59
62
|
const stat = statSync(file);
|
|
@@ -63,15 +66,25 @@ function fileStamp(file) {
|
|
|
63
66
|
return '-';
|
|
64
67
|
}
|
|
65
68
|
}
|
|
66
|
-
/**
|
|
67
|
-
function
|
|
68
|
-
|
|
69
|
+
/** Both sources: the usage store carries every host's rows, agent.db only omp's quota snapshots. */
|
|
70
|
+
function storeStamp() {
|
|
71
|
+
return `${fileStamp(statsDbPath())}|${fileStamp(agentDbPath())}`;
|
|
72
|
+
}
|
|
73
|
+
/** Syncs before reading past the throttle; without it a page misses pi and OpenCode rows. */
|
|
74
|
+
function agentStats(range, view = 'all', offset = 0) {
|
|
69
75
|
const held = agentMemo;
|
|
70
|
-
if (held && held.range === range && held.view === view && held.
|
|
76
|
+
if (held && held.range === range && held.view === view && held.offset === offset && held.stamp === storeStamp() && Date.now() - held.at < AGENT_REFRESH_MS) {
|
|
71
77
|
return held.value;
|
|
72
78
|
}
|
|
73
|
-
|
|
74
|
-
|
|
79
|
+
if (Date.now() - lastSyncAt >= AGENT_SYNC_MS) {
|
|
80
|
+
lastSyncAt = Date.now();
|
|
81
|
+
try {
|
|
82
|
+
syncUsageDb();
|
|
83
|
+
}
|
|
84
|
+
catch { /* a store that will not write still reads the rows it already holds */ }
|
|
85
|
+
}
|
|
86
|
+
const value = readAgentStats(range, view, Date.now(), offset);
|
|
87
|
+
agentMemo = { range, view, offset, stamp: storeStamp(), at: Date.now(), value };
|
|
75
88
|
return value;
|
|
76
89
|
}
|
|
77
90
|
/** `?range=` on any of the omp routes; an unknown value falls back to the default. */
|
|
@@ -86,8 +99,14 @@ function viewFrom(url) {
|
|
|
86
99
|
const raw = queryParam(url, 'view');
|
|
87
100
|
return isAgentView(raw) ? raw : 'all';
|
|
88
101
|
}
|
|
89
|
-
|
|
90
|
-
|
|
102
|
+
/** `?offset=` in whole rows, floored; a later page starts further back instead of clamping at the cap. */
|
|
103
|
+
function offsetFrom(url) {
|
|
104
|
+
const raw = queryParam(url, 'offset');
|
|
105
|
+
const value = raw === null ? 0 : Number(raw);
|
|
106
|
+
return Number.isFinite(value) && value > 0 ? Math.floor(value) : 0;
|
|
107
|
+
}
|
|
108
|
+
function agentJson(range, view, offset) {
|
|
109
|
+
return JSON.stringify({ range, view, offset, agent: agentStats(range, view, offset) });
|
|
91
110
|
}
|
|
92
111
|
// Runs outside any agent session, so it reports the persisted defaults.
|
|
93
112
|
async function statusJson() {
|
|
@@ -98,6 +117,7 @@ async function statusJson() {
|
|
|
98
117
|
caveman: profile.cavemanDefault,
|
|
99
118
|
rtk: profile.rtkDefault ? 'on' : 'off',
|
|
100
119
|
ponytail: profile.ponytailDefault,
|
|
120
|
+
pstack: profile.pstackDefault,
|
|
101
121
|
});
|
|
102
122
|
}
|
|
103
123
|
// Local-only health: find each agent on PATH, run `--version`, report both.
|
|
@@ -282,18 +302,29 @@ function computeDoctorRows() {
|
|
|
282
302
|
}
|
|
283
303
|
catch { /* missing config is reported by extension rows */ }
|
|
284
304
|
const duplicateExtensions = [...new Set(explicitEntries.filter((entry, index) => explicitEntries.indexOf(entry) !== index))];
|
|
285
|
-
addon('Unique config registrations',
|
|
286
|
-
// A listed extension whose file is gone makes the host warn on every load, so surface it
|
|
305
|
+
addon('Unique config registrations', duplicateExtensions.length === 0, duplicateExtensions.length ? duplicateExtensions.join(', ') : 'ok', 'Extensions');
|
|
306
|
+
// A listed extension whose file is gone makes the host warn on every load, so surface it here.
|
|
287
307
|
const dangling = explicitEntries
|
|
288
308
|
.filter((entry) => entry.includes(`extensions${path.sep}`) || entry.includes('extensions/'))
|
|
289
309
|
.filter((entry) => !ok(path.isAbsolute(entry) ? entry : path.resolve(OMP_AGENT_DIR, entry)));
|
|
290
310
|
addon('Extension files present', dangling.length === 0, dangling.length ? dangling.join(', ') : 'ok', 'Extensions');
|
|
291
|
-
ext('Caveman extension', path.join(tersioPluginDir, 'extensions', 'caveman
|
|
292
|
-
ext('
|
|
293
|
-
|
|
311
|
+
ext('Caveman extension', path.join(tersioPluginDir, 'extensions', 'caveman', 'index.ts'));
|
|
312
|
+
ext('pstack extension', path.join(tersioPluginDir, 'extensions', 'pstack', 'index.ts'));
|
|
313
|
+
ext('RTK extension', path.join(tersioPluginDir, 'extensions', 'rtk', 'session.ts'));
|
|
314
|
+
const rule = path.join(tersioPluginDir, 'extensions', 'caveman', 'rule.md');
|
|
294
315
|
const ruleAge = ageStr(rule);
|
|
295
316
|
const ruleAt = absTime(rule);
|
|
296
317
|
addon('Caveman rule', ruleAge !== null, ruleAge && ruleAt ? `(updated ${ruleAge} · ${ruleAt})` : rule);
|
|
318
|
+
// The pstack body is a Tersio fork with no remote, so this row proves it is usable, not current.
|
|
319
|
+
const pstackRule = path.join(tersioPluginDir, 'extensions', 'pstack', 'rule.md');
|
|
320
|
+
let pstackProblem = 'missing — run: tersio install';
|
|
321
|
+
try {
|
|
322
|
+
pstackProblem = ruleBodyProblem(readFileSync(pstackRule, 'utf8'));
|
|
323
|
+
}
|
|
324
|
+
catch { /* missing stays */ }
|
|
325
|
+
const pstackAge = ageStr(pstackRule);
|
|
326
|
+
const pstackAt = absTime(pstackRule);
|
|
327
|
+
addon('pstack rule', pstackProblem === null, pstackProblem ?? `(updated ${pstackAge} · ${pstackAt})`);
|
|
297
328
|
const rtkVer = rtkBin ? rtkVersion(rtkBin) : null;
|
|
298
329
|
const rtkAge = rtkBin ? ageStr(rtkBin) : null;
|
|
299
330
|
const rtkAt = rtkBin ? absTime(rtkBin) : null;
|
|
@@ -314,7 +345,7 @@ function computeDoctorRows() {
|
|
|
314
345
|
addon('Prices feed', pricesAge !== null, pricesAge && pricesAt ? `(pulled ${pricesAge} · ${pricesAt})` : prices, 'Usage');
|
|
315
346
|
return rows;
|
|
316
347
|
}
|
|
317
|
-
//
|
|
348
|
+
// A report holding any of these labels is stale by definition.
|
|
318
349
|
const RETIRED_DIAG_LABELS = new Set([
|
|
319
350
|
'OMP CLI',
|
|
320
351
|
'Tersio CLI',
|
|
@@ -335,11 +366,10 @@ const RETIRED_DIAG_LABELS = new Set([
|
|
|
335
366
|
function isRetiredReport(report) {
|
|
336
367
|
return report.rows.some((r) => RETIRED_DIAG_LABELS.has(r.label));
|
|
337
368
|
}
|
|
338
|
-
// Recompute when forced, missing, retired, or past
|
|
369
|
+
// Recompute when forced, missing, retired, or past schedule; manual schedules recompute on open, dated ones reuse.
|
|
339
370
|
function getDoctorReport(force) {
|
|
340
371
|
const saved = readDiagReport();
|
|
341
372
|
const ttl = saved ? DIAG_TTL_MS[saved.schedule] : 0;
|
|
342
|
-
// Manual recomputes on open; a dated schedule reuses the saved report.
|
|
343
373
|
if (!force && saved && !isRetiredReport(saved) && ttl > 0 && Date.now() - saved.checkedAt < ttl)
|
|
344
374
|
return saved;
|
|
345
375
|
const report = { rows: computeDoctorRows(), checkedAt: Date.now(), schedule: saved?.schedule ?? 'manual' };
|
|
@@ -373,6 +403,12 @@ async function saveDefaults(body) {
|
|
|
373
403
|
return { ok: false, error: 'unknown ponytail level' };
|
|
374
404
|
profile.ponytailDefault = v;
|
|
375
405
|
}
|
|
406
|
+
if (body.pstackDefault !== undefined) {
|
|
407
|
+
const v = String(body.pstackDefault);
|
|
408
|
+
if (!PSTACK_DEFAULTS.has(v))
|
|
409
|
+
return { ok: false, error: 'unknown pstack level' };
|
|
410
|
+
profile.pstackDefault = v;
|
|
411
|
+
}
|
|
376
412
|
if (body.backupSchedule !== undefined) {
|
|
377
413
|
const v = String(body.backupSchedule);
|
|
378
414
|
if (!BACKUP_SCHEDULES.has(v))
|
|
@@ -392,6 +428,7 @@ async function saveDefaults(body) {
|
|
|
392
428
|
cavemanDefault: profile.cavemanDefault,
|
|
393
429
|
rtkDefault: profile.rtkDefault,
|
|
394
430
|
ponytailDefault: profile.ponytailDefault,
|
|
431
|
+
pstackDefault: profile.pstackDefault,
|
|
395
432
|
backupSchedule: profile.backupSchedule,
|
|
396
433
|
},
|
|
397
434
|
};
|
|
@@ -423,7 +460,7 @@ function openBrowser(url) {
|
|
|
423
460
|
const cmd = process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'start' : 'xdg-open';
|
|
424
461
|
spawn(cmd, [url], { detached: true, stdio: 'ignore' }).unref();
|
|
425
462
|
}
|
|
426
|
-
// Replacers throughout: session data holds `$'` (shell quoting) and
|
|
463
|
+
// Replacers throughout: session data holds `$'` (shell quoting) and `</script>`, which String.replace would mangle.
|
|
427
464
|
function escapeInline(json) {
|
|
428
465
|
return json.replace(/<\/(script)/gi, '<\\/$1');
|
|
429
466
|
}
|
|
@@ -444,20 +481,13 @@ function parseOwnJson(json, field) {
|
|
|
444
481
|
async function exportDashboard(exportFile, currency) {
|
|
445
482
|
requireDashboardBundle();
|
|
446
483
|
const [bundle, icon] = await Promise.all([readSegment(DASHBOARD_INDEX), brandDataUri()]);
|
|
447
|
-
//
|
|
448
|
-
// `fetch("brand.webp")` depending on the bundler's quote choice. Match either, or the exported
|
|
449
|
-
// card silently loses its mark. `atob` returns one char per byte, and a Blob encodes a string
|
|
450
|
-
// as UTF-8, so the bytes go through a Uint8Array first: a webp is binary and every byte at or
|
|
451
|
-
// above 0x80 would otherwise double and corrupt the image.
|
|
484
|
+
// Minified: fetch reads `fetch(\`brand.webp\`)` or `fetch("brand.webp")`; a mismatch loses the card's mark.
|
|
452
485
|
const b64 = icon.split(',')[1] ?? '';
|
|
453
|
-
// atob yields one
|
|
454
|
-
// Uint8Array or every byte at or above 0x80 doubles and the webp comes out corrupt.
|
|
486
|
+
// atob yields one byte per char, so a Uint8Array is needed or bytes >= 0x80 double and corrupt the webp.
|
|
455
487
|
const brandBytes = `(() => { const c = atob(${JSON.stringify(b64)}); const v = new Uint8Array(c.length); for (let i = 0; i < c.length; i++) v[i] = c.charCodeAt(i); return v; })()`;
|
|
456
488
|
const inline = bundle
|
|
457
489
|
.replace('window.__TERSIO_SNAP = null;', () => `window.__TERSIO_SNAP = ${escapeInline(JSON.stringify({ currency, agent: agentStats(DEFAULT_AGENT_RANGE), health: parseOwnJson(healthJson(), 'health'), doctor: getDoctorReport(false) }))};`)
|
|
458
|
-
// A mark
|
|
459
|
-
// esbuild compiled with a backtick. Match any of `"`, `'`, and `` ` ``, or an exported file
|
|
460
|
-
// keeps a bare reference that file:// cannot resolve.
|
|
490
|
+
// A mark is an HTML attribute or JSX prop; match every quote or the file keeps an unresolvable reference.
|
|
461
491
|
.replace(/((?:src|href)[=:][ \t]*)([`"'])brand\.webp\2/g, (_all, head, quote) => `${head}${quote}${icon}${quote}`)
|
|
462
492
|
.replace(/fetch\(([`"])brand\.webp\1\)/g, () => `Promise.resolve({ ok: true, blob: async () => new Blob([${brandBytes}], { type: "image/webp" }) })`);
|
|
463
493
|
await fs.writeFile(exportFile, inline, 'utf8');
|
|
@@ -492,7 +522,7 @@ async function runDashboard(options) {
|
|
|
492
522
|
}
|
|
493
523
|
const action = String(body.action ?? '');
|
|
494
524
|
const name = String(body.name ?? '');
|
|
495
|
-
// A missing
|
|
525
|
+
// A missing SQLite engine, or a locked file, is a 500 with a message rather than a silent no-op.
|
|
496
526
|
try {
|
|
497
527
|
if (action === 'create') {
|
|
498
528
|
const made = await createDataBackup();
|
|
@@ -530,6 +560,7 @@ async function runDashboard(options) {
|
|
|
530
560
|
cavemanDefault: profile.cavemanDefault,
|
|
531
561
|
rtkDefault: profile.rtkDefault,
|
|
532
562
|
ponytailDefault: profile.ponytailDefault,
|
|
563
|
+
pstackDefault: profile.pstackDefault,
|
|
533
564
|
backupSchedule: profile.backupSchedule,
|
|
534
565
|
currency: options.currency,
|
|
535
566
|
}));
|
|
@@ -576,7 +607,7 @@ async function runDashboard(options) {
|
|
|
576
607
|
}
|
|
577
608
|
if (req.url === '/api/agent' || req.url?.startsWith('/api/agent?')) {
|
|
578
609
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
579
|
-
res.end(agentJson(rangeFrom(req.url), viewFrom(req.url)));
|
|
610
|
+
res.end(agentJson(rangeFrom(req.url), viewFrom(req.url), offsetFrom(req.url)));
|
|
580
611
|
return;
|
|
581
612
|
}
|
|
582
613
|
if (req.url?.startsWith('/api/agent/session')) {
|
|
@@ -691,4 +722,4 @@ async function runDashboard(options) {
|
|
|
691
722
|
});
|
|
692
723
|
});
|
|
693
724
|
}
|
|
694
|
-
export { runDashboard, saveDashboardCurrency, readDiagSchedule, setDiagSchedule };
|
|
725
|
+
export { runDashboard, saveDashboardCurrency, readDiagSchedule, setDiagSchedule, offsetFrom };
|
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 };
|