tabctl 0.6.0-rc.3 → 0.6.0-rc.5
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 +35 -15
- package/dist/extension/background.js +96 -17
- package/dist/extension/manifest.json +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -234,21 +234,24 @@ See [CLI.md](CLI.md#configuration) for full details.
|
|
|
234
234
|
- Undo log: `<dataDir>/undo.jsonl` (default: `~/.local/state/tabctl/undo.jsonl`)
|
|
235
235
|
- Browser-state history DB: `<dataDir>/state.db` (default: `~/.local/state/tabctl/state.db`)
|
|
236
236
|
- Profile registry: `<configDir>/profiles.json`
|
|
237
|
-
-
|
|
237
|
+
- Windows pipe endpoint file: `<dataDir>/pipe-endpoint`
|
|
238
238
|
|
|
239
239
|
## Windows + WSL transport
|
|
240
240
|
|
|
241
|
-
On Windows, the host exposes a
|
|
241
|
+
On Windows, the host exposes a named-pipe endpoint model:
|
|
242
242
|
- Windows native clients use a named pipe endpoint (`\\.\pipe\tabctl-<hash>`).
|
|
243
|
-
- WSL/Linux clients use
|
|
243
|
+
- WSL/Linux clients use a Windows named-pipe bridge; the Windows host publishes `<dataDir>/pipe-endpoint`, and the WSL CLI relays through `powershell.exe`.
|
|
244
244
|
|
|
245
245
|
WSL endpoint discovery (CLI):
|
|
246
|
-
1. `TABCTL_SOCKET` (explicit endpoint)
|
|
247
|
-
2. `
|
|
248
|
-
3. `tcp-port` file discovery from resolved data dir (and equivalent `/mnt/c/Users/*/.../tabctl/.../tcp-port` locations).
|
|
249
|
-
4. Fallback: `tcp://127.0.0.1:38000`.
|
|
246
|
+
1. `TABCTL_SOCKET` (explicit endpoint).
|
|
247
|
+
2. `pipe-endpoint` file discovery from resolved data dir (and equivalent `/mnt/c/Users/*/.../tabctl/.../pipe-endpoint` locations).
|
|
250
248
|
|
|
251
|
-
|
|
249
|
+
WSL named-pipe mode:
|
|
250
|
+
- This is the default WSL transport now.
|
|
251
|
+
- The CLI discovers the pipe endpoint from `<dataDir>/pipe-endpoint` (including the mirrored `/mnt/c/Users/*/...` candidate paths used for other WSL bridge files).
|
|
252
|
+
- TCP is disabled for the WSL transport path.
|
|
253
|
+
|
|
254
|
+
Relevant knobs: `TABCTL_SOCKET`, `TABCTL_PROFILE`, `TABCTL_DATA_DIR`, `TABCTL_STATE_DIR`, `TABCTL_CONFIG_DIR`.
|
|
252
255
|
|
|
253
256
|
## Troubleshooting (setup/ping on Windows + WSL)
|
|
254
257
|
|
|
@@ -258,9 +261,9 @@ Relevant knobs: `TABCTL_SOCKET`, `TABCTL_TCP_PORT`, `TABCTL_PROFILE`, `TABCTL_DA
|
|
|
258
261
|
- Runtime command runs can auto-sync extension files when host/extension versions drift; rerun `tabctl query 'mutation { reloadExtension { reloading } }'` if the browser does not pick up changes immediately.
|
|
259
262
|
- For local release-like testing while developing, force runtime sync behavior with `TABCTL_AUTO_SYNC_MODE=release-like`.
|
|
260
263
|
- Disable runtime sync entirely with `TABCTL_AUTO_SYNC_MODE=off`.
|
|
261
|
-
- `tabctl ping --json` is the
|
|
262
|
-
- Version metadata is intentionally health-only: regular GraphQL payloads do not include version fields unless you explicitly query health surfaces
|
|
263
|
-
- `tabctl ping` returns connect errors (`ENOENT`, `ECONNREFUSED`, timeout): ensure extension is loaded and active, rerun `tabctl setup`, and in WSL verify
|
|
264
|
+
- `tabctl ping --json` is a host connectivity/health check; use it to confirm the native host is reachable and healthy.
|
|
265
|
+
- Version metadata is intentionally health-only: regular GraphQL payloads do not include version fields unless you explicitly query health surfaces, which may expose fields such as `versionsInSync`, `hostBaseVersion`, and `baseVersion`.
|
|
266
|
+
- `tabctl ping` returns connect errors (`ENOENT`, `ECONNREFUSED`, timeout): ensure extension is loaded and active, rerun `tabctl setup`, and in WSL verify the profile data dir contains a current `pipe-endpoint` file.
|
|
264
267
|
- `tabctl doctor --fix --json` includes per-profile connectivity diagnostics in `data.profiles[].connectivity`; if ping remains unhealthy after local repairs, follow `manualSteps`.
|
|
265
268
|
|
|
266
269
|
Local release-like sync test recipe:
|
|
@@ -271,7 +274,7 @@ tabctl setup --browser edge --extension-id <extension-id> --release-tag v0.5.2
|
|
|
271
274
|
# 2) Run the current binary with forced release-like auto-sync
|
|
272
275
|
TABCTL_AUTO_SYNC_MODE=release-like cargo run --manifest-path rust/Cargo.toml -p tabctl -- query '{ tabs { total } }'
|
|
273
276
|
|
|
274
|
-
# 3) Verify host/
|
|
277
|
+
# 3) Verify host connectivity/health after auto-sync
|
|
275
278
|
tabctl ping --json
|
|
276
279
|
```
|
|
277
280
|
|
|
@@ -323,8 +326,7 @@ Policy is shared across all profiles.
|
|
|
323
326
|
## Security
|
|
324
327
|
- The native host is locked to your extension ID.
|
|
325
328
|
- All data stays local; no external API keys are used.
|
|
326
|
-
-
|
|
327
|
-
- TCP transport is available on all platforms via `TABCTL_HOST_TCP=1` (host) and `TABCTL_TRANSPORT=tcp` (CLI). All TCP connections are authenticated. See [CLI.md](CLI.md) for details.
|
|
329
|
+
- WSL ↔ Windows communication uses a local named-pipe bridge via `powershell.exe`; no TCP fallback is used on that path.
|
|
328
330
|
|
|
329
331
|
## Development
|
|
330
332
|
|
|
@@ -342,8 +344,20 @@ npm test # unit tests
|
|
|
342
344
|
Rust-only validation:
|
|
343
345
|
```bash
|
|
344
346
|
npm run rust:verify
|
|
347
|
+
npm run check:targets # local cross-target cfg/type check
|
|
345
348
|
```
|
|
346
349
|
|
|
350
|
+
On macOS, `npm run check:targets` can use Zig for the C cross-compiler needed
|
|
351
|
+
by `libsqlite3-sys`:
|
|
352
|
+
|
|
353
|
+
```bash
|
|
354
|
+
brew install zig
|
|
355
|
+
```
|
|
356
|
+
|
|
357
|
+
The script auto-detects Zig outside CI and wires the Linux/Windows C compiler
|
|
358
|
+
environment for the check. The pre-push hook does not run this optional check;
|
|
359
|
+
run it manually when you want local cross-target coverage.
|
|
360
|
+
|
|
347
361
|
Browser-backed integration harness (requires built dist artifacts and Chrome):
|
|
348
362
|
```bash
|
|
349
363
|
npm run test:integration
|
|
@@ -369,8 +383,14 @@ Pre-release staging flow:
|
|
|
369
383
|
- `bump:rc` promotes alpha to `x.y.z-rc.1` (or increments RC)
|
|
370
384
|
- `bump:stable` drops the prerelease suffix for final stable publish
|
|
371
385
|
|
|
372
|
-
Release
|
|
386
|
+
Release automation:
|
|
387
|
+
- Run the **Prepare Release** workflow to choose or auto-detect the next version and open a release PR.
|
|
388
|
+
- When that PR merges, **Tag Release** creates `v<version>` and dispatches **Release**.
|
|
389
|
+
- The root `package.json` version and `optionalDependencies.tabctl-win32-x64` are kept in sync by `scripts/bump-version.js`.
|
|
390
|
+
|
|
391
|
+
Release publishing (`.github/workflows/release.yml`) supports both tag pushes and explicit workflow dispatch, and enforces:
|
|
373
392
|
- Git tag must match `package.json` version (`v<version>`)
|
|
393
|
+
- `package.json.optionalDependencies["tabctl-win32-x64"]` must match `package.json` version
|
|
374
394
|
- prerelease tags publish to `alpha`/`rc`; stable publishes to `latest`
|
|
375
395
|
- `npm run build` and `npm test` must pass before publish
|
|
376
396
|
- release assets include `tabctl-extension.zip` plus `tabctl-extension.zip.sha256`
|
|
@@ -474,8 +474,13 @@
|
|
|
474
474
|
dirty: parsed.dirty
|
|
475
475
|
};
|
|
476
476
|
var KEEPALIVE_ALARM = "tabctl-keepalive";
|
|
477
|
+
var RECONNECT_ALARM = "tabctl-reconnect";
|
|
477
478
|
var KEEPALIVE_INTERVAL_MINUTES = 1;
|
|
478
479
|
var BROWSER_STATE_SYNC_DEBOUNCE_MS = 750;
|
|
480
|
+
var RECONNECT_INITIAL_DELAY_MS = 250;
|
|
481
|
+
var RECONNECT_MAX_DELAY_MS = 3e4;
|
|
482
|
+
var RECONNECT_ALARM_MIN_DELAY_MS = 3e4;
|
|
483
|
+
var RECONNECT_STABLE_RESET_MS = 5e3;
|
|
479
484
|
var screenshot = require_screenshot();
|
|
480
485
|
var content = require_content();
|
|
481
486
|
var { delay, executeWithTimeout } = content;
|
|
@@ -487,7 +492,10 @@
|
|
|
487
492
|
return n;
|
|
488
493
|
}
|
|
489
494
|
var state = {
|
|
490
|
-
port: null
|
|
495
|
+
port: null,
|
|
496
|
+
reconnectTimer: null,
|
|
497
|
+
reconnectStableTimer: null,
|
|
498
|
+
reconnectAttempt: 0
|
|
491
499
|
};
|
|
492
500
|
var browserState = {
|
|
493
501
|
nextId: 1,
|
|
@@ -500,17 +508,71 @@
|
|
|
500
508
|
function log(...args) {
|
|
501
509
|
console.log("[tabctl]", ...args);
|
|
502
510
|
}
|
|
503
|
-
function
|
|
504
|
-
|
|
511
|
+
function reconnectDelayMs(attempt) {
|
|
512
|
+
return Math.min(RECONNECT_INITIAL_DELAY_MS * 2 ** attempt, RECONNECT_MAX_DELAY_MS);
|
|
513
|
+
}
|
|
514
|
+
function clearReconnectTimer() {
|
|
515
|
+
if (!state.reconnectTimer) {
|
|
516
|
+
return;
|
|
517
|
+
}
|
|
518
|
+
clearTimeout(state.reconnectTimer);
|
|
519
|
+
state.reconnectTimer = null;
|
|
520
|
+
}
|
|
521
|
+
function clearReconnectAlarm() {
|
|
522
|
+
chrome.alarms.clear(RECONNECT_ALARM);
|
|
523
|
+
}
|
|
524
|
+
function clearReconnectStableTimer() {
|
|
525
|
+
if (!state.reconnectStableTimer) {
|
|
526
|
+
return;
|
|
527
|
+
}
|
|
528
|
+
clearTimeout(state.reconnectStableTimer);
|
|
529
|
+
state.reconnectStableTimer = null;
|
|
530
|
+
}
|
|
531
|
+
function scheduleReconnect(reason) {
|
|
532
|
+
if (state.port || state.reconnectTimer) {
|
|
505
533
|
return;
|
|
506
534
|
}
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
535
|
+
const attempt = state.reconnectAttempt;
|
|
536
|
+
const delayMs = reconnectDelayMs(attempt);
|
|
537
|
+
state.reconnectAttempt += 1;
|
|
538
|
+
log("Scheduling native host reconnect", { reason, delayMs, attempt });
|
|
539
|
+
chrome.alarms.create(RECONNECT_ALARM, {
|
|
540
|
+
delayInMinutes: Math.max(delayMs, RECONNECT_ALARM_MIN_DELAY_MS) / 6e4
|
|
541
|
+
});
|
|
542
|
+
state.reconnectTimer = setTimeout(() => {
|
|
543
|
+
state.reconnectTimer = null;
|
|
544
|
+
clearReconnectAlarm();
|
|
545
|
+
connectNative();
|
|
546
|
+
}, delayMs);
|
|
547
|
+
}
|
|
548
|
+
function resetReconnectBackoffAfterStablePort(port) {
|
|
549
|
+
clearReconnectStableTimer();
|
|
550
|
+
state.reconnectStableTimer = setTimeout(() => {
|
|
551
|
+
state.reconnectStableTimer = null;
|
|
552
|
+
if (state.port === port) {
|
|
553
|
+
state.reconnectAttempt = 0;
|
|
554
|
+
}
|
|
555
|
+
}, RECONNECT_STABLE_RESET_MS);
|
|
556
|
+
}
|
|
557
|
+
function ensureKeepaliveAlarm() {
|
|
558
|
+
chrome.alarms.create(KEEPALIVE_ALARM, { periodInMinutes: KEEPALIVE_INTERVAL_MINUTES });
|
|
559
|
+
}
|
|
560
|
+
function sendResponse(port, id, ok, payload) {
|
|
561
|
+
if (!port) {
|
|
562
|
+
log("dropping response because native port is unavailable", { id, ok });
|
|
510
563
|
return;
|
|
511
564
|
}
|
|
512
|
-
|
|
513
|
-
|
|
565
|
+
try {
|
|
566
|
+
if (ok) {
|
|
567
|
+
const data = typeof payload === "object" && payload !== null ? payload : { payload };
|
|
568
|
+
port.postMessage({ id, ok: true, data });
|
|
569
|
+
return;
|
|
570
|
+
}
|
|
571
|
+
const error = payload instanceof Error ? { message: payload.message, stack: payload.stack } : payload;
|
|
572
|
+
port.postMessage({ id, ok: false, error });
|
|
573
|
+
} catch (error) {
|
|
574
|
+
log("failed to send native response", { id, ok, error });
|
|
575
|
+
}
|
|
514
576
|
}
|
|
515
577
|
function connectNative() {
|
|
516
578
|
if (state.port) {
|
|
@@ -518,8 +580,13 @@
|
|
|
518
580
|
}
|
|
519
581
|
try {
|
|
520
582
|
const port = chrome.runtime.connectNative(HOST_NAME);
|
|
583
|
+
clearReconnectTimer();
|
|
584
|
+
clearReconnectAlarm();
|
|
521
585
|
state.port = port;
|
|
522
|
-
port
|
|
586
|
+
resetReconnectBackoffAfterStablePort(port);
|
|
587
|
+
port.onMessage.addListener((message) => {
|
|
588
|
+
void handleNativeMessage(port, message);
|
|
589
|
+
});
|
|
523
590
|
port.onDisconnect.addListener(() => {
|
|
524
591
|
const lastError = chrome.runtime.lastError;
|
|
525
592
|
if (lastError) {
|
|
@@ -527,12 +594,17 @@
|
|
|
527
594
|
} else {
|
|
528
595
|
log("Native host disconnected");
|
|
529
596
|
}
|
|
530
|
-
state.port
|
|
597
|
+
if (state.port === port) {
|
|
598
|
+
state.port = null;
|
|
599
|
+
}
|
|
600
|
+
clearReconnectStableTimer();
|
|
601
|
+
scheduleReconnect("disconnect");
|
|
531
602
|
});
|
|
532
603
|
log("Native host connected");
|
|
533
604
|
queueBrowserStateSync("startup");
|
|
534
605
|
} catch (error) {
|
|
535
606
|
log("Native host connection failed", error);
|
|
607
|
+
scheduleReconnect("connect-failed");
|
|
536
608
|
}
|
|
537
609
|
}
|
|
538
610
|
function nextBrowserStateId() {
|
|
@@ -639,7 +711,7 @@
|
|
|
639
711
|
windowId: tab.windowId,
|
|
640
712
|
groupId: tab.groupId,
|
|
641
713
|
incognito: tab.incognito,
|
|
642
|
-
url: tab.url,
|
|
714
|
+
url: tab.url || tab.pendingUrl,
|
|
643
715
|
title: tab.title,
|
|
644
716
|
index: tab.index
|
|
645
717
|
});
|
|
@@ -745,20 +817,24 @@
|
|
|
745
817
|
}
|
|
746
818
|
connectNative();
|
|
747
819
|
registerBrowserStateListeners();
|
|
820
|
+
ensureKeepaliveAlarm();
|
|
748
821
|
chrome.runtime.onInstalled.addListener(() => {
|
|
749
822
|
connectNative();
|
|
750
|
-
|
|
823
|
+
ensureKeepaliveAlarm();
|
|
751
824
|
});
|
|
752
825
|
chrome.runtime.onStartup.addListener(() => {
|
|
753
826
|
connectNative();
|
|
754
|
-
|
|
827
|
+
ensureKeepaliveAlarm();
|
|
755
828
|
});
|
|
756
829
|
chrome.alarms.onAlarm.addListener((alarm) => {
|
|
757
830
|
if (alarm.name === KEEPALIVE_ALARM) {
|
|
758
831
|
connectNative();
|
|
832
|
+
} else if (alarm.name === RECONNECT_ALARM) {
|
|
833
|
+
clearReconnectTimer();
|
|
834
|
+
connectNative();
|
|
759
835
|
}
|
|
760
836
|
});
|
|
761
|
-
async function handleNativeMessage(message) {
|
|
837
|
+
async function handleNativeMessage(requestPort, message) {
|
|
762
838
|
if (!message || typeof message !== "object") {
|
|
763
839
|
return;
|
|
764
840
|
}
|
|
@@ -768,9 +844,9 @@
|
|
|
768
844
|
}
|
|
769
845
|
try {
|
|
770
846
|
const data = await handleAction(action, params || {}, id);
|
|
771
|
-
sendResponse(id, true, data);
|
|
847
|
+
sendResponse(requestPort, id, true, data);
|
|
772
848
|
} catch (error) {
|
|
773
|
-
sendResponse(id, false, error);
|
|
849
|
+
sendResponse(requestPort, id, false, error);
|
|
774
850
|
}
|
|
775
851
|
}
|
|
776
852
|
async function handleAction(action, params, requestId) {
|
|
@@ -790,6 +866,9 @@
|
|
|
790
866
|
component: "extension"
|
|
791
867
|
};
|
|
792
868
|
case "reload":
|
|
869
|
+
chrome.alarms.create(RECONNECT_ALARM, {
|
|
870
|
+
delayInMinutes: RECONNECT_ALARM_MIN_DELAY_MS / 6e4
|
|
871
|
+
});
|
|
793
872
|
setTimeout(() => chrome.runtime.reload(), 100);
|
|
794
873
|
return { reloading: true };
|
|
795
874
|
// --- Primitives: thin Chrome API wrappers (p: prefix) ---
|
|
@@ -877,7 +956,7 @@
|
|
|
877
956
|
windowId: win.id,
|
|
878
957
|
index: tab.index,
|
|
879
958
|
incognito: win.incognito || false,
|
|
880
|
-
url: tab.url,
|
|
959
|
+
url: tab.url || tab.pendingUrl,
|
|
881
960
|
title: tab.title,
|
|
882
961
|
active: tab.active,
|
|
883
962
|
pinned: tab.pinned,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tabctl",
|
|
3
|
-
"version": "0.6.0-rc.
|
|
3
|
+
"version": "0.6.0-rc.5",
|
|
4
4
|
"description": "CLI tool to manage and analyze browser tabs",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -52,7 +52,7 @@
|
|
|
52
52
|
"typescript": "^5.4.5"
|
|
53
53
|
},
|
|
54
54
|
"optionalDependencies": {
|
|
55
|
-
"tabctl-win32-x64": "0.
|
|
55
|
+
"tabctl-win32-x64": "0.6.0-rc.5"
|
|
56
56
|
},
|
|
57
57
|
"dependencies": {
|
|
58
58
|
"normalize-url": "^8.1.1"
|