tabctl 0.5.3 → 0.6.0-alpha.10

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.
Files changed (51) hide show
  1. package/README.md +135 -35
  2. package/dist/extension/background.js +179 -3155
  3. package/dist/extension/lib/content.js +0 -115
  4. package/dist/extension/lib/screenshot.js +0 -93
  5. package/dist/extension/manifest.json +2 -2
  6. package/package.json +13 -5
  7. package/dist/cli/lib/args.js +0 -141
  8. package/dist/cli/lib/client.js +0 -83
  9. package/dist/cli/lib/commands/doctor.js +0 -134
  10. package/dist/cli/lib/commands/index.js +0 -51
  11. package/dist/cli/lib/commands/list.js +0 -159
  12. package/dist/cli/lib/commands/meta.js +0 -229
  13. package/dist/cli/lib/commands/params-groups.js +0 -48
  14. package/dist/cli/lib/commands/params-move.js +0 -44
  15. package/dist/cli/lib/commands/params.js +0 -314
  16. package/dist/cli/lib/commands/profile.js +0 -91
  17. package/dist/cli/lib/commands/setup.js +0 -294
  18. package/dist/cli/lib/constants.js +0 -30
  19. package/dist/cli/lib/help.js +0 -205
  20. package/dist/cli/lib/options-commands.js +0 -274
  21. package/dist/cli/lib/options-groups.js +0 -41
  22. package/dist/cli/lib/options.js +0 -125
  23. package/dist/cli/lib/output.js +0 -147
  24. package/dist/cli/lib/pagination.js +0 -55
  25. package/dist/cli/lib/policy-filter.js +0 -202
  26. package/dist/cli/lib/policy.js +0 -91
  27. package/dist/cli/lib/report.js +0 -61
  28. package/dist/cli/lib/response.js +0 -235
  29. package/dist/cli/lib/scope.js +0 -250
  30. package/dist/cli/lib/snapshot.js +0 -216
  31. package/dist/cli/lib/types.js +0 -2
  32. package/dist/cli/tabctl.js +0 -475
  33. package/dist/extension/lib/archive.js +0 -444
  34. package/dist/extension/lib/deps.js +0 -4
  35. package/dist/extension/lib/groups.js +0 -529
  36. package/dist/extension/lib/inspect.js +0 -252
  37. package/dist/extension/lib/move.js +0 -342
  38. package/dist/extension/lib/tabs.js +0 -456
  39. package/dist/extension/lib/undo-handlers.js +0 -447
  40. package/dist/host/host.bundle.js +0 -670
  41. package/dist/host/host.js +0 -143
  42. package/dist/host/host.sh +0 -5
  43. package/dist/host/launcher/go.mod +0 -3
  44. package/dist/host/launcher/main.go +0 -109
  45. package/dist/host/lib/handlers.js +0 -327
  46. package/dist/host/lib/undo.js +0 -60
  47. package/dist/shared/config.js +0 -134
  48. package/dist/shared/extension-sync.js +0 -170
  49. package/dist/shared/profiles.js +0 -78
  50. package/dist/shared/version.js +0 -8
  51. package/dist/shared/wrapper-health.js +0 -132
@@ -1,21 +1,10 @@
1
1
  "use strict";
2
2
  // Content script execution utilities — extracted from background.ts (pure structural refactor).
3
3
  Object.defineProperty(exports, "__esModule", { value: true });
4
- exports.SETTLE_POLL_INTERVAL_MS = exports.SETTLE_STABILITY_MS = void 0;
5
- exports.isScriptableUrl = isScriptableUrl;
6
4
  exports.delay = delay;
7
5
  exports.executeWithTimeout = executeWithTimeout;
8
6
  exports.extractPageMeta = extractPageMeta;
9
7
  exports.extractSelectorSignal = extractSelectorSignal;
10
- exports.waitForTabLoad = waitForTabLoad;
11
- exports.waitForDomReady = waitForDomReady;
12
- exports.waitForSettle = waitForSettle;
13
- exports.waitForTabReady = waitForTabReady;
14
- exports.SETTLE_STABILITY_MS = 500;
15
- exports.SETTLE_POLL_INTERVAL_MS = 50;
16
- function isScriptableUrl(url) {
17
- return typeof url === "string" && (url.startsWith("http://") || url.startsWith("https://"));
18
- }
19
8
  function delay(ms) {
20
9
  return new Promise((resolve) => setTimeout(resolve, ms));
21
10
  }
@@ -184,107 +173,3 @@ async function extractSelectorSignal(tabId, specs, timeoutMs, selectorValueMaxLe
184
173
  }
185
174
  return result;
186
175
  }
187
- function waitForTabLoad(tabId, timeoutMs) {
188
- return new Promise((resolve) => {
189
- let settled = false;
190
- const done = () => {
191
- if (settled) {
192
- return;
193
- }
194
- settled = true;
195
- chrome.tabs.onUpdated.removeListener(onUpdated);
196
- resolve();
197
- };
198
- const onUpdated = (updatedTabId, info) => {
199
- if (updatedTabId === tabId && info.status === "complete") {
200
- done();
201
- }
202
- };
203
- chrome.tabs.onUpdated.addListener(onUpdated);
204
- chrome.tabs.get(tabId).then((tab) => {
205
- if (tab.status === "complete") {
206
- done();
207
- }
208
- }).catch(() => {
209
- done();
210
- });
211
- setTimeout(done, timeoutMs);
212
- });
213
- }
214
- async function waitForDomReady(tabId, timeoutMs) {
215
- const result = await executeWithTimeout(tabId, timeoutMs, () => {
216
- if (document.readyState === "interactive" || document.readyState === "complete") {
217
- return true;
218
- }
219
- return new Promise((resolve) => {
220
- const onReady = () => {
221
- document.removeEventListener("DOMContentLoaded", onReady);
222
- resolve(true);
223
- };
224
- document.addEventListener("DOMContentLoaded", onReady, { once: true });
225
- setTimeout(() => {
226
- document.removeEventListener("DOMContentLoaded", onReady);
227
- resolve(false);
228
- }, Math.max(0, timeoutMs - 50));
229
- });
230
- });
231
- if (result === null) {
232
- await delay(Math.min(200, Math.max(50, Math.floor(timeoutMs / 10))));
233
- }
234
- }
235
- async function waitForSettle(tabId, timeoutMs) {
236
- const startTime = Date.now();
237
- let lastUrl = "";
238
- let lastTitle = "";
239
- let stableStart = Date.now();
240
- while (Date.now() - startTime < timeoutMs) {
241
- const tab = await chrome.tabs.get(tabId).catch(() => null);
242
- if (!tab)
243
- return;
244
- const currentUrl = tab.url || "";
245
- const currentTitle = tab.title || "";
246
- // Reset stability timer if URL or title changed
247
- if (currentUrl !== lastUrl || currentTitle !== lastTitle) {
248
- lastUrl = currentUrl;
249
- lastTitle = currentTitle;
250
- stableStart = Date.now();
251
- }
252
- else if (isScriptableUrl(currentUrl) &&
253
- tab.status === "complete" &&
254
- Date.now() - stableStart >= exports.SETTLE_STABILITY_MS) {
255
- // Page is loaded, URL is valid, and stable for long enough
256
- return;
257
- }
258
- await delay(exports.SETTLE_POLL_INTERVAL_MS);
259
- }
260
- // Timeout reached, continue anyway
261
- }
262
- async function waitForTabReady(tabId, params, fallbackTimeoutMs) {
263
- const waitFor = typeof params.waitFor === "string" ? params.waitFor.trim().toLowerCase() : "";
264
- if (!waitFor || waitFor === "none") {
265
- return;
266
- }
267
- const timeoutRaw = Number(params.waitTimeoutMs);
268
- const timeoutMs = Number.isFinite(timeoutRaw) && timeoutRaw > 0 ? Math.floor(timeoutRaw) : fallbackTimeoutMs;
269
- // settle mode handles its own URL checking, so skip the early return
270
- if (waitFor === "settle") {
271
- await waitForSettle(tabId, timeoutMs);
272
- return;
273
- }
274
- try {
275
- const tab = await chrome.tabs.get(tabId);
276
- if (!isScriptableUrl(tab.url)) {
277
- return;
278
- }
279
- }
280
- catch {
281
- return;
282
- }
283
- if (waitFor === "load") {
284
- await waitForTabLoad(tabId, timeoutMs);
285
- return;
286
- }
287
- if (waitFor === "dom") {
288
- await waitForDomReady(tabId, timeoutMs);
289
- }
290
- }
@@ -13,7 +13,6 @@ exports.captureVisible = captureVisible;
13
13
  exports.getPageMetrics = getPageMetrics;
14
14
  exports.scrollToPosition = scrollToPosition;
15
15
  exports.captureTabTiles = captureTabTiles;
16
- exports.screenshotTabs = screenshotTabs;
17
16
  exports.SCREENSHOT_TILE_MAX_DIM = 2000;
18
17
  exports.SCREENSHOT_MAX_BYTES = 2_000_000;
19
18
  exports.SCREENSHOT_QUALITY = 80;
@@ -273,95 +272,3 @@ async function captureTabTiles(tab, options, deps) {
273
272
  await scrollToPosition(tabId, exports.SCREENSHOT_PROCESS_TIMEOUT_MS, startScrollX, startScrollY, deps);
274
273
  return tiles;
275
274
  }
276
- async function screenshotTabs(params, requestId, deps) {
277
- const snapshot = await deps.getTabSnapshot();
278
- const selection = deps.selectTabsByScope(snapshot, params);
279
- if (selection.error) {
280
- throw selection.error;
281
- }
282
- const mode = params.mode === "full" ? "full" : "viewport";
283
- const format = params.format === "jpeg" ? "jpeg" : "png";
284
- const qualityRaw = Number(params.quality);
285
- const quality = Number.isFinite(qualityRaw) ? Math.min(100, Math.max(0, Math.floor(qualityRaw))) : exports.SCREENSHOT_QUALITY;
286
- const tileMaxDimRaw = Number(params.tileMaxDim);
287
- const tileMaxDim = Number.isFinite(tileMaxDimRaw) && tileMaxDimRaw > 0 ? Math.floor(tileMaxDimRaw) : exports.SCREENSHOT_TILE_MAX_DIM;
288
- const adjustedTileMaxDim = tileMaxDim < 50 ? 50 : tileMaxDim;
289
- const maxBytesRaw = Number(params.maxBytes);
290
- const maxBytes = Number.isFinite(maxBytesRaw) && maxBytesRaw > 0 ? Math.floor(maxBytesRaw) : exports.SCREENSHOT_MAX_BYTES;
291
- const adjustedMaxBytes = maxBytes < 50_000 ? 50_000 : maxBytes;
292
- const progressEnabled = params.progress === true;
293
- const tabs = selection.tabs;
294
- const entries = [];
295
- let totalTiles = 0;
296
- const startedAt = Date.now();
297
- for (let index = 0; index < tabs.length; index += 1) {
298
- const tab = tabs[index];
299
- const tabId = tab.tabId;
300
- const url = tab.url;
301
- if (!deps.isScriptableUrl(url)) {
302
- entries.push({
303
- tabId,
304
- windowId: tab.windowId,
305
- groupId: tab.groupId,
306
- url: tab.url,
307
- title: tab.title,
308
- error: { message: "unsupported_url" },
309
- tiles: [],
310
- });
311
- if (progressEnabled) {
312
- deps.sendProgress(requestId, { phase: "screenshot", processed: index + 1, total: tabs.length, tabId });
313
- }
314
- continue;
315
- }
316
- let tiles = [];
317
- let error = null;
318
- try {
319
- const windowId = tab.windowId;
320
- const activeTabs = await chrome.tabs.query({ windowId, active: true });
321
- const activeTabId = activeTabs[0]?.id ?? null;
322
- if (activeTabId && activeTabId !== tabId) {
323
- await chrome.tabs.update(tabId, { active: true });
324
- await deps.delay(exports.SCREENSHOT_SCROLL_DELAY_MS);
325
- }
326
- try {
327
- await deps.waitForTabReady(tabId, params, exports.SCREENSHOT_PROCESS_TIMEOUT_MS);
328
- tiles = await captureTabTiles(tab, { mode, format, quality, tileMaxDim: adjustedTileMaxDim, maxBytes: adjustedMaxBytes }, deps);
329
- }
330
- finally {
331
- if (activeTabId && activeTabId !== tabId) {
332
- await chrome.tabs.update(activeTabId, { active: true });
333
- }
334
- }
335
- }
336
- catch (err) {
337
- const message = err instanceof Error ? err.message : "capture_failed";
338
- error = { message };
339
- }
340
- totalTiles += tiles.length;
341
- entries.push({
342
- tabId: tab.tabId,
343
- windowId: tab.windowId,
344
- groupId: tab.groupId,
345
- url: tab.url,
346
- title: tab.title,
347
- tiles,
348
- ...(error ? { error } : {}),
349
- });
350
- if (progressEnabled) {
351
- deps.sendProgress(requestId, { phase: "screenshot", processed: index + 1, total: tabs.length, tabId });
352
- }
353
- }
354
- return {
355
- generatedAt: Date.now(),
356
- totals: { tabs: tabs.length, tiles: totalTiles },
357
- meta: {
358
- durationMs: Date.now() - startedAt,
359
- mode,
360
- format,
361
- quality: format === "jpeg" ? quality : null,
362
- tileMaxDim: adjustedTileMaxDim,
363
- maxBytes: adjustedMaxBytes,
364
- },
365
- entries,
366
- };
367
- }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "manifest_version": 3,
3
3
  "name": "Tab Control",
4
- "version": "0.5.3",
4
+ "version": "0.6.0",
5
5
  "description": "Archive and manage browser tabs with CLI support",
6
6
  "permissions": [
7
7
  "tabs",
@@ -19,5 +19,5 @@
19
19
  "background": {
20
20
  "service_worker": "background.js"
21
21
  },
22
- "version_name": "0.5.3"
22
+ "version_name": "0.6.0-alpha.10"
23
23
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tabctl",
3
- "version": "0.5.3",
3
+ "version": "0.6.0-alpha.10",
4
4
  "description": "CLI tool to manage and analyze browser tabs",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -27,13 +27,21 @@
27
27
  "dist/extension"
28
28
  ],
29
29
  "scripts": {
30
- "build": "node scripts/gen-version.js && tsc -p tsconfig.json && node scripts/copy-artifacts.js && node scripts/build-launcher.js && node scripts/bundle-extension.js",
30
+ "build": "node scripts/gen-version.js && tsc -p tsconfig.json && node scripts/copy-artifacts.js && node scripts/bundle-extension.js && cargo build --manifest-path rust/Cargo.toml --workspace",
31
+ "build:launcher": "node scripts/build-launcher.js",
31
32
  "bump:major": "node scripts/bump-version.js major",
32
33
  "bump:minor": "node scripts/bump-version.js minor",
33
34
  "bump:patch": "node scripts/bump-version.js patch",
34
- "test": "npm run build && node --test --test-timeout=5000 dist/tests/unit/*.js && node dist/scripts/integration-test.js",
35
- "test:unit": "npm run build && node --test --test-timeout=5000 dist/tests/unit/*.js",
36
- "test:integration": "node dist/scripts/integration-test.js",
35
+ "bump:alpha": "node scripts/bump-version.js alpha",
36
+ "check:targets": "bash scripts/check-targets.sh",
37
+ "bump:rc": "node scripts/bump-version.js rc",
38
+ "bump:stable": "node scripts/bump-version.js stable",
39
+ "rust:check": "cargo check --manifest-path rust/Cargo.toml --workspace --all-targets",
40
+ "rust:test": "cargo test --manifest-path rust/Cargo.toml --workspace --all-targets",
41
+ "rust:verify": "cargo fmt --manifest-path rust/Cargo.toml --all -- --check && cargo clippy --manifest-path rust/Cargo.toml --workspace --all-targets -- -D warnings && npm run rust:test",
42
+ "test": "npm run build && npm run rust:verify",
43
+ "test:unit": "npm run rust:test",
44
+ "test:integration": "cargo test --manifest-path rust/Cargo.toml --test browser_integration --test browser_coverage --test browser_primitives --test local_commands -- --ignored --nocapture --test-threads=1",
37
45
  "clean": "node -e \"fs.rmSync('dist',{recursive:true,force:true})\" ",
38
46
  "prepare": "git rev-parse --git-dir >/dev/null 2>&1 && git config core.hooksPath .githooks || true"
39
47
  },
@@ -1,141 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.normalizeGroupColor = normalizeGroupColor;
4
- exports.normalizeSignals = normalizeSignals;
5
- exports.validateSignals = validateSignals;
6
- exports.parseArgs = parseArgs;
7
- const constants_1 = require("./constants");
8
- const options_1 = require("./options");
9
- const output_1 = require("./output");
10
- function normalizeGroupColor(value) {
11
- if (typeof value !== "string") {
12
- return undefined;
13
- }
14
- const trimmed = value.trim().toLowerCase();
15
- if (!trimmed) {
16
- return undefined;
17
- }
18
- if (!constants_1.GROUP_COLORS.has(trimmed)) {
19
- (0, output_1.errorOut)(`Invalid color: ${value}. Use one of: ${Array.from(constants_1.GROUP_COLORS).join(", ")}`);
20
- }
21
- return trimmed;
22
- }
23
- function normalizeSignals(value) {
24
- if (!Array.isArray(value)) {
25
- return [];
26
- }
27
- return value.map((signal) => String(signal).trim()).filter(Boolean);
28
- }
29
- function validateSignals(signals) {
30
- for (const signal of signals) {
31
- if (!constants_1.SUPPORTED_SIGNAL_SET.has(signal)) {
32
- (0, output_1.errorOut)(`Unknown signal: ${signal}. Use one of: ${constants_1.SUPPORTED_SIGNALS.join(", ")}`);
33
- }
34
- }
35
- }
36
- function normalizeCommand(value) {
37
- if (!value) {
38
- return value;
39
- }
40
- if (value === "groups" || value === "group") {
41
- return "group-list";
42
- }
43
- const meta = options_1.COMMANDS[value];
44
- if (!meta?.aliases || meta.aliases.length === 0) {
45
- return value;
46
- }
47
- return meta.aliases[0] ?? value;
48
- }
49
- function parseArgs(argv) {
50
- const args = [...argv];
51
- let command;
52
- const options = { _: [] };
53
- const warnings = [];
54
- const pendingFlags = [];
55
- const allowedFlags = (0, options_1.getAllowedFlags)();
56
- const booleanFlags = (0, options_1.getBooleanFlags)();
57
- while (args.length > 0) {
58
- const arg = args.shift();
59
- if (!arg.startsWith("--")) {
60
- if (!command) {
61
- command = normalizeCommand(arg);
62
- if (command) {
63
- const commandAllowedFlags = (0, options_1.getCommandAllowedFlags)(command);
64
- for (const pending of pendingFlags) {
65
- if (!commandAllowedFlags.has(pending)) {
66
- warnings.push(`--${pending} is not supported by ${command}`);
67
- }
68
- }
69
- }
70
- continue;
71
- }
72
- options._.push(arg);
73
- continue;
74
- }
75
- const key = arg.slice(2);
76
- if (!allowedFlags.has(key)) {
77
- if (key === "format") {
78
- (0, output_1.errorOut)("Unknown option: --format");
79
- }
80
- (0, output_1.errorOut)(`Unknown option: --${key}`);
81
- }
82
- if (command) {
83
- const commandAllowedFlags = (0, options_1.getCommandAllowedFlags)(command);
84
- if (!commandAllowedFlags.has(key)) {
85
- warnings.push(`--${key} is not supported by ${command}`);
86
- }
87
- }
88
- else {
89
- pendingFlags.push(key);
90
- }
91
- // Boolean flags (no value needed)
92
- if (booleanFlags.has(key)) {
93
- options[key] = true;
94
- continue;
95
- }
96
- // Value required
97
- const value = args.shift();
98
- if (value == null) {
99
- (0, output_1.errorOut)(`Missing value for --${key}`);
100
- }
101
- // Repeatable flags (accumulate into arrays)
102
- if (key === "signal") {
103
- if (!options.signal) {
104
- options.signal = [];
105
- }
106
- options.signal.push(value);
107
- continue;
108
- }
109
- if (key === "tab") {
110
- if (!options.tab) {
111
- options.tab = [];
112
- }
113
- options.tab.push(value);
114
- continue;
115
- }
116
- if (key === "agent") {
117
- if (!options.agent) {
118
- options.agent = [];
119
- }
120
- options.agent.push(value);
121
- continue;
122
- }
123
- if (key === "url") {
124
- if (!options.url) {
125
- options.url = [];
126
- }
127
- options.url.push(value);
128
- continue;
129
- }
130
- if (key === "selector") {
131
- if (!options.selector) {
132
- options.selector = [];
133
- }
134
- options.selector.push(value);
135
- continue;
136
- }
137
- // Single value flags
138
- options[key] = value;
139
- }
140
- return { command, options, warnings };
141
- }
@@ -1,83 +0,0 @@
1
- "use strict";
2
- var __importDefault = (this && this.__importDefault) || function (mod) {
3
- return (mod && mod.__esModule) ? mod : { "default": mod };
4
- };
5
- Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.createRequestId = createRequestId;
7
- exports.sendRequest = sendRequest;
8
- exports.fetchSnapshot = fetchSnapshot;
9
- exports.sendFireAndForget = sendFireAndForget;
10
- const node_net_1 = __importDefault(require("node:net"));
11
- const constants_1 = require("./constants");
12
- function createRequestId() {
13
- return `req-${Date.now()}-${Math.random().toString(16).slice(2, 10)}`;
14
- }
15
- function sendRequest(payload, onProgress) {
16
- return new Promise((resolve, reject) => {
17
- const { socketPath } = (0, constants_1.resolveConfig)();
18
- const client = node_net_1.default.createConnection(socketPath);
19
- let buffer = "";
20
- client.on("connect", () => {
21
- client.write(`${JSON.stringify(payload)}\n`);
22
- });
23
- client.on("data", (data) => {
24
- buffer += data;
25
- let index;
26
- while ((index = buffer.indexOf("\n")) >= 0) {
27
- const line = buffer.slice(0, index).trim();
28
- buffer = buffer.slice(index + 1);
29
- if (!line) {
30
- continue;
31
- }
32
- let response;
33
- try {
34
- response = JSON.parse(line);
35
- }
36
- catch (error) {
37
- client.end();
38
- client.destroy();
39
- reject(error);
40
- return;
41
- }
42
- if (response.progress && onProgress) {
43
- onProgress(response);
44
- continue;
45
- }
46
- client.end();
47
- client.destroy();
48
- resolve(response);
49
- return;
50
- }
51
- });
52
- client.on("error", (error) => {
53
- reject(error);
54
- });
55
- });
56
- }
57
- async function fetchSnapshot() {
58
- const response = await sendRequest({ id: createRequestId(), action: "list", params: {} });
59
- if (!response.ok) {
60
- return null;
61
- }
62
- return response.data;
63
- }
64
- /** Send a request without waiting for a response (fire-and-forget). */
65
- function sendFireAndForget(payload) {
66
- try {
67
- const { socketPath } = (0, constants_1.resolveConfig)();
68
- const client = node_net_1.default.createConnection(socketPath);
69
- client.on("connect", () => {
70
- client.write(`${JSON.stringify(payload)}\n`);
71
- // Unref after write so Node can exit without waiting for response
72
- client.unref();
73
- const timer = setTimeout(() => { client.end(); client.destroy(); }, 200);
74
- timer.unref();
75
- });
76
- client.on("error", () => {
77
- // Silently ignore — this is best-effort
78
- });
79
- }
80
- catch {
81
- // Silently ignore
82
- }
83
- }
@@ -1,134 +0,0 @@
1
- "use strict";
2
- /**
3
- * Doctor command handler: diagnose and repair profile health.
4
- *
5
- * Checks each profile's wrapper for valid Node/host paths, verifies
6
- * extension sync status, and optionally auto-repairs broken wrappers.
7
- */
8
- var __importDefault = (this && this.__importDefault) || function (mod) {
9
- return (mod && mod.__esModule) ? mod : { "default": mod };
10
- };
11
- Object.defineProperty(exports, "__esModule", { value: true });
12
- exports.runDoctor = runDoctor;
13
- const node_fs_1 = __importDefault(require("node:fs"));
14
- const node_path_1 = __importDefault(require("node:path"));
15
- const config_1 = require("../../../shared/config");
16
- const profiles_1 = require("../../../shared/profiles");
17
- const wrapper_health_1 = require("../../../shared/wrapper-health");
18
- const extension_sync_1 = require("../../../shared/extension-sync");
19
- const setup_1 = require("./setup");
20
- const output_1 = require("../output");
21
- function checkProfile(name, entry, fix) {
22
- const wrapperPath = (0, wrapper_health_1.resolveWrapperPath)(entry.dataDir);
23
- const check = (0, wrapper_health_1.checkWrapper)(wrapperPath);
24
- const issues = [...check.issues];
25
- let fixed = false;
26
- if (fix && !check.ok && check.info) {
27
- const needsNodeFix = !node_fs_1.default.existsSync(check.info.nodePath);
28
- const needsHostFix = !node_fs_1.default.existsSync(check.info.hostPath);
29
- if (needsNodeFix || needsHostFix) {
30
- const newNodePath = needsNodeFix ? process.execPath : check.info.nodePath;
31
- let newHostPath = check.info.hostPath;
32
- if (needsHostFix) {
33
- // Use the stable bundled host path
34
- try {
35
- const config = (0, config_1.resolveConfig)();
36
- newHostPath = (0, extension_sync_1.resolveInstalledHostPath)(config.baseDataDir);
37
- if (!node_fs_1.default.existsSync(newHostPath)) {
38
- issues.push(`Bundled host not found at ${newHostPath} — run: tabctl setup --browser ${entry.browser}`);
39
- }
40
- }
41
- catch {
42
- issues.push("Could not resolve bundled host path");
43
- }
44
- }
45
- try {
46
- (0, setup_1.writeWrapper)(newNodePath, newHostPath, check.info.profileName, node_path_1.default.dirname(wrapperPath));
47
- fixed = true;
48
- // Update issue messages to show they were fixed
49
- const fixedIssues = [];
50
- if (needsNodeFix) {
51
- fixedIssues.push(`Fixed Node path: ${check.info.nodePath} → ${newNodePath}`);
52
- }
53
- if (needsHostFix) {
54
- fixedIssues.push(`Fixed host path: ${check.info.hostPath} → ${newHostPath}`);
55
- }
56
- // Replace original issues with fixed messages
57
- issues.length = 0;
58
- issues.push(...fixedIssues);
59
- }
60
- catch (err) {
61
- issues.push(`Failed to fix wrapper: ${err instanceof Error ? err.message : String(err)}`);
62
- }
63
- }
64
- }
65
- return {
66
- ok: check.ok || fixed,
67
- browser: entry.browser,
68
- dataDir: entry.dataDir,
69
- wrapperPath,
70
- issues,
71
- fixed,
72
- };
73
- }
74
- function runDoctor(options, prettyOutput) {
75
- const fix = options.fix === true;
76
- const config = (0, config_1.resolveConfig)();
77
- const registry = (0, profiles_1.loadProfiles)(config.configDir);
78
- const profileNames = Object.keys(registry.profiles);
79
- if (profileNames.length === 0) {
80
- (0, output_1.errorOut)("No profiles configured. Run: tabctl setup --browser <edge|chrome>");
81
- }
82
- // Check each profile
83
- const profiles = {};
84
- for (const name of profileNames) {
85
- profiles[name] = checkProfile(name, registry.profiles[name], fix);
86
- }
87
- // Check extension sync status
88
- let extensionCheck;
89
- try {
90
- const sync = (0, extension_sync_1.checkExtensionSync)(config.baseDataDir);
91
- extensionCheck = {
92
- ok: !sync.needsSync,
93
- synced: !sync.needsSync,
94
- bundledVersion: sync.bundledVersion,
95
- installedVersion: sync.installedVersion,
96
- };
97
- }
98
- catch {
99
- extensionCheck = {
100
- ok: false,
101
- synced: false,
102
- bundledVersion: null,
103
- installedVersion: null,
104
- };
105
- }
106
- // Summary
107
- const total = profileNames.length;
108
- const healthy = Object.values(profiles).filter(p => p.ok).length;
109
- const broken = total - healthy;
110
- const fixed = Object.values(profiles).filter(p => p.fixed).length;
111
- const allOk = broken === 0 && extensionCheck.ok;
112
- (0, output_1.printJson)({
113
- ok: allOk,
114
- action: "doctor",
115
- data: {
116
- profiles,
117
- extension: extensionCheck,
118
- summary: { total, healthy, broken, fixed },
119
- },
120
- }, prettyOutput);
121
- // Helpful stderr hints
122
- if (!allOk && !fix) {
123
- const brokenNames = Object.entries(profiles)
124
- .filter(([, p]) => !p.ok)
125
- .map(([n]) => n);
126
- if (brokenNames.length > 0) {
127
- process.stderr.write(`\nBroken profiles: ${brokenNames.join(", ")}\n`);
128
- process.stderr.write("Run: tabctl doctor --fix\n\n");
129
- }
130
- }
131
- if (fixed > 0) {
132
- process.stderr.write(`\nFixed ${fixed} profile(s). Verify: tabctl ping\n\n`);
133
- }
134
- }