tabctl 0.5.3 → 0.6.0-alpha.2

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 (42) hide show
  1. package/README.md +98 -35
  2. package/dist/extension/background.js +2 -0
  3. package/dist/extension/manifest.json +2 -2
  4. package/package.json +13 -5
  5. package/dist/cli/lib/args.js +0 -141
  6. package/dist/cli/lib/client.js +0 -83
  7. package/dist/cli/lib/commands/doctor.js +0 -134
  8. package/dist/cli/lib/commands/index.js +0 -51
  9. package/dist/cli/lib/commands/list.js +0 -159
  10. package/dist/cli/lib/commands/meta.js +0 -229
  11. package/dist/cli/lib/commands/params-groups.js +0 -48
  12. package/dist/cli/lib/commands/params-move.js +0 -44
  13. package/dist/cli/lib/commands/params.js +0 -314
  14. package/dist/cli/lib/commands/profile.js +0 -91
  15. package/dist/cli/lib/commands/setup.js +0 -294
  16. package/dist/cli/lib/constants.js +0 -30
  17. package/dist/cli/lib/help.js +0 -205
  18. package/dist/cli/lib/options-commands.js +0 -274
  19. package/dist/cli/lib/options-groups.js +0 -41
  20. package/dist/cli/lib/options.js +0 -125
  21. package/dist/cli/lib/output.js +0 -147
  22. package/dist/cli/lib/pagination.js +0 -55
  23. package/dist/cli/lib/policy-filter.js +0 -202
  24. package/dist/cli/lib/policy.js +0 -91
  25. package/dist/cli/lib/report.js +0 -61
  26. package/dist/cli/lib/response.js +0 -235
  27. package/dist/cli/lib/scope.js +0 -250
  28. package/dist/cli/lib/snapshot.js +0 -216
  29. package/dist/cli/lib/types.js +0 -2
  30. package/dist/cli/tabctl.js +0 -475
  31. package/dist/host/host.bundle.js +0 -670
  32. package/dist/host/host.js +0 -143
  33. package/dist/host/host.sh +0 -5
  34. package/dist/host/launcher/go.mod +0 -3
  35. package/dist/host/launcher/main.go +0 -109
  36. package/dist/host/lib/handlers.js +0 -327
  37. package/dist/host/lib/undo.js +0 -60
  38. package/dist/shared/config.js +0 -134
  39. package/dist/shared/extension-sync.js +0 -170
  40. package/dist/shared/profiles.js +0 -78
  41. package/dist/shared/version.js +0 -8
  42. package/dist/shared/wrapper-health.js +0 -132
package/README.md CHANGED
@@ -7,13 +7,21 @@ A command-line instrument for browser tab orchestration — list, search, group,
7
7
  ## Install
8
8
 
9
9
  ```bash
10
- npm install -g tabctl
11
- tabctl setup --browser chrome
12
- # Load the extension: chrome://extensions → Developer mode → Load unpacked → paste: ~/.local/state/tabctl/extension/
10
+ mise use -g github:ekroon/tabctl # install the tabctl binary
11
+ tabctl setup --browser edge --extension-id <id> # or: --browser chrome --extension-id <id>
12
+ # Load the extension: edge://extensions → Developer mode → Load unpacked → paste: ~/.local/state/tabctl/extension/
13
13
  tabctl ping
14
14
  ```
15
15
 
16
- If it pings back, the wire is live. You're connected.
16
+ Setup writes the wrapper script, native messaging manifest, and registers the profile in one step. Works on macOS, Linux, and Windows. If it pings back, the wire is live. You're connected.
17
+
18
+ ### Alternative: build from source
19
+
20
+ ```bash
21
+ cargo install --path rust/crates/tabctl
22
+ ```
23
+
24
+ > **Legacy:** `npm install -g tabctl` still works for the Node.js-based distribution but is no longer the primary install method. No Node.js or Go is required at runtime — the single `tabctl` binary handles everything.
17
25
 
18
26
  ## Agent Skill
19
27
 
@@ -54,48 +62,54 @@ When tabctl is installed as a skill, your agent sees what you see. Just talk to
54
62
 
55
63
  ---
56
64
 
57
- `tabctl` works through a lightweight local stack: the CLI talks to a native messaging host, which proxies requests to a browser extension. The host only runs while the browser is open and the extension is connected.
65
+ `tabctl` is a single Rust binary that serves as both the CLI and the native messaging host. The CLI sends commands over a Unix socket (or named pipe on Windows) to the host, which proxies them to the browser extension via native messaging. The `tabctl host` subcommand is the native messaging entry point — invoked automatically by the browser, not manually.
58
66
 
59
67
  This repo contains:
60
- - Chrome/Edge extension (tab/group inspection + actions)
61
- - Native messaging host (Node)
62
- - CLI (`tabctl`) for on-demand workflows
68
+ - Chrome/Edge extension (`src/extension/`, the only TypeScript component)
69
+ - Rust workspace (`rust/crates/*`) — single `tabctl` binary for CLI + host + shared runtime
70
+ - Node packaging/build scripts for distribution (legacy)
63
71
 
64
72
  ## Quick Start
65
73
 
66
74
  ### 1. Build and install
67
75
 
76
+ ```bash
77
+ cargo install --path rust/crates/tabctl # puts tabctl on your PATH
78
+ ```
79
+
80
+ For development with the full build pipeline (extension + Rust):
81
+
68
82
  ```bash
69
83
  npm install
70
84
  npm run build
71
- npm link # puts tabctl on your PATH
72
85
  ```
73
86
 
74
- If you haven't run `npm link`, you can always use `node ./cli/tabctl.js` instead of `tabctl`.
75
-
76
87
  ### 2. Set up your browser
77
88
 
78
- Run setup it syncs the extension, tells you where to load it, and auto-derives the extension ID:
89
+ Run setup with your browser's extension ID it writes the manifest, wrapper script, and registers the profile:
79
90
 
80
91
  <!-- test: "setup explicit --extension-id overrides auto-derived ID" -->
81
92
  ```bash
82
- tabctl setup --browser chrome
93
+ tabctl setup --browser chrome --extension-id <your-extension-id>
83
94
  ```
84
95
 
85
96
  This will:
86
- 1. Copy the extension to a stable location (`~/.local/state/tabctl/extension/`)
87
- 2. Print the path (and copy it to your clipboard)
88
- 3. Ask you to load it as an unpacked extension in `chrome://extensions`
89
- 4. Auto-derive the extension ID from the installed path
97
+ 1. Write the native messaging manifest and wrapper script
98
+ 2. Register the browser profile in `profiles.json`
99
+ 3. Download the version-pinned release extension asset (`tabctl-extension.zip` + `.sha256`) into the tabctl data directory
100
+ 4. Copy the extension to a stable location (`~/.local/state/tabctl/extension/`)
101
+ 5. Print the path for loading as an unpacked extension in `chrome://extensions`
102
+
103
+ Without `--extension-id`, setup only downloads the extension and outputs JSON (no manifest or wrapper writes).
90
104
 
91
105
  > **Edge?** Use `--browser edge` and load from `edge://extensions` instead.
106
+ >
107
+ > **Cross-platform:** setup works on macOS, Linux, and Windows. On Windows, setup verifies connectivity after writing setup artifacts and checks the runtime extension ID reported by the browser. Connectivity failures and runtime extension ID mismatches exit non-zero and print manual recovery steps (including expected vs runtime IDs).
92
108
 
93
- If you need to override the auto-derived ID (e.g. for a custom extension path):
94
-
95
- <!-- test: "setup writes native host manifest for chrome" -->
96
- ```bash
97
- tabctl setup --browser chrome --extension-id <your-extension-id>
98
- ```
109
+ Optional setup release overrides:
110
+ - Flags: `--release-repo`, `--release-tag` (or `--release-version`), `--release-asset`, `--skip-extension-download`
111
+ - Env vars: `TABCTL_RELEASE_REPO`, `TABCTL_RELEASE_TAG`, `TABCTL_RELEASE_ASSET`, `TABCTL_SETUP_FETCH_EXTENSION=0`
112
+ - Precedence: flags override env vars, then built-in defaults; if download fails, setup continues and includes warning details in setup output.
99
113
 
100
114
  ### 3. Verify and explore
101
115
 
@@ -209,6 +223,28 @@ See [CLI.md](CLI.md#configuration) for full details.
209
223
  - Socket: `<dataDir>/tabctl.sock` (default: `~/.local/state/tabctl/tabctl.sock`)
210
224
  - Undo log: `<dataDir>/undo.jsonl` (default: `~/.local/state/tabctl/undo.jsonl`)
211
225
  - Profile registry: `<configDir>/profiles.json`
226
+ - WSL TCP port file: `<dataDir>/tcp-port` (written by the Windows host)
227
+
228
+ ## Windows + WSL transport
229
+
230
+ On Windows, the host exposes a dual endpoint model:
231
+ - Windows native clients use a named pipe endpoint (`\\.\pipe\tabctl-<hash>`).
232
+ - WSL/Linux clients use `tcp://127.0.0.1:<port>`, with the host writing `<dataDir>/tcp-port`.
233
+
234
+ WSL endpoint discovery (CLI):
235
+ 1. `TABCTL_SOCKET` (explicit endpoint); if this is a pipe endpoint in WSL, CLI still prefers discovered TCP.
236
+ 2. `TABCTL_TCP_PORT` (forces `127.0.0.1:<port>`).
237
+ 3. `tcp-port` file discovery from resolved data dir (and equivalent `/mnt/c/Users/*/.../tabctl/.../tcp-port` locations).
238
+ 4. Fallback: `tcp://127.0.0.1:38000`.
239
+
240
+ Relevant knobs: `TABCTL_SOCKET`, `TABCTL_TCP_PORT`, `TABCTL_PROFILE`, `TABCTL_DATA_DIR`, `TABCTL_STATE_DIR`, `TABCTL_CONFIG_DIR`.
241
+
242
+ ## Troubleshooting (setup/ping on Windows + WSL)
243
+
244
+ - `tabctl setup` fails with `Windows setup verification failed`: check `data.verification.reason` in JSON output (`ping-timeout`, `socket-not-found`, `socket-refused`, `ping-not-ok`, `extension-id-mismatch`), then follow printed manual steps.
245
+ - Runtime ID mismatch (`extension-id-mismatch`): compare expected vs runtime IDs from setup output, then rerun setup with the runtime ID shown by `edge://extensions` / `chrome://extensions`:
246
+ - `tabctl setup --browser <edge|chrome> --extension-id <runtime-id>`
247
+ - `tabctl ping` returns connect errors (`ENOENT`, `ECONNREFUSED`, timeout): ensure extension is loaded and active, rerun `tabctl setup`, and in WSL verify `TABCTL_TCP_PORT` or `<dataDir>/tcp-port` matches a listening localhost port.
212
248
 
213
249
  ## Multi-Browser Setup
214
250
 
@@ -219,10 +255,10 @@ tabctl supports multiple browser profiles. Each profile connects to a different
219
255
  <!-- test: "setup writes native host manifest", "setup writes native host manifest for chrome", "setup --name creates custom-named profile", "profile-list with multiple profiles shows all", "profile-switch success updates default", "--profile flag overrides active profile" -->
220
256
  ```bash
221
257
  # Setup for Edge
222
- tabctl setup --browser edge
258
+ tabctl setup --browser edge --extension-id <edge-extension-id>
223
259
 
224
260
  # Setup for Chrome (with custom name)
225
- tabctl setup --browser chrome --name chrome-work
261
+ tabctl setup --browser chrome --name chrome-work --extension-id <chrome-extension-id>
226
262
 
227
263
  # List profiles
228
264
  tabctl profile-list
@@ -261,21 +297,29 @@ Policy is shared across all profiles.
261
297
 
262
298
  ## Development
263
299
 
264
- ### TypeScript workflow
265
- Source lives in `src/` and compiles to `build/`, then syncs to the runtime locations:
266
- - `src/extension/background.ts` -> `extension/background.js`
267
- - `src/host/host.ts` -> `host/host.js`
268
- - `src/cli/tabctl.ts` -> `cli/tabctl.js`
269
- - `src/tests/unit/*.ts` -> `tests/unit/*.js`
300
+ ### Build workflow
301
+ The single `tabctl` binary is built from the Rust workspace (`rust/`). TypeScript is limited to the browser extension boundary (`src/extension/`). No Node.js or Go is required at runtime.
270
302
 
271
- Build and test:
303
+ Build and verify:
272
304
 
273
305
  ```bash
274
- npm install
275
- npm run build
276
- npm test
306
+ cargo build --release -p tabctl # build the binary
307
+ npm install && npm run build # full pipeline (extension + Rust)
308
+ npm test # unit tests
309
+ ```
310
+
311
+ Rust-only validation:
312
+ ```bash
313
+ npm run rust:verify
277
314
  ```
278
315
 
316
+ Integration script (currently Rust-suite parity in CI/local):
317
+ ```bash
318
+ npm run test:integration
319
+ ```
320
+
321
+ WSL CI validates the WSL->Windows invocation bridge (`test.yml` `wsl` job) with phases: `prerequisites`, `diagnostics`, `build_and_unit`, `setup_validation`, `windows_invocation`, `integration`. Runtime/build execution is delegated to Windows commands (`cmd.exe`/`powershell.exe`), so WSL-local Rust compilation is not required.
322
+
279
323
  ### Versioning
280
324
  The base version lives in `package.json` and is embedded into the CLI, host, and extension at build time.
281
325
 
@@ -284,6 +328,25 @@ Commands:
284
328
  npm run bump:patch
285
329
  npm run bump:minor
286
330
  npm run bump:major
331
+ npm run bump:alpha
332
+ npm run bump:rc
333
+ npm run bump:stable
334
+ ```
335
+
336
+ Pre-release staging flow:
337
+ - `bump:alpha` creates/increments `x.y.z-alpha.N`
338
+ - `bump:rc` promotes alpha to `x.y.z-rc.1` (or increments RC)
339
+ - `bump:stable` drops the prerelease suffix for final stable publish
340
+
341
+ Release publishing (`.github/workflows/publish.yml`) enforces:
342
+ - Git tag must match `package.json` version (`v<version>`)
343
+ - prerelease tags publish to `alpha`/`rc`; stable publishes to `latest`
344
+ - `npm run build` and `npm test` must pass before publish
345
+ - release assets include `tabctl-extension.zip` plus `tabctl-extension.zip.sha256`
346
+
347
+ Fetch the extension asset from a release with:
348
+ ```bash
349
+ tabctl extension-fetch --version 0.5.3
287
350
  ```
288
351
 
289
352
  Local builds default to a dev version when a `.git` directory is present, appending the short SHA.
@@ -3408,6 +3408,7 @@
3408
3408
  case "ping":
3409
3409
  return {
3410
3410
  now: Date.now(),
3411
+ runtimeId: chrome.runtime.id,
3411
3412
  version: VERSION_INFO.version,
3412
3413
  baseVersion: VERSION_INFO.baseVersion,
3413
3414
  gitSha: VERSION_INFO.gitSha,
@@ -3416,6 +3417,7 @@
3416
3417
  };
3417
3418
  case "version":
3418
3419
  return {
3420
+ runtimeId: chrome.runtime.id,
3419
3421
  version: VERSION_INFO.version,
3420
3422
  baseVersion: VERSION_INFO.baseVersion,
3421
3423
  gitSha: VERSION_INFO.gitSha,
@@ -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-alpha.2",
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.2"
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.2",
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": "npm run rust:test",
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
- }