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.
- package/README.md +98 -35
- package/dist/extension/background.js +2 -0
- package/dist/extension/manifest.json +2 -2
- package/package.json +13 -5
- package/dist/cli/lib/args.js +0 -141
- package/dist/cli/lib/client.js +0 -83
- package/dist/cli/lib/commands/doctor.js +0 -134
- package/dist/cli/lib/commands/index.js +0 -51
- package/dist/cli/lib/commands/list.js +0 -159
- package/dist/cli/lib/commands/meta.js +0 -229
- package/dist/cli/lib/commands/params-groups.js +0 -48
- package/dist/cli/lib/commands/params-move.js +0 -44
- package/dist/cli/lib/commands/params.js +0 -314
- package/dist/cli/lib/commands/profile.js +0 -91
- package/dist/cli/lib/commands/setup.js +0 -294
- package/dist/cli/lib/constants.js +0 -30
- package/dist/cli/lib/help.js +0 -205
- package/dist/cli/lib/options-commands.js +0 -274
- package/dist/cli/lib/options-groups.js +0 -41
- package/dist/cli/lib/options.js +0 -125
- package/dist/cli/lib/output.js +0 -147
- package/dist/cli/lib/pagination.js +0 -55
- package/dist/cli/lib/policy-filter.js +0 -202
- package/dist/cli/lib/policy.js +0 -91
- package/dist/cli/lib/report.js +0 -61
- package/dist/cli/lib/response.js +0 -235
- package/dist/cli/lib/scope.js +0 -250
- package/dist/cli/lib/snapshot.js +0 -216
- package/dist/cli/lib/types.js +0 -2
- package/dist/cli/tabctl.js +0 -475
- package/dist/host/host.bundle.js +0 -670
- package/dist/host/host.js +0 -143
- package/dist/host/host.sh +0 -5
- package/dist/host/launcher/go.mod +0 -3
- package/dist/host/launcher/main.go +0 -109
- package/dist/host/lib/handlers.js +0 -327
- package/dist/host/lib/undo.js +0 -60
- package/dist/shared/config.js +0 -134
- package/dist/shared/extension-sync.js +0 -170
- package/dist/shared/profiles.js +0 -78
- package/dist/shared/version.js +0 -8
- 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
|
-
|
|
11
|
-
tabctl setup --browser chrome
|
|
12
|
-
# Load the 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`
|
|
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 (
|
|
61
|
-
-
|
|
62
|
-
-
|
|
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
|
|
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.
|
|
87
|
-
2.
|
|
88
|
-
3.
|
|
89
|
-
4.
|
|
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
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
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
|
-
###
|
|
265
|
-
|
|
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
|
|
303
|
+
Build and verify:
|
|
272
304
|
|
|
273
305
|
```bash
|
|
274
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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/
|
|
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
|
-
"
|
|
35
|
-
"
|
|
36
|
-
"
|
|
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
|
},
|
package/dist/cli/lib/args.js
DELETED
|
@@ -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
|
-
}
|
package/dist/cli/lib/client.js
DELETED
|
@@ -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
|
-
}
|