tabctl 0.3.1 → 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/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 fs_1 = __importDefault(require("fs"));
8
- const net_1 = __importDefault(require("net"));
9
- const crypto_1 = __importDefault(require("crypto"));
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
- fs_1.default.mkdirSync(SOCKET_DIR, { recursive: true, mode: 0o700 });
28
+ node_fs_1.default.mkdirSync(SOCKET_DIR, { recursive: true, mode: 0o700 });
29
29
  }
30
30
  function createId(prefix) {
31
- return `${prefix}-${Date.now()}-${crypto_1.default.randomBytes(4).toString("hex")}`;
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);
@@ -72,10 +72,10 @@ process.stdin.on("end", () => {
72
72
  function startSocketServer() {
73
73
  ensureDir();
74
74
  // Named pipes on Windows don't use filesystem paths; skip cleanup
75
- if (process.platform !== "win32" && fs_1.default.existsSync(SOCKET_PATH)) {
76
- fs_1.default.unlinkSync(SOCKET_PATH);
75
+ if (process.platform !== "win32" && node_fs_1.default.existsSync(SOCKET_PATH)) {
76
+ node_fs_1.default.unlinkSync(SOCKET_PATH);
77
77
  }
78
- const server = net_1.default.createServer((socket) => {
78
+ const server = node_net_1.default.createServer((socket) => {
79
79
  socket.setEncoding("utf8");
80
80
  let buffer = "";
81
81
  socket.on("data", (data) => {
@@ -117,7 +117,7 @@ function startSocketServer() {
117
117
  server.listen(SOCKET_PATH, () => {
118
118
  if (process.platform !== "win32") {
119
119
  try {
120
- fs_1.default.chmodSync(SOCKET_PATH, 0o600);
120
+ node_fs_1.default.chmodSync(SOCKET_PATH, 0o600);
121
121
  }
122
122
  catch { /* ignore on platforms without chmod */ }
123
123
  }
@@ -128,8 +128,8 @@ function startSocketServer() {
128
128
  function cleanupAndExit(code) {
129
129
  try {
130
130
  // Named pipes on Windows don't need filesystem cleanup
131
- if (process.platform !== "win32" && fs_1.default.existsSync(SOCKET_PATH)) {
132
- fs_1.default.unlinkSync(SOCKET_PATH);
131
+ if (process.platform !== "win32" && node_fs_1.default.existsSync(SOCKET_PATH)) {
132
+ node_fs_1.default.unlinkSync(SOCKET_PATH);
133
133
  }
134
134
  }
135
135
  catch {
@@ -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
- const UNDO_ACTIONS = new Set([
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
- const LOCAL_ACTIONS = new Set(["history", "undo", "version"]);
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;
@@ -8,17 +8,17 @@ exports.readUndoRecords = readUndoRecords;
8
8
  exports.filterByRetention = filterByRetention;
9
9
  exports.findUndoRecord = findUndoRecord;
10
10
  exports.findLatestUndoRecord = findLatestUndoRecord;
11
- const fs_1 = __importDefault(require("fs"));
12
- const path_1 = __importDefault(require("path"));
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 = path_1.default.dirname(filePath);
16
- fs_1.default.mkdirSync(dir, { recursive: true, mode: 0o700 });
17
- fs_1.default.appendFileSync(filePath, `${JSON.stringify(record)}\n`, "utf8");
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 = fs_1.default.readFileSync(filePath, "utf8");
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) {
@@ -7,30 +7,30 @@ exports.resolveSocketPath = resolveSocketPath;
7
7
  exports.resetConfig = resetConfig;
8
8
  exports.expandEnvVars = expandEnvVars;
9
9
  exports.resolveConfig = resolveConfig;
10
- const os_1 = __importDefault(require("os"));
11
- const path_1 = __importDefault(require("path"));
12
- const crypto_1 = __importDefault(require("crypto"));
13
- const fs_1 = __importDefault(require("fs"));
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
14
  function defaultConfigBase() {
15
15
  if (process.platform === "win32") {
16
- return process.env.APPDATA || path_1.default.join(os_1.default.homedir(), "AppData", "Roaming");
16
+ return process.env.APPDATA || node_path_1.default.join(node_os_1.default.homedir(), "AppData", "Roaming");
17
17
  }
18
- return path_1.default.join(os_1.default.homedir(), ".config");
18
+ return node_path_1.default.join(node_os_1.default.homedir(), ".config");
19
19
  }
20
20
  function defaultStateBase() {
21
21
  if (process.platform === "win32") {
22
- return process.env.LOCALAPPDATA || path_1.default.join(os_1.default.homedir(), "AppData", "Local");
22
+ return process.env.LOCALAPPDATA || node_path_1.default.join(node_os_1.default.homedir(), "AppData", "Local");
23
23
  }
24
- return path_1.default.join(os_1.default.homedir(), ".local", "state");
24
+ return node_path_1.default.join(node_os_1.default.homedir(), ".local", "state");
25
25
  }
26
26
  /** Resolve the IPC socket/pipe path for the given data directory. */
27
27
  function resolveSocketPath(dataDir) {
28
28
  if (process.platform === "win32") {
29
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);
30
+ const hash = node_crypto_1.default.createHash("sha256").update(dataDir).digest("hex").slice(0, 12);
31
31
  return `\\\\.\\pipe\\tabctl-${hash}`;
32
32
  }
33
- return path_1.default.join(dataDir, "tabctl.sock");
33
+ return node_path_1.default.join(dataDir, "tabctl.sock");
34
34
  }
35
35
  let cached;
36
36
  function resetConfig() {
@@ -48,11 +48,11 @@ function resolveConfig(profileName) {
48
48
  return cached;
49
49
  // Config dir resolution
50
50
  const configDir = process.env.TABCTL_CONFIG_DIR
51
- || path_1.default.join(process.env.XDG_CONFIG_HOME || defaultConfigBase(), "tabctl");
51
+ || node_path_1.default.join(process.env.XDG_CONFIG_HOME || defaultConfigBase(), "tabctl");
52
52
  // Read optional config.json
53
53
  let fileConfig = {};
54
54
  try {
55
- const raw = fs_1.default.readFileSync(path_1.default.join(configDir, "config.json"), "utf-8");
55
+ const raw = node_fs_1.default.readFileSync(node_path_1.default.join(configDir, "config.json"), "utf-8");
56
56
  fileConfig = JSON.parse(raw);
57
57
  }
58
58
  catch {
@@ -62,15 +62,15 @@ function resolveConfig(profileName) {
62
62
  let dataDir;
63
63
  if (typeof fileConfig.dataDir === "string" && fileConfig.dataDir) {
64
64
  dataDir = expandEnvVars(fileConfig.dataDir);
65
- if (!path_1.default.isAbsolute(dataDir)) {
65
+ if (!node_path_1.default.isAbsolute(dataDir)) {
66
66
  throw new Error(`dataDir in config.json must be an absolute path (got: ${dataDir}). Use $HOME or full paths.`);
67
67
  }
68
68
  }
69
69
  else if (process.env.TABCTL_CONFIG_DIR) {
70
- dataDir = path_1.default.join(configDir, "data");
70
+ dataDir = node_path_1.default.join(configDir, "data");
71
71
  }
72
72
  else {
73
- dataDir = path_1.default.join(process.env.XDG_STATE_HOME || defaultStateBase(), "tabctl");
73
+ dataDir = node_path_1.default.join(process.env.XDG_STATE_HOME || defaultStateBase(), "tabctl");
74
74
  }
75
75
  const baseDataDir = dataDir;
76
76
  // Profile resolution (read profiles.json inline to avoid circular import)
@@ -79,7 +79,7 @@ function resolveConfig(profileName) {
79
79
  let activeProfileName;
80
80
  if (effectiveProfile) {
81
81
  try {
82
- const raw = fs_1.default.readFileSync(path_1.default.join(configDir, "profiles.json"), "utf-8");
82
+ const raw = node_fs_1.default.readFileSync(node_path_1.default.join(configDir, "profiles.json"), "utf-8");
83
83
  const registry = JSON.parse(raw);
84
84
  const profile = registry.profiles[effectiveProfile];
85
85
  if (profile) {
@@ -105,7 +105,7 @@ function resolveConfig(profileName) {
105
105
  else {
106
106
  // No explicit profile — check for a default in profiles.json
107
107
  try {
108
- const raw = fs_1.default.readFileSync(path_1.default.join(configDir, "profiles.json"), "utf-8");
108
+ const raw = node_fs_1.default.readFileSync(node_path_1.default.join(configDir, "profiles.json"), "utf-8");
109
109
  const registry = JSON.parse(raw);
110
110
  if (registry.default && registry.profiles[registry.default]) {
111
111
  dataDir = registry.profiles[registry.default].dataDir;
@@ -121,9 +121,9 @@ function resolveConfig(profileName) {
121
121
  dataDir,
122
122
  baseDataDir,
123
123
  socketPath: process.env.TABCTL_SOCKET || resolveSocketPath(dataDir),
124
- undoLog: path_1.default.join(dataDir, "undo.jsonl"),
124
+ undoLog: node_path_1.default.join(dataDir, "undo.jsonl"),
125
125
  wrapperDir: dataDir,
126
- policyPath: path_1.default.join(configDir, "policy.json"),
126
+ policyPath: node_path_1.default.join(configDir, "policy.json"),
127
127
  activeProfileName,
128
128
  };
129
129
  // Only cache no-arg calls
@@ -14,9 +14,9 @@ exports.readHostVersion = readHostVersion;
14
14
  exports.syncExtension = syncExtension;
15
15
  exports.syncHost = syncHost;
16
16
  exports.checkExtensionSync = checkExtensionSync;
17
- const path_1 = __importDefault(require("path"));
18
- const fs_1 = __importDefault(require("fs"));
19
- const crypto_1 = __importDefault(require("crypto"));
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"));
20
20
  const config_1 = require("./config");
21
21
  exports.EXTENSION_DIR_NAME = "extension";
22
22
  exports.HOST_BUNDLE_NAME = "host.bundle.js";
@@ -25,35 +25,35 @@ exports.HOST_BUNDLE_NAME = "host.bundle.js";
25
25
  * Chromium computes: SHA256(absolute_path) → first 32 hex chars → map 0-f to a-p.
26
26
  */
27
27
  function deriveExtensionId(extensionDir) {
28
- const hash = crypto_1.default.createHash("sha256").update(extensionDir).digest("hex").slice(0, 32);
28
+ const hash = node_crypto_1.default.createHash("sha256").update(extensionDir).digest("hex").slice(0, 32);
29
29
  return hash.split("").map(c => String.fromCharCode("a".charCodeAt(0) + parseInt(c, 16))).join("");
30
30
  }
31
31
  function resolveBundledExtensionDir() {
32
- const dir = path_1.default.resolve(__dirname, "../extension");
33
- const manifest = path_1.default.join(dir, "manifest.json");
34
- if (!fs_1.default.existsSync(dir) || !fs_1.default.existsSync(manifest)) {
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)) {
35
35
  throw new Error(`Bundled extension not found at ${dir}`);
36
36
  }
37
37
  return dir;
38
38
  }
39
39
  function resolveBundledHostPath() {
40
- const p = path_1.default.resolve(__dirname, "../host", exports.HOST_BUNDLE_NAME);
41
- if (!fs_1.default.existsSync(p)) {
40
+ const p = node_path_1.default.resolve(__dirname, "../host", exports.HOST_BUNDLE_NAME);
41
+ if (!node_fs_1.default.existsSync(p)) {
42
42
  throw new Error(`Bundled host not found at ${p}`);
43
43
  }
44
44
  return p;
45
45
  }
46
46
  function resolveInstalledExtensionDir(dataDir) {
47
47
  const dir = dataDir ?? (0, config_1.resolveConfig)().baseDataDir;
48
- return path_1.default.join(dir, exports.EXTENSION_DIR_NAME);
48
+ return node_path_1.default.join(dir, exports.EXTENSION_DIR_NAME);
49
49
  }
50
50
  function resolveInstalledHostPath(dataDir) {
51
51
  const dir = dataDir ?? (0, config_1.resolveConfig)().baseDataDir;
52
- return path_1.default.join(dir, exports.HOST_BUNDLE_NAME);
52
+ return node_path_1.default.join(dir, exports.HOST_BUNDLE_NAME);
53
53
  }
54
54
  function readExtensionVersion(extensionDir) {
55
55
  try {
56
- const raw = fs_1.default.readFileSync(path_1.default.join(extensionDir, "manifest.json"), "utf-8");
56
+ const raw = node_fs_1.default.readFileSync(node_path_1.default.join(extensionDir, "manifest.json"), "utf-8");
57
57
  const manifest = JSON.parse(raw);
58
58
  return typeof manifest.version === "string" ? manifest.version : null;
59
59
  }
@@ -64,7 +64,7 @@ function readExtensionVersion(extensionDir) {
64
64
  /** Read the BASE_VERSION constant from a bundled host.bundle.js file. */
65
65
  function readHostVersion(hostPath) {
66
66
  try {
67
- const content = fs_1.default.readFileSync(hostPath, "utf-8");
67
+ const content = node_fs_1.default.readFileSync(hostPath, "utf-8");
68
68
  const match = content.match(/\bBASE_VERSION\s*=\s*"([^"]+)"/);
69
69
  return match ? match[1] : null;
70
70
  }
@@ -77,10 +77,10 @@ function syncExtension(dataDir) {
77
77
  const installedDir = resolveInstalledExtensionDir(dataDir);
78
78
  const bundledVersion = readExtensionVersion(bundledDir);
79
79
  const installedVersion = readExtensionVersion(installedDir);
80
- const needsCopy = !fs_1.default.existsSync(installedDir) || bundledVersion !== installedVersion;
80
+ const needsCopy = !node_fs_1.default.existsSync(installedDir) || bundledVersion !== installedVersion;
81
81
  if (needsCopy) {
82
- fs_1.default.mkdirSync(installedDir, { recursive: true });
83
- fs_1.default.cpSync(bundledDir, installedDir, { recursive: true });
82
+ node_fs_1.default.mkdirSync(installedDir, { recursive: true });
83
+ node_fs_1.default.cpSync(bundledDir, installedDir, { recursive: true });
84
84
  }
85
85
  return {
86
86
  synced: needsCopy,
@@ -94,10 +94,10 @@ function syncHost(dataDir) {
94
94
  const installedPath = resolveInstalledHostPath(dataDir);
95
95
  const bundledVersion = readHostVersion(bundledPath);
96
96
  const installedVersion = readHostVersion(installedPath);
97
- const needsCopy = !fs_1.default.existsSync(installedPath) || bundledVersion !== installedVersion;
97
+ const needsCopy = !node_fs_1.default.existsSync(installedPath) || bundledVersion !== installedVersion;
98
98
  if (needsCopy) {
99
- fs_1.default.mkdirSync(path_1.default.dirname(installedPath), { recursive: true });
100
- fs_1.default.copyFileSync(bundledPath, installedPath);
99
+ node_fs_1.default.mkdirSync(node_path_1.default.dirname(installedPath), { recursive: true });
100
+ node_fs_1.default.copyFileSync(bundledPath, installedPath);
101
101
  }
102
102
  return {
103
103
  synced: needsCopy,
@@ -111,7 +111,7 @@ function checkExtensionSync(dataDir) {
111
111
  const installedDir = resolveInstalledExtensionDir(dataDir);
112
112
  const bundledVersion = readExtensionVersion(bundledDir);
113
113
  const installedVersion = readExtensionVersion(installedDir);
114
- const exists = fs_1.default.existsSync(installedDir) && installedVersion !== null;
114
+ const exists = node_fs_1.default.existsSync(installedDir) && installedVersion !== null;
115
115
  const needsSync = !exists || bundledVersion !== installedVersion;
116
116
  const needsReload = exists && bundledVersion !== installedVersion;
117
117
  return {
@@ -11,8 +11,8 @@ exports.addProfile = addProfile;
11
11
  exports.removeProfile = removeProfile;
12
12
  exports.getActiveProfile = getActiveProfile;
13
13
  exports.listProfiles = listProfiles;
14
- const path_1 = __importDefault(require("path"));
15
- const fs_1 = __importDefault(require("fs"));
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 = fs_1.default.readFileSync(path_1.default.join(dir, exports.PROFILES_FILE), "utf-8");
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
- fs_1.default.mkdirSync(dir, { recursive: true });
37
- fs_1.default.writeFileSync(path_1.default.join(dir, exports.PROFILES_FILE), JSON.stringify(registry, null, 2) + "\n");
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);
@@ -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.3.1";
5
- exports.VERSION = "0.3.1";
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.1",
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": ">=20"
18
+ "node": ">=24"
19
19
  },
20
20
  "bin": {
21
21
  "tabctl": "dist/cli/tabctl.js"
@@ -34,7 +34,8 @@
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": "node -e \"fs.rmSync('dist',{recursive:true,force:true})\" "
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",
@@ -44,5 +45,8 @@
44
45
  },
45
46
  "optionalDependencies": {
46
47
  "tabctl-win32-x64": "0.3.0"
48
+ },
49
+ "dependencies": {
50
+ "normalize-url": "^8.1.1"
47
51
  }
48
52
  }