tabctl 0.2.1 → 0.3.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 +1 -1
- package/dist/cli/lib/client.js +21 -0
- package/dist/cli/lib/commands/meta.js +4 -1
- package/dist/cli/lib/commands/setup.js +107 -22
- package/dist/cli/lib/options-commands.js +3 -0
- package/dist/cli/lib/output.js +36 -1
- package/dist/cli/tabctl.js +4 -0
- package/dist/extension/background.js +3 -0
- package/dist/extension/manifest.json +2 -2
- package/dist/host/host.bundle.js +667 -0
- package/dist/host/host.js +22 -3
- package/dist/host/launcher/go.mod +3 -0
- package/dist/host/launcher/main.go +109 -0
- package/dist/shared/config.js +26 -3
- package/dist/shared/extension-sync.js +56 -2
- package/dist/shared/version.js +2 -2
- package/package.json +6 -3
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
// Tiny native messaging host launcher for Windows.
|
|
2
|
+
//
|
|
3
|
+
// Chrome launches native messaging hosts as sub-processes. On Windows, if the
|
|
4
|
+
// host is a .cmd/.bat file, Chrome invokes it through cmd.exe which opens
|
|
5
|
+
// stdin/stdout in text mode — corrupting the binary 4-byte length-prefixed
|
|
6
|
+
// native messaging protocol.
|
|
7
|
+
//
|
|
8
|
+
// This launcher reads a config file (host-launcher.cfg) next to the exe,
|
|
9
|
+
// sets environment variables, and proxies stdin/stdout between Chrome and
|
|
10
|
+
// Node.js in binary mode.
|
|
11
|
+
//
|
|
12
|
+
// Build: GOOS=windows GOARCH=amd64 go build -o tabctl-host.exe .
|
|
13
|
+
|
|
14
|
+
package main
|
|
15
|
+
|
|
16
|
+
import (
|
|
17
|
+
"bufio"
|
|
18
|
+
"fmt"
|
|
19
|
+
"io"
|
|
20
|
+
"os"
|
|
21
|
+
"os/exec"
|
|
22
|
+
"path/filepath"
|
|
23
|
+
"strings"
|
|
24
|
+
"sync"
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
func main() {
|
|
28
|
+
exePath, err := os.Executable()
|
|
29
|
+
if err != nil {
|
|
30
|
+
fmt.Fprintf(os.Stderr, "tabctl-host: cannot resolve exe path: %v\n", err)
|
|
31
|
+
os.Exit(1)
|
|
32
|
+
}
|
|
33
|
+
exeDir := filepath.Dir(exePath)
|
|
34
|
+
cfgPath := filepath.Join(exeDir, "host-launcher.cfg")
|
|
35
|
+
|
|
36
|
+
f, err := os.Open(cfgPath)
|
|
37
|
+
if err != nil {
|
|
38
|
+
fmt.Fprintf(os.Stderr, "tabctl-host: cannot open %s: %v\n", cfgPath, err)
|
|
39
|
+
os.Exit(1)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
scanner := bufio.NewScanner(f)
|
|
43
|
+
|
|
44
|
+
if !scanner.Scan() {
|
|
45
|
+
fmt.Fprintf(os.Stderr, "tabctl-host: missing node path in %s\n", cfgPath)
|
|
46
|
+
os.Exit(1)
|
|
47
|
+
}
|
|
48
|
+
nodePath := strings.TrimSpace(scanner.Text())
|
|
49
|
+
|
|
50
|
+
if !scanner.Scan() {
|
|
51
|
+
fmt.Fprintf(os.Stderr, "tabctl-host: missing host path in %s\n", cfgPath)
|
|
52
|
+
os.Exit(1)
|
|
53
|
+
}
|
|
54
|
+
hostPath := strings.TrimSpace(scanner.Text())
|
|
55
|
+
|
|
56
|
+
for scanner.Scan() {
|
|
57
|
+
line := strings.TrimSpace(scanner.Text())
|
|
58
|
+
if line == "" {
|
|
59
|
+
continue
|
|
60
|
+
}
|
|
61
|
+
if eq := strings.IndexByte(line, '='); eq > 0 {
|
|
62
|
+
os.Setenv(line[:eq], line[eq+1:])
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
f.Close()
|
|
66
|
+
|
|
67
|
+
// Proxy stdin/stdout via pipes to preserve binary mode.
|
|
68
|
+
cmd := exec.Command(nodePath, hostPath)
|
|
69
|
+
cmd.Stderr = os.Stderr
|
|
70
|
+
|
|
71
|
+
nodeIn, err := cmd.StdinPipe()
|
|
72
|
+
if err != nil {
|
|
73
|
+
fmt.Fprintf(os.Stderr, "tabctl-host: stdin pipe: %v\n", err)
|
|
74
|
+
os.Exit(1)
|
|
75
|
+
}
|
|
76
|
+
nodeOut, err := cmd.StdoutPipe()
|
|
77
|
+
if err != nil {
|
|
78
|
+
fmt.Fprintf(os.Stderr, "tabctl-host: stdout pipe: %v\n", err)
|
|
79
|
+
os.Exit(1)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if err := cmd.Start(); err != nil {
|
|
83
|
+
fmt.Fprintf(os.Stderr, "tabctl-host: failed to start: %v\n", err)
|
|
84
|
+
os.Exit(1)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
var wg sync.WaitGroup
|
|
88
|
+
wg.Add(2)
|
|
89
|
+
|
|
90
|
+
go func() {
|
|
91
|
+
defer wg.Done()
|
|
92
|
+
io.Copy(nodeIn, os.Stdin)
|
|
93
|
+
nodeIn.Close()
|
|
94
|
+
}()
|
|
95
|
+
|
|
96
|
+
go func() {
|
|
97
|
+
defer wg.Done()
|
|
98
|
+
io.Copy(os.Stdout, nodeOut)
|
|
99
|
+
}()
|
|
100
|
+
|
|
101
|
+
wg.Wait()
|
|
102
|
+
|
|
103
|
+
if err := cmd.Wait(); err != nil {
|
|
104
|
+
if exitErr, ok := err.(*exec.ExitError); ok {
|
|
105
|
+
os.Exit(exitErr.ExitCode())
|
|
106
|
+
}
|
|
107
|
+
os.Exit(1)
|
|
108
|
+
}
|
|
109
|
+
}
|
package/dist/shared/config.js
CHANGED
|
@@ -3,12 +3,35 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
3
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.resolveSocketPath = resolveSocketPath;
|
|
6
7
|
exports.resetConfig = resetConfig;
|
|
7
8
|
exports.expandEnvVars = expandEnvVars;
|
|
8
9
|
exports.resolveConfig = resolveConfig;
|
|
9
10
|
const os_1 = __importDefault(require("os"));
|
|
10
11
|
const path_1 = __importDefault(require("path"));
|
|
12
|
+
const crypto_1 = __importDefault(require("crypto"));
|
|
11
13
|
const fs_1 = __importDefault(require("fs"));
|
|
14
|
+
function defaultConfigBase() {
|
|
15
|
+
if (process.platform === "win32") {
|
|
16
|
+
return process.env.APPDATA || path_1.default.join(os_1.default.homedir(), "AppData", "Roaming");
|
|
17
|
+
}
|
|
18
|
+
return path_1.default.join(os_1.default.homedir(), ".config");
|
|
19
|
+
}
|
|
20
|
+
function defaultStateBase() {
|
|
21
|
+
if (process.platform === "win32") {
|
|
22
|
+
return process.env.LOCALAPPDATA || path_1.default.join(os_1.default.homedir(), "AppData", "Local");
|
|
23
|
+
}
|
|
24
|
+
return path_1.default.join(os_1.default.homedir(), ".local", "state");
|
|
25
|
+
}
|
|
26
|
+
/** Resolve the IPC socket/pipe path for the given data directory. */
|
|
27
|
+
function resolveSocketPath(dataDir) {
|
|
28
|
+
if (process.platform === "win32") {
|
|
29
|
+
// Windows: use named pipes (Unix domain sockets are unreliable)
|
|
30
|
+
const hash = crypto_1.default.createHash("sha256").update(dataDir).digest("hex").slice(0, 12);
|
|
31
|
+
return `\\\\.\\pipe\\tabctl-${hash}`;
|
|
32
|
+
}
|
|
33
|
+
return path_1.default.join(dataDir, "tabctl.sock");
|
|
34
|
+
}
|
|
12
35
|
let cached;
|
|
13
36
|
function resetConfig() {
|
|
14
37
|
cached = undefined;
|
|
@@ -25,7 +48,7 @@ function resolveConfig(profileName) {
|
|
|
25
48
|
return cached;
|
|
26
49
|
// Config dir resolution
|
|
27
50
|
const configDir = process.env.TABCTL_CONFIG_DIR
|
|
28
|
-
|| path_1.default.join(process.env.XDG_CONFIG_HOME ||
|
|
51
|
+
|| path_1.default.join(process.env.XDG_CONFIG_HOME || defaultConfigBase(), "tabctl");
|
|
29
52
|
// Read optional config.json
|
|
30
53
|
let fileConfig = {};
|
|
31
54
|
try {
|
|
@@ -47,7 +70,7 @@ function resolveConfig(profileName) {
|
|
|
47
70
|
dataDir = path_1.default.join(configDir, "data");
|
|
48
71
|
}
|
|
49
72
|
else {
|
|
50
|
-
dataDir = path_1.default.join(process.env.XDG_STATE_HOME ||
|
|
73
|
+
dataDir = path_1.default.join(process.env.XDG_STATE_HOME || defaultStateBase(), "tabctl");
|
|
51
74
|
}
|
|
52
75
|
const baseDataDir = dataDir;
|
|
53
76
|
// Profile resolution (read profiles.json inline to avoid circular import)
|
|
@@ -97,7 +120,7 @@ function resolveConfig(profileName) {
|
|
|
97
120
|
configDir,
|
|
98
121
|
dataDir,
|
|
99
122
|
baseDataDir,
|
|
100
|
-
socketPath: process.env.TABCTL_SOCKET ||
|
|
123
|
+
socketPath: process.env.TABCTL_SOCKET || resolveSocketPath(dataDir),
|
|
101
124
|
undoLog: path_1.default.join(dataDir, "undo.jsonl"),
|
|
102
125
|
wrapperDir: dataDir,
|
|
103
126
|
policyPath: path_1.default.join(configDir, "policy.json"),
|
|
@@ -3,16 +3,31 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
3
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
exports.EXTENSION_DIR_NAME = void 0;
|
|
6
|
+
exports.HOST_BUNDLE_NAME = exports.EXTENSION_DIR_NAME = void 0;
|
|
7
|
+
exports.deriveExtensionId = deriveExtensionId;
|
|
7
8
|
exports.resolveBundledExtensionDir = resolveBundledExtensionDir;
|
|
9
|
+
exports.resolveBundledHostPath = resolveBundledHostPath;
|
|
8
10
|
exports.resolveInstalledExtensionDir = resolveInstalledExtensionDir;
|
|
11
|
+
exports.resolveInstalledHostPath = resolveInstalledHostPath;
|
|
9
12
|
exports.readExtensionVersion = readExtensionVersion;
|
|
13
|
+
exports.readHostVersion = readHostVersion;
|
|
10
14
|
exports.syncExtension = syncExtension;
|
|
15
|
+
exports.syncHost = syncHost;
|
|
11
16
|
exports.checkExtensionSync = checkExtensionSync;
|
|
12
17
|
const path_1 = __importDefault(require("path"));
|
|
13
18
|
const fs_1 = __importDefault(require("fs"));
|
|
19
|
+
const crypto_1 = __importDefault(require("crypto"));
|
|
14
20
|
const config_1 = require("./config");
|
|
15
21
|
exports.EXTENSION_DIR_NAME = "extension";
|
|
22
|
+
exports.HOST_BUNDLE_NAME = "host.bundle.js";
|
|
23
|
+
/**
|
|
24
|
+
* Derive the Chrome/Edge extension ID for an unpacked extension path.
|
|
25
|
+
* Chromium computes: SHA256(absolute_path) → first 32 hex chars → map 0-f to a-p.
|
|
26
|
+
*/
|
|
27
|
+
function deriveExtensionId(extensionDir) {
|
|
28
|
+
const hash = crypto_1.default.createHash("sha256").update(extensionDir).digest("hex").slice(0, 32);
|
|
29
|
+
return hash.split("").map(c => String.fromCharCode("a".charCodeAt(0) + parseInt(c, 16))).join("");
|
|
30
|
+
}
|
|
16
31
|
function resolveBundledExtensionDir() {
|
|
17
32
|
const dir = path_1.default.resolve(__dirname, "../extension");
|
|
18
33
|
const manifest = path_1.default.join(dir, "manifest.json");
|
|
@@ -21,10 +36,21 @@ function resolveBundledExtensionDir() {
|
|
|
21
36
|
}
|
|
22
37
|
return dir;
|
|
23
38
|
}
|
|
39
|
+
function resolveBundledHostPath() {
|
|
40
|
+
const p = path_1.default.resolve(__dirname, "../host", exports.HOST_BUNDLE_NAME);
|
|
41
|
+
if (!fs_1.default.existsSync(p)) {
|
|
42
|
+
throw new Error(`Bundled host not found at ${p}`);
|
|
43
|
+
}
|
|
44
|
+
return p;
|
|
45
|
+
}
|
|
24
46
|
function resolveInstalledExtensionDir(dataDir) {
|
|
25
|
-
const dir = dataDir ?? (0, config_1.resolveConfig)().
|
|
47
|
+
const dir = dataDir ?? (0, config_1.resolveConfig)().baseDataDir;
|
|
26
48
|
return path_1.default.join(dir, exports.EXTENSION_DIR_NAME);
|
|
27
49
|
}
|
|
50
|
+
function resolveInstalledHostPath(dataDir) {
|
|
51
|
+
const dir = dataDir ?? (0, config_1.resolveConfig)().baseDataDir;
|
|
52
|
+
return path_1.default.join(dir, exports.HOST_BUNDLE_NAME);
|
|
53
|
+
}
|
|
28
54
|
function readExtensionVersion(extensionDir) {
|
|
29
55
|
try {
|
|
30
56
|
const raw = fs_1.default.readFileSync(path_1.default.join(extensionDir, "manifest.json"), "utf-8");
|
|
@@ -35,6 +61,17 @@ function readExtensionVersion(extensionDir) {
|
|
|
35
61
|
return null;
|
|
36
62
|
}
|
|
37
63
|
}
|
|
64
|
+
/** Read the BASE_VERSION constant from a bundled host.bundle.js file. */
|
|
65
|
+
function readHostVersion(hostPath) {
|
|
66
|
+
try {
|
|
67
|
+
const content = fs_1.default.readFileSync(hostPath, "utf-8");
|
|
68
|
+
const match = content.match(/\bBASE_VERSION\s*=\s*"([^"]+)"/);
|
|
69
|
+
return match ? match[1] : null;
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
38
75
|
function syncExtension(dataDir) {
|
|
39
76
|
const bundledDir = resolveBundledExtensionDir();
|
|
40
77
|
const installedDir = resolveInstalledExtensionDir(dataDir);
|
|
@@ -52,6 +89,23 @@ function syncExtension(dataDir) {
|
|
|
52
89
|
extensionDir: installedDir,
|
|
53
90
|
};
|
|
54
91
|
}
|
|
92
|
+
function syncHost(dataDir) {
|
|
93
|
+
const bundledPath = resolveBundledHostPath();
|
|
94
|
+
const installedPath = resolveInstalledHostPath(dataDir);
|
|
95
|
+
const bundledVersion = readHostVersion(bundledPath);
|
|
96
|
+
const installedVersion = readHostVersion(installedPath);
|
|
97
|
+
const needsCopy = !fs_1.default.existsSync(installedPath) || bundledVersion !== installedVersion;
|
|
98
|
+
if (needsCopy) {
|
|
99
|
+
fs_1.default.mkdirSync(path_1.default.dirname(installedPath), { recursive: true });
|
|
100
|
+
fs_1.default.copyFileSync(bundledPath, installedPath);
|
|
101
|
+
}
|
|
102
|
+
return {
|
|
103
|
+
synced: needsCopy,
|
|
104
|
+
bundledVersion,
|
|
105
|
+
installedVersion,
|
|
106
|
+
hostPath: installedPath,
|
|
107
|
+
};
|
|
108
|
+
}
|
|
55
109
|
function checkExtensionSync(dataDir) {
|
|
56
110
|
const bundledDir = resolveBundledExtensionDir();
|
|
57
111
|
const installedDir = resolveInstalledExtensionDir(dataDir);
|
package/dist/shared/version.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.DIRTY = exports.GIT_SHA = exports.VERSION = exports.BASE_VERSION = void 0;
|
|
4
|
-
exports.BASE_VERSION = "0.
|
|
5
|
-
exports.VERSION = "0.
|
|
4
|
+
exports.BASE_VERSION = "0.3.1";
|
|
5
|
+
exports.VERSION = "0.3.1";
|
|
6
6
|
exports.GIT_SHA = null;
|
|
7
7
|
exports.DIRTY = false;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tabctl",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.1",
|
|
4
4
|
"description": "CLI tool to manage and analyze browser tabs",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -27,19 +27,22 @@
|
|
|
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/bundle-extension.js",
|
|
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",
|
|
31
31
|
"bump:major": "node scripts/bump-version.js major",
|
|
32
32
|
"bump:minor": "node scripts/bump-version.js minor",
|
|
33
33
|
"bump:patch": "node scripts/bump-version.js patch",
|
|
34
34
|
"test": "npm run build && node --test --test-timeout=5000 dist/tests/unit/*.js",
|
|
35
35
|
"test:unit": "npm run build && node --test --test-timeout=5000 dist/tests/unit/*.js",
|
|
36
36
|
"test:integration": "node dist/scripts/integration-test.js",
|
|
37
|
-
"clean": "
|
|
37
|
+
"clean": "node -e \"fs.rmSync('dist',{recursive:true,force:true})\" "
|
|
38
38
|
},
|
|
39
39
|
"devDependencies": {
|
|
40
40
|
"@types/chrome": "^0.0.277",
|
|
41
41
|
"@types/node": "^24",
|
|
42
42
|
"esbuild": "^0.27.3",
|
|
43
43
|
"typescript": "^5.4.5"
|
|
44
|
+
},
|
|
45
|
+
"optionalDependencies": {
|
|
46
|
+
"tabctl-win32-x64": "0.3.0"
|
|
44
47
|
}
|
|
45
48
|
}
|