tabctl 0.3.0 → 0.4.0
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 +7 -5
- package/dist/cli/lib/client.js +23 -2
- package/dist/cli/lib/commands/index.js +2 -1
- package/dist/cli/lib/commands/meta.js +16 -13
- package/dist/cli/lib/commands/params-groups.js +8 -0
- package/dist/cli/lib/commands/params.js +6 -3
- package/dist/cli/lib/commands/setup.js +104 -126
- package/dist/cli/lib/options-commands.js +13 -1
- package/dist/cli/lib/output.js +36 -1
- package/dist/cli/lib/policy-filter.js +1 -1
- package/dist/cli/lib/policy.js +3 -3
- package/dist/cli/lib/response.js +9 -9
- package/dist/cli/tabctl.js +9 -1
- package/dist/extension/background.js +429 -43
- package/dist/extension/lib/groups.js +89 -3
- package/dist/extension/lib/screenshot.js +2 -2
- package/dist/extension/lib/tabs.js +97 -36
- package/dist/extension/lib/undo-handlers.js +8 -0
- package/dist/extension/manifest.json +2 -2
- package/dist/host/host.bundle.js +669 -0
- package/dist/host/host.js +30 -11
- package/dist/host/launcher/go.mod +3 -0
- package/dist/host/launcher/main.go +109 -0
- package/dist/host/lib/handlers.js +7 -5
- package/dist/host/lib/undo.js +6 -6
- package/dist/shared/config.js +36 -13
- package/dist/shared/extension-sync.js +59 -15
- package/dist/shared/profiles.js +5 -5
- package/dist/shared/version.js +2 -2
- package/package.json +11 -4
package/dist/host/host.js
CHANGED
|
@@ -4,9 +4,9 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
4
4
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
5
5
|
};
|
|
6
6
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
-
const
|
|
8
|
-
const
|
|
9
|
-
const
|
|
7
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
8
|
+
const node_net_1 = __importDefault(require("node:net"));
|
|
9
|
+
const node_crypto_1 = __importDefault(require("node:crypto"));
|
|
10
10
|
const config_1 = require("../shared/config");
|
|
11
11
|
const handlers_1 = require("./lib/handlers");
|
|
12
12
|
let config;
|
|
@@ -25,10 +25,10 @@ function log(...args) {
|
|
|
25
25
|
process.stderr.write(`[tabctl-host] ${args.join(" ")}\n`);
|
|
26
26
|
}
|
|
27
27
|
function ensureDir() {
|
|
28
|
-
|
|
28
|
+
node_fs_1.default.mkdirSync(SOCKET_DIR, { recursive: true, mode: 0o700 });
|
|
29
29
|
}
|
|
30
30
|
function createId(prefix) {
|
|
31
|
-
return `${prefix}-${Date.now()}-${
|
|
31
|
+
return `${prefix}-${Date.now()}-${node_crypto_1.default.randomBytes(4).toString("hex")}`;
|
|
32
32
|
}
|
|
33
33
|
function sendNative(message) {
|
|
34
34
|
const json = JSON.stringify(message);
|
|
@@ -71,10 +71,11 @@ process.stdin.on("end", () => {
|
|
|
71
71
|
});
|
|
72
72
|
function startSocketServer() {
|
|
73
73
|
ensureDir();
|
|
74
|
-
|
|
75
|
-
|
|
74
|
+
// Named pipes on Windows don't use filesystem paths; skip cleanup
|
|
75
|
+
if (process.platform !== "win32" && node_fs_1.default.existsSync(SOCKET_PATH)) {
|
|
76
|
+
node_fs_1.default.unlinkSync(SOCKET_PATH);
|
|
76
77
|
}
|
|
77
|
-
const server =
|
|
78
|
+
const server = node_net_1.default.createServer((socket) => {
|
|
78
79
|
socket.setEncoding("utf8");
|
|
79
80
|
let buffer = "";
|
|
80
81
|
socket.on("data", (data) => {
|
|
@@ -101,16 +102,34 @@ function startSocketServer() {
|
|
|
101
102
|
log("CLI socket error", error.message);
|
|
102
103
|
});
|
|
103
104
|
});
|
|
105
|
+
let retries = 0;
|
|
106
|
+
const maxRetries = process.platform === "win32" ? 5 : 0;
|
|
107
|
+
server.on("error", (err) => {
|
|
108
|
+
if (err.code === "EADDRINUSE" && retries < maxRetries) {
|
|
109
|
+
retries++;
|
|
110
|
+
log(`Socket in use, retrying (${retries}/${maxRetries})…`);
|
|
111
|
+
setTimeout(() => server.listen(SOCKET_PATH), 500);
|
|
112
|
+
}
|
|
113
|
+
else {
|
|
114
|
+
throw err;
|
|
115
|
+
}
|
|
116
|
+
});
|
|
104
117
|
server.listen(SOCKET_PATH, () => {
|
|
105
|
-
|
|
118
|
+
if (process.platform !== "win32") {
|
|
119
|
+
try {
|
|
120
|
+
node_fs_1.default.chmodSync(SOCKET_PATH, 0o600);
|
|
121
|
+
}
|
|
122
|
+
catch { /* ignore on platforms without chmod */ }
|
|
123
|
+
}
|
|
106
124
|
log(`Listening on ${SOCKET_PATH}`);
|
|
107
125
|
});
|
|
108
126
|
return server;
|
|
109
127
|
}
|
|
110
128
|
function cleanupAndExit(code) {
|
|
111
129
|
try {
|
|
112
|
-
|
|
113
|
-
|
|
130
|
+
// Named pipes on Windows don't need filesystem cleanup
|
|
131
|
+
if (process.platform !== "win32" && node_fs_1.default.existsSync(SOCKET_PATH)) {
|
|
132
|
+
node_fs_1.default.unlinkSync(SOCKET_PATH);
|
|
114
133
|
}
|
|
115
134
|
}
|
|
116
135
|
catch {
|
|
@@ -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
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.LOCAL_ACTIONS = exports.UNDO_ACTIONS = void 0;
|
|
3
4
|
exports.respond = respond;
|
|
4
5
|
exports.refreshTimeout = refreshTimeout;
|
|
5
6
|
exports.forwardToExtension = forwardToExtension;
|
|
@@ -11,17 +12,18 @@ const REQUEST_TIMEOUT_MS = 30000;
|
|
|
11
12
|
const MAX_RESPONSE_BYTES = 20 * 1024 * 1024;
|
|
12
13
|
const HISTORY_LIMIT_DEFAULT = 20;
|
|
13
14
|
const RETENTION_DAYS = 30;
|
|
14
|
-
|
|
15
|
+
exports.UNDO_ACTIONS = new Set([
|
|
15
16
|
"archive",
|
|
16
17
|
"close",
|
|
17
18
|
"group-update",
|
|
18
19
|
"group-ungroup",
|
|
19
20
|
"group-assign",
|
|
21
|
+
"group-gather",
|
|
20
22
|
"move-tab",
|
|
21
23
|
"move-group",
|
|
22
24
|
"merge-window",
|
|
23
25
|
]);
|
|
24
|
-
|
|
26
|
+
exports.LOCAL_ACTIONS = new Set(["history", "undo", "version"]);
|
|
25
27
|
function respond(socket, payload) {
|
|
26
28
|
const serialized = JSON.stringify(payload);
|
|
27
29
|
if (Buffer.byteLength(serialized, "utf8") > MAX_RESPONSE_BYTES) {
|
|
@@ -58,7 +60,7 @@ function forwardToExtension(deps, socket, request, overrides = {}) {
|
|
|
58
60
|
if (txid) {
|
|
59
61
|
params.txid = txid;
|
|
60
62
|
}
|
|
61
|
-
if (!LOCAL_ACTIONS.has(request.action)) {
|
|
63
|
+
if (!exports.LOCAL_ACTIONS.has(request.action)) {
|
|
62
64
|
params.client = {
|
|
63
65
|
component: "host",
|
|
64
66
|
version: version_1.VERSION,
|
|
@@ -156,7 +158,7 @@ function handleNativeMessage(deps, payload) {
|
|
|
156
158
|
});
|
|
157
159
|
return;
|
|
158
160
|
}
|
|
159
|
-
if (UNDO_ACTIONS.has(pendingRequest.action)) {
|
|
161
|
+
if (exports.UNDO_ACTIONS.has(pendingRequest.action)) {
|
|
160
162
|
const record = {
|
|
161
163
|
txid: pendingRequest.txid,
|
|
162
164
|
createdAt: Date.now(),
|
|
@@ -316,7 +318,7 @@ function handleCliRequest(deps, socket, request) {
|
|
|
316
318
|
}, { txid });
|
|
317
319
|
return;
|
|
318
320
|
}
|
|
319
|
-
if (UNDO_ACTIONS.has(action)) {
|
|
321
|
+
if (exports.UNDO_ACTIONS.has(action)) {
|
|
320
322
|
const txid = deps.createId("tx");
|
|
321
323
|
forwardToExtension(deps, socket, request, { txid });
|
|
322
324
|
return;
|
package/dist/host/lib/undo.js
CHANGED
|
@@ -8,17 +8,17 @@ exports.readUndoRecords = readUndoRecords;
|
|
|
8
8
|
exports.filterByRetention = filterByRetention;
|
|
9
9
|
exports.findUndoRecord = findUndoRecord;
|
|
10
10
|
exports.findLatestUndoRecord = findLatestUndoRecord;
|
|
11
|
-
const
|
|
12
|
-
const
|
|
11
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
12
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
13
13
|
const DEFAULT_RETENTION_DAYS = 30;
|
|
14
14
|
function appendUndoRecord(filePath, record) {
|
|
15
|
-
const dir =
|
|
16
|
-
|
|
17
|
-
|
|
15
|
+
const dir = node_path_1.default.dirname(filePath);
|
|
16
|
+
node_fs_1.default.mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
17
|
+
node_fs_1.default.appendFileSync(filePath, `${JSON.stringify(record)}\n`, "utf8");
|
|
18
18
|
}
|
|
19
19
|
function readUndoRecords(filePath) {
|
|
20
20
|
try {
|
|
21
|
-
const content =
|
|
21
|
+
const content = node_fs_1.default.readFileSync(filePath, "utf8");
|
|
22
22
|
const lines = content.split("\n").filter(Boolean);
|
|
23
23
|
const records = [];
|
|
24
24
|
for (const line of lines) {
|
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
|
-
const
|
|
10
|
-
const
|
|
11
|
-
const
|
|
10
|
+
const node_os_1 = __importDefault(require("node:os"));
|
|
11
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
12
|
+
const node_crypto_1 = __importDefault(require("node:crypto"));
|
|
13
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
14
|
+
function defaultConfigBase() {
|
|
15
|
+
if (process.platform === "win32") {
|
|
16
|
+
return process.env.APPDATA || node_path_1.default.join(node_os_1.default.homedir(), "AppData", "Roaming");
|
|
17
|
+
}
|
|
18
|
+
return node_path_1.default.join(node_os_1.default.homedir(), ".config");
|
|
19
|
+
}
|
|
20
|
+
function defaultStateBase() {
|
|
21
|
+
if (process.platform === "win32") {
|
|
22
|
+
return process.env.LOCALAPPDATA || node_path_1.default.join(node_os_1.default.homedir(), "AppData", "Local");
|
|
23
|
+
}
|
|
24
|
+
return node_path_1.default.join(node_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 = node_crypto_1.default.createHash("sha256").update(dataDir).digest("hex").slice(0, 12);
|
|
31
|
+
return `\\\\.\\pipe\\tabctl-${hash}`;
|
|
32
|
+
}
|
|
33
|
+
return node_path_1.default.join(dataDir, "tabctl.sock");
|
|
34
|
+
}
|
|
12
35
|
let cached;
|
|
13
36
|
function resetConfig() {
|
|
14
37
|
cached = undefined;
|
|
@@ -25,11 +48,11 @@ function resolveConfig(profileName) {
|
|
|
25
48
|
return cached;
|
|
26
49
|
// Config dir resolution
|
|
27
50
|
const configDir = process.env.TABCTL_CONFIG_DIR
|
|
28
|
-
||
|
|
51
|
+
|| node_path_1.default.join(process.env.XDG_CONFIG_HOME || defaultConfigBase(), "tabctl");
|
|
29
52
|
// Read optional config.json
|
|
30
53
|
let fileConfig = {};
|
|
31
54
|
try {
|
|
32
|
-
const raw =
|
|
55
|
+
const raw = node_fs_1.default.readFileSync(node_path_1.default.join(configDir, "config.json"), "utf-8");
|
|
33
56
|
fileConfig = JSON.parse(raw);
|
|
34
57
|
}
|
|
35
58
|
catch {
|
|
@@ -39,15 +62,15 @@ function resolveConfig(profileName) {
|
|
|
39
62
|
let dataDir;
|
|
40
63
|
if (typeof fileConfig.dataDir === "string" && fileConfig.dataDir) {
|
|
41
64
|
dataDir = expandEnvVars(fileConfig.dataDir);
|
|
42
|
-
if (!
|
|
65
|
+
if (!node_path_1.default.isAbsolute(dataDir)) {
|
|
43
66
|
throw new Error(`dataDir in config.json must be an absolute path (got: ${dataDir}). Use $HOME or full paths.`);
|
|
44
67
|
}
|
|
45
68
|
}
|
|
46
69
|
else if (process.env.TABCTL_CONFIG_DIR) {
|
|
47
|
-
dataDir =
|
|
70
|
+
dataDir = node_path_1.default.join(configDir, "data");
|
|
48
71
|
}
|
|
49
72
|
else {
|
|
50
|
-
dataDir =
|
|
73
|
+
dataDir = node_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)
|
|
@@ -56,7 +79,7 @@ function resolveConfig(profileName) {
|
|
|
56
79
|
let activeProfileName;
|
|
57
80
|
if (effectiveProfile) {
|
|
58
81
|
try {
|
|
59
|
-
const raw =
|
|
82
|
+
const raw = node_fs_1.default.readFileSync(node_path_1.default.join(configDir, "profiles.json"), "utf-8");
|
|
60
83
|
const registry = JSON.parse(raw);
|
|
61
84
|
const profile = registry.profiles[effectiveProfile];
|
|
62
85
|
if (profile) {
|
|
@@ -82,7 +105,7 @@ function resolveConfig(profileName) {
|
|
|
82
105
|
else {
|
|
83
106
|
// No explicit profile — check for a default in profiles.json
|
|
84
107
|
try {
|
|
85
|
-
const raw =
|
|
108
|
+
const raw = node_fs_1.default.readFileSync(node_path_1.default.join(configDir, "profiles.json"), "utf-8");
|
|
86
109
|
const registry = JSON.parse(raw);
|
|
87
110
|
if (registry.default && registry.profiles[registry.default]) {
|
|
88
111
|
dataDir = registry.profiles[registry.default].dataDir;
|
|
@@ -97,10 +120,10 @@ function resolveConfig(profileName) {
|
|
|
97
120
|
configDir,
|
|
98
121
|
dataDir,
|
|
99
122
|
baseDataDir,
|
|
100
|
-
socketPath: process.env.TABCTL_SOCKET ||
|
|
101
|
-
undoLog:
|
|
123
|
+
socketPath: process.env.TABCTL_SOCKET || resolveSocketPath(dataDir),
|
|
124
|
+
undoLog: node_path_1.default.join(dataDir, "undo.jsonl"),
|
|
102
125
|
wrapperDir: dataDir,
|
|
103
|
-
policyPath:
|
|
126
|
+
policyPath: node_path_1.default.join(configDir, "policy.json"),
|
|
104
127
|
activeProfileName,
|
|
105
128
|
};
|
|
106
129
|
// Only cache no-arg calls
|
|
@@ -3,41 +3,57 @@ 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
7
|
exports.deriveExtensionId = deriveExtensionId;
|
|
8
8
|
exports.resolveBundledExtensionDir = resolveBundledExtensionDir;
|
|
9
|
+
exports.resolveBundledHostPath = resolveBundledHostPath;
|
|
9
10
|
exports.resolveInstalledExtensionDir = resolveInstalledExtensionDir;
|
|
11
|
+
exports.resolveInstalledHostPath = resolveInstalledHostPath;
|
|
10
12
|
exports.readExtensionVersion = readExtensionVersion;
|
|
13
|
+
exports.readHostVersion = readHostVersion;
|
|
11
14
|
exports.syncExtension = syncExtension;
|
|
15
|
+
exports.syncHost = syncHost;
|
|
12
16
|
exports.checkExtensionSync = checkExtensionSync;
|
|
13
|
-
const
|
|
14
|
-
const
|
|
15
|
-
const
|
|
17
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
18
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
19
|
+
const node_crypto_1 = __importDefault(require("node:crypto"));
|
|
16
20
|
const config_1 = require("./config");
|
|
17
21
|
exports.EXTENSION_DIR_NAME = "extension";
|
|
22
|
+
exports.HOST_BUNDLE_NAME = "host.bundle.js";
|
|
18
23
|
/**
|
|
19
24
|
* Derive the Chrome/Edge extension ID for an unpacked extension path.
|
|
20
25
|
* Chromium computes: SHA256(absolute_path) → first 32 hex chars → map 0-f to a-p.
|
|
21
26
|
*/
|
|
22
27
|
function deriveExtensionId(extensionDir) {
|
|
23
|
-
const hash =
|
|
28
|
+
const hash = node_crypto_1.default.createHash("sha256").update(extensionDir).digest("hex").slice(0, 32);
|
|
24
29
|
return hash.split("").map(c => String.fromCharCode("a".charCodeAt(0) + parseInt(c, 16))).join("");
|
|
25
30
|
}
|
|
26
31
|
function resolveBundledExtensionDir() {
|
|
27
|
-
const dir =
|
|
28
|
-
const manifest =
|
|
29
|
-
if (!
|
|
32
|
+
const dir = node_path_1.default.resolve(__dirname, "../extension");
|
|
33
|
+
const manifest = node_path_1.default.join(dir, "manifest.json");
|
|
34
|
+
if (!node_fs_1.default.existsSync(dir) || !node_fs_1.default.existsSync(manifest)) {
|
|
30
35
|
throw new Error(`Bundled extension not found at ${dir}`);
|
|
31
36
|
}
|
|
32
37
|
return dir;
|
|
33
38
|
}
|
|
39
|
+
function resolveBundledHostPath() {
|
|
40
|
+
const p = node_path_1.default.resolve(__dirname, "../host", exports.HOST_BUNDLE_NAME);
|
|
41
|
+
if (!node_fs_1.default.existsSync(p)) {
|
|
42
|
+
throw new Error(`Bundled host not found at ${p}`);
|
|
43
|
+
}
|
|
44
|
+
return p;
|
|
45
|
+
}
|
|
34
46
|
function resolveInstalledExtensionDir(dataDir) {
|
|
35
|
-
const dir = dataDir ?? (0, config_1.resolveConfig)().
|
|
36
|
-
return
|
|
47
|
+
const dir = dataDir ?? (0, config_1.resolveConfig)().baseDataDir;
|
|
48
|
+
return node_path_1.default.join(dir, exports.EXTENSION_DIR_NAME);
|
|
49
|
+
}
|
|
50
|
+
function resolveInstalledHostPath(dataDir) {
|
|
51
|
+
const dir = dataDir ?? (0, config_1.resolveConfig)().baseDataDir;
|
|
52
|
+
return node_path_1.default.join(dir, exports.HOST_BUNDLE_NAME);
|
|
37
53
|
}
|
|
38
54
|
function readExtensionVersion(extensionDir) {
|
|
39
55
|
try {
|
|
40
|
-
const raw =
|
|
56
|
+
const raw = node_fs_1.default.readFileSync(node_path_1.default.join(extensionDir, "manifest.json"), "utf-8");
|
|
41
57
|
const manifest = JSON.parse(raw);
|
|
42
58
|
return typeof manifest.version === "string" ? manifest.version : null;
|
|
43
59
|
}
|
|
@@ -45,15 +61,26 @@ function readExtensionVersion(extensionDir) {
|
|
|
45
61
|
return null;
|
|
46
62
|
}
|
|
47
63
|
}
|
|
64
|
+
/** Read the BASE_VERSION constant from a bundled host.bundle.js file. */
|
|
65
|
+
function readHostVersion(hostPath) {
|
|
66
|
+
try {
|
|
67
|
+
const content = node_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
|
+
}
|
|
48
75
|
function syncExtension(dataDir) {
|
|
49
76
|
const bundledDir = resolveBundledExtensionDir();
|
|
50
77
|
const installedDir = resolveInstalledExtensionDir(dataDir);
|
|
51
78
|
const bundledVersion = readExtensionVersion(bundledDir);
|
|
52
79
|
const installedVersion = readExtensionVersion(installedDir);
|
|
53
|
-
const needsCopy = !
|
|
80
|
+
const needsCopy = !node_fs_1.default.existsSync(installedDir) || bundledVersion !== installedVersion;
|
|
54
81
|
if (needsCopy) {
|
|
55
|
-
|
|
56
|
-
|
|
82
|
+
node_fs_1.default.mkdirSync(installedDir, { recursive: true });
|
|
83
|
+
node_fs_1.default.cpSync(bundledDir, installedDir, { recursive: true });
|
|
57
84
|
}
|
|
58
85
|
return {
|
|
59
86
|
synced: needsCopy,
|
|
@@ -62,12 +89,29 @@ function syncExtension(dataDir) {
|
|
|
62
89
|
extensionDir: installedDir,
|
|
63
90
|
};
|
|
64
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 = !node_fs_1.default.existsSync(installedPath) || bundledVersion !== installedVersion;
|
|
98
|
+
if (needsCopy) {
|
|
99
|
+
node_fs_1.default.mkdirSync(node_path_1.default.dirname(installedPath), { recursive: true });
|
|
100
|
+
node_fs_1.default.copyFileSync(bundledPath, installedPath);
|
|
101
|
+
}
|
|
102
|
+
return {
|
|
103
|
+
synced: needsCopy,
|
|
104
|
+
bundledVersion,
|
|
105
|
+
installedVersion,
|
|
106
|
+
hostPath: installedPath,
|
|
107
|
+
};
|
|
108
|
+
}
|
|
65
109
|
function checkExtensionSync(dataDir) {
|
|
66
110
|
const bundledDir = resolveBundledExtensionDir();
|
|
67
111
|
const installedDir = resolveInstalledExtensionDir(dataDir);
|
|
68
112
|
const bundledVersion = readExtensionVersion(bundledDir);
|
|
69
113
|
const installedVersion = readExtensionVersion(installedDir);
|
|
70
|
-
const exists =
|
|
114
|
+
const exists = node_fs_1.default.existsSync(installedDir) && installedVersion !== null;
|
|
71
115
|
const needsSync = !exists || bundledVersion !== installedVersion;
|
|
72
116
|
const needsReload = exists && bundledVersion !== installedVersion;
|
|
73
117
|
return {
|
package/dist/shared/profiles.js
CHANGED
|
@@ -11,8 +11,8 @@ exports.addProfile = addProfile;
|
|
|
11
11
|
exports.removeProfile = removeProfile;
|
|
12
12
|
exports.getActiveProfile = getActiveProfile;
|
|
13
13
|
exports.listProfiles = listProfiles;
|
|
14
|
-
const
|
|
15
|
-
const
|
|
14
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
15
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
16
16
|
const config_1 = require("./config");
|
|
17
17
|
exports.PROFILE_NAME_PATTERN = /^[a-z0-9-]+$/;
|
|
18
18
|
exports.PROFILES_FILE = "profiles.json";
|
|
@@ -24,7 +24,7 @@ function validateProfileName(name) {
|
|
|
24
24
|
function loadProfiles(configDir) {
|
|
25
25
|
const dir = configDir ?? (0, config_1.resolveConfig)().configDir;
|
|
26
26
|
try {
|
|
27
|
-
const raw =
|
|
27
|
+
const raw = node_fs_1.default.readFileSync(node_path_1.default.join(dir, exports.PROFILES_FILE), "utf-8");
|
|
28
28
|
return JSON.parse(raw);
|
|
29
29
|
}
|
|
30
30
|
catch {
|
|
@@ -33,8 +33,8 @@ function loadProfiles(configDir) {
|
|
|
33
33
|
}
|
|
34
34
|
function saveProfiles(registry, configDir) {
|
|
35
35
|
const dir = configDir ?? (0, config_1.resolveConfig)().configDir;
|
|
36
|
-
|
|
37
|
-
|
|
36
|
+
node_fs_1.default.mkdirSync(dir, { recursive: true });
|
|
37
|
+
node_fs_1.default.writeFileSync(node_path_1.default.join(dir, exports.PROFILES_FILE), JSON.stringify(registry, null, 2) + "\n");
|
|
38
38
|
}
|
|
39
39
|
function addProfile(name, entry, configDir) {
|
|
40
40
|
validateProfileName(name);
|
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.4.0";
|
|
5
|
+
exports.VERSION = "0.4.0";
|
|
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.4.0",
|
|
4
4
|
"description": "CLI tool to manage and analyze browser tabs",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
"chrome"
|
|
16
16
|
],
|
|
17
17
|
"engines": {
|
|
18
|
-
"node": ">=
|
|
18
|
+
"node": ">=24"
|
|
19
19
|
},
|
|
20
20
|
"bin": {
|
|
21
21
|
"tabctl": "dist/cli/tabctl.js"
|
|
@@ -27,19 +27,26 @@
|
|
|
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
|
+
"prepare": "git rev-parse --git-dir >/dev/null 2>&1 && git config core.hooksPath .githooks || true"
|
|
38
39
|
},
|
|
39
40
|
"devDependencies": {
|
|
40
41
|
"@types/chrome": "^0.0.277",
|
|
41
42
|
"@types/node": "^24",
|
|
42
43
|
"esbuild": "^0.27.3",
|
|
43
44
|
"typescript": "^5.4.5"
|
|
45
|
+
},
|
|
46
|
+
"optionalDependencies": {
|
|
47
|
+
"tabctl-win32-x64": "0.3.0"
|
|
48
|
+
},
|
|
49
|
+
"dependencies": {
|
|
50
|
+
"normalize-url": "^8.1.1"
|
|
44
51
|
}
|
|
45
52
|
}
|