tabctl 0.5.3 → 0.6.0-alpha.1

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 +88 -20
  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,14 +7,22 @@ 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 # or: --browser chrome
12
+ # Load the extension: edge://extensions → Developer mode → Load unpacked → paste: ~/.local/state/tabctl/extension/
13
13
  tabctl ping
14
14
  ```
15
15
 
16
16
  If it pings back, the wire is live. You're connected.
17
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.
25
+
18
26
  ## Agent Skill
19
27
 
20
28
  Give your coding agent eyes into the browser. One command and it learns the protocol.
@@ -54,25 +62,28 @@ 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
89
  Run setup — it syncs the extension, tells you where to load it, and auto-derives the extension ID:
@@ -87,8 +98,16 @@ This will:
87
98
  2. Print the path (and copy it to your clipboard)
88
99
  3. Ask you to load it as an unpacked extension in `chrome://extensions`
89
100
  4. Auto-derive the extension ID from the installed path
101
+ 5. Download the version-pinned release extension asset (`tabctl-extension.zip` + `.sha256`) into the tabctl data directory for offline/manual recovery
102
+
103
+ Optional setup release overrides:
104
+ - Flags: `--release-repo`, `--release-tag` (or `--release-version`), `--release-asset`, `--skip-extension-download`
105
+ - Env vars: `TABCTL_RELEASE_REPO`, `TABCTL_RELEASE_TAG`, `TABCTL_RELEASE_ASSET`, `TABCTL_SETUP_FETCH_EXTENSION=0`
106
+ - Precedence: flags override env vars, then built-in defaults; if download fails, setup continues and includes warning details in setup output.
90
107
 
91
108
  > **Edge?** Use `--browser edge` and load from `edge://extensions` instead.
109
+ >
110
+ > **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
111
 
93
112
  If you need to override the auto-derived ID (e.g. for a custom extension path):
94
113
 
@@ -209,6 +228,28 @@ See [CLI.md](CLI.md#configuration) for full details.
209
228
  - Socket: `<dataDir>/tabctl.sock` (default: `~/.local/state/tabctl/tabctl.sock`)
210
229
  - Undo log: `<dataDir>/undo.jsonl` (default: `~/.local/state/tabctl/undo.jsonl`)
211
230
  - Profile registry: `<configDir>/profiles.json`
231
+ - WSL TCP port file: `<dataDir>/tcp-port` (written by the Windows host)
232
+
233
+ ## Windows + WSL transport
234
+
235
+ On Windows, the host exposes a dual endpoint model:
236
+ - Windows native clients use a named pipe endpoint (`\\.\pipe\tabctl-<hash>`).
237
+ - WSL/Linux clients use `tcp://127.0.0.1:<port>`, with the host writing `<dataDir>/tcp-port`.
238
+
239
+ WSL endpoint discovery (CLI):
240
+ 1. `TABCTL_SOCKET` (explicit endpoint); if this is a pipe endpoint in WSL, CLI still prefers discovered TCP.
241
+ 2. `TABCTL_TCP_PORT` (forces `127.0.0.1:<port>`).
242
+ 3. `tcp-port` file discovery from resolved data dir (and equivalent `/mnt/c/Users/*/.../tabctl/.../tcp-port` locations).
243
+ 4. Fallback: `tcp://127.0.0.1:38000`.
244
+
245
+ Relevant knobs: `TABCTL_SOCKET`, `TABCTL_TCP_PORT`, `TABCTL_PROFILE`, `TABCTL_DATA_DIR`, `TABCTL_STATE_DIR`, `TABCTL_CONFIG_DIR`.
246
+
247
+ ## Troubleshooting (setup/ping on Windows + WSL)
248
+
249
+ - `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.
250
+ - 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`:
251
+ - `tabctl setup --browser <edge|chrome> --extension-id <runtime-id>`
252
+ - `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
253
 
213
254
  ## Multi-Browser Setup
214
255
 
@@ -261,21 +302,29 @@ Policy is shared across all profiles.
261
302
 
262
303
  ## Development
263
304
 
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`
305
+ ### Build workflow
306
+ 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
307
 
271
- Build and test:
308
+ Build and verify:
272
309
 
273
310
  ```bash
274
- npm install
275
- npm run build
276
- npm test
311
+ cargo build --release -p tabctl # build the binary
312
+ npm install && npm run build # full pipeline (extension + Rust)
313
+ npm test # unit tests
314
+ ```
315
+
316
+ Rust-only validation:
317
+ ```bash
318
+ npm run rust:verify
319
+ ```
320
+
321
+ Integration script (currently Rust-suite parity in CI/local):
322
+ ```bash
323
+ npm run test:integration
277
324
  ```
278
325
 
326
+ 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.
327
+
279
328
  ### Versioning
280
329
  The base version lives in `package.json` and is embedded into the CLI, host, and extension at build time.
281
330
 
@@ -284,6 +333,25 @@ Commands:
284
333
  npm run bump:patch
285
334
  npm run bump:minor
286
335
  npm run bump:major
336
+ npm run bump:alpha
337
+ npm run bump:rc
338
+ npm run bump:stable
339
+ ```
340
+
341
+ Pre-release staging flow:
342
+ - `bump:alpha` creates/increments `x.y.z-alpha.N`
343
+ - `bump:rc` promotes alpha to `x.y.z-rc.1` (or increments RC)
344
+ - `bump:stable` drops the prerelease suffix for final stable publish
345
+
346
+ Release publishing (`.github/workflows/publish.yml`) enforces:
347
+ - Git tag must match `package.json` version (`v<version>`)
348
+ - prerelease tags publish to `alpha`/`rc`; stable publishes to `latest`
349
+ - `npm run build` and `npm test` must pass before publish
350
+ - release assets include `tabctl-extension.zip` plus `tabctl-extension.zip.sha256`
351
+
352
+ Fetch the extension asset from a release with:
353
+ ```bash
354
+ tabctl extension-fetch --version 0.5.3
287
355
  ```
288
356
 
289
357
  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.1",
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.1"
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.1",
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
- }
@@ -1,51 +0,0 @@
1
- "use strict";
2
- /**
3
- * Command module index.
4
- * Re-exports all command handlers and parameter builders.
5
- */
6
- Object.defineProperty(exports, "__esModule", { value: true });
7
- exports.buildUndoParams = exports.buildHistoryParams = exports.buildScreenshotParams = exports.buildReportParams = exports.buildCloseParams = exports.buildArchiveParams = exports.buildMergeWindowParams = exports.buildMoveGroupParams = exports.buildMoveTabParams = exports.buildGroupGatherParams = exports.buildGroupAssignParams = exports.buildGroupUngroupParams = exports.buildGroupUpdateParams = exports.buildOpenParams = exports.buildRefreshParams = exports.buildFocusParams = exports.buildInspectParams = exports.buildAnalyzeParams = exports.runProfileRemove = exports.runProfileSwitch = exports.runProfileShow = exports.runProfileList = exports.runGroupList = exports.runList = exports.runPing = exports.runUndo = exports.runHistory = exports.runPolicy = exports.runVersion = exports.runSkillInstall = exports.runDoctor = exports.runSetup = void 0;
8
- // Setup command
9
- var setup_1 = require("./setup");
10
- Object.defineProperty(exports, "runSetup", { enumerable: true, get: function () { return setup_1.runSetup; } });
11
- // Doctor command
12
- var doctor_1 = require("./doctor");
13
- Object.defineProperty(exports, "runDoctor", { enumerable: true, get: function () { return doctor_1.runDoctor; } });
14
- // Meta commands (version, ping, skill, policy, history, undo)
15
- var meta_1 = require("./meta");
16
- Object.defineProperty(exports, "runSkillInstall", { enumerable: true, get: function () { return meta_1.runSkillInstall; } });
17
- Object.defineProperty(exports, "runVersion", { enumerable: true, get: function () { return meta_1.runVersion; } });
18
- Object.defineProperty(exports, "runPolicy", { enumerable: true, get: function () { return meta_1.runPolicy; } });
19
- Object.defineProperty(exports, "runHistory", { enumerable: true, get: function () { return meta_1.runHistory; } });
20
- Object.defineProperty(exports, "runUndo", { enumerable: true, get: function () { return meta_1.runUndo; } });
21
- Object.defineProperty(exports, "runPing", { enumerable: true, get: function () { return meta_1.runPing; } });
22
- // List commands (list, group-list)
23
- var list_1 = require("./list");
24
- Object.defineProperty(exports, "runList", { enumerable: true, get: function () { return list_1.runList; } });
25
- Object.defineProperty(exports, "runGroupList", { enumerable: true, get: function () { return list_1.runGroupList; } });
26
- // Profile commands (profile-list, profile-show, profile-switch, profile-remove)
27
- var profile_1 = require("./profile");
28
- Object.defineProperty(exports, "runProfileList", { enumerable: true, get: function () { return profile_1.runProfileList; } });
29
- Object.defineProperty(exports, "runProfileShow", { enumerable: true, get: function () { return profile_1.runProfileShow; } });
30
- Object.defineProperty(exports, "runProfileSwitch", { enumerable: true, get: function () { return profile_1.runProfileSwitch; } });
31
- Object.defineProperty(exports, "runProfileRemove", { enumerable: true, get: function () { return profile_1.runProfileRemove; } });
32
- // Parameter builders for all commands
33
- var params_1 = require("./params");
34
- Object.defineProperty(exports, "buildAnalyzeParams", { enumerable: true, get: function () { return params_1.buildAnalyzeParams; } });
35
- Object.defineProperty(exports, "buildInspectParams", { enumerable: true, get: function () { return params_1.buildInspectParams; } });
36
- Object.defineProperty(exports, "buildFocusParams", { enumerable: true, get: function () { return params_1.buildFocusParams; } });
37
- Object.defineProperty(exports, "buildRefreshParams", { enumerable: true, get: function () { return params_1.buildRefreshParams; } });
38
- Object.defineProperty(exports, "buildOpenParams", { enumerable: true, get: function () { return params_1.buildOpenParams; } });
39
- Object.defineProperty(exports, "buildGroupUpdateParams", { enumerable: true, get: function () { return params_1.buildGroupUpdateParams; } });
40
- Object.defineProperty(exports, "buildGroupUngroupParams", { enumerable: true, get: function () { return params_1.buildGroupUngroupParams; } });
41
- Object.defineProperty(exports, "buildGroupAssignParams", { enumerable: true, get: function () { return params_1.buildGroupAssignParams; } });
42
- Object.defineProperty(exports, "buildGroupGatherParams", { enumerable: true, get: function () { return params_1.buildGroupGatherParams; } });
43
- Object.defineProperty(exports, "buildMoveTabParams", { enumerable: true, get: function () { return params_1.buildMoveTabParams; } });
44
- Object.defineProperty(exports, "buildMoveGroupParams", { enumerable: true, get: function () { return params_1.buildMoveGroupParams; } });
45
- Object.defineProperty(exports, "buildMergeWindowParams", { enumerable: true, get: function () { return params_1.buildMergeWindowParams; } });
46
- Object.defineProperty(exports, "buildArchiveParams", { enumerable: true, get: function () { return params_1.buildArchiveParams; } });
47
- Object.defineProperty(exports, "buildCloseParams", { enumerable: true, get: function () { return params_1.buildCloseParams; } });
48
- Object.defineProperty(exports, "buildReportParams", { enumerable: true, get: function () { return params_1.buildReportParams; } });
49
- Object.defineProperty(exports, "buildScreenshotParams", { enumerable: true, get: function () { return params_1.buildScreenshotParams; } });
50
- Object.defineProperty(exports, "buildHistoryParams", { enumerable: true, get: function () { return params_1.buildHistoryParams; } });
51
- Object.defineProperty(exports, "buildUndoParams", { enumerable: true, get: function () { return params_1.buildUndoParams; } });