tabctl 0.3.1 → 0.5.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 +9 -7
- package/dist/cli/lib/client.js +3 -3
- package/dist/cli/lib/commands/doctor.js +134 -0
- package/dist/cli/lib/commands/index.js +5 -1
- package/dist/cli/lib/commands/meta.js +12 -12
- package/dist/cli/lib/commands/params-groups.js +8 -0
- package/dist/cli/lib/commands/params.js +6 -6
- package/dist/cli/lib/commands/setup.js +37 -136
- package/dist/cli/lib/constants.js +1 -1
- package/dist/cli/lib/options-commands.js +16 -1
- package/dist/cli/lib/output.js +39 -0
- package/dist/cli/lib/policy-filter.js +2 -2
- package/dist/cli/lib/policy.js +3 -3
- package/dist/cli/lib/response.js +11 -11
- package/dist/cli/tabctl.js +9 -1
- package/dist/extension/background.js +431 -133
- package/dist/extension/lib/content.js +0 -30
- package/dist/extension/lib/groups.js +89 -3
- package/dist/extension/lib/inspect.js +3 -67
- 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 +45 -43
- package/dist/host/host.js +11 -11
- package/dist/host/lib/handlers.js +7 -5
- package/dist/host/lib/undo.js +6 -6
- package/dist/shared/config.js +19 -19
- package/dist/shared/extension-sync.js +20 -20
- package/dist/shared/profiles.js +5 -5
- package/dist/shared/version.js +2 -2
- package/dist/shared/wrapper-health.js +132 -0
- package/package.json +8 -4
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
/**
|
|
3
|
-
* Setup command handler:
|
|
3
|
+
* Setup command handler: browser profile configuration.
|
|
4
4
|
* Extracted from meta.ts for modularity.
|
|
5
5
|
*/
|
|
6
6
|
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
@@ -9,16 +9,13 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
9
9
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
10
10
|
exports.resolveBrowser = resolveBrowser;
|
|
11
11
|
exports.resolveExtensionId = resolveExtensionId;
|
|
12
|
-
exports.promptExtensionId = promptExtensionId;
|
|
13
12
|
exports.resolveNodePath = resolveNodePath;
|
|
14
13
|
exports.resolveManifestDir = resolveManifestDir;
|
|
15
14
|
exports.writeWrapper = writeWrapper;
|
|
16
15
|
exports.runSetup = runSetup;
|
|
17
|
-
const
|
|
18
|
-
const
|
|
19
|
-
const
|
|
20
|
-
const readline_1 = __importDefault(require("readline"));
|
|
21
|
-
const node_child_process_1 = require("node:child_process");
|
|
16
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
17
|
+
const node_os_1 = __importDefault(require("node:os"));
|
|
18
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
22
19
|
const constants_1 = require("../constants");
|
|
23
20
|
const output_1 = require("../output");
|
|
24
21
|
const profiles_1 = require("../../../shared/profiles");
|
|
@@ -49,76 +46,6 @@ function resolveExtensionId(options, required) {
|
|
|
49
46
|
}
|
|
50
47
|
return value;
|
|
51
48
|
}
|
|
52
|
-
async function promptExtensionId(browser) {
|
|
53
|
-
const maxAttempts = 3;
|
|
54
|
-
const extPage = browser === "chrome" ? "chrome://extensions" : "edge://extensions";
|
|
55
|
-
const instructions = [
|
|
56
|
-
"",
|
|
57
|
-
"Next steps:",
|
|
58
|
-
` 1. Open ${extPage}`,
|
|
59
|
-
" 2. Enable Developer mode",
|
|
60
|
-
' 3. Click "Load unpacked" and select the path above',
|
|
61
|
-
" 4. Copy the extension ID shown on the extensions page",
|
|
62
|
-
"",
|
|
63
|
-
].join("\n");
|
|
64
|
-
process.stderr.write(instructions);
|
|
65
|
-
// Collect lines from stdin and provide them on demand
|
|
66
|
-
const lines = [];
|
|
67
|
-
let closed = false;
|
|
68
|
-
let waiting = null;
|
|
69
|
-
const rl = readline_1.default.createInterface({ input: process.stdin, output: process.stderr, terminal: false });
|
|
70
|
-
rl.on("line", (line) => {
|
|
71
|
-
if (waiting) {
|
|
72
|
-
const cb = waiting;
|
|
73
|
-
waiting = null;
|
|
74
|
-
cb(line.trim());
|
|
75
|
-
}
|
|
76
|
-
else {
|
|
77
|
-
lines.push(line.trim());
|
|
78
|
-
}
|
|
79
|
-
});
|
|
80
|
-
rl.on("close", () => {
|
|
81
|
-
closed = true;
|
|
82
|
-
if (waiting) {
|
|
83
|
-
const cb = waiting;
|
|
84
|
-
waiting = null;
|
|
85
|
-
cb(null);
|
|
86
|
-
}
|
|
87
|
-
});
|
|
88
|
-
const nextLine = (prompt) => {
|
|
89
|
-
process.stderr.write(prompt);
|
|
90
|
-
if (lines.length > 0) {
|
|
91
|
-
return Promise.resolve(lines.shift());
|
|
92
|
-
}
|
|
93
|
-
if (closed)
|
|
94
|
-
return Promise.resolve(null);
|
|
95
|
-
return new Promise((resolve) => { waiting = resolve; });
|
|
96
|
-
};
|
|
97
|
-
try {
|
|
98
|
-
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
99
|
-
const raw = await nextLine("Paste the extension ID: ");
|
|
100
|
-
if (raw === null) {
|
|
101
|
-
(0, output_1.errorOut)("No input received (stdin closed).");
|
|
102
|
-
}
|
|
103
|
-
const value = raw.toLowerCase();
|
|
104
|
-
if (constants_1.EXTENSION_ID_PATTERN.test(value)) {
|
|
105
|
-
return value;
|
|
106
|
-
}
|
|
107
|
-
const remaining = maxAttempts - attempt;
|
|
108
|
-
if (remaining > 0) {
|
|
109
|
-
process.stderr.write(`Invalid extension ID (expected 32 lowercase a-p characters). ${remaining} attempt(s) remaining.\n`);
|
|
110
|
-
}
|
|
111
|
-
else {
|
|
112
|
-
(0, output_1.errorOut)("Invalid extension ID after 3 attempts.");
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
finally {
|
|
117
|
-
rl.close();
|
|
118
|
-
}
|
|
119
|
-
// unreachable due to errorOut, but satisfies TypeScript
|
|
120
|
-
return "";
|
|
121
|
-
}
|
|
122
49
|
function resolveNodePath(options) {
|
|
123
50
|
const raw = typeof options.node === "string"
|
|
124
51
|
? String(options.node)
|
|
@@ -127,12 +54,12 @@ function resolveNodePath(options) {
|
|
|
127
54
|
if (!value) {
|
|
128
55
|
(0, output_1.errorOut)("Node binary not found. Set --node or TABCTL_NODE.");
|
|
129
56
|
}
|
|
130
|
-
if (!
|
|
57
|
+
if (!node_path_1.default.isAbsolute(value)) {
|
|
131
58
|
(0, output_1.errorOut)(`Node path must be absolute: ${value}`);
|
|
132
59
|
}
|
|
133
60
|
if (process.platform !== "win32") {
|
|
134
61
|
try {
|
|
135
|
-
|
|
62
|
+
node_fs_1.default.accessSync(value, node_fs_1.default.constants.X_OK);
|
|
136
63
|
}
|
|
137
64
|
catch {
|
|
138
65
|
(0, output_1.errorOut)(`Node binary not executable: ${value}`);
|
|
@@ -140,7 +67,7 @@ function resolveNodePath(options) {
|
|
|
140
67
|
}
|
|
141
68
|
else {
|
|
142
69
|
try {
|
|
143
|
-
|
|
70
|
+
node_fs_1.default.accessSync(value, node_fs_1.default.constants.R_OK);
|
|
144
71
|
}
|
|
145
72
|
catch {
|
|
146
73
|
(0, output_1.errorOut)(`Node binary not found: ${value}`);
|
|
@@ -160,36 +87,36 @@ function resolveHostPath(dataDir) {
|
|
|
160
87
|
}
|
|
161
88
|
}
|
|
162
89
|
function resolveManifestDir(browser) {
|
|
163
|
-
const home =
|
|
90
|
+
const home = node_os_1.default.homedir();
|
|
164
91
|
if (!home) {
|
|
165
92
|
(0, output_1.errorOut)("Home directory not found.");
|
|
166
93
|
}
|
|
167
94
|
if (process.platform === "win32") {
|
|
168
95
|
// Windows: registry-based is preferred, but file-based works with --user-data-dir.
|
|
169
96
|
// For system-wide, we point to the per-user NativeMessagingHosts under LOCALAPPDATA.
|
|
170
|
-
const base = process.env.LOCALAPPDATA ||
|
|
97
|
+
const base = process.env.LOCALAPPDATA || node_path_1.default.join(home, "AppData", "Local");
|
|
171
98
|
if (browser === "edge") {
|
|
172
|
-
return
|
|
99
|
+
return node_path_1.default.join(base, "Microsoft", "Edge", "User Data", "NativeMessagingHosts");
|
|
173
100
|
}
|
|
174
|
-
return
|
|
101
|
+
return node_path_1.default.join(base, "Google", "Chrome", "User Data", "NativeMessagingHosts");
|
|
175
102
|
}
|
|
176
103
|
if (process.platform === "linux") {
|
|
177
104
|
if (browser === "edge") {
|
|
178
|
-
return
|
|
105
|
+
return node_path_1.default.join(home, ".config", "microsoft-edge", "NativeMessagingHosts");
|
|
179
106
|
}
|
|
180
|
-
return
|
|
107
|
+
return node_path_1.default.join(home, ".config", "google-chrome", "NativeMessagingHosts");
|
|
181
108
|
}
|
|
182
109
|
// macOS
|
|
183
110
|
if (browser === "edge") {
|
|
184
|
-
return
|
|
111
|
+
return node_path_1.default.join(home, "Library", "Application Support", "Microsoft Edge", "NativeMessagingHosts");
|
|
185
112
|
}
|
|
186
|
-
return
|
|
113
|
+
return node_path_1.default.join(home, "Library", "Application Support", "Google", "Chrome", "NativeMessagingHosts");
|
|
187
114
|
}
|
|
188
115
|
function writeWrapper(nodePath, hostPath, profileName, wrapperDir) {
|
|
189
|
-
|
|
116
|
+
node_fs_1.default.mkdirSync(wrapperDir, { recursive: true });
|
|
190
117
|
if (process.platform !== "win32") {
|
|
191
118
|
try {
|
|
192
|
-
|
|
119
|
+
node_fs_1.default.chmodSync(wrapperDir, 0o700);
|
|
193
120
|
}
|
|
194
121
|
catch { /* ignore */ }
|
|
195
122
|
}
|
|
@@ -205,28 +132,28 @@ function writeWrapper(nodePath, hostPath, profileName, wrapperDir) {
|
|
|
205
132
|
// Not installed
|
|
206
133
|
}
|
|
207
134
|
if (exeSrc) {
|
|
208
|
-
const exeDst =
|
|
209
|
-
|
|
135
|
+
const exeDst = node_path_1.default.join(wrapperDir, "tabctl-host.exe");
|
|
136
|
+
node_fs_1.default.copyFileSync(exeSrc, exeDst);
|
|
210
137
|
const cfgLines = [nodePath, hostPath];
|
|
211
138
|
if (profileName) {
|
|
212
139
|
cfgLines.push(`TABCTL_PROFILE=${profileName}`);
|
|
213
140
|
}
|
|
214
141
|
cfgLines.push("");
|
|
215
|
-
|
|
142
|
+
node_fs_1.default.writeFileSync(node_path_1.default.join(wrapperDir, "host-launcher.cfg"), cfgLines.join("\r\n"), "utf8");
|
|
216
143
|
return exeDst;
|
|
217
144
|
}
|
|
218
145
|
// Fallback: .cmd wrapper (won't work with Chrome native messaging)
|
|
219
|
-
const wrapperPath =
|
|
146
|
+
const wrapperPath = node_path_1.default.join(wrapperDir, "tabctl-host.cmd");
|
|
220
147
|
const lines = ["@echo off"];
|
|
221
148
|
if (profileName) {
|
|
222
149
|
lines.push(`set TABCTL_PROFILE=${profileName}`);
|
|
223
150
|
}
|
|
224
151
|
lines.push(`"${nodePath}" "${hostPath}" %*`);
|
|
225
152
|
lines.push("");
|
|
226
|
-
|
|
153
|
+
node_fs_1.default.writeFileSync(wrapperPath, lines.join("\r\n"), "utf8");
|
|
227
154
|
return wrapperPath;
|
|
228
155
|
}
|
|
229
|
-
const wrapperPath =
|
|
156
|
+
const wrapperPath = node_path_1.default.join(wrapperDir, "tabctl-host.sh");
|
|
230
157
|
const escapedNode = nodePath.replace(/"/g, "\\\"");
|
|
231
158
|
const escapedHost = hostPath.replace(/"/g, "\\\"");
|
|
232
159
|
const lines = [
|
|
@@ -239,11 +166,11 @@ function writeWrapper(nodePath, hostPath, profileName, wrapperDir) {
|
|
|
239
166
|
lines.push(`exec \"${escapedNode}\" \"${escapedHost}\"`);
|
|
240
167
|
lines.push("");
|
|
241
168
|
const wrapper = lines.join("\n");
|
|
242
|
-
|
|
243
|
-
|
|
169
|
+
node_fs_1.default.writeFileSync(wrapperPath, wrapper, "utf8");
|
|
170
|
+
node_fs_1.default.chmodSync(wrapperPath, 0o700);
|
|
244
171
|
return wrapperPath;
|
|
245
172
|
}
|
|
246
|
-
|
|
173
|
+
function runSetup(options, prettyOutput) {
|
|
247
174
|
const browser = resolveBrowser(options.browser);
|
|
248
175
|
if (!browser) {
|
|
249
176
|
(0, output_1.errorOut)("Missing or invalid --browser (edge|chrome)");
|
|
@@ -263,41 +190,15 @@ async function runSetup(options, prettyOutput) {
|
|
|
263
190
|
let extensionId = resolveExtensionId(options, false);
|
|
264
191
|
if (!extensionId) {
|
|
265
192
|
// Auto-derive from the installed extension path (Chromium uses SHA256 of the path)
|
|
266
|
-
|
|
267
|
-
|
|
193
|
+
// Prefer the just-synced path; fall back to resolving independently
|
|
194
|
+
const installedDir = extensionSync?.extensionDir ?? (0, extension_sync_1.resolveInstalledExtensionDir)(config.baseDataDir);
|
|
195
|
+
if (node_fs_1.default.existsSync(node_path_1.default.join(installedDir, "manifest.json"))) {
|
|
268
196
|
extensionId = (0, extension_sync_1.deriveExtensionId)(installedDir);
|
|
269
197
|
process.stderr.write(`Extension ID derived from: ${installedDir}\n`);
|
|
270
198
|
}
|
|
271
199
|
}
|
|
272
200
|
if (!extensionId) {
|
|
273
|
-
|
|
274
|
-
if (extensionSync?.extensionDir) {
|
|
275
|
-
process.stderr.write(`\nExtension synced to: ${extensionSync.extensionDir}\n`);
|
|
276
|
-
try {
|
|
277
|
-
const clipArgs = [];
|
|
278
|
-
let clipCmd;
|
|
279
|
-
if (process.platform === "darwin") {
|
|
280
|
-
clipCmd = "pbcopy";
|
|
281
|
-
}
|
|
282
|
-
else if (process.platform === "win32") {
|
|
283
|
-
clipCmd = "clip";
|
|
284
|
-
}
|
|
285
|
-
else {
|
|
286
|
-
clipCmd = "xclip";
|
|
287
|
-
clipArgs.push("-selection", "clipboard");
|
|
288
|
-
}
|
|
289
|
-
const clip = (0, node_child_process_1.spawn)(clipCmd, clipArgs, { stdio: ["pipe", "ignore", "ignore"] });
|
|
290
|
-
clip.stdin.end(extensionSync.extensionDir);
|
|
291
|
-
clip.on("exit", (code) => {
|
|
292
|
-
if (code === 0)
|
|
293
|
-
process.stderr.write("(Path copied to clipboard)\n");
|
|
294
|
-
});
|
|
295
|
-
}
|
|
296
|
-
catch {
|
|
297
|
-
// clipboard copy is best-effort
|
|
298
|
-
}
|
|
299
|
-
}
|
|
300
|
-
extensionId = await promptExtensionId(browser);
|
|
201
|
+
(0, output_1.errorOut)("Could not derive extension ID (extension not synced). Use --extension-id <id> or set TABCTL_EXTENSION_ID.");
|
|
301
202
|
}
|
|
302
203
|
// Profile name: --name flag or browser type
|
|
303
204
|
const profileName = typeof options.name === "string" && options.name.trim()
|
|
@@ -310,20 +211,20 @@ async function runSetup(options, prettyOutput) {
|
|
|
310
211
|
(0, output_1.errorOut)(err.message);
|
|
311
212
|
}
|
|
312
213
|
// Profile data dir (use baseDataDir to avoid nesting under another profile)
|
|
313
|
-
const profileDataDir =
|
|
314
|
-
|
|
214
|
+
const profileDataDir = node_path_1.default.join(config.baseDataDir, "profiles", profileName);
|
|
215
|
+
node_fs_1.default.mkdirSync(profileDataDir, { recursive: true });
|
|
315
216
|
// Write profile-specific wrapper
|
|
316
217
|
const wrapperPath = writeWrapper(nodePath, hostPath, profileName, profileDataDir);
|
|
317
218
|
// Resolve manifest directory: custom user-data-dir or system-wide
|
|
318
219
|
const rawUserDataDir = typeof options["user-data-dir"] === "string"
|
|
319
220
|
? options["user-data-dir"].trim()
|
|
320
221
|
: "";
|
|
321
|
-
const userDataDir = rawUserDataDir ?
|
|
222
|
+
const userDataDir = rawUserDataDir ? node_path_1.default.resolve(rawUserDataDir) : "";
|
|
322
223
|
const manifestDir = userDataDir
|
|
323
|
-
?
|
|
224
|
+
? node_path_1.default.join(userDataDir, "NativeMessagingHosts")
|
|
324
225
|
: resolveManifestDir(browser);
|
|
325
|
-
|
|
326
|
-
const manifestPath =
|
|
226
|
+
node_fs_1.default.mkdirSync(manifestDir, { recursive: true });
|
|
227
|
+
const manifestPath = node_path_1.default.join(manifestDir, `${constants_1.HOST_NAME}.json`);
|
|
327
228
|
const manifest = {
|
|
328
229
|
name: constants_1.HOST_NAME,
|
|
329
230
|
description: constants_1.HOST_DESCRIPTION,
|
|
@@ -331,7 +232,7 @@ async function runSetup(options, prettyOutput) {
|
|
|
331
232
|
type: "stdio",
|
|
332
233
|
allowed_origins: [`chrome-extension://${extensionId}/`],
|
|
333
234
|
};
|
|
334
|
-
|
|
235
|
+
node_fs_1.default.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2), "utf8");
|
|
335
236
|
// Register profile
|
|
336
237
|
const profileEntry = {
|
|
337
238
|
browser,
|
|
@@ -26,5 +26,5 @@ exports.GROUP_COLORS = new Set([
|
|
|
26
26
|
exports.DEFAULT_PAGE_LIMIT = 100;
|
|
27
27
|
exports.SKILL_NAME = "tabctl";
|
|
28
28
|
exports.SKILL_REPO = process.env.TABCTL_SKILL_REPO || "https://github.com/ekroon/tabctl";
|
|
29
|
-
exports.SUPPORTED_SIGNALS = ["page-meta", "
|
|
29
|
+
exports.SUPPORTED_SIGNALS = ["page-meta", "selector"];
|
|
30
30
|
exports.SUPPORTED_SIGNAL_SET = new Set(exports.SUPPORTED_SIGNALS);
|
|
@@ -85,6 +85,8 @@ exports.COMMANDS = {
|
|
|
85
85
|
{ flag: "--window-group <name>", desc: "Find window containing group" },
|
|
86
86
|
{ flag: "--window-tab <id>", desc: "Find window containing tab" },
|
|
87
87
|
{ flag: "--window-url <substring>", desc: "Find window containing URL" },
|
|
88
|
+
{ flag: "--new-group", desc: "Force new group even if one exists" },
|
|
89
|
+
{ flag: "--allow-duplicates", desc: "Open duplicate URLs" },
|
|
88
90
|
],
|
|
89
91
|
},
|
|
90
92
|
"group-list": {
|
|
@@ -129,6 +131,13 @@ exports.COMMANDS = {
|
|
|
129
131
|
{ flag: "--expanded", desc: "Expand group after assign" },
|
|
130
132
|
],
|
|
131
133
|
},
|
|
134
|
+
"group-gather": {
|
|
135
|
+
description: "Merge duplicate groups with the same name",
|
|
136
|
+
options: [
|
|
137
|
+
{ flag: "--window <id|active|last-focused>", desc: "Target window" },
|
|
138
|
+
{ flag: "--group <name>", desc: "Group name to gather (optional, gathers all if omitted)" },
|
|
139
|
+
],
|
|
140
|
+
},
|
|
132
141
|
"move-tab": {
|
|
133
142
|
description: "Move a tab to a new position",
|
|
134
143
|
options: [
|
|
@@ -167,7 +176,7 @@ exports.COMMANDS = {
|
|
|
167
176
|
description: "Configure tabctl connection",
|
|
168
177
|
options: [
|
|
169
178
|
{ flag: "--browser edge|chrome", desc: "Browser type" },
|
|
170
|
-
{ flag: "--extension-id <id>", desc: "
|
|
179
|
+
{ flag: "--extension-id <id>", desc: "Override auto-derived extension ID" },
|
|
171
180
|
{ flag: "--node <path>", desc: "Path to Node.js executable" },
|
|
172
181
|
{ flag: "--name <name>", desc: "Profile name (default: browser type)" },
|
|
173
182
|
{ flag: "--user-data-dir <path>", desc: "Chrome/Edge user data directory for custom profiles" },
|
|
@@ -247,6 +256,12 @@ exports.COMMANDS = {
|
|
|
247
256
|
{ flag: "<name>", desc: "Profile name (positional)" },
|
|
248
257
|
],
|
|
249
258
|
},
|
|
259
|
+
doctor: {
|
|
260
|
+
description: "Diagnose and repair profile health",
|
|
261
|
+
options: [
|
|
262
|
+
{ flag: "--fix", desc: "Auto-repair broken wrappers" },
|
|
263
|
+
],
|
|
264
|
+
},
|
|
250
265
|
version: {
|
|
251
266
|
description: "Show version information",
|
|
252
267
|
},
|
package/dist/cli/lib/output.js
CHANGED
|
@@ -8,8 +8,10 @@ const version_1 = require("../../shared/version");
|
|
|
8
8
|
const profiles_1 = require("../../shared/profiles");
|
|
9
9
|
const extension_sync_1 = require("../../shared/extension-sync");
|
|
10
10
|
const config_1 = require("../../shared/config");
|
|
11
|
+
const wrapper_health_1 = require("../../shared/wrapper-health");
|
|
11
12
|
const client_1 = require("./client");
|
|
12
13
|
const client_2 = require("./client");
|
|
14
|
+
const setup_1 = require("./commands/setup");
|
|
13
15
|
function printJson(payload, pretty = true) {
|
|
14
16
|
try {
|
|
15
17
|
const active = (0, profiles_1.getActiveProfile)();
|
|
@@ -78,6 +80,13 @@ function emitVersionWarnings(response, fallbackAction) {
|
|
|
78
80
|
process.stderr.write(`[tabctl] host is stale (${hostVersion}), reloading extension...\n`);
|
|
79
81
|
}
|
|
80
82
|
(0, client_1.sendFireAndForget)({ id: (0, client_2.createRequestId)(), action: "reload", params: {} });
|
|
83
|
+
// Check and fix wrapper Node path for the active profile
|
|
84
|
+
try {
|
|
85
|
+
repairActiveWrapper(config.baseDataDir);
|
|
86
|
+
}
|
|
87
|
+
catch {
|
|
88
|
+
// Wrapper repair is best-effort
|
|
89
|
+
}
|
|
81
90
|
}
|
|
82
91
|
catch {
|
|
83
92
|
process.stderr.write(`[tabctl] version mismatch: cli ${version_1.VERSION}, host ${hostVersion}. Run: tabctl setup\n`);
|
|
@@ -98,3 +107,33 @@ function emitVersionWarnings(response, fallbackAction) {
|
|
|
98
107
|
process.stderr.write("[tabctl] extension version unavailable. Reload the extension in your browser\n");
|
|
99
108
|
}
|
|
100
109
|
}
|
|
110
|
+
/**
|
|
111
|
+
* Repair the active profile's wrapper if the Node path is broken.
|
|
112
|
+
* Conservative: only fixes paths that are confirmed missing.
|
|
113
|
+
*/
|
|
114
|
+
function repairActiveWrapper(baseDataDir) {
|
|
115
|
+
const active = (0, profiles_1.getActiveProfile)();
|
|
116
|
+
if (!active)
|
|
117
|
+
return;
|
|
118
|
+
const wrapperPath = (0, wrapper_health_1.resolveWrapperPath)(active.profile.dataDir);
|
|
119
|
+
const check = (0, wrapper_health_1.checkWrapper)(wrapperPath);
|
|
120
|
+
if (check.ok || !check.info)
|
|
121
|
+
return;
|
|
122
|
+
const fs = require("node:fs");
|
|
123
|
+
const path = require("node:path");
|
|
124
|
+
const needsNodeFix = !fs.existsSync(check.info.nodePath);
|
|
125
|
+
const needsHostFix = !fs.existsSync(check.info.hostPath);
|
|
126
|
+
if (!needsNodeFix && !needsHostFix)
|
|
127
|
+
return;
|
|
128
|
+
const newNodePath = needsNodeFix ? process.execPath : check.info.nodePath;
|
|
129
|
+
const newHostPath = needsHostFix
|
|
130
|
+
? (0, extension_sync_1.resolveInstalledHostPath)(baseDataDir)
|
|
131
|
+
: check.info.hostPath;
|
|
132
|
+
(0, setup_1.writeWrapper)(newNodePath, newHostPath, check.info.profileName, path.dirname(wrapperPath));
|
|
133
|
+
if (needsNodeFix) {
|
|
134
|
+
process.stderr.write(`[tabctl] fixed wrapper Node path: ${check.info.nodePath} → ${newNodePath}\n`);
|
|
135
|
+
}
|
|
136
|
+
if (needsHostFix) {
|
|
137
|
+
process.stderr.write(`[tabctl] fixed wrapper host path: ${check.info.hostPath} → ${newHostPath}\n`);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
@@ -120,7 +120,7 @@ function applyPolicyFilter(command, params, snapshot, policyContext, policySumma
|
|
|
120
120
|
protected: protectedTabs.map(mapProtectedTab),
|
|
121
121
|
};
|
|
122
122
|
}
|
|
123
|
-
else if (command === "group-update" || command === "group-ungroup") {
|
|
123
|
+
else if (command === "group-update" || command === "group-ungroup" || command === "group-gather") {
|
|
124
124
|
if (!eligibleIds.length || protectedTabs.length > 0) {
|
|
125
125
|
earlyResponse = {
|
|
126
126
|
ok: true,
|
|
@@ -147,7 +147,7 @@ function applyPolicyFilter(command, params, snapshot, policyContext, policySumma
|
|
|
147
147
|
generatedAt,
|
|
148
148
|
staleDays: params.staleDays || 0,
|
|
149
149
|
totals: { tabs: 0, analyzed: 0, candidates: 0 },
|
|
150
|
-
meta: { durationMs: 0
|
|
150
|
+
meta: { durationMs: 0 },
|
|
151
151
|
candidates: [],
|
|
152
152
|
analysisId: null,
|
|
153
153
|
policy: policySummary,
|
package/dist/cli/lib/policy.js
CHANGED
|
@@ -10,7 +10,7 @@ exports.getDomain = getDomain;
|
|
|
10
10
|
exports.evaluateTab = evaluateTab;
|
|
11
11
|
exports.annotateEntry = annotateEntry;
|
|
12
12
|
exports.summarizePolicy = summarizePolicy;
|
|
13
|
-
const
|
|
13
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
14
14
|
const config_1 = require("../../shared/config");
|
|
15
15
|
function defaultPolicyPath() {
|
|
16
16
|
return (0, config_1.resolveConfig)().policyPath;
|
|
@@ -25,10 +25,10 @@ function defaultPolicyTemplate() {
|
|
|
25
25
|
}
|
|
26
26
|
function loadPolicy() {
|
|
27
27
|
const resolvedPath = defaultPolicyPath();
|
|
28
|
-
if (!
|
|
28
|
+
if (!node_fs_1.default.existsSync(resolvedPath)) {
|
|
29
29
|
return { policy: null, path: resolvedPath };
|
|
30
30
|
}
|
|
31
|
-
const raw =
|
|
31
|
+
const raw = node_fs_1.default.readFileSync(resolvedPath, "utf8");
|
|
32
32
|
const parsed = JSON.parse(raw);
|
|
33
33
|
return { policy: parsed, path: resolvedPath };
|
|
34
34
|
}
|
package/dist/cli/lib/response.js
CHANGED
|
@@ -9,8 +9,8 @@ exports.buildDedupeOutput = buildDedupeOutput;
|
|
|
9
9
|
exports.extractDedupePlan = extractDedupePlan;
|
|
10
10
|
exports.formatReport = formatReport;
|
|
11
11
|
exports.writeScreenshots = writeScreenshots;
|
|
12
|
-
const
|
|
13
|
-
const
|
|
12
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
13
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
14
14
|
const report_1 = require("./report");
|
|
15
15
|
const policy_1 = require("./policy");
|
|
16
16
|
const output_1 = require("./output");
|
|
@@ -74,7 +74,7 @@ function buildDedupeOutput(response, includeStale, closeData, confirmed) {
|
|
|
74
74
|
const candidates = Array.isArray(data.candidates) ? data.candidates : [];
|
|
75
75
|
const planned = candidates.filter((candidate) => {
|
|
76
76
|
const reasons = Array.isArray(candidate.reasons) ? candidate.reasons : [];
|
|
77
|
-
const hasDuplicate = reasons.some((reason) => reason.type === "duplicate"
|
|
77
|
+
const hasDuplicate = reasons.some((reason) => reason.type === "duplicate");
|
|
78
78
|
const hasStale = reasons.some((reason) => reason.type === "stale");
|
|
79
79
|
return hasDuplicate || (includeStale && hasStale);
|
|
80
80
|
});
|
|
@@ -127,7 +127,7 @@ function extractDedupePlan(response, includeStale) {
|
|
|
127
127
|
const candidates = Array.isArray(data.candidates) ? data.candidates : [];
|
|
128
128
|
const planned = candidates.filter((candidate) => {
|
|
129
129
|
const reasons = Array.isArray(candidate.reasons) ? candidate.reasons : [];
|
|
130
|
-
const hasDuplicate = reasons.some((reason) => reason.type === "duplicate"
|
|
130
|
+
const hasDuplicate = reasons.some((reason) => reason.type === "duplicate");
|
|
131
131
|
const hasStale = reasons.some((reason) => reason.type === "stale");
|
|
132
132
|
return hasDuplicate || (includeStale && hasStale);
|
|
133
133
|
});
|
|
@@ -170,7 +170,7 @@ function formatReport(response, options, prettyOutput) {
|
|
|
170
170
|
(0, output_1.errorOut)(`Unknown report format: ${format}`);
|
|
171
171
|
}
|
|
172
172
|
if (options.out) {
|
|
173
|
-
|
|
173
|
+
node_fs_1.default.writeFileSync(String(options.out), content, "utf8");
|
|
174
174
|
(0, output_1.printJson)({ ok: true, data: { writtenTo: options.out, format, count: entries.length, ...(page ? { page } : {}) } }, prettyOutput);
|
|
175
175
|
return { printed: true };
|
|
176
176
|
}
|
|
@@ -190,13 +190,13 @@ function writeScreenshots(response, options, prettyOutput) {
|
|
|
190
190
|
const page = data && "page" in data ? data.page : undefined;
|
|
191
191
|
const outDir = options.out
|
|
192
192
|
? String(options.out)
|
|
193
|
-
:
|
|
194
|
-
|
|
193
|
+
: node_path_1.default.join(process.cwd(), ".tabctl", "screenshots", String(Date.now()));
|
|
194
|
+
node_fs_1.default.mkdirSync(outDir, { recursive: true });
|
|
195
195
|
let filesWritten = 0;
|
|
196
196
|
const sanitized = entries.map((entry) => {
|
|
197
197
|
const tabId = entry.tabId;
|
|
198
|
-
const tabDir =
|
|
199
|
-
|
|
198
|
+
const tabDir = node_path_1.default.join(outDir, String(tabId ?? "unknown"));
|
|
199
|
+
node_fs_1.default.mkdirSync(tabDir, { recursive: true });
|
|
200
200
|
const tiles = Array.isArray(entry.tiles) ? entry.tiles : [];
|
|
201
201
|
const sanitizedTiles = tiles.map((tile) => {
|
|
202
202
|
const rawUrl = tile.dataUrl;
|
|
@@ -215,9 +215,9 @@ function writeScreenshots(response, options, prettyOutput) {
|
|
|
215
215
|
const total = Number.isFinite(tile.total) ? Number(tile.total) : null;
|
|
216
216
|
const suffix = total && total > 1 ? `-of-${total}` : "";
|
|
217
217
|
const filename = `screenshot-${index}${suffix}.${ext}`;
|
|
218
|
-
const filePath =
|
|
218
|
+
const filePath = node_path_1.default.join(tabDir, filename);
|
|
219
219
|
const buffer = Buffer.from(base64, "base64");
|
|
220
|
-
|
|
220
|
+
node_fs_1.default.writeFileSync(filePath, buffer);
|
|
221
221
|
filesWritten += 1;
|
|
222
222
|
return {
|
|
223
223
|
...rest,
|
package/dist/cli/tabctl.js
CHANGED
|
@@ -153,6 +153,10 @@ async function main() {
|
|
|
153
153
|
await (0, commands_1.runSetup)(options, prettyOutput);
|
|
154
154
|
return;
|
|
155
155
|
}
|
|
156
|
+
if (command === "doctor") {
|
|
157
|
+
(0, commands_1.runDoctor)(options, prettyOutput);
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
156
160
|
if (command === "version") {
|
|
157
161
|
(0, commands_1.runVersion)(prettyOutput);
|
|
158
162
|
return;
|
|
@@ -242,6 +246,10 @@ async function main() {
|
|
|
242
246
|
action = "group-assign";
|
|
243
247
|
params = (0, commands_2.buildGroupAssignParams)(options);
|
|
244
248
|
break;
|
|
249
|
+
case "group-gather":
|
|
250
|
+
action = "group-gather";
|
|
251
|
+
params = (0, commands_2.buildGroupGatherParams)(options);
|
|
252
|
+
break;
|
|
245
253
|
case "move-tab":
|
|
246
254
|
action = "move-tab";
|
|
247
255
|
params = (0, commands_2.buildMoveTabParams)(options);
|
|
@@ -303,7 +311,7 @@ async function main() {
|
|
|
303
311
|
(0, output_1.errorOut)("merge-window --close-source requires --confirm");
|
|
304
312
|
}
|
|
305
313
|
}
|
|
306
|
-
if (enforcePolicy && ["analyze", "inspect", "report", "screenshot", "close", "archive", "focus", "refresh", "move-tab", "move-group", "group-assign", "group-update", "group-ungroup", "merge-window"].includes(command)) {
|
|
314
|
+
if (enforcePolicy && ["analyze", "inspect", "report", "screenshot", "close", "archive", "focus", "refresh", "move-tab", "move-group", "group-assign", "group-update", "group-ungroup", "group-gather", "merge-window"].includes(command)) {
|
|
307
315
|
if (command === "close" && options.apply) {
|
|
308
316
|
(0, output_1.errorOut)("Policy blocks close --apply; use explicit tab targets.");
|
|
309
317
|
}
|