tabctl 0.5.2 → 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.
- package/README.md +138 -20
- 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 -283
- 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
|
@@ -1,28 +1,89 @@
|
|
|
1
1
|
# tabctl
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Every open tab is a thread you forgot to pull. Tabctl finds them all.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
A command-line instrument for browser tab orchestration — list, search, group, archive, close, undo — wired into Edge or Chrome through a native messaging bridge. Built for humans who hoard tabs and the AI agents who clean up after them.
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
-
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```bash
|
|
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
|
+
tabctl ping
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
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.
|
|
25
|
+
|
|
26
|
+
## Agent Skill
|
|
27
|
+
|
|
28
|
+
Give your coding agent eyes into the browser. One command and it learns the protocol.
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
tabctl skill
|
|
32
|
+
# or: npx skills add https://github.com/ekroon/tabctl --skill tabctl -a opencode -a github-copilot -a claude-code
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Safety
|
|
36
|
+
|
|
37
|
+
Nothing leaves your machine. No cloud. No telemetry. Just a socket between your terminal and your browser, quiet as rain on neon.
|
|
38
|
+
|
|
39
|
+
Every mutation is undoable — `tabctl undo` rewinds closes, archives, and group changes like they never happened. A configurable policy layer shields pinned tabs and protected domains from accidental destruction. You pull the trigger; tabctl keeps the safety on until you mean it.
|
|
40
|
+
|
|
41
|
+
## What You Can Say
|
|
42
|
+
|
|
43
|
+
When tabctl is installed as a skill, your agent sees what you see. Just talk to it.
|
|
44
|
+
|
|
45
|
+
> *"Which of my tabs can I close?"*
|
|
46
|
+
> The agent scans for duplicates, stale pages, and tabs you haven't touched in days — then offers to clean house.
|
|
47
|
+
|
|
48
|
+
> *"Are any of my open tabs relevant to my note on Project Helios?"*
|
|
49
|
+
> When connected to Obsidian, your agent cross-references every open tab against your notes and surfaces the ones that matter.
|
|
50
|
+
|
|
51
|
+
> *"I just finished researching service mesh architectures. Organize what I found."*
|
|
52
|
+
> Groups your tabs by theme, extracts key URLs, and drops a summary into your notes — before you forget what you were looking at.
|
|
53
|
+
|
|
54
|
+
> *"Where's that AWS pricing page I had open somewhere?"*
|
|
55
|
+
> The agent searches your open tabs and groups by title and URL — and brings it back into focus.
|
|
56
|
+
|
|
57
|
+
> *"Pull every error message from my open Sentry tabs into a markdown table."*
|
|
58
|
+
> The agent reads each tab, extracts what you need, and formats it — no copy-paste, no context switching.
|
|
59
|
+
|
|
60
|
+
> *"Group everything by project. You know which ones."*
|
|
61
|
+
> Your agent infers context from URLs, titles, and your workspace — then sorts ninety tabs into five groups with names that actually make sense.
|
|
62
|
+
|
|
63
|
+
---
|
|
11
64
|
|
|
12
|
-
The host
|
|
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.
|
|
66
|
+
|
|
67
|
+
This repo contains:
|
|
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)
|
|
13
71
|
|
|
14
72
|
## Quick Start
|
|
15
73
|
|
|
16
74
|
### 1. Build and install
|
|
17
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
|
+
|
|
18
82
|
```bash
|
|
19
83
|
npm install
|
|
20
84
|
npm run build
|
|
21
|
-
npm link # puts tabctl on your PATH
|
|
22
85
|
```
|
|
23
86
|
|
|
24
|
-
If you haven't run `npm link`, you can always use `node ./cli/tabctl.js` instead of `tabctl`.
|
|
25
|
-
|
|
26
87
|
### 2. Set up your browser
|
|
27
88
|
|
|
28
89
|
Run setup — it syncs the extension, tells you where to load it, and auto-derives the extension ID:
|
|
@@ -37,8 +98,16 @@ This will:
|
|
|
37
98
|
2. Print the path (and copy it to your clipboard)
|
|
38
99
|
3. Ask you to load it as an unpacked extension in `chrome://extensions`
|
|
39
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.
|
|
40
107
|
|
|
41
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).
|
|
42
111
|
|
|
43
112
|
If you need to override the auto-derived ID (e.g. for a custom extension path):
|
|
44
113
|
|
|
@@ -159,6 +228,28 @@ See [CLI.md](CLI.md#configuration) for full details.
|
|
|
159
228
|
- Socket: `<dataDir>/tabctl.sock` (default: `~/.local/state/tabctl/tabctl.sock`)
|
|
160
229
|
- Undo log: `<dataDir>/undo.jsonl` (default: `~/.local/state/tabctl/undo.jsonl`)
|
|
161
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.
|
|
162
253
|
|
|
163
254
|
## Multi-Browser Setup
|
|
164
255
|
|
|
@@ -211,21 +302,29 @@ Policy is shared across all profiles.
|
|
|
211
302
|
|
|
212
303
|
## Development
|
|
213
304
|
|
|
214
|
-
###
|
|
215
|
-
|
|
216
|
-
- `src/extension/background.ts` -> `extension/background.js`
|
|
217
|
-
- `src/host/host.ts` -> `host/host.js`
|
|
218
|
-
- `src/cli/tabctl.ts` -> `cli/tabctl.js`
|
|
219
|
-
- `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.
|
|
220
307
|
|
|
221
|
-
Build and
|
|
308
|
+
Build and verify:
|
|
222
309
|
|
|
223
310
|
```bash
|
|
224
|
-
|
|
225
|
-
npm run build
|
|
226
|
-
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
|
|
227
324
|
```
|
|
228
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
|
+
|
|
229
328
|
### Versioning
|
|
230
329
|
The base version lives in `package.json` and is embedded into the CLI, host, and extension at build time.
|
|
231
330
|
|
|
@@ -234,6 +333,25 @@ Commands:
|
|
|
234
333
|
npm run bump:patch
|
|
235
334
|
npm run bump:minor
|
|
236
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
|
|
237
355
|
```
|
|
238
356
|
|
|
239
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.
|
|
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.
|
|
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.
|
|
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/
|
|
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
|
-
}
|