@thinkrun/cli 0.1.27
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 +349 -0
- package/dist/bin/thinkrun.d.ts +6 -0
- package/dist/bin/thinkrun.d.ts.map +1 -0
- package/dist/bin/thinkrun.js +124 -0
- package/dist/bin/thinkrun.js.map +1 -0
- package/dist/scripts/browse.sh +1107 -0
- package/dist/src/adapters/cloud.d.ts +79 -0
- package/dist/src/adapters/cloud.d.ts.map +1 -0
- package/dist/src/adapters/cloud.js +637 -0
- package/dist/src/adapters/cloud.js.map +1 -0
- package/dist/src/adapters/index.d.ts +47 -0
- package/dist/src/adapters/index.d.ts.map +1 -0
- package/dist/src/adapters/index.js +211 -0
- package/dist/src/adapters/index.js.map +1 -0
- package/dist/src/adapters/local-command-retry.d.ts +12 -0
- package/dist/src/adapters/local-command-retry.d.ts.map +1 -0
- package/dist/src/adapters/local-command-retry.js +224 -0
- package/dist/src/adapters/local-command-retry.js.map +1 -0
- package/dist/src/adapters/local.d.ts +136 -0
- package/dist/src/adapters/local.d.ts.map +1 -0
- package/dist/src/adapters/local.js +1273 -0
- package/dist/src/adapters/local.js.map +1 -0
- package/dist/src/adapters/types.d.ts +45 -0
- package/dist/src/adapters/types.d.ts.map +1 -0
- package/dist/src/adapters/types.js +6 -0
- package/dist/src/adapters/types.js.map +1 -0
- package/dist/src/commands/actions.d.ts +135 -0
- package/dist/src/commands/actions.d.ts.map +1 -0
- package/dist/src/commands/actions.js +2207 -0
- package/dist/src/commands/actions.js.map +1 -0
- package/dist/src/commands/agent-init.d.ts +16 -0
- package/dist/src/commands/agent-init.d.ts.map +1 -0
- package/dist/src/commands/agent-init.js +222 -0
- package/dist/src/commands/agent-init.js.map +1 -0
- package/dist/src/commands/analyze.d.ts +11 -0
- package/dist/src/commands/analyze.d.ts.map +1 -0
- package/dist/src/commands/analyze.js +238 -0
- package/dist/src/commands/analyze.js.map +1 -0
- package/dist/src/commands/cache.d.ts +6 -0
- package/dist/src/commands/cache.d.ts.map +1 -0
- package/dist/src/commands/cache.js +147 -0
- package/dist/src/commands/cache.js.map +1 -0
- package/dist/src/commands/cloud.d.ts +6 -0
- package/dist/src/commands/cloud.d.ts.map +1 -0
- package/dist/src/commands/cloud.js +332 -0
- package/dist/src/commands/cloud.js.map +1 -0
- package/dist/src/commands/config.d.ts +7 -0
- package/dist/src/commands/config.d.ts.map +1 -0
- package/dist/src/commands/config.js +208 -0
- package/dist/src/commands/config.js.map +1 -0
- package/dist/src/commands/doctor.d.ts +127 -0
- package/dist/src/commands/doctor.d.ts.map +1 -0
- package/dist/src/commands/doctor.js +684 -0
- package/dist/src/commands/doctor.js.map +1 -0
- package/dist/src/commands/evaluate-helpers.d.ts +6 -0
- package/dist/src/commands/evaluate-helpers.d.ts.map +1 -0
- package/dist/src/commands/evaluate-helpers.js +13 -0
- package/dist/src/commands/evaluate-helpers.js.map +1 -0
- package/dist/src/commands/install.d.ts +118 -0
- package/dist/src/commands/install.d.ts.map +1 -0
- package/dist/src/commands/install.js +975 -0
- package/dist/src/commands/install.js.map +1 -0
- package/dist/src/commands/release.d.ts +7 -0
- package/dist/src/commands/release.d.ts.map +1 -0
- package/dist/src/commands/release.js +123 -0
- package/dist/src/commands/release.js.map +1 -0
- package/dist/src/commands/reset-connection.d.ts +17 -0
- package/dist/src/commands/reset-connection.d.ts.map +1 -0
- package/dist/src/commands/reset-connection.js +141 -0
- package/dist/src/commands/reset-connection.js.map +1 -0
- package/dist/src/commands/session-debug.d.ts +23 -0
- package/dist/src/commands/session-debug.d.ts.map +1 -0
- package/dist/src/commands/session-debug.js +267 -0
- package/dist/src/commands/session-debug.js.map +1 -0
- package/dist/src/commands/setup.d.ts +53 -0
- package/dist/src/commands/setup.d.ts.map +1 -0
- package/dist/src/commands/setup.js +249 -0
- package/dist/src/commands/setup.js.map +1 -0
- package/dist/src/config/store.d.ts +39 -0
- package/dist/src/config/store.d.ts.map +1 -0
- package/dist/src/config/store.js +290 -0
- package/dist/src/config/store.js.map +1 -0
- package/dist/src/daemon/access.d.ts +53 -0
- package/dist/src/daemon/access.d.ts.map +1 -0
- package/dist/src/daemon/access.js +87 -0
- package/dist/src/daemon/access.js.map +1 -0
- package/dist/src/daemon/bridge-envelope.d.ts +96 -0
- package/dist/src/daemon/bridge-envelope.d.ts.map +1 -0
- package/dist/src/daemon/bridge-envelope.js +235 -0
- package/dist/src/daemon/bridge-envelope.js.map +1 -0
- package/dist/src/daemon/utils.d.ts +43 -0
- package/dist/src/daemon/utils.d.ts.map +1 -0
- package/dist/src/daemon/utils.js +134 -0
- package/dist/src/daemon/utils.js.map +1 -0
- package/dist/src/errors.d.ts +60 -0
- package/dist/src/errors.d.ts.map +1 -0
- package/dist/src/errors.js +87 -0
- package/dist/src/errors.js.map +1 -0
- package/dist/src/local-bridge-timing.d.ts +31 -0
- package/dist/src/local-bridge-timing.d.ts.map +1 -0
- package/dist/src/local-bridge-timing.js +41 -0
- package/dist/src/local-bridge-timing.js.map +1 -0
- package/dist/src/obstacle-recovery/classify-script.d.ts +16 -0
- package/dist/src/obstacle-recovery/classify-script.d.ts.map +1 -0
- package/dist/src/obstacle-recovery/classify-script.js +53 -0
- package/dist/src/obstacle-recovery/classify-script.js.map +1 -0
- package/dist/src/obstacle-recovery/obstacle-classifier.d.ts +21 -0
- package/dist/src/obstacle-recovery/obstacle-classifier.d.ts.map +1 -0
- package/dist/src/obstacle-recovery/obstacle-classifier.js +37 -0
- package/dist/src/obstacle-recovery/obstacle-classifier.js.map +1 -0
- package/dist/src/obstacle-recovery/state-fingerprint.d.ts +26 -0
- package/dist/src/obstacle-recovery/state-fingerprint.d.ts.map +1 -0
- package/dist/src/obstacle-recovery/state-fingerprint.js +85 -0
- package/dist/src/obstacle-recovery/state-fingerprint.js.map +1 -0
- package/dist/src/obstacle-recovery/types.d.ts +44 -0
- package/dist/src/obstacle-recovery/types.d.ts.map +1 -0
- package/dist/src/obstacle-recovery/types.js +16 -0
- package/dist/src/obstacle-recovery/types.js.map +1 -0
- package/dist/src/output/formatter.d.ts +55 -0
- package/dist/src/output/formatter.d.ts.map +1 -0
- package/dist/src/output/formatter.js +55 -0
- package/dist/src/output/formatter.js.map +1 -0
- package/dist/src/output/mode.d.ts +11 -0
- package/dist/src/output/mode.d.ts.map +1 -0
- package/dist/src/output/mode.js +16 -0
- package/dist/src/output/mode.js.map +1 -0
- package/dist/src/protected-flow/detector.d.ts +26 -0
- package/dist/src/protected-flow/detector.d.ts.map +1 -0
- package/dist/src/protected-flow/detector.js +75 -0
- package/dist/src/protected-flow/detector.js.map +1 -0
- package/dist/src/protected-flow/types.d.ts +24 -0
- package/dist/src/protected-flow/types.d.ts.map +1 -0
- package/dist/src/protected-flow/types.js +28 -0
- package/dist/src/protected-flow/types.js.map +1 -0
- package/dist/src/session/agent-identity.d.ts +65 -0
- package/dist/src/session/agent-identity.d.ts.map +1 -0
- package/dist/src/session/agent-identity.js +133 -0
- package/dist/src/session/agent-identity.js.map +1 -0
- package/dist/src/session/cli-session-sync.d.ts +72 -0
- package/dist/src/session/cli-session-sync.d.ts.map +1 -0
- package/dist/src/session/cli-session-sync.js +244 -0
- package/dist/src/session/cli-session-sync.js.map +1 -0
- package/dist/src/session/context.d.ts +24 -0
- package/dist/src/session/context.d.ts.map +1 -0
- package/dist/src/session/context.js +165 -0
- package/dist/src/session/context.js.map +1 -0
- package/dist/src/session/continuity.d.ts +33 -0
- package/dist/src/session/continuity.d.ts.map +1 -0
- package/dist/src/session/continuity.js +179 -0
- package/dist/src/session/continuity.js.map +1 -0
- package/dist/src/session/errors.d.ts +9 -0
- package/dist/src/session/errors.d.ts.map +1 -0
- package/dist/src/session/errors.js +31 -0
- package/dist/src/session/errors.js.map +1 -0
- package/dist/src/session/local-continuity.d.ts +16 -0
- package/dist/src/session/local-continuity.d.ts.map +1 -0
- package/dist/src/session/local-continuity.js +146 -0
- package/dist/src/session/local-continuity.js.map +1 -0
- package/dist/src/session/signal-handler.d.ts +24 -0
- package/dist/src/session/signal-handler.d.ts.map +1 -0
- package/dist/src/session/signal-handler.js +35 -0
- package/dist/src/session/signal-handler.js.map +1 -0
- package/dist/src/shared/local-recovery-policy.d.ts +40 -0
- package/dist/src/shared/local-recovery-policy.d.ts.map +1 -0
- package/dist/src/shared/local-recovery-policy.js +59 -0
- package/dist/src/shared/local-recovery-policy.js.map +1 -0
- package/dist/src/shared/recovery-state.d.ts +3 -0
- package/dist/src/shared/recovery-state.d.ts.map +1 -0
- package/dist/src/shared/recovery-state.js +9 -0
- package/dist/src/shared/recovery-state.js.map +1 -0
- package/dist/src/types.d.ts +131 -0
- package/dist/src/types.d.ts.map +1 -0
- package/dist/src/types.js +5 -0
- package/dist/src/types.js.map +1 -0
- package/dist/src/utils.d.ts +50 -0
- package/dist/src/utils.d.ts.map +1 -0
- package/dist/src/utils.js +147 -0
- package/dist/src/utils.js.map +1 -0
- package/dist/src/working-location.d.ts +107 -0
- package/dist/src/working-location.d.ts.map +1 -0
- package/dist/src/working-location.js +651 -0
- package/dist/src/working-location.js.map +1 -0
- package/package.json +65 -0
|
@@ -0,0 +1,684 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Diagnostic command for local/cloud endpoint clarity.
|
|
3
|
+
* Reflects effective command-path health: warns when routing would fail.
|
|
4
|
+
*/
|
|
5
|
+
import { Command } from 'commander';
|
|
6
|
+
import chalk from 'chalk';
|
|
7
|
+
import { config } from '../config/store.js';
|
|
8
|
+
import { resolveRegistrationExtensionIdFromSources } from './install.js';
|
|
9
|
+
import { isLocalApiUrl, readBridgePort as _readBridgePort, DEFAULT_BRIDGE_PORT as _DEFAULT_BRIDGE_PORT, getBridgePortFileMtimeMs, getStaleBridgePortFileAgeMs, } from '../utils.js';
|
|
10
|
+
import { getResolutionInfo } from '../adapters/index.js';
|
|
11
|
+
import { checkNativeHost, isValidExtensionId, resolveExtensionId, } from './install.js';
|
|
12
|
+
import { getSessionContext } from '../session/context.js';
|
|
13
|
+
import { getDaemonJsonPath } from '../daemon/access.js';
|
|
14
|
+
import { probeDaemonHealth, DAEMON_PROTOCOL_VERSION, isPidAlive } from '../daemon/utils.js';
|
|
15
|
+
import { existsSync, readFileSync } from 'fs';
|
|
16
|
+
import { execSync } from 'child_process';
|
|
17
|
+
/** Prefix used by the native-host circuit breaker when setting lastDisconnectReason. */
|
|
18
|
+
export const CIRCUIT_BREAKER_REASON_PREFIX = 'Circuit breaker:';
|
|
19
|
+
export function getManifestRegistrationIssue(opts) {
|
|
20
|
+
if (Array.isArray(opts.manifestEntries)) {
|
|
21
|
+
const relevantEntries = opts.manifestEntries.filter((entry) => entry.relevant);
|
|
22
|
+
if (relevantEntries.length > 0) {
|
|
23
|
+
const hasMatchingRelevantEntry = relevantEntries.some((entry) => entry.manifestParseStatus === 'parsed' &&
|
|
24
|
+
Array.isArray(entry.manifestExtensionIds) &&
|
|
25
|
+
entry.manifestExtensionIds.includes(opts.effectiveExtensionId));
|
|
26
|
+
const hasDisagreeingRelevantEntry = relevantEntries.some((entry) => !entry.manifestExists ||
|
|
27
|
+
entry.manifestParseStatus !== 'parsed' ||
|
|
28
|
+
!Array.isArray(entry.manifestExtensionIds) ||
|
|
29
|
+
!entry.manifestExtensionIds.includes(opts.effectiveExtensionId));
|
|
30
|
+
if (hasMatchingRelevantEntry && hasDisagreeingRelevantEntry) {
|
|
31
|
+
return 'inconsistent';
|
|
32
|
+
}
|
|
33
|
+
if (hasMatchingRelevantEntry) {
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
if (relevantEntries.some((entry) => entry.manifestParseStatus === 'parse_error')) {
|
|
37
|
+
return 'parse_error';
|
|
38
|
+
}
|
|
39
|
+
if (relevantEntries.some((entry) => !entry.manifestExists)) {
|
|
40
|
+
return 'missing';
|
|
41
|
+
}
|
|
42
|
+
return 'wrong_id';
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
if (Array.isArray(opts.manifestExtensionIds)) {
|
|
46
|
+
return opts.manifestExtensionIds.includes(opts.effectiveExtensionId) ? null : 'wrong_id';
|
|
47
|
+
}
|
|
48
|
+
if (isValidExtensionId(opts.registeredExtensionId)
|
|
49
|
+
&& opts.registeredExtensionId !== opts.effectiveExtensionId) {
|
|
50
|
+
return 'wrong_id';
|
|
51
|
+
}
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
export function hasExtensionRegistrationMismatch(opts) {
|
|
55
|
+
return getManifestRegistrationIssue(opts) !== null;
|
|
56
|
+
}
|
|
57
|
+
export function resolveDoctorExtensionId(opts) {
|
|
58
|
+
return resolveRegistrationExtensionIdFromSources({
|
|
59
|
+
extensionId: opts.resolvedExtensionId.source === 'flag' ? opts.resolvedExtensionId.extensionId : undefined,
|
|
60
|
+
envExtensionId: opts.resolvedExtensionId.source === 'env' ? opts.resolvedExtensionId.extensionId : undefined,
|
|
61
|
+
configuredExtensionId: opts.resolvedExtensionId.source === 'config' ? opts.resolvedExtensionId.extensionId : undefined,
|
|
62
|
+
registeredExtensionId: opts.registeredExtensionId,
|
|
63
|
+
preferPackagedDefaultExtensionId: opts.preferPackagedDefaultExtensionId,
|
|
64
|
+
manifestExtensionIds: opts.manifestExtensionIds,
|
|
65
|
+
manifestParseStatus: opts.manifestParseStatus,
|
|
66
|
+
manifestExists: opts.manifestExists,
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
// Re-exported for backward compatibility — consumers should import from '../utils.js' directly.
|
|
70
|
+
export const DEFAULT_BRIDGE_PORT = _DEFAULT_BRIDGE_PORT;
|
|
71
|
+
export const readBridgePort = _readBridgePort;
|
|
72
|
+
export async function checkHealth(url, apiKey, fetchFn = fetch) {
|
|
73
|
+
try {
|
|
74
|
+
let healthUrl;
|
|
75
|
+
try {
|
|
76
|
+
const base = new URL(url);
|
|
77
|
+
if (base.protocol !== 'http:' && base.protocol !== 'https:') {
|
|
78
|
+
return { ok: false, message: `Unsupported protocol: ${base.protocol}` };
|
|
79
|
+
}
|
|
80
|
+
const baseHref = base.href.endsWith('/') ? base.href : `${base.href}/`;
|
|
81
|
+
healthUrl = new URL('health', baseHref).toString();
|
|
82
|
+
}
|
|
83
|
+
catch {
|
|
84
|
+
return { ok: false, message: 'Invalid URL' };
|
|
85
|
+
}
|
|
86
|
+
const headers = {};
|
|
87
|
+
if (apiKey)
|
|
88
|
+
headers['x-api-key'] = apiKey;
|
|
89
|
+
const res = await fetchFn(healthUrl, {
|
|
90
|
+
headers,
|
|
91
|
+
signal: AbortSignal.timeout(2500),
|
|
92
|
+
});
|
|
93
|
+
if (!res.ok) {
|
|
94
|
+
return { ok: false, message: `HTTP ${res.status}` };
|
|
95
|
+
}
|
|
96
|
+
let body;
|
|
97
|
+
try {
|
|
98
|
+
body = (await res.json());
|
|
99
|
+
}
|
|
100
|
+
catch {
|
|
101
|
+
return { ok: false, message: 'Invalid /health response (not JSON)' };
|
|
102
|
+
}
|
|
103
|
+
const inner = body?.data;
|
|
104
|
+
const svc = inner?.service || body?.service;
|
|
105
|
+
const extConnected = typeof inner?.extensionConnected === 'boolean'
|
|
106
|
+
? inner.extensionConnected
|
|
107
|
+
: typeof body?.extensionConnected === 'boolean'
|
|
108
|
+
? body.extensionConnected
|
|
109
|
+
: undefined;
|
|
110
|
+
const browserName = inner?.browser?.name ?? body?.browser?.name;
|
|
111
|
+
const lastReason = inner?.lastExtensionDisconnectReason ?? body?.lastExtensionDisconnectReason;
|
|
112
|
+
const lastAt = inner?.lastExtensionDisconnectAt ?? body?.lastExtensionDisconnectAt;
|
|
113
|
+
// The local native-host /health producer already emits these recovery fields
|
|
114
|
+
// (see extension/native-host/routes.ts -> handleHealth). Keep reading both the
|
|
115
|
+
// nested `data` shape and the flat shape because older callers may proxy either.
|
|
116
|
+
const recoveryState = inner?.recoveryState ?? body?.recoveryState;
|
|
117
|
+
const recentDisconnectCount = typeof inner?.recentDisconnectCount === 'number'
|
|
118
|
+
? inner.recentDisconnectCount
|
|
119
|
+
: typeof body?.recentDisconnectCount === 'number'
|
|
120
|
+
? body.recentDisconnectCount
|
|
121
|
+
: undefined;
|
|
122
|
+
const lastRecoveredAt = inner?.lastRecoveredAt ?? body?.lastRecoveredAt;
|
|
123
|
+
if (svc) {
|
|
124
|
+
if (typeof extConnected === 'boolean') {
|
|
125
|
+
const browserSuffix = typeof browserName === 'string' ? `, browser=${browserName}` : '';
|
|
126
|
+
let msg = `${svc} (extensionConnected=${extConnected}${browserSuffix})`;
|
|
127
|
+
if (typeof recoveryState === 'string' && recoveryState !== 'healthy') {
|
|
128
|
+
msg += `, recoveryState=${recoveryState}`;
|
|
129
|
+
}
|
|
130
|
+
if (typeof recentDisconnectCount === 'number' && recentDisconnectCount > 0) {
|
|
131
|
+
msg += `, recentDisconnects=${recentDisconnectCount}`;
|
|
132
|
+
}
|
|
133
|
+
if (typeof lastRecoveredAt === 'string' && lastRecoveredAt.length > 0) {
|
|
134
|
+
msg += `, lastRecoveredAt=${lastRecoveredAt}`;
|
|
135
|
+
}
|
|
136
|
+
const shouldSurfaceLastDisconnect = typeof lastReason === 'string' &&
|
|
137
|
+
lastReason.length > 0 &&
|
|
138
|
+
(extConnected === false || recoveryState === 'recently_recovered' || recoveryState === 'flapping');
|
|
139
|
+
if (shouldSurfaceLastDisconnect) {
|
|
140
|
+
msg += `, lastDisconnect=${lastReason}`;
|
|
141
|
+
if (typeof lastAt === 'string' && lastAt.length > 0)
|
|
142
|
+
msg += ` @ ${lastAt}`;
|
|
143
|
+
}
|
|
144
|
+
const disconnectReason = typeof lastReason === 'string' ? lastReason : undefined;
|
|
145
|
+
// Only set isCircuitBreakerTrip when the extension is actually disconnected;
|
|
146
|
+
// leave undefined when connected so callers can distinguish "connected" from "disconnected but not CB".
|
|
147
|
+
// Note: the native host clears lastExtensionDisconnectReason on reconnect (bridge-server.ts line 493),
|
|
148
|
+
// so a stale CB reason will never appear when extensionConnected=true — no freshness guard needed.
|
|
149
|
+
const isCircuitBreakerTrip = extConnected === false && typeof disconnectReason === 'string'
|
|
150
|
+
? disconnectReason.startsWith(CIRCUIT_BREAKER_REASON_PREFIX)
|
|
151
|
+
: undefined;
|
|
152
|
+
return {
|
|
153
|
+
ok: true,
|
|
154
|
+
message: msg,
|
|
155
|
+
extensionConnected: extConnected,
|
|
156
|
+
lastDisconnectReason: disconnectReason,
|
|
157
|
+
recoveryState,
|
|
158
|
+
recentDisconnectCount,
|
|
159
|
+
lastRecoveredAt,
|
|
160
|
+
isCircuitBreakerTrip,
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
return { ok: true, message: `${svc}` };
|
|
164
|
+
}
|
|
165
|
+
return { ok: true, message: 'healthy' };
|
|
166
|
+
}
|
|
167
|
+
catch (error) {
|
|
168
|
+
const message = error instanceof Error ? error.message : 'unreachable';
|
|
169
|
+
return { ok: false, message };
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* Classify the current disconnect state into one of four mutually exclusive buckets.
|
|
174
|
+
*
|
|
175
|
+
* Priority order (highest to lowest):
|
|
176
|
+
* daemon_down > breaker_open > socket_down > socket_up_tab_gone
|
|
177
|
+
*/
|
|
178
|
+
export function classifyDisconnectState(opts) {
|
|
179
|
+
if (!opts.daemonReachable) {
|
|
180
|
+
return {
|
|
181
|
+
state: 'daemon_down',
|
|
182
|
+
hint: 'Daemon is not running. Run: thinkrun setup or thinkrun doctor --fix',
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
if (opts.isBreakerOpen) {
|
|
186
|
+
return {
|
|
187
|
+
state: 'breaker_open',
|
|
188
|
+
hint: 'Circuit breaker is open from repeated transport failures. Run: thinkrun reset-connection',
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
if (opts.extensionSocketConnected === false) {
|
|
192
|
+
return {
|
|
193
|
+
state: 'socket_down',
|
|
194
|
+
hint: 'Extension is not connected to the daemon. Reload the ThinkRun extension or restart the browser.',
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
if (opts.extensionSocketConnected === undefined) {
|
|
198
|
+
// Daemon is up but we haven't observed the socket state — do NOT conclude
|
|
199
|
+
// the tab is gone (that requires a confirmed-up socket).
|
|
200
|
+
return {
|
|
201
|
+
state: 'socket_state_unknown',
|
|
202
|
+
hint: 'Daemon is up but the extension socket state is unknown yet. Retry shortly, or run: thinkrun doctor',
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
return {
|
|
206
|
+
state: 'socket_up_tab_gone',
|
|
207
|
+
hint: 'The attached tab is no longer available. Run: thinkrun attach <tabId>',
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
// Returns plain-text hint for extension disconnect (circuit-breaker vs reload branch); exported for testing.
|
|
211
|
+
export function getExtensionDisconnectHint(result, disconnectStateOpts) {
|
|
212
|
+
if (result.recoveryState === 'recovering') {
|
|
213
|
+
return 'Bridge is reconnecting automatically after a recent disconnect. Wait a few seconds, then retry the command.';
|
|
214
|
+
}
|
|
215
|
+
if (result.recoveryState === 'flapping') {
|
|
216
|
+
return 'Bridge is flapping from repeated disconnects. Avoid patient retry loops; reload the ThinkRun extension or stabilize the page/browser before retrying.';
|
|
217
|
+
}
|
|
218
|
+
if (result.recoveryState === 'recently_recovered') {
|
|
219
|
+
return 'Bridge recently recovered from disconnect churn. If the next local command still fails, retry once before assuming the bridge is fully healthy.';
|
|
220
|
+
}
|
|
221
|
+
if (result.extensionConnected !== false)
|
|
222
|
+
return null;
|
|
223
|
+
// When full signal data is available, use the structured taxonomy for a precise hint.
|
|
224
|
+
if (disconnectStateOpts) {
|
|
225
|
+
const diagnosis = classifyDisconnectState(disconnectStateOpts);
|
|
226
|
+
return diagnosis.hint;
|
|
227
|
+
}
|
|
228
|
+
if (result.isCircuitBreakerTrip) {
|
|
229
|
+
return 'False circuit-breaker trip. Run: thinkrun reset-connection';
|
|
230
|
+
}
|
|
231
|
+
// No reason available or non-CB disconnect — default to reload guidance (safer than reset-connection).
|
|
232
|
+
return 'Extension disconnected. Reload the ThinkRun extension in your Chromium-based browser.';
|
|
233
|
+
}
|
|
234
|
+
export function getSuggestedNextStep(opts) {
|
|
235
|
+
const { apiKey, localEndpoint, apiHealthOk, activeLocalTabId, activeCloudSessionId, activeSessionId, bridgeRelevant, bridgeHealthOk, commandPathOk, extensionConnected, isCircuitBreakerTrip, recoveryState, recentDisconnectCount, configuredExtensionId, extensionIdSource, manifestMismatch, manifestIssue, manifestParseStatus, } = opts;
|
|
236
|
+
if (bridgeRelevant && bridgeHealthOk === false) {
|
|
237
|
+
if (manifestIssue === 'inconsistent') {
|
|
238
|
+
return 'Local native-host manifests disagree across detected Chromium-based browsers. Run: thinkrun setup';
|
|
239
|
+
}
|
|
240
|
+
if (manifestIssue === 'parse_error' || manifestParseStatus === 'parse_error') {
|
|
241
|
+
return 'Local native-host manifest is unreadable. Run: thinkrun setup';
|
|
242
|
+
}
|
|
243
|
+
if (manifestIssue === 'missing') {
|
|
244
|
+
return 'Local native-host manifest is missing for a detected Chromium-based browser. Run: thinkrun setup';
|
|
245
|
+
}
|
|
246
|
+
if (manifestMismatch && extensionIdSource === 'default') {
|
|
247
|
+
return 'Local bridge is unreachable. If you are using the packaged ThinkRun extension, rerun: thinkrun setup\nIf you are using an unpacked/dev extension, copy its ID from chrome://extensions and run: thinkrun config set-extension-id <id>\nThen rerun: thinkrun setup';
|
|
248
|
+
}
|
|
249
|
+
if (manifestMismatch && configuredExtensionId) {
|
|
250
|
+
return `Local bridge is unreachable. Verify the installed ThinkRun extension ID in chrome://extensions matches ${configuredExtensionId}, then rerun: thinkrun setup`;
|
|
251
|
+
}
|
|
252
|
+
return 'Local bridge is unreachable. Start a Chromium-based browser with the ThinkRun extension active, then run thinkrun doctor again.';
|
|
253
|
+
}
|
|
254
|
+
if (bridgeRelevant) {
|
|
255
|
+
if (manifestIssue === 'inconsistent') {
|
|
256
|
+
return 'Local native-host manifests disagree across detected Chromium-based browsers. Run: thinkrun setup';
|
|
257
|
+
}
|
|
258
|
+
if (manifestIssue === 'parse_error' || manifestParseStatus === 'parse_error') {
|
|
259
|
+
return 'Local native-host manifest is unreadable. Run: thinkrun setup';
|
|
260
|
+
}
|
|
261
|
+
if (manifestIssue === 'missing') {
|
|
262
|
+
return 'Local native-host manifest is missing for a detected Chromium-based browser. Run: thinkrun setup';
|
|
263
|
+
}
|
|
264
|
+
if (manifestMismatch && extensionIdSource === 'default') {
|
|
265
|
+
return 'Local native-host manifest is registered for a different extension ID. If you are using the packaged ThinkRun extension, rerun: thinkrun setup\nIf you are using an unpacked/dev extension, copy its ID from chrome://extensions and run: thinkrun config set-extension-id <id>\nThen rerun: thinkrun setup';
|
|
266
|
+
}
|
|
267
|
+
if (manifestMismatch && configuredExtensionId) {
|
|
268
|
+
return `Local native-host manifest does not match ${configuredExtensionId}. Verify the installed ThinkRun extension ID in chrome://extensions, then rerun: thinkrun setup`;
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
if (!bridgeRelevant && activeCloudSessionId && !apiKey && !localEndpoint) {
|
|
272
|
+
return 'Set API key: thinkrun config set-key <key>';
|
|
273
|
+
}
|
|
274
|
+
if (bridgeRelevant && recoveryState === 'recovering') {
|
|
275
|
+
return 'Local bridge is reconnecting automatically. Wait a few seconds, then retry the local command.';
|
|
276
|
+
}
|
|
277
|
+
if (bridgeRelevant && extensionConnected === false) {
|
|
278
|
+
if (isCircuitBreakerTrip) {
|
|
279
|
+
return 'Local bridge is disconnected by the circuit breaker. Run: thinkrun reset-connection';
|
|
280
|
+
}
|
|
281
|
+
return 'Extension disconnected. Reload the ThinkRun extension in your Chromium-based browser, then run thinkrun doctor again.';
|
|
282
|
+
}
|
|
283
|
+
if (bridgeRelevant && !activeLocalTabId) {
|
|
284
|
+
return 'No active local tab. Run: thinkrun tabs\nThen: thinkrun attach <tabId>';
|
|
285
|
+
}
|
|
286
|
+
// Only prompt for API key when in cloud mode — local mode does not require one.
|
|
287
|
+
if (!apiKey && !localEndpoint && !bridgeRelevant) {
|
|
288
|
+
return 'Set API key: thinkrun config set-key <key>';
|
|
289
|
+
}
|
|
290
|
+
else if (!apiHealthOk && localEndpoint) {
|
|
291
|
+
return `Local endpoint is down. Start mech-browser-service or verify apiUrl/port.`;
|
|
292
|
+
}
|
|
293
|
+
else if (!apiHealthOk) {
|
|
294
|
+
return 'Cloud endpoint unreachable. Check network and apiUrl.';
|
|
295
|
+
}
|
|
296
|
+
else if (!activeCloudSessionId && !activeSessionId) {
|
|
297
|
+
if (localEndpoint) {
|
|
298
|
+
return 'No active session. Run: thinkrun cloud start\nIf server already has sessions: thinkrun cloud list -> thinkrun cloud use <sessionId>';
|
|
299
|
+
}
|
|
300
|
+
return 'No active session. Run: thinkrun cloud start';
|
|
301
|
+
}
|
|
302
|
+
else if (commandPathOk === false) {
|
|
303
|
+
return 'Command path is not ready. Run: thinkrun session debug for full routing details.';
|
|
304
|
+
}
|
|
305
|
+
else if (bridgeRelevant && activeLocalTabId) {
|
|
306
|
+
if (recoveryState === 'flapping') {
|
|
307
|
+
return 'Local bridge is flapping from repeated disconnects. Avoid patient retry loops; reload the ThinkRun extension or stabilize the page/browser before retrying.';
|
|
308
|
+
}
|
|
309
|
+
if (recoveryState === 'recently_recovered' || recoveryState === 'recovering') {
|
|
310
|
+
return 'Local bridge recently recovered from disconnect churn. If the next command still fails, retry once or run thinkrun session debug for live routing state.';
|
|
311
|
+
}
|
|
312
|
+
if ((recentDisconnectCount ?? 0) > 0) {
|
|
313
|
+
return 'Setup is usable, but the local bridge has been unstable recently. Try a safe read like: thinkrun snapshot';
|
|
314
|
+
}
|
|
315
|
+
return 'Setup looks good. Try: thinkrun navigate https://example.com';
|
|
316
|
+
}
|
|
317
|
+
else {
|
|
318
|
+
return 'Setup looks good. Try: thinkrun navigate https://example.com';
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
/** Check whether `bun` is available on PATH. */
|
|
322
|
+
export function isBunAvailable() {
|
|
323
|
+
try {
|
|
324
|
+
execSync('bun --version', { stdio: 'ignore', timeout: 500 });
|
|
325
|
+
return true;
|
|
326
|
+
}
|
|
327
|
+
catch {
|
|
328
|
+
return false;
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
/**
|
|
332
|
+
* Inspect daemon state for the doctor section.
|
|
333
|
+
* Only shown when localTransport=daemon OR daemon.json exists.
|
|
334
|
+
*/
|
|
335
|
+
export async function checkDaemonDoctor() {
|
|
336
|
+
const localTransport = config.get('localTransport') ?? 'native';
|
|
337
|
+
const daemonJsonPath = getDaemonJsonPath();
|
|
338
|
+
const daemonJsonExists = existsSync(daemonJsonPath);
|
|
339
|
+
const show = localTransport === 'daemon' || daemonJsonExists;
|
|
340
|
+
if (!show) {
|
|
341
|
+
return { show: false, status: 'down', bunMissing: false, flagOwnerMismatch: false };
|
|
342
|
+
}
|
|
343
|
+
const bunMissing = !isBunAvailable();
|
|
344
|
+
// Read raw config for owner + pid checks
|
|
345
|
+
let rawCfg = null;
|
|
346
|
+
if (daemonJsonExists) {
|
|
347
|
+
try {
|
|
348
|
+
rawCfg = JSON.parse(readFileSync(daemonJsonPath, 'utf-8'));
|
|
349
|
+
}
|
|
350
|
+
catch {
|
|
351
|
+
// unreadable
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
// Flag/owner mismatch: daemon.json says owner='native' but flag=daemon (or absent but flag=daemon)
|
|
355
|
+
const owner = rawCfg ? rawCfg.owner : undefined;
|
|
356
|
+
const flagOwnerMismatch = (localTransport === 'daemon' && owner === 'native') ||
|
|
357
|
+
(localTransport === 'native' && owner === 'daemon' && daemonJsonExists);
|
|
358
|
+
if (!daemonJsonExists || !rawCfg) {
|
|
359
|
+
return { show, status: 'down', bunMissing, flagOwnerMismatch };
|
|
360
|
+
}
|
|
361
|
+
const port = typeof rawCfg.port === 'number' ? rawCfg.port : undefined;
|
|
362
|
+
const pid = typeof rawCfg.pid === 'number' ? rawCfg.pid : undefined;
|
|
363
|
+
const protocolVersion = typeof rawCfg.protocolVersion === 'number' ? rawCfg.protocolVersion : undefined;
|
|
364
|
+
const binaryVersion = typeof rawCfg.binaryVersion === 'string' ? rawCfg.binaryVersion : undefined;
|
|
365
|
+
if (!port) {
|
|
366
|
+
return { show, status: 'down', bunMissing, flagOwnerMismatch };
|
|
367
|
+
}
|
|
368
|
+
// Stale-file detection: pid is recorded but the process is dead
|
|
369
|
+
if (typeof pid === 'number' && !isPidAlive(pid)) {
|
|
370
|
+
return { show, status: 'orphaned', port, pid, protocolVersion, binaryVersion, bunMissing, flagOwnerMismatch };
|
|
371
|
+
}
|
|
372
|
+
// Protocol version mismatch
|
|
373
|
+
if (typeof protocolVersion === 'number' && protocolVersion !== DAEMON_PROTOCOL_VERSION) {
|
|
374
|
+
return { show, status: 'mismatch', port, pid, protocolVersion, binaryVersion, bunMissing, flagOwnerMismatch };
|
|
375
|
+
}
|
|
376
|
+
// Health probe
|
|
377
|
+
const alive = await probeDaemonHealth(port);
|
|
378
|
+
if (!alive) {
|
|
379
|
+
const status = typeof pid === 'number' ? 'orphaned' : 'down';
|
|
380
|
+
return { show, status, port, pid, protocolVersion, binaryVersion, bunMissing, flagOwnerMismatch };
|
|
381
|
+
}
|
|
382
|
+
return { show, status: 'up', port, pid, protocolVersion, binaryVersion, bunMissing, flagOwnerMismatch };
|
|
383
|
+
}
|
|
384
|
+
export function getDoctorSessionDisplay(activeSessionId, sessionContext) {
|
|
385
|
+
try {
|
|
386
|
+
const context = sessionContext ?? getSessionContext();
|
|
387
|
+
if (context?.mode === 'local') {
|
|
388
|
+
return {
|
|
389
|
+
activeLocalTab: context.tabId,
|
|
390
|
+
activeCloudSession: '(none)',
|
|
391
|
+
activeSessionSummary: `local tab ${context.tabId}`,
|
|
392
|
+
};
|
|
393
|
+
}
|
|
394
|
+
if (context?.mode === 'cloud') {
|
|
395
|
+
return {
|
|
396
|
+
activeLocalTab: '(none)',
|
|
397
|
+
activeCloudSession: context.sessionId,
|
|
398
|
+
activeSessionSummary: `cloud session ${context.sessionId}`,
|
|
399
|
+
};
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
catch {
|
|
403
|
+
// Fall back to the raw activeSessionId display when the stored context is mixed or invalid.
|
|
404
|
+
}
|
|
405
|
+
return {
|
|
406
|
+
activeLocalTab: '(none)',
|
|
407
|
+
activeCloudSession: '(none)',
|
|
408
|
+
activeSessionSummary: activeSessionId || '(none)',
|
|
409
|
+
};
|
|
410
|
+
}
|
|
411
|
+
export function createDoctorCommand() {
|
|
412
|
+
return new Command('doctor')
|
|
413
|
+
.description('Diagnose endpoint, auth, and session setup')
|
|
414
|
+
.option('--full', 'Run full checks including remote checksum verification')
|
|
415
|
+
.option('--offline', 'Skip network checks (API health / key acceptance ping)')
|
|
416
|
+
.action(async (opts) => {
|
|
417
|
+
const apiUrl = config.get('apiUrl') || 'https://api.thinkbrowse.io';
|
|
418
|
+
const apiKey = config.get('apiKey');
|
|
419
|
+
const activeSessionId = config.get('activeSessionId');
|
|
420
|
+
const localEndpoint = isLocalApiUrl(apiUrl);
|
|
421
|
+
const bridgePort = readBridgePort();
|
|
422
|
+
const bridgeUrl = `http://127.0.0.1:${bridgePort}`;
|
|
423
|
+
const portFileMtime = getBridgePortFileMtimeMs();
|
|
424
|
+
const stalePortFileMs = 5 * 60 * 1000;
|
|
425
|
+
const nowMs = Date.now();
|
|
426
|
+
let sessionContext;
|
|
427
|
+
try {
|
|
428
|
+
sessionContext = getSessionContext();
|
|
429
|
+
}
|
|
430
|
+
catch {
|
|
431
|
+
sessionContext = undefined;
|
|
432
|
+
}
|
|
433
|
+
const sessionDisplay = getDoctorSessionDisplay(activeSessionId, sessionContext);
|
|
434
|
+
const registeredExtensionId = config.get('registeredExtensionId');
|
|
435
|
+
const preferPackagedDefaultExtensionId = config.get('preferPackagedDefaultExtensionId') === true;
|
|
436
|
+
const [apiHealth, bridgeHealth, resolution, nativeHost, daemonDoctor] = await Promise.all([
|
|
437
|
+
opts.offline
|
|
438
|
+
? Promise.resolve({ ok: true, message: 'skipped (--offline)' })
|
|
439
|
+
: checkHealth(apiUrl, apiKey),
|
|
440
|
+
checkHealth(bridgeUrl),
|
|
441
|
+
getResolutionInfo(),
|
|
442
|
+
checkNativeHost({ verifyChecksum: opts.full ?? false }),
|
|
443
|
+
checkDaemonDoctor(),
|
|
444
|
+
]);
|
|
445
|
+
const doctorExtensionId = resolveDoctorExtensionId({
|
|
446
|
+
resolvedExtensionId: resolveExtensionId(),
|
|
447
|
+
registeredExtensionId,
|
|
448
|
+
preferPackagedDefaultExtensionId,
|
|
449
|
+
manifestExtensionIds: nativeHost.manifestExtensionIds,
|
|
450
|
+
manifestParseStatus: nativeHost.manifestParseStatus,
|
|
451
|
+
manifestExists: nativeHost.manifestExists,
|
|
452
|
+
});
|
|
453
|
+
const effectiveExtensionId = doctorExtensionId.extensionId;
|
|
454
|
+
const extensionIdSource = doctorExtensionId.source;
|
|
455
|
+
const manifestIssue = nativeHost.binaryExists
|
|
456
|
+
? getManifestRegistrationIssue({
|
|
457
|
+
manifestEntries: nativeHost.manifestEntries,
|
|
458
|
+
manifestExtensionIds: nativeHost.manifestExtensionIds,
|
|
459
|
+
registeredExtensionId,
|
|
460
|
+
effectiveExtensionId,
|
|
461
|
+
})
|
|
462
|
+
: null;
|
|
463
|
+
const manifestMismatch = nativeHost.binaryExists ? manifestIssue !== null : false;
|
|
464
|
+
const commandPathOk = resolution.resolvedMode !== null;
|
|
465
|
+
console.log(chalk.bold('ThinkRun Doctor'));
|
|
466
|
+
console.log('');
|
|
467
|
+
console.log(' API URL: ', chalk.cyan(apiUrl), localEndpoint ? chalk.gray('(local)') : chalk.gray('(cloud)'));
|
|
468
|
+
console.log(' API Key: ', chalk.cyan(config.maskApiKey(apiKey)));
|
|
469
|
+
console.log(' Active Local Tab:', chalk.cyan(sessionDisplay.activeLocalTab));
|
|
470
|
+
console.log(' Cloud Session: ', chalk.cyan(sessionDisplay.activeCloudSession));
|
|
471
|
+
console.log(' Session Target: ', chalk.cyan(sessionDisplay.activeSessionSummary));
|
|
472
|
+
console.log(' Bridge Port: ', chalk.cyan(String(bridgePort)));
|
|
473
|
+
console.log(' Local Transport:', chalk.cyan(config.get('localTransport') ?? 'native'), chalk.gray('(native=default)'));
|
|
474
|
+
const distinctManifestExtensionIds = Array.isArray(nativeHost.manifestExtensionIds)
|
|
475
|
+
? Array.from(new Set(nativeHost.manifestExtensionIds))
|
|
476
|
+
: [];
|
|
477
|
+
const registeredSourceFromReadableManifest = extensionIdSource === 'registered' &&
|
|
478
|
+
nativeHost.manifestParseStatus === 'parsed' &&
|
|
479
|
+
distinctManifestExtensionIds.length === 1 &&
|
|
480
|
+
distinctManifestExtensionIds[0] === effectiveExtensionId;
|
|
481
|
+
const extensionIdSourceLabel = extensionIdSource === 'config'
|
|
482
|
+
? '(configured)'
|
|
483
|
+
: extensionIdSource === 'registered'
|
|
484
|
+
? registeredSourceFromReadableManifest
|
|
485
|
+
? '(registered manifest ID)'
|
|
486
|
+
: '(cached registered ID)'
|
|
487
|
+
: extensionIdSource === 'env'
|
|
488
|
+
? '(env override)'
|
|
489
|
+
: extensionIdSource === 'flag'
|
|
490
|
+
? '(command override)'
|
|
491
|
+
: '(default packaged ID)';
|
|
492
|
+
console.log(' Extension ID: ', chalk.cyan(effectiveExtensionId), chalk.gray(extensionIdSourceLabel));
|
|
493
|
+
if (Array.isArray(nativeHost.manifestExtensionIds) && nativeHost.manifestExtensionIds.length > 0) {
|
|
494
|
+
console.log(' Manifest IDs: ', chalk.cyan(nativeHost.manifestExtensionIds.join(', ')));
|
|
495
|
+
}
|
|
496
|
+
if (isValidExtensionId(registeredExtensionId) && registeredExtensionId !== effectiveExtensionId) {
|
|
497
|
+
console.log(' Registered ID: ', chalk.cyan(registeredExtensionId), chalk.gray('(last successful local registration)'));
|
|
498
|
+
}
|
|
499
|
+
console.log('');
|
|
500
|
+
// --- API key health (PRD 0086 B4) ---
|
|
501
|
+
console.log('API Key:');
|
|
502
|
+
const keyHealth = config.getKeyHealth();
|
|
503
|
+
if (keyHealth.state === 'missing') {
|
|
504
|
+
console.log(` ${chalk.red('✗')} Key: not configured`);
|
|
505
|
+
console.log(` Run: ${chalk.cyan('thinkrun config set-key <your-api-key>')}`);
|
|
506
|
+
}
|
|
507
|
+
else if (keyHealth.state === 'malformed') {
|
|
508
|
+
console.log(` ${chalk.red('✗')} Key: quarantined malformed value (${keyHealth.quarantinedMasked})`);
|
|
509
|
+
console.log(` The stored key failed validation — it may have been overwritten (e.g. browser autofill).`);
|
|
510
|
+
console.log(` Run: ${chalk.cyan('thinkrun config set-key <your-api-key>')}`);
|
|
511
|
+
}
|
|
512
|
+
else {
|
|
513
|
+
const shapeNote = keyHealth.shapeValid === false
|
|
514
|
+
? ` ${chalk.yellow('(unrecognized shape — saved with --force?)')}`
|
|
515
|
+
: '';
|
|
516
|
+
console.log(` ${chalk.green('✓')} Key: ${keyHealth.maskedKey}${shapeNote}`);
|
|
517
|
+
if (opts.offline) {
|
|
518
|
+
console.log(` ${chalk.gray('-')} Server acceptance: skipped (--offline)`);
|
|
519
|
+
}
|
|
520
|
+
else {
|
|
521
|
+
// checkHealth already performed the authenticated ping with this key.
|
|
522
|
+
console.log(` ${apiHealth.ok ? chalk.green('✓') : chalk.red('✗')} Server acceptance: ${apiHealth.ok ? 'accepted' : `rejected — ${apiHealth.message}`}`);
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
console.log('');
|
|
526
|
+
console.log('Checks:');
|
|
527
|
+
console.log(` ${apiHealth.ok ? chalk.green('✓') : chalk.red('✗')} API health: ${apiHealth.message}`);
|
|
528
|
+
const bridgeNote = localEndpoint ? '' : chalk.gray(' (local only)');
|
|
529
|
+
console.log(` ${bridgeHealth.ok ? chalk.green('✓') : chalk.red('✗')} Bridge health: ${bridgeHealth.message}${bridgeNote}`);
|
|
530
|
+
const bridgeRelevant = resolution.contextMode === 'local' ||
|
|
531
|
+
resolution.resolvedMode === 'local';
|
|
532
|
+
// Only show extension recovery hints when the CLI is actually trying to use
|
|
533
|
+
// local browser context. In cloud mode, the bridge row is labelled "(local only)"
|
|
534
|
+
// and extension guidance would distract from the real diagnosis.
|
|
535
|
+
if (bridgeRelevant) {
|
|
536
|
+
const extensionHint = getExtensionDisconnectHint(bridgeHealth);
|
|
537
|
+
if (extensionHint !== null) {
|
|
538
|
+
console.log(` ${chalk.yellow('→')} ${extensionHint}`);
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
console.log(` ${commandPathOk ? chalk.green('✓') : chalk.red('✗')} Command path: ${commandPathOk ? `routes to ${resolution.resolvedMode}` : resolution.error ?? 'would fail'}`);
|
|
542
|
+
const stalePortAgeMs = getStaleBridgePortFileAgeMs(portFileMtime, { nowMs, maxAgeMs: stalePortFileMs });
|
|
543
|
+
if (stalePortAgeMs !== null) {
|
|
544
|
+
const ageMin = Math.max(1, Math.round(stalePortAgeMs / 60_000));
|
|
545
|
+
console.log('');
|
|
546
|
+
console.log(chalk.yellow('Note:'), `~/.thinkbrowse/port was last written about ${ageMin} minute(s) ago. After a native-host restart or port rotation, reload the ThinkRun extension (or re-open the browser) so the file matches the live bridge, then run doctor again.`);
|
|
547
|
+
}
|
|
548
|
+
console.log('');
|
|
549
|
+
console.log('Suggested next step:');
|
|
550
|
+
const suggestion = getSuggestedNextStep({
|
|
551
|
+
apiKey,
|
|
552
|
+
localEndpoint,
|
|
553
|
+
apiHealthOk: apiHealth.ok,
|
|
554
|
+
activeLocalTabId: sessionContext?.mode === 'local' ? sessionContext.tabId : undefined,
|
|
555
|
+
activeCloudSessionId: sessionContext?.mode === 'cloud' ? sessionContext.sessionId : undefined,
|
|
556
|
+
activeSessionId,
|
|
557
|
+
bridgeRelevant,
|
|
558
|
+
bridgeHealthOk: bridgeHealth.ok,
|
|
559
|
+
commandPathOk,
|
|
560
|
+
extensionConnected: bridgeHealth.extensionConnected,
|
|
561
|
+
isCircuitBreakerTrip: bridgeHealth.isCircuitBreakerTrip,
|
|
562
|
+
recoveryState: bridgeHealth.recoveryState,
|
|
563
|
+
recentDisconnectCount: bridgeHealth.recentDisconnectCount,
|
|
564
|
+
configuredExtensionId: effectiveExtensionId,
|
|
565
|
+
extensionIdSource,
|
|
566
|
+
manifestMismatch,
|
|
567
|
+
manifestIssue,
|
|
568
|
+
manifestParseStatus: nativeHost.manifestParseStatus,
|
|
569
|
+
});
|
|
570
|
+
for (const line of suggestion.split('\n')) {
|
|
571
|
+
console.log(` - ${line}`);
|
|
572
|
+
}
|
|
573
|
+
if (!commandPathOk && resolution.error) {
|
|
574
|
+
console.log('');
|
|
575
|
+
console.log(chalk.yellow(' Run') + ' thinkrun session debug' + chalk.yellow(' for full routing details.'));
|
|
576
|
+
}
|
|
577
|
+
// --- Native Host section ---
|
|
578
|
+
console.log('');
|
|
579
|
+
console.log('Native Host:');
|
|
580
|
+
console.log(` Platform: ${nativeHost.platform}`);
|
|
581
|
+
if (!nativeHost.binaryExists) {
|
|
582
|
+
console.log(` ${chalk.red('✗')} Binary: not found`);
|
|
583
|
+
console.log(` Run: ${chalk.cyan('thinkrun setup')}`);
|
|
584
|
+
console.log(` (If you installed via ${chalk.cyan('bun install -g')}, Bun blocks postinstall scripts — ${chalk.cyan('thinkrun setup')} installs the binary manually)`);
|
|
585
|
+
}
|
|
586
|
+
else {
|
|
587
|
+
const versionLabel = nativeHost.installedVersion
|
|
588
|
+
? nativeHost.versionMatch
|
|
589
|
+
? `v${nativeHost.installedVersion}`
|
|
590
|
+
: `v${nativeHost.installedVersion} ${chalk.yellow(`(outdated — expected v${nativeHost.expectedVersion})`)}`
|
|
591
|
+
: 'unknown version';
|
|
592
|
+
console.log(` ${nativeHost.versionMatch ? chalk.green('✓') : chalk.yellow('!')} Binary: ${nativeHost.binaryPath} (${versionLabel})`);
|
|
593
|
+
if (nativeHost.checksumVerified === true) {
|
|
594
|
+
console.log(` ${chalk.green('✓')} Checksum: verified`);
|
|
595
|
+
}
|
|
596
|
+
else if (nativeHost.checksumVerified === false) {
|
|
597
|
+
console.log(` ${chalk.red('✗')} Checksum: mismatch`);
|
|
598
|
+
}
|
|
599
|
+
else {
|
|
600
|
+
const checksumNote = opts.full ? 'could not verify (offline?)' : 'not checked (run with --full to verify)';
|
|
601
|
+
console.log(` ${chalk.gray('-')} Checksum: ${checksumNote}`);
|
|
602
|
+
}
|
|
603
|
+
if (process.platform === 'win32') {
|
|
604
|
+
console.log(` ${chalk.gray('-')} Manifest: N/A (Windows — registry-based)`);
|
|
605
|
+
}
|
|
606
|
+
else {
|
|
607
|
+
if (manifestIssue === 'parse_error' || nativeHost.manifestParseStatus === 'parse_error') {
|
|
608
|
+
console.log(` ${chalk.red('✗')} Manifest: unreadable (${nativeHost.manifestParseError ?? 'could not read native messaging manifest'})`);
|
|
609
|
+
console.log(` Run: ${chalk.cyan('thinkrun setup')}`);
|
|
610
|
+
}
|
|
611
|
+
else if (manifestIssue === 'missing') {
|
|
612
|
+
console.log(` ${chalk.red('✗')} Manifest: missing for at least one detected Chromium-based browser`);
|
|
613
|
+
console.log(` Run: ${chalk.cyan('thinkrun setup')}`);
|
|
614
|
+
}
|
|
615
|
+
else if (manifestIssue === 'inconsistent') {
|
|
616
|
+
console.log(` ${chalk.yellow('!')} Manifest: detected browser registrations are inconsistent for extension ID ${effectiveExtensionId}`);
|
|
617
|
+
console.log(` Run: ${chalk.cyan('thinkrun setup')}`);
|
|
618
|
+
}
|
|
619
|
+
else if (manifestIssue === 'wrong_id') {
|
|
620
|
+
console.log(` ${chalk.yellow('!')} Manifest: detected browser registrations disagree with extension ID ${effectiveExtensionId}`);
|
|
621
|
+
console.log(` Run: ${chalk.cyan('thinkrun setup')}`);
|
|
622
|
+
}
|
|
623
|
+
else {
|
|
624
|
+
console.log(` ${nativeHost.manifestExists ? chalk.green('✓') : chalk.red('✗')} Manifest: ${nativeHost.manifestExists ? 'registered' : `not found at ${nativeHost.manifestPath}`}`);
|
|
625
|
+
}
|
|
626
|
+
if (nativeHost.manifestExtensionIds && nativeHost.manifestExtensionIds.length > 0) {
|
|
627
|
+
console.log(` ${chalk.gray('-')} Allowed IDs: ${nativeHost.manifestExtensionIds.join(', ')}`);
|
|
628
|
+
if (manifestIssue === 'wrong_id' || manifestIssue === 'inconsistent') {
|
|
629
|
+
console.log(` ${chalk.yellow('!')} Manifest/CLI mismatch: doctor expects ${effectiveExtensionId}. Re-run ${chalk.cyan('thinkrun setup')} after setting the correct extension ID.`);
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
if (!nativeHost.versionMatch) {
|
|
634
|
+
console.log(` Run: ${chalk.cyan('thinkrun setup --force')}`);
|
|
635
|
+
}
|
|
636
|
+
if (extensionIdSource === 'default') {
|
|
637
|
+
console.log(` Using packaged extension ID fallback. For unpacked/dev installs, run: ${chalk.cyan('thinkrun config set-extension-id <id>')}`);
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
// --- Daemon section (PRD 0086 task 4.7) ---
|
|
641
|
+
// Show when localTransport=daemon or daemon.json exists on disk.
|
|
642
|
+
if (daemonDoctor.show) {
|
|
643
|
+
console.log('');
|
|
644
|
+
console.log('Daemon:');
|
|
645
|
+
const localTransport = config.get('localTransport') ?? 'native';
|
|
646
|
+
console.log(` Transport flag: ${chalk.cyan(localTransport)}`);
|
|
647
|
+
if (daemonDoctor.flagOwnerMismatch) {
|
|
648
|
+
console.log(` ${chalk.yellow('!')} Flag/owner mismatch: localTransport=${localTransport} but daemon.json owner differs.`);
|
|
649
|
+
console.log(` Set consistently: ${chalk.cyan('thinkrun config set localTransport native')} (or daemon)`);
|
|
650
|
+
}
|
|
651
|
+
switch (daemonDoctor.status) {
|
|
652
|
+
case 'up':
|
|
653
|
+
console.log(` ${chalk.green('✓')} Status: up (port=${daemonDoctor.port}, pid=${daemonDoctor.pid ?? 'unknown'})`);
|
|
654
|
+
if (daemonDoctor.protocolVersion !== undefined) {
|
|
655
|
+
const vMatch = daemonDoctor.protocolVersion === DAEMON_PROTOCOL_VERSION;
|
|
656
|
+
console.log(` ${vMatch ? chalk.green('✓') : chalk.yellow('!')} Protocol version: ${daemonDoctor.protocolVersion}${vMatch ? '' : ` ${chalk.yellow(`(expected ${DAEMON_PROTOCOL_VERSION})`)}`}`);
|
|
657
|
+
}
|
|
658
|
+
if (daemonDoctor.binaryVersion) {
|
|
659
|
+
console.log(` ${chalk.gray('-')} Daemon version: ${daemonDoctor.binaryVersion}`);
|
|
660
|
+
}
|
|
661
|
+
break;
|
|
662
|
+
case 'down':
|
|
663
|
+
console.log(` ${chalk.red('✗')} Status: down (daemon.json absent or unreadable)`);
|
|
664
|
+
if (localTransport === 'daemon') {
|
|
665
|
+
console.log(` To start: set THINKRUN_DAEMON_SPIKE=1 (THINKBROWSE_DAEMON_SPIKE also works), then run a local command`);
|
|
666
|
+
}
|
|
667
|
+
break;
|
|
668
|
+
case 'orphaned':
|
|
669
|
+
console.log(` ${chalk.yellow('!')} Status: orphaned (port=${daemonDoctor.port ?? '?'}, pid=${daemonDoctor.pid ?? '?'} — process dead)`);
|
|
670
|
+
console.log(` daemon.json is stale. It will be cleaned up on next daemon start.`);
|
|
671
|
+
break;
|
|
672
|
+
case 'mismatch':
|
|
673
|
+
console.log(` ${chalk.yellow('!')} Status: protocol version mismatch (daemon=${daemonDoctor.protocolVersion ?? '?'}, expected=${DAEMON_PROTOCOL_VERSION})`);
|
|
674
|
+
console.log(` Update the thinkrun CLI package to resolve the mismatch.`);
|
|
675
|
+
break;
|
|
676
|
+
}
|
|
677
|
+
if (daemonDoctor.bunMissing) {
|
|
678
|
+
console.log(` ${chalk.yellow('!')} bun not found on PATH — daemon requires bun (packaging TODO: task 4.2)`);
|
|
679
|
+
console.log(` Install bun: https://bun.sh`);
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
});
|
|
683
|
+
}
|
|
684
|
+
//# sourceMappingURL=doctor.js.map
|