tabctl 0.4.0 → 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 +2 -2
- package/dist/cli/lib/commands/doctor.js +134 -0
- package/dist/cli/lib/commands/index.js +4 -1
- package/dist/cli/lib/commands/params.js +0 -3
- package/dist/cli/lib/constants.js +1 -1
- package/dist/cli/lib/options-commands.js +6 -0
- package/dist/cli/lib/output.js +39 -0
- package/dist/cli/lib/policy-filter.js +1 -1
- package/dist/cli/lib/response.js +2 -2
- package/dist/cli/tabctl.js +4 -0
- package/dist/extension/background.js +5 -90
- package/dist/extension/lib/content.js +0 -30
- package/dist/extension/lib/inspect.js +3 -67
- package/dist/extension/manifest.json +2 -2
- package/dist/host/host.bundle.js +2 -2
- package/dist/shared/version.js +2 -2
- package/dist/shared/wrapper-health.js +132 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -59,7 +59,7 @@ tabctl list # see your open tabs
|
|
|
59
59
|
|
|
60
60
|
## Commands
|
|
61
61
|
|
|
62
|
-
<!-- test: "list sends list action", "analyze passes tab ids and
|
|
62
|
+
<!-- test: "list sends list action", "analyze passes tab ids and progress option", "inspect passes signal options", "close without confirm fails", "report format md returns markdown content", "undo sends undo action with txid" -->
|
|
63
63
|
| Command | Description |
|
|
64
64
|
|---------|-------------|
|
|
65
65
|
| `tabctl list` | List open tabs and groups |
|
|
@@ -255,7 +255,7 @@ Notes:
|
|
|
255
255
|
- `list` and `group-list` paginate by default (limit 100); use `--limit`, `--offset`, or `--no-page`.
|
|
256
256
|
- Use `--group-id -1` or `--ungrouped` to target ungrouped tabs.
|
|
257
257
|
- `--selector` implies `--signal selector`.
|
|
258
|
-
- Unknown inspect signals are rejected (valid: `page-meta`, `
|
|
258
|
+
- Unknown inspect signals are rejected (valid: `page-meta`, `selector`).
|
|
259
259
|
- Selector `attr` supports `href-url`/`src-url` to return absolute http(s) URLs.
|
|
260
260
|
- `screenshot --out` writes per-tab folders into the target directory.
|
|
261
261
|
- `tabctl undo` accepts a positional txid, `--txid`, or `--latest`.
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Doctor command handler: diagnose and repair profile health.
|
|
4
|
+
*
|
|
5
|
+
* Checks each profile's wrapper for valid Node/host paths, verifies
|
|
6
|
+
* extension sync status, and optionally auto-repairs broken wrappers.
|
|
7
|
+
*/
|
|
8
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
9
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
10
|
+
};
|
|
11
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
exports.runDoctor = runDoctor;
|
|
13
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
14
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
15
|
+
const config_1 = require("../../../shared/config");
|
|
16
|
+
const profiles_1 = require("../../../shared/profiles");
|
|
17
|
+
const wrapper_health_1 = require("../../../shared/wrapper-health");
|
|
18
|
+
const extension_sync_1 = require("../../../shared/extension-sync");
|
|
19
|
+
const setup_1 = require("./setup");
|
|
20
|
+
const output_1 = require("../output");
|
|
21
|
+
function checkProfile(name, entry, fix) {
|
|
22
|
+
const wrapperPath = (0, wrapper_health_1.resolveWrapperPath)(entry.dataDir);
|
|
23
|
+
const check = (0, wrapper_health_1.checkWrapper)(wrapperPath);
|
|
24
|
+
const issues = [...check.issues];
|
|
25
|
+
let fixed = false;
|
|
26
|
+
if (fix && !check.ok && check.info) {
|
|
27
|
+
const needsNodeFix = !node_fs_1.default.existsSync(check.info.nodePath);
|
|
28
|
+
const needsHostFix = !node_fs_1.default.existsSync(check.info.hostPath);
|
|
29
|
+
if (needsNodeFix || needsHostFix) {
|
|
30
|
+
const newNodePath = needsNodeFix ? process.execPath : check.info.nodePath;
|
|
31
|
+
let newHostPath = check.info.hostPath;
|
|
32
|
+
if (needsHostFix) {
|
|
33
|
+
// Use the stable bundled host path
|
|
34
|
+
try {
|
|
35
|
+
const config = (0, config_1.resolveConfig)();
|
|
36
|
+
newHostPath = (0, extension_sync_1.resolveInstalledHostPath)(config.baseDataDir);
|
|
37
|
+
if (!node_fs_1.default.existsSync(newHostPath)) {
|
|
38
|
+
issues.push(`Bundled host not found at ${newHostPath} — run: tabctl setup --browser ${entry.browser}`);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
issues.push("Could not resolve bundled host path");
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
try {
|
|
46
|
+
(0, setup_1.writeWrapper)(newNodePath, newHostPath, check.info.profileName, node_path_1.default.dirname(wrapperPath));
|
|
47
|
+
fixed = true;
|
|
48
|
+
// Update issue messages to show they were fixed
|
|
49
|
+
const fixedIssues = [];
|
|
50
|
+
if (needsNodeFix) {
|
|
51
|
+
fixedIssues.push(`Fixed Node path: ${check.info.nodePath} → ${newNodePath}`);
|
|
52
|
+
}
|
|
53
|
+
if (needsHostFix) {
|
|
54
|
+
fixedIssues.push(`Fixed host path: ${check.info.hostPath} → ${newHostPath}`);
|
|
55
|
+
}
|
|
56
|
+
// Replace original issues with fixed messages
|
|
57
|
+
issues.length = 0;
|
|
58
|
+
issues.push(...fixedIssues);
|
|
59
|
+
}
|
|
60
|
+
catch (err) {
|
|
61
|
+
issues.push(`Failed to fix wrapper: ${err instanceof Error ? err.message : String(err)}`);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return {
|
|
66
|
+
ok: check.ok || fixed,
|
|
67
|
+
browser: entry.browser,
|
|
68
|
+
dataDir: entry.dataDir,
|
|
69
|
+
wrapperPath,
|
|
70
|
+
issues,
|
|
71
|
+
fixed,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
function runDoctor(options, prettyOutput) {
|
|
75
|
+
const fix = options.fix === true;
|
|
76
|
+
const config = (0, config_1.resolveConfig)();
|
|
77
|
+
const registry = (0, profiles_1.loadProfiles)(config.configDir);
|
|
78
|
+
const profileNames = Object.keys(registry.profiles);
|
|
79
|
+
if (profileNames.length === 0) {
|
|
80
|
+
(0, output_1.errorOut)("No profiles configured. Run: tabctl setup --browser <edge|chrome>");
|
|
81
|
+
}
|
|
82
|
+
// Check each profile
|
|
83
|
+
const profiles = {};
|
|
84
|
+
for (const name of profileNames) {
|
|
85
|
+
profiles[name] = checkProfile(name, registry.profiles[name], fix);
|
|
86
|
+
}
|
|
87
|
+
// Check extension sync status
|
|
88
|
+
let extensionCheck;
|
|
89
|
+
try {
|
|
90
|
+
const sync = (0, extension_sync_1.checkExtensionSync)(config.baseDataDir);
|
|
91
|
+
extensionCheck = {
|
|
92
|
+
ok: !sync.needsSync,
|
|
93
|
+
synced: !sync.needsSync,
|
|
94
|
+
bundledVersion: sync.bundledVersion,
|
|
95
|
+
installedVersion: sync.installedVersion,
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
catch {
|
|
99
|
+
extensionCheck = {
|
|
100
|
+
ok: false,
|
|
101
|
+
synced: false,
|
|
102
|
+
bundledVersion: null,
|
|
103
|
+
installedVersion: null,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
// Summary
|
|
107
|
+
const total = profileNames.length;
|
|
108
|
+
const healthy = Object.values(profiles).filter(p => p.ok).length;
|
|
109
|
+
const broken = total - healthy;
|
|
110
|
+
const fixed = Object.values(profiles).filter(p => p.fixed).length;
|
|
111
|
+
const allOk = broken === 0 && extensionCheck.ok;
|
|
112
|
+
(0, output_1.printJson)({
|
|
113
|
+
ok: allOk,
|
|
114
|
+
action: "doctor",
|
|
115
|
+
data: {
|
|
116
|
+
profiles,
|
|
117
|
+
extension: extensionCheck,
|
|
118
|
+
summary: { total, healthy, broken, fixed },
|
|
119
|
+
},
|
|
120
|
+
}, prettyOutput);
|
|
121
|
+
// Helpful stderr hints
|
|
122
|
+
if (!allOk && !fix) {
|
|
123
|
+
const brokenNames = Object.entries(profiles)
|
|
124
|
+
.filter(([, p]) => !p.ok)
|
|
125
|
+
.map(([n]) => n);
|
|
126
|
+
if (brokenNames.length > 0) {
|
|
127
|
+
process.stderr.write(`\nBroken profiles: ${brokenNames.join(", ")}\n`);
|
|
128
|
+
process.stderr.write("Run: tabctl doctor --fix\n\n");
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
if (fixed > 0) {
|
|
132
|
+
process.stderr.write(`\nFixed ${fixed} profile(s). Verify: tabctl ping\n\n`);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
@@ -4,10 +4,13 @@
|
|
|
4
4
|
* Re-exports all command handlers and parameter builders.
|
|
5
5
|
*/
|
|
6
6
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
-
exports.buildUndoParams = exports.buildHistoryParams = exports.buildScreenshotParams = exports.buildReportParams = exports.buildCloseParams = exports.buildArchiveParams = exports.buildMergeWindowParams = exports.buildMoveGroupParams = exports.buildMoveTabParams = exports.buildGroupGatherParams = exports.buildGroupAssignParams = exports.buildGroupUngroupParams = exports.buildGroupUpdateParams = exports.buildOpenParams = exports.buildRefreshParams = exports.buildFocusParams = exports.buildInspectParams = exports.buildAnalyzeParams = exports.runProfileRemove = exports.runProfileSwitch = exports.runProfileShow = exports.runProfileList = exports.runGroupList = exports.runList = exports.runPing = exports.runUndo = exports.runHistory = exports.runPolicy = exports.runVersion = exports.runSkillInstall = exports.runSetup = void 0;
|
|
7
|
+
exports.buildUndoParams = exports.buildHistoryParams = exports.buildScreenshotParams = exports.buildReportParams = exports.buildCloseParams = exports.buildArchiveParams = exports.buildMergeWindowParams = exports.buildMoveGroupParams = exports.buildMoveTabParams = exports.buildGroupGatherParams = exports.buildGroupAssignParams = exports.buildGroupUngroupParams = exports.buildGroupUpdateParams = exports.buildOpenParams = exports.buildRefreshParams = exports.buildFocusParams = exports.buildInspectParams = exports.buildAnalyzeParams = exports.runProfileRemove = exports.runProfileSwitch = exports.runProfileShow = exports.runProfileList = exports.runGroupList = exports.runList = exports.runPing = exports.runUndo = exports.runHistory = exports.runPolicy = exports.runVersion = exports.runSkillInstall = exports.runDoctor = exports.runSetup = void 0;
|
|
8
8
|
// Setup command
|
|
9
9
|
var setup_1 = require("./setup");
|
|
10
10
|
Object.defineProperty(exports, "runSetup", { enumerable: true, get: function () { return setup_1.runSetup; } });
|
|
11
|
+
// Doctor command
|
|
12
|
+
var doctor_1 = require("./doctor");
|
|
13
|
+
Object.defineProperty(exports, "runDoctor", { enumerable: true, get: function () { return doctor_1.runDoctor; } });
|
|
11
14
|
// Meta commands (version, ping, skill, policy, history, undo)
|
|
12
15
|
var meta_1 = require("./meta");
|
|
13
16
|
Object.defineProperty(exports, "runSkillInstall", { enumerable: true, get: function () { return meta_1.runSkillInstall; } });
|
|
@@ -46,14 +46,11 @@ function buildAnalyzeParams(options) {
|
|
|
46
46
|
const { tabIds, groupTitle, groupId, windowId, all } = buildScopeFromOptions(options);
|
|
47
47
|
return {
|
|
48
48
|
staleDays: options["stale-days"] ? Number(options["stale-days"]) : undefined,
|
|
49
|
-
checkGitHub: Boolean(options.github),
|
|
50
49
|
tabIds,
|
|
51
50
|
groupTitle,
|
|
52
51
|
groupId,
|
|
53
52
|
windowId,
|
|
54
53
|
all,
|
|
55
|
-
githubConcurrency: options["github-concurrency"] ? Number(options["github-concurrency"]) : undefined,
|
|
56
|
-
githubTimeoutMs: options["github-timeout-ms"] ? Number(options["github-timeout-ms"]) : undefined,
|
|
57
54
|
progress: Boolean(options.progress),
|
|
58
55
|
};
|
|
59
56
|
}
|
|
@@ -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);
|
|
@@ -256,6 +256,12 @@ exports.COMMANDS = {
|
|
|
256
256
|
{ flag: "<name>", desc: "Profile name (positional)" },
|
|
257
257
|
],
|
|
258
258
|
},
|
|
259
|
+
doctor: {
|
|
260
|
+
description: "Diagnose and repair profile health",
|
|
261
|
+
options: [
|
|
262
|
+
{ flag: "--fix", desc: "Auto-repair broken wrappers" },
|
|
263
|
+
],
|
|
264
|
+
},
|
|
259
265
|
version: {
|
|
260
266
|
description: "Show version information",
|
|
261
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
|
+
}
|
|
@@ -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/response.js
CHANGED
|
@@ -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
|
});
|
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;
|
|
@@ -400,8 +400,6 @@
|
|
|
400
400
|
exports.isScriptableUrl = isScriptableUrl2;
|
|
401
401
|
exports.delay = delay2;
|
|
402
402
|
exports.executeWithTimeout = executeWithTimeout2;
|
|
403
|
-
exports.isGitHubIssueOrPr = isGitHubIssueOrPr2;
|
|
404
|
-
exports.detectGitHubState = detectGitHubState2;
|
|
405
403
|
exports.extractPageMeta = extractPageMeta2;
|
|
406
404
|
exports.extractSelectorSignal = extractSelectorSignal2;
|
|
407
405
|
exports.waitForTabLoad = waitForTabLoad2;
|
|
@@ -439,32 +437,6 @@
|
|
|
439
437
|
return null;
|
|
440
438
|
}
|
|
441
439
|
}
|
|
442
|
-
function isGitHubIssueOrPr2(url) {
|
|
443
|
-
if (!url) {
|
|
444
|
-
return false;
|
|
445
|
-
}
|
|
446
|
-
return /^https:\/\/github\.com\/[^/]+\/[^/]+\/(issues|pull)\/\d+/.test(url);
|
|
447
|
-
}
|
|
448
|
-
async function detectGitHubState2(tabId, timeoutMs) {
|
|
449
|
-
const result = await executeWithTimeout2(tabId, timeoutMs, () => {
|
|
450
|
-
const stateEl = document.querySelector(".gh-header-meta .State") || document.querySelector(".State") || document.querySelector(".js-issue-state");
|
|
451
|
-
if (!stateEl) {
|
|
452
|
-
return null;
|
|
453
|
-
}
|
|
454
|
-
const text = (stateEl.textContent || "").trim().toLowerCase();
|
|
455
|
-
if (text.includes("merged")) {
|
|
456
|
-
return "merged";
|
|
457
|
-
}
|
|
458
|
-
if (text.includes("closed")) {
|
|
459
|
-
return "closed";
|
|
460
|
-
}
|
|
461
|
-
if (text.includes("open")) {
|
|
462
|
-
return "open";
|
|
463
|
-
}
|
|
464
|
-
return null;
|
|
465
|
-
});
|
|
466
|
-
return typeof result === "string" ? result : null;
|
|
467
|
-
}
|
|
468
440
|
async function extractPageMeta2(tabId, timeoutMs, descriptionMaxLength) {
|
|
469
441
|
const result = await executeWithTimeout2(tabId, timeoutMs, () => {
|
|
470
442
|
const pickContent = (selector) => {
|
|
@@ -2193,7 +2165,7 @@
|
|
|
2193
2165
|
exports.analyzeTabs = analyzeTabs;
|
|
2194
2166
|
exports.inspectTabs = inspectTabs;
|
|
2195
2167
|
var content2 = require_content();
|
|
2196
|
-
var { isScriptableUrl: isScriptableUrl2,
|
|
2168
|
+
var { isScriptableUrl: isScriptableUrl2, extractPageMeta: extractPageMeta2, extractSelectorSignal: extractSelectorSignal2, waitForSettle: waitForSettle2, waitForTabReady: waitForTabReady2 } = content2;
|
|
2197
2169
|
var tabs2 = require_tabs();
|
|
2198
2170
|
var { normalizeUrl: normalizeUrl2 } = tabs2;
|
|
2199
2171
|
exports.DEFAULT_STALE_DAYS = 30;
|
|
@@ -2201,11 +2173,6 @@
|
|
|
2201
2173
|
exports.SELECTOR_VALUE_MAX_LENGTH = 500;
|
|
2202
2174
|
async function analyzeTabs(params, requestId, deps2) {
|
|
2203
2175
|
const staleDays = Number.isFinite(params.staleDays) ? params.staleDays : exports.DEFAULT_STALE_DAYS;
|
|
2204
|
-
const checkGitHub = params.checkGitHub === true;
|
|
2205
|
-
const githubConcurrencyRaw = Number(params.githubConcurrency);
|
|
2206
|
-
const githubConcurrency = Number.isFinite(githubConcurrencyRaw) && githubConcurrencyRaw > 0 ? Math.min(10, Math.floor(githubConcurrencyRaw)) : 4;
|
|
2207
|
-
const githubTimeoutRaw = Number(params.githubTimeoutMs);
|
|
2208
|
-
const githubTimeoutMs = Number.isFinite(githubTimeoutRaw) && githubTimeoutRaw > 0 ? Math.floor(githubTimeoutRaw) : 4e3;
|
|
2209
2176
|
const progressEnabled = params.progress === true;
|
|
2210
2177
|
const snapshot = await deps2.getTabSnapshot();
|
|
2211
2178
|
const selection = deps2.selectTabsByScope(snapshot, params);
|
|
@@ -2216,9 +2183,6 @@
|
|
|
2216
2183
|
const scopeTabs = selectedTabs;
|
|
2217
2184
|
const now = Date.now();
|
|
2218
2185
|
const startedAt = Date.now();
|
|
2219
|
-
let githubChecked = 0;
|
|
2220
|
-
let githubTotal = 0;
|
|
2221
|
-
let githubMatched = 0;
|
|
2222
2186
|
const normalizedMap = /* @__PURE__ */ new Map();
|
|
2223
2187
|
const duplicates = /* @__PURE__ */ new Map();
|
|
2224
2188
|
for (const tab of scopeTabs) {
|
|
@@ -2257,45 +2221,9 @@
|
|
|
2257
2221
|
}
|
|
2258
2222
|
}
|
|
2259
2223
|
}
|
|
2260
|
-
const githubTabs = checkGitHub ? selectedTabs.filter((tab) => isGitHubIssueOrPr2(tab.url) && isScriptableUrl2(tab.url)) : [];
|
|
2261
|
-
githubTotal = githubTabs.length;
|
|
2262
|
-
if (checkGitHub && githubTabs.length > 0) {
|
|
2263
|
-
let index = 0;
|
|
2264
|
-
const total = githubTabs.length;
|
|
2265
|
-
const workers = Array.from({ length: Math.min(githubConcurrency, total) }, async () => {
|
|
2266
|
-
while (true) {
|
|
2267
|
-
const currentIndex = index;
|
|
2268
|
-
if (currentIndex >= total) {
|
|
2269
|
-
return;
|
|
2270
|
-
}
|
|
2271
|
-
index += 1;
|
|
2272
|
-
const tab = githubTabs[currentIndex];
|
|
2273
|
-
const state2 = await detectGitHubState2(tab.tabId, githubTimeoutMs);
|
|
2274
|
-
githubChecked += 1;
|
|
2275
|
-
if (state2 === "closed" || state2 === "merged") {
|
|
2276
|
-
githubMatched += 1;
|
|
2277
|
-
addReason(tab, {
|
|
2278
|
-
type: "closed_issue",
|
|
2279
|
-
detail: `GitHub state: ${state2}`
|
|
2280
|
-
});
|
|
2281
|
-
}
|
|
2282
|
-
if (progressEnabled) {
|
|
2283
|
-
deps2.sendProgress(requestId, {
|
|
2284
|
-
phase: "github",
|
|
2285
|
-
processed: githubChecked,
|
|
2286
|
-
total,
|
|
2287
|
-
matched: githubMatched,
|
|
2288
|
-
tabId: tab.tabId,
|
|
2289
|
-
timeoutMs: githubTimeoutMs
|
|
2290
|
-
});
|
|
2291
|
-
}
|
|
2292
|
-
}
|
|
2293
|
-
});
|
|
2294
|
-
await Promise.all(workers);
|
|
2295
|
-
}
|
|
2296
2224
|
const candidates = Array.from(candidateMap.values()).map((entry) => {
|
|
2297
2225
|
const reasons = entry.reasons;
|
|
2298
|
-
const severity = reasons.some((reason) => reason.type === "duplicate"
|
|
2226
|
+
const severity = reasons.some((reason) => reason.type === "duplicate") ? "high" : "medium";
|
|
2299
2227
|
return {
|
|
2300
2228
|
tabId: entry.tab.tabId,
|
|
2301
2229
|
windowId: entry.tab.windowId,
|
|
@@ -2316,11 +2244,7 @@
|
|
|
2316
2244
|
candidates: candidates.length
|
|
2317
2245
|
},
|
|
2318
2246
|
meta: {
|
|
2319
|
-
durationMs: Date.now() - startedAt
|
|
2320
|
-
githubChecked,
|
|
2321
|
-
githubTotal,
|
|
2322
|
-
githubMatched,
|
|
2323
|
-
githubTimeoutMs
|
|
2247
|
+
durationMs: Date.now() - startedAt
|
|
2324
2248
|
},
|
|
2325
2249
|
candidates
|
|
2326
2250
|
};
|
|
@@ -2377,16 +2301,7 @@
|
|
|
2377
2301
|
}));
|
|
2378
2302
|
const signalDefs = [];
|
|
2379
2303
|
for (const signalId of signalList) {
|
|
2380
|
-
if (signalId === "
|
|
2381
|
-
signalDefs.push({
|
|
2382
|
-
id: signalId,
|
|
2383
|
-
match: (tab) => isGitHubIssueOrPr2(tab.url) && isScriptableUrl2(tab.url),
|
|
2384
|
-
run: async (tabId) => {
|
|
2385
|
-
const state2 = await detectGitHubState2(tabId, signalTimeoutMs);
|
|
2386
|
-
return state2 ? { state: state2 } : null;
|
|
2387
|
-
}
|
|
2388
|
-
});
|
|
2389
|
-
} else if (signalId === "page-meta") {
|
|
2304
|
+
if (signalId === "page-meta") {
|
|
2390
2305
|
signalDefs.push({
|
|
2391
2306
|
id: signalId,
|
|
2392
2307
|
match: (tab) => isScriptableUrl2(tab.url),
|
|
@@ -3358,7 +3273,7 @@
|
|
|
3358
3273
|
var KEEPALIVE_INTERVAL_MINUTES = 1;
|
|
3359
3274
|
var screenshot = require_screenshot();
|
|
3360
3275
|
var content = require_content();
|
|
3361
|
-
var { delay, executeWithTimeout, isScriptableUrl,
|
|
3276
|
+
var { delay, executeWithTimeout, isScriptableUrl, extractPageMeta, extractSelectorSignal, waitForTabLoad, waitForDomReady, waitForSettle, waitForTabReady, SETTLE_STABILITY_MS, SETTLE_POLL_INTERVAL_MS } = content;
|
|
3362
3277
|
var groups = require_groups();
|
|
3363
3278
|
var tabs = require_tabs();
|
|
3364
3279
|
var { getMostRecentFocusedWindowId, normalizeTabIndex } = tabs;
|
|
@@ -5,8 +5,6 @@ exports.SETTLE_POLL_INTERVAL_MS = exports.SETTLE_STABILITY_MS = void 0;
|
|
|
5
5
|
exports.isScriptableUrl = isScriptableUrl;
|
|
6
6
|
exports.delay = delay;
|
|
7
7
|
exports.executeWithTimeout = executeWithTimeout;
|
|
8
|
-
exports.isGitHubIssueOrPr = isGitHubIssueOrPr;
|
|
9
|
-
exports.detectGitHubState = detectGitHubState;
|
|
10
8
|
exports.extractPageMeta = extractPageMeta;
|
|
11
9
|
exports.extractSelectorSignal = extractSelectorSignal;
|
|
12
10
|
exports.waitForTabLoad = waitForTabLoad;
|
|
@@ -45,34 +43,6 @@ async function executeWithTimeout(tabId, timeoutMs, func, args = []) {
|
|
|
45
43
|
return null;
|
|
46
44
|
}
|
|
47
45
|
}
|
|
48
|
-
function isGitHubIssueOrPr(url) {
|
|
49
|
-
if (!url) {
|
|
50
|
-
return false;
|
|
51
|
-
}
|
|
52
|
-
return /^https:\/\/github\.com\/[^/]+\/[^/]+\/(issues|pull)\/\d+/.test(url);
|
|
53
|
-
}
|
|
54
|
-
async function detectGitHubState(tabId, timeoutMs) {
|
|
55
|
-
const result = await executeWithTimeout(tabId, timeoutMs, () => {
|
|
56
|
-
const stateEl = document.querySelector(".gh-header-meta .State") ||
|
|
57
|
-
document.querySelector(".State") ||
|
|
58
|
-
document.querySelector(".js-issue-state");
|
|
59
|
-
if (!stateEl) {
|
|
60
|
-
return null;
|
|
61
|
-
}
|
|
62
|
-
const text = (stateEl.textContent || "").trim().toLowerCase();
|
|
63
|
-
if (text.includes("merged")) {
|
|
64
|
-
return "merged";
|
|
65
|
-
}
|
|
66
|
-
if (text.includes("closed")) {
|
|
67
|
-
return "closed";
|
|
68
|
-
}
|
|
69
|
-
if (text.includes("open")) {
|
|
70
|
-
return "open";
|
|
71
|
-
}
|
|
72
|
-
return null;
|
|
73
|
-
});
|
|
74
|
-
return typeof result === "string" ? result : null;
|
|
75
|
-
}
|
|
76
46
|
async function extractPageMeta(tabId, timeoutMs, descriptionMaxLength) {
|
|
77
47
|
const result = await executeWithTimeout(tabId, timeoutMs, () => {
|
|
78
48
|
const pickContent = (selector) => {
|
|
@@ -5,7 +5,7 @@ exports.SELECTOR_VALUE_MAX_LENGTH = exports.DESCRIPTION_MAX_LENGTH = exports.DEF
|
|
|
5
5
|
exports.analyzeTabs = analyzeTabs;
|
|
6
6
|
exports.inspectTabs = inspectTabs;
|
|
7
7
|
const content = require("./content");
|
|
8
|
-
const { isScriptableUrl,
|
|
8
|
+
const { isScriptableUrl, extractPageMeta, extractSelectorSignal, waitForSettle, waitForTabReady } = content;
|
|
9
9
|
const tabs = require("./tabs");
|
|
10
10
|
const { normalizeUrl } = tabs;
|
|
11
11
|
exports.DEFAULT_STALE_DAYS = 30;
|
|
@@ -13,15 +13,6 @@ exports.DESCRIPTION_MAX_LENGTH = 250;
|
|
|
13
13
|
exports.SELECTOR_VALUE_MAX_LENGTH = 500;
|
|
14
14
|
async function analyzeTabs(params, requestId, deps) {
|
|
15
15
|
const staleDays = Number.isFinite(params.staleDays) ? params.staleDays : exports.DEFAULT_STALE_DAYS;
|
|
16
|
-
const checkGitHub = params.checkGitHub === true;
|
|
17
|
-
const githubConcurrencyRaw = Number(params.githubConcurrency);
|
|
18
|
-
const githubConcurrency = Number.isFinite(githubConcurrencyRaw) && githubConcurrencyRaw > 0
|
|
19
|
-
? Math.min(10, Math.floor(githubConcurrencyRaw))
|
|
20
|
-
: 4;
|
|
21
|
-
const githubTimeoutRaw = Number(params.githubTimeoutMs);
|
|
22
|
-
const githubTimeoutMs = Number.isFinite(githubTimeoutRaw) && githubTimeoutRaw > 0
|
|
23
|
-
? Math.floor(githubTimeoutRaw)
|
|
24
|
-
: 4000;
|
|
25
16
|
const progressEnabled = params.progress === true;
|
|
26
17
|
const snapshot = await deps.getTabSnapshot();
|
|
27
18
|
const selection = deps.selectTabsByScope(snapshot, params);
|
|
@@ -32,9 +23,6 @@ async function analyzeTabs(params, requestId, deps) {
|
|
|
32
23
|
const scopeTabs = selectedTabs;
|
|
33
24
|
const now = Date.now();
|
|
34
25
|
const startedAt = Date.now();
|
|
35
|
-
let githubChecked = 0;
|
|
36
|
-
let githubTotal = 0;
|
|
37
|
-
let githubMatched = 0;
|
|
38
26
|
const normalizedMap = new Map();
|
|
39
27
|
const duplicates = new Map();
|
|
40
28
|
for (const tab of scopeTabs) {
|
|
@@ -74,47 +62,9 @@ async function analyzeTabs(params, requestId, deps) {
|
|
|
74
62
|
}
|
|
75
63
|
}
|
|
76
64
|
}
|
|
77
|
-
const githubTabs = checkGitHub
|
|
78
|
-
? selectedTabs.filter((tab) => isGitHubIssueOrPr(tab.url) && isScriptableUrl(tab.url))
|
|
79
|
-
: [];
|
|
80
|
-
githubTotal = githubTabs.length;
|
|
81
|
-
if (checkGitHub && githubTabs.length > 0) {
|
|
82
|
-
let index = 0;
|
|
83
|
-
const total = githubTabs.length;
|
|
84
|
-
const workers = Array.from({ length: Math.min(githubConcurrency, total) }, async () => {
|
|
85
|
-
while (true) {
|
|
86
|
-
const currentIndex = index;
|
|
87
|
-
if (currentIndex >= total) {
|
|
88
|
-
return;
|
|
89
|
-
}
|
|
90
|
-
index += 1;
|
|
91
|
-
const tab = githubTabs[currentIndex];
|
|
92
|
-
const state = await detectGitHubState(tab.tabId, githubTimeoutMs);
|
|
93
|
-
githubChecked += 1;
|
|
94
|
-
if (state === "closed" || state === "merged") {
|
|
95
|
-
githubMatched += 1;
|
|
96
|
-
addReason(tab, {
|
|
97
|
-
type: "closed_issue",
|
|
98
|
-
detail: `GitHub state: ${state}`,
|
|
99
|
-
});
|
|
100
|
-
}
|
|
101
|
-
if (progressEnabled) {
|
|
102
|
-
deps.sendProgress(requestId, {
|
|
103
|
-
phase: "github",
|
|
104
|
-
processed: githubChecked,
|
|
105
|
-
total,
|
|
106
|
-
matched: githubMatched,
|
|
107
|
-
tabId: tab.tabId,
|
|
108
|
-
timeoutMs: githubTimeoutMs,
|
|
109
|
-
});
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
});
|
|
113
|
-
await Promise.all(workers);
|
|
114
|
-
}
|
|
115
65
|
const candidates = Array.from(candidateMap.values()).map((entry) => {
|
|
116
66
|
const reasons = entry.reasons;
|
|
117
|
-
const severity = reasons.some((reason) => reason.type === "duplicate"
|
|
67
|
+
const severity = reasons.some((reason) => reason.type === "duplicate")
|
|
118
68
|
? "high"
|
|
119
69
|
: "medium";
|
|
120
70
|
return {
|
|
@@ -138,10 +88,6 @@ async function analyzeTabs(params, requestId, deps) {
|
|
|
138
88
|
},
|
|
139
89
|
meta: {
|
|
140
90
|
durationMs: Date.now() - startedAt,
|
|
141
|
-
githubChecked,
|
|
142
|
-
githubTotal,
|
|
143
|
-
githubMatched,
|
|
144
|
-
githubTimeoutMs,
|
|
145
91
|
},
|
|
146
92
|
candidates,
|
|
147
93
|
};
|
|
@@ -210,17 +156,7 @@ async function inspectTabs(params, requestId, deps) {
|
|
|
210
156
|
}));
|
|
211
157
|
const signalDefs = [];
|
|
212
158
|
for (const signalId of signalList) {
|
|
213
|
-
if (signalId === "
|
|
214
|
-
signalDefs.push({
|
|
215
|
-
id: signalId,
|
|
216
|
-
match: (tab) => isGitHubIssueOrPr(tab.url) && isScriptableUrl(tab.url),
|
|
217
|
-
run: async (tabId) => {
|
|
218
|
-
const state = await detectGitHubState(tabId, signalTimeoutMs);
|
|
219
|
-
return state ? { state } : null;
|
|
220
|
-
},
|
|
221
|
-
});
|
|
222
|
-
}
|
|
223
|
-
else if (signalId === "page-meta") {
|
|
159
|
+
if (signalId === "page-meta") {
|
|
224
160
|
signalDefs.push({
|
|
225
161
|
id: signalId,
|
|
226
162
|
match: (tab) => isScriptableUrl(tab.url),
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"manifest_version": 3,
|
|
3
3
|
"name": "Tab Control",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.5.0",
|
|
5
5
|
"description": "Archive and manage browser tabs with CLI support",
|
|
6
6
|
"permissions": [
|
|
7
7
|
"tabs",
|
|
@@ -19,5 +19,5 @@
|
|
|
19
19
|
"background": {
|
|
20
20
|
"service_worker": "background.js"
|
|
21
21
|
},
|
|
22
|
-
"version_name": "0.
|
|
22
|
+
"version_name": "0.5.0"
|
|
23
23
|
}
|
package/dist/host/host.bundle.js
CHANGED
|
@@ -129,8 +129,8 @@ var require_version = __commonJS({
|
|
|
129
129
|
"use strict";
|
|
130
130
|
Object.defineProperty(exports2, "__esModule", { value: true });
|
|
131
131
|
exports2.DIRTY = exports2.GIT_SHA = exports2.VERSION = exports2.BASE_VERSION = void 0;
|
|
132
|
-
exports2.BASE_VERSION = "0.
|
|
133
|
-
exports2.VERSION = "0.
|
|
132
|
+
exports2.BASE_VERSION = "0.5.0";
|
|
133
|
+
exports2.VERSION = "0.5.0";
|
|
134
134
|
exports2.GIT_SHA = null;
|
|
135
135
|
exports2.DIRTY = false;
|
|
136
136
|
}
|
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.5.0";
|
|
5
|
+
exports.VERSION = "0.5.0";
|
|
6
6
|
exports.GIT_SHA = null;
|
|
7
7
|
exports.DIRTY = false;
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Wrapper health checks — parse, validate, and repair profile wrapper scripts.
|
|
4
|
+
*
|
|
5
|
+
* Wrappers are generated by `tabctl setup` and launch the native messaging host.
|
|
6
|
+
* They hardcode absolute paths to Node and the host bundle, which can break when
|
|
7
|
+
* Node is upgraded (e.g., via mise/nvm). This module detects and fixes stale paths.
|
|
8
|
+
*/
|
|
9
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
10
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
11
|
+
};
|
|
12
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
13
|
+
exports.parseWrapper = parseWrapper;
|
|
14
|
+
exports.checkWrapper = checkWrapper;
|
|
15
|
+
exports.resolveWrapperPath = resolveWrapperPath;
|
|
16
|
+
exports.resolveWrapperTextPath = resolveWrapperTextPath;
|
|
17
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
18
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
19
|
+
// Unix: exec "nodePath" "hostPath"
|
|
20
|
+
const SH_EXEC_RE = /^exec\s+"([^"]+)"\s+"([^"]+)"$/m;
|
|
21
|
+
// Unix: export TABCTL_PROFILE="name"
|
|
22
|
+
const SH_PROFILE_RE = /^export\s+TABCTL_PROFILE="([^"]+)"$/m;
|
|
23
|
+
// Windows .cmd: "nodePath" "hostPath" %*
|
|
24
|
+
const CMD_EXEC_RE = /^"([^"]+)"\s+"([^"]+)"\s+%\*$/m;
|
|
25
|
+
// Windows .cmd: set TABCTL_PROFILE=name
|
|
26
|
+
const CMD_PROFILE_RE = /^set\s+TABCTL_PROFILE=(.+)$/m;
|
|
27
|
+
// Windows .cfg (Go launcher): line 1 = node, line 2 = host, optional TABCTL_PROFILE=name
|
|
28
|
+
const CFG_PROFILE_RE = /^TABCTL_PROFILE=(.+)$/m;
|
|
29
|
+
/**
|
|
30
|
+
* Parse a wrapper script to extract Node path, host path, and profile name.
|
|
31
|
+
* Supports .sh (Unix), .cmd (Windows fallback), and .cfg (Go launcher) formats.
|
|
32
|
+
*/
|
|
33
|
+
function parseWrapper(wrapperPath) {
|
|
34
|
+
try {
|
|
35
|
+
const ext = node_path_1.default.extname(wrapperPath).toLowerCase();
|
|
36
|
+
// .cfg: Go launcher config (host-launcher.cfg next to .exe)
|
|
37
|
+
if (ext === ".cfg") {
|
|
38
|
+
const content = node_fs_1.default.readFileSync(wrapperPath, "utf-8");
|
|
39
|
+
const lines = content.split(/\r?\n/).filter(l => l.trim());
|
|
40
|
+
if (lines.length < 2)
|
|
41
|
+
return null;
|
|
42
|
+
const profileMatch = content.match(CFG_PROFILE_RE);
|
|
43
|
+
return {
|
|
44
|
+
nodePath: lines[0].trim(),
|
|
45
|
+
hostPath: lines[1].trim(),
|
|
46
|
+
profileName: profileMatch ? profileMatch[1].trim() : null,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
// .exe: look for host-launcher.cfg in the same directory
|
|
50
|
+
if (ext === ".exe") {
|
|
51
|
+
const cfgPath = node_path_1.default.join(node_path_1.default.dirname(wrapperPath), "host-launcher.cfg");
|
|
52
|
+
if (node_fs_1.default.existsSync(cfgPath)) {
|
|
53
|
+
return parseWrapper(cfgPath);
|
|
54
|
+
}
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
const content = node_fs_1.default.readFileSync(wrapperPath, "utf-8");
|
|
58
|
+
// .cmd: Windows batch
|
|
59
|
+
if (ext === ".cmd") {
|
|
60
|
+
const execMatch = content.match(CMD_EXEC_RE);
|
|
61
|
+
if (!execMatch)
|
|
62
|
+
return null;
|
|
63
|
+
const profileMatch = content.match(CMD_PROFILE_RE);
|
|
64
|
+
return {
|
|
65
|
+
nodePath: execMatch[1],
|
|
66
|
+
hostPath: execMatch[2],
|
|
67
|
+
profileName: profileMatch ? profileMatch[1].trim() : null,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
// .sh or extensionless: Unix shell
|
|
71
|
+
const execMatch = content.match(SH_EXEC_RE);
|
|
72
|
+
if (!execMatch)
|
|
73
|
+
return null;
|
|
74
|
+
const profileMatch = content.match(SH_PROFILE_RE);
|
|
75
|
+
return {
|
|
76
|
+
nodePath: execMatch[1],
|
|
77
|
+
hostPath: execMatch[2],
|
|
78
|
+
profileName: profileMatch ? profileMatch[1] : null,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Check wrapper health: parse it and verify all referenced paths exist.
|
|
87
|
+
*/
|
|
88
|
+
function checkWrapper(wrapperPath) {
|
|
89
|
+
const issues = [];
|
|
90
|
+
if (!node_fs_1.default.existsSync(wrapperPath)) {
|
|
91
|
+
return { ok: false, wrapperPath, info: null, issues: [`Wrapper not found: ${wrapperPath}`] };
|
|
92
|
+
}
|
|
93
|
+
const info = parseWrapper(wrapperPath);
|
|
94
|
+
if (!info) {
|
|
95
|
+
return { ok: false, wrapperPath, info: null, issues: [`Could not parse wrapper: ${wrapperPath}`] };
|
|
96
|
+
}
|
|
97
|
+
if (!node_fs_1.default.existsSync(info.nodePath)) {
|
|
98
|
+
issues.push(`Node path not found: ${info.nodePath}`);
|
|
99
|
+
}
|
|
100
|
+
if (!node_fs_1.default.existsSync(info.hostPath)) {
|
|
101
|
+
issues.push(`Host path not found: ${info.hostPath}`);
|
|
102
|
+
}
|
|
103
|
+
return { ok: issues.length === 0, wrapperPath, info, issues };
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Resolve the wrapper path for a profile data directory.
|
|
107
|
+
* Returns the path (may not exist yet).
|
|
108
|
+
*/
|
|
109
|
+
function resolveWrapperPath(profileDataDir) {
|
|
110
|
+
if (process.platform === "win32") {
|
|
111
|
+
const exePath = node_path_1.default.join(profileDataDir, "tabctl-host.exe");
|
|
112
|
+
if (node_fs_1.default.existsSync(exePath))
|
|
113
|
+
return exePath;
|
|
114
|
+
const cmdPath = node_path_1.default.join(profileDataDir, "tabctl-host.cmd");
|
|
115
|
+
if (node_fs_1.default.existsSync(cmdPath))
|
|
116
|
+
return cmdPath;
|
|
117
|
+
return exePath; // default to .exe
|
|
118
|
+
}
|
|
119
|
+
return node_path_1.default.join(profileDataDir, "tabctl-host.sh");
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Resolve the text-editable config path for a wrapper.
|
|
123
|
+
* For .exe wrappers, this is the adjacent host-launcher.cfg.
|
|
124
|
+
* For .sh/.cmd, it's the wrapper itself.
|
|
125
|
+
*/
|
|
126
|
+
function resolveWrapperTextPath(wrapperPath) {
|
|
127
|
+
const ext = node_path_1.default.extname(wrapperPath).toLowerCase();
|
|
128
|
+
if (ext === ".exe") {
|
|
129
|
+
return node_path_1.default.join(node_path_1.default.dirname(wrapperPath), "host-launcher.cfg");
|
|
130
|
+
}
|
|
131
|
+
return wrapperPath;
|
|
132
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tabctl",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "CLI tool to manage and analyze browser tabs",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -31,7 +31,7 @@
|
|
|
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
|
-
"test": "npm run build && node --test --test-timeout=5000 dist/tests/unit/*.js",
|
|
34
|
+
"test": "npm run build && node --test --test-timeout=5000 dist/tests/unit/*.js && node dist/scripts/integration-test.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
37
|
"clean": "node -e \"fs.rmSync('dist',{recursive:true,force:true})\" ",
|