tabctl 0.5.3 → 0.6.0-alpha.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/README.md +88 -20
  2. package/dist/extension/background.js +2 -0
  3. package/dist/extension/manifest.json +2 -2
  4. package/package.json +13 -5
  5. package/dist/cli/lib/args.js +0 -141
  6. package/dist/cli/lib/client.js +0 -83
  7. package/dist/cli/lib/commands/doctor.js +0 -134
  8. package/dist/cli/lib/commands/index.js +0 -51
  9. package/dist/cli/lib/commands/list.js +0 -159
  10. package/dist/cli/lib/commands/meta.js +0 -229
  11. package/dist/cli/lib/commands/params-groups.js +0 -48
  12. package/dist/cli/lib/commands/params-move.js +0 -44
  13. package/dist/cli/lib/commands/params.js +0 -314
  14. package/dist/cli/lib/commands/profile.js +0 -91
  15. package/dist/cli/lib/commands/setup.js +0 -294
  16. package/dist/cli/lib/constants.js +0 -30
  17. package/dist/cli/lib/help.js +0 -205
  18. package/dist/cli/lib/options-commands.js +0 -274
  19. package/dist/cli/lib/options-groups.js +0 -41
  20. package/dist/cli/lib/options.js +0 -125
  21. package/dist/cli/lib/output.js +0 -147
  22. package/dist/cli/lib/pagination.js +0 -55
  23. package/dist/cli/lib/policy-filter.js +0 -202
  24. package/dist/cli/lib/policy.js +0 -91
  25. package/dist/cli/lib/report.js +0 -61
  26. package/dist/cli/lib/response.js +0 -235
  27. package/dist/cli/lib/scope.js +0 -250
  28. package/dist/cli/lib/snapshot.js +0 -216
  29. package/dist/cli/lib/types.js +0 -2
  30. package/dist/cli/tabctl.js +0 -475
  31. package/dist/host/host.bundle.js +0 -670
  32. package/dist/host/host.js +0 -143
  33. package/dist/host/host.sh +0 -5
  34. package/dist/host/launcher/go.mod +0 -3
  35. package/dist/host/launcher/main.go +0 -109
  36. package/dist/host/lib/handlers.js +0 -327
  37. package/dist/host/lib/undo.js +0 -60
  38. package/dist/shared/config.js +0 -134
  39. package/dist/shared/extension-sync.js +0 -170
  40. package/dist/shared/profiles.js +0 -78
  41. package/dist/shared/version.js +0 -8
  42. package/dist/shared/wrapper-health.js +0 -132
@@ -1,134 +0,0 @@
1
- "use strict";
2
- var __importDefault = (this && this.__importDefault) || function (mod) {
3
- return (mod && mod.__esModule) ? mod : { "default": mod };
4
- };
5
- Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.resolveSocketPath = resolveSocketPath;
7
- exports.resetConfig = resetConfig;
8
- exports.expandEnvVars = expandEnvVars;
9
- exports.resolveConfig = resolveConfig;
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
- }
35
- let cached;
36
- function resetConfig() {
37
- cached = undefined;
38
- }
39
- function expandEnvVars(value) {
40
- return value.replace(/\$\{(\w+)\}|\$(\w+)/g, (match, braced, bare) => {
41
- const varName = braced || bare;
42
- return process.env[varName] ?? match;
43
- });
44
- }
45
- function resolveConfig(profileName) {
46
- // Use cache only for no-arg calls (legacy mode)
47
- if (!profileName && cached)
48
- return cached;
49
- // Config dir resolution
50
- const configDir = process.env.TABCTL_CONFIG_DIR
51
- || node_path_1.default.join(process.env.XDG_CONFIG_HOME || defaultConfigBase(), "tabctl");
52
- // Read optional config.json
53
- let fileConfig = {};
54
- try {
55
- const raw = node_fs_1.default.readFileSync(node_path_1.default.join(configDir, "config.json"), "utf-8");
56
- fileConfig = JSON.parse(raw);
57
- }
58
- catch {
59
- // missing or malformed — treat as empty
60
- }
61
- // Data dir resolution
62
- let dataDir;
63
- if (typeof fileConfig.dataDir === "string" && fileConfig.dataDir) {
64
- dataDir = expandEnvVars(fileConfig.dataDir);
65
- if (!node_path_1.default.isAbsolute(dataDir)) {
66
- throw new Error(`dataDir in config.json must be an absolute path (got: ${dataDir}). Use $HOME or full paths.`);
67
- }
68
- }
69
- else if (process.env.TABCTL_CONFIG_DIR) {
70
- dataDir = node_path_1.default.join(configDir, "data");
71
- }
72
- else {
73
- dataDir = node_path_1.default.join(process.env.XDG_STATE_HOME || defaultStateBase(), "tabctl");
74
- }
75
- const baseDataDir = dataDir;
76
- // Profile resolution (read profiles.json inline to avoid circular import)
77
- const explicitProfile = profileName;
78
- const effectiveProfile = profileName || process.env.TABCTL_PROFILE;
79
- let activeProfileName;
80
- if (effectiveProfile) {
81
- try {
82
- const raw = node_fs_1.default.readFileSync(node_path_1.default.join(configDir, "profiles.json"), "utf-8");
83
- const registry = JSON.parse(raw);
84
- const profile = registry.profiles[effectiveProfile];
85
- if (profile) {
86
- dataDir = profile.dataDir;
87
- activeProfileName = effectiveProfile;
88
- }
89
- else if (explicitProfile) {
90
- throw new Error(`Profile "${explicitProfile}" not found in profiles.json`);
91
- }
92
- }
93
- catch (err) {
94
- // Re-throw profile-not-found errors
95
- if (err instanceof Error && err.message.includes("not found in profiles.json")) {
96
- throw err;
97
- }
98
- // If profiles.json is missing/malformed and profile was explicitly requested, error
99
- if (explicitProfile) {
100
- throw new Error(`Profile "${explicitProfile}" not found: no profiles.json`);
101
- }
102
- // Otherwise (env var only), silently fall through to legacy mode
103
- }
104
- }
105
- else {
106
- // No explicit profile — check for a default in profiles.json
107
- try {
108
- const raw = node_fs_1.default.readFileSync(node_path_1.default.join(configDir, "profiles.json"), "utf-8");
109
- const registry = JSON.parse(raw);
110
- if (registry.default && registry.profiles[registry.default]) {
111
- dataDir = registry.profiles[registry.default].dataDir;
112
- activeProfileName = registry.default;
113
- }
114
- }
115
- catch {
116
- // No profiles.json — legacy mode
117
- }
118
- }
119
- const result = {
120
- configDir,
121
- dataDir,
122
- baseDataDir,
123
- socketPath: process.env.TABCTL_SOCKET || resolveSocketPath(dataDir),
124
- undoLog: node_path_1.default.join(dataDir, "undo.jsonl"),
125
- wrapperDir: dataDir,
126
- policyPath: node_path_1.default.join(configDir, "policy.json"),
127
- activeProfileName,
128
- };
129
- // Only cache no-arg calls
130
- if (!profileName) {
131
- cached = result;
132
- }
133
- return result;
134
- }
@@ -1,170 +0,0 @@
1
- "use strict";
2
- var __importDefault = (this && this.__importDefault) || function (mod) {
3
- return (mod && mod.__esModule) ? mod : { "default": mod };
4
- };
5
- Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.HOST_BUNDLE_NAME = exports.EXTENSION_DIR_NAME = void 0;
7
- exports.deriveExtensionId = deriveExtensionId;
8
- exports.resolveBundledExtensionDir = resolveBundledExtensionDir;
9
- exports.resolveBundledHostPath = resolveBundledHostPath;
10
- exports.resolveInstalledExtensionDir = resolveInstalledExtensionDir;
11
- exports.resolveInstalledHostPath = resolveInstalledHostPath;
12
- exports.readExtensionVersion = readExtensionVersion;
13
- exports.readHostVersion = readHostVersion;
14
- exports.compareBaseVersions = compareBaseVersions;
15
- exports.isDevBuild = isDevBuild;
16
- exports.syncExtension = syncExtension;
17
- exports.syncHost = syncHost;
18
- exports.checkExtensionSync = checkExtensionSync;
19
- const node_path_1 = __importDefault(require("node:path"));
20
- const node_fs_1 = __importDefault(require("node:fs"));
21
- const node_crypto_1 = __importDefault(require("node:crypto"));
22
- const config_1 = require("./config");
23
- const version_1 = require("./version");
24
- exports.EXTENSION_DIR_NAME = "extension";
25
- exports.HOST_BUNDLE_NAME = "host.bundle.js";
26
- /**
27
- * Derive the Chrome/Edge extension ID for an unpacked extension path.
28
- * Chromium computes: SHA256(absolute_path) → first 32 hex chars → map 0-f to a-p.
29
- */
30
- function deriveExtensionId(extensionDir) {
31
- const hash = node_crypto_1.default.createHash("sha256").update(extensionDir).digest("hex").slice(0, 32);
32
- return hash.split("").map(c => String.fromCharCode("a".charCodeAt(0) + parseInt(c, 16))).join("");
33
- }
34
- function resolveBundledExtensionDir() {
35
- const dir = node_path_1.default.resolve(__dirname, "../extension");
36
- const manifest = node_path_1.default.join(dir, "manifest.json");
37
- if (!node_fs_1.default.existsSync(dir) || !node_fs_1.default.existsSync(manifest)) {
38
- throw new Error(`Bundled extension not found at ${dir}`);
39
- }
40
- return dir;
41
- }
42
- function resolveBundledHostPath() {
43
- const p = node_path_1.default.resolve(__dirname, "../host", exports.HOST_BUNDLE_NAME);
44
- if (!node_fs_1.default.existsSync(p)) {
45
- throw new Error(`Bundled host not found at ${p}`);
46
- }
47
- return p;
48
- }
49
- function resolveInstalledExtensionDir(dataDir) {
50
- const dir = dataDir ?? (0, config_1.resolveConfig)().baseDataDir;
51
- return node_path_1.default.join(dir, exports.EXTENSION_DIR_NAME);
52
- }
53
- function resolveInstalledHostPath(dataDir) {
54
- const dir = dataDir ?? (0, config_1.resolveConfig)().baseDataDir;
55
- return node_path_1.default.join(dir, exports.HOST_BUNDLE_NAME);
56
- }
57
- function readExtensionVersion(extensionDir) {
58
- try {
59
- const raw = node_fs_1.default.readFileSync(node_path_1.default.join(extensionDir, "manifest.json"), "utf-8");
60
- const manifest = JSON.parse(raw);
61
- return typeof manifest.version === "string" ? manifest.version : null;
62
- }
63
- catch {
64
- return null;
65
- }
66
- }
67
- /** Read the BASE_VERSION constant from a bundled host.bundle.js file. */
68
- function readHostVersion(hostPath) {
69
- try {
70
- const content = node_fs_1.default.readFileSync(hostPath, "utf-8");
71
- const match = content.match(/\bBASE_VERSION\s*=\s*"([^"]+)"/);
72
- return match ? match[1] : null;
73
- }
74
- catch {
75
- return null;
76
- }
77
- }
78
- /**
79
- * Compare two semver versions by their base (major.minor.patch) components.
80
- * Strips any prerelease/build metadata before comparing.
81
- * Returns -1 if a < b, 0 if equal, 1 if a > b.
82
- */
83
- function compareBaseVersions(a, b) {
84
- const strip = (v) => v.replace(/[-+].*$/, "");
85
- const pa = strip(a).split(".").map(Number);
86
- const pb = strip(b).split(".").map(Number);
87
- for (let i = 0; i < 3; i++) {
88
- const va = pa[i] ?? 0;
89
- const vb = pb[i] ?? 0;
90
- if (va < vb)
91
- return -1;
92
- if (va > vb)
93
- return 1;
94
- }
95
- return 0;
96
- }
97
- /** Returns true when the current CLI is a dev build. */
98
- function isDevBuild() {
99
- return version_1.DEV_BUILD;
100
- }
101
- function syncExtension(dataDir, options) {
102
- const bundledDir = resolveBundledExtensionDir();
103
- const installedDir = resolveInstalledExtensionDir(dataDir);
104
- const bundledVersion = readExtensionVersion(bundledDir);
105
- const installedVersion = readExtensionVersion(installedDir);
106
- if (!options?.force) {
107
- // Dev builds never overwrite installed files
108
- if (isDevBuild()) {
109
- return { synced: false, bundledVersion, installedVersion, extensionDir: installedDir };
110
- }
111
- // Downgrade protection: don't replace a newer installed version
112
- if (bundledVersion && installedVersion && compareBaseVersions(bundledVersion, installedVersion) < 0) {
113
- return { synced: false, bundledVersion, installedVersion, extensionDir: installedDir };
114
- }
115
- }
116
- const needsCopy = !node_fs_1.default.existsSync(installedDir) || bundledVersion !== installedVersion;
117
- if (needsCopy) {
118
- node_fs_1.default.mkdirSync(installedDir, { recursive: true });
119
- node_fs_1.default.cpSync(bundledDir, installedDir, { recursive: true });
120
- }
121
- return {
122
- synced: needsCopy,
123
- bundledVersion,
124
- installedVersion,
125
- extensionDir: installedDir,
126
- };
127
- }
128
- function syncHost(dataDir, options) {
129
- const bundledPath = resolveBundledHostPath();
130
- const installedPath = resolveInstalledHostPath(dataDir);
131
- const bundledVersion = readHostVersion(bundledPath);
132
- const installedVersion = readHostVersion(installedPath);
133
- if (!options?.force) {
134
- // Dev builds never overwrite installed files
135
- if (isDevBuild()) {
136
- return { synced: false, bundledVersion, installedVersion, hostPath: installedPath };
137
- }
138
- // Downgrade protection: don't replace a newer installed version
139
- if (bundledVersion && installedVersion && compareBaseVersions(bundledVersion, installedVersion) < 0) {
140
- return { synced: false, bundledVersion, installedVersion, hostPath: installedPath };
141
- }
142
- }
143
- const needsCopy = !node_fs_1.default.existsSync(installedPath) || bundledVersion !== installedVersion;
144
- if (needsCopy) {
145
- node_fs_1.default.mkdirSync(node_path_1.default.dirname(installedPath), { recursive: true });
146
- node_fs_1.default.copyFileSync(bundledPath, installedPath);
147
- }
148
- return {
149
- synced: needsCopy,
150
- bundledVersion,
151
- installedVersion,
152
- hostPath: installedPath,
153
- };
154
- }
155
- function checkExtensionSync(dataDir) {
156
- const bundledDir = resolveBundledExtensionDir();
157
- const installedDir = resolveInstalledExtensionDir(dataDir);
158
- const bundledVersion = readExtensionVersion(bundledDir);
159
- const installedVersion = readExtensionVersion(installedDir);
160
- const exists = node_fs_1.default.existsSync(installedDir) && installedVersion !== null;
161
- const needsSync = !exists || bundledVersion !== installedVersion;
162
- const needsReload = exists && bundledVersion !== installedVersion;
163
- return {
164
- needsSync,
165
- needsReload,
166
- bundledVersion,
167
- installedVersion,
168
- extensionDir: installedDir,
169
- };
170
- }
@@ -1,78 +0,0 @@
1
- "use strict";
2
- var __importDefault = (this && this.__importDefault) || function (mod) {
3
- return (mod && mod.__esModule) ? mod : { "default": mod };
4
- };
5
- Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.PROFILES_FILE = exports.PROFILE_NAME_PATTERN = void 0;
7
- exports.validateProfileName = validateProfileName;
8
- exports.loadProfiles = loadProfiles;
9
- exports.saveProfiles = saveProfiles;
10
- exports.addProfile = addProfile;
11
- exports.removeProfile = removeProfile;
12
- exports.getActiveProfile = getActiveProfile;
13
- exports.listProfiles = listProfiles;
14
- const node_path_1 = __importDefault(require("node:path"));
15
- const node_fs_1 = __importDefault(require("node:fs"));
16
- const config_1 = require("./config");
17
- exports.PROFILE_NAME_PATTERN = /^[a-z0-9-]+$/;
18
- exports.PROFILES_FILE = "profiles.json";
19
- function validateProfileName(name) {
20
- if (!exports.PROFILE_NAME_PATTERN.test(name)) {
21
- throw new Error(`Invalid profile name "${name}": only lowercase letters, digits, and hyphens are allowed`);
22
- }
23
- }
24
- function loadProfiles(configDir) {
25
- const dir = configDir ?? (0, config_1.resolveConfig)().configDir;
26
- try {
27
- const raw = node_fs_1.default.readFileSync(node_path_1.default.join(dir, exports.PROFILES_FILE), "utf-8");
28
- return JSON.parse(raw);
29
- }
30
- catch {
31
- return { default: null, profiles: {} };
32
- }
33
- }
34
- function saveProfiles(registry, configDir) {
35
- const dir = configDir ?? (0, config_1.resolveConfig)().configDir;
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
- }
39
- function addProfile(name, entry, configDir) {
40
- validateProfileName(name);
41
- const registry = loadProfiles(configDir);
42
- const isFirst = Object.keys(registry.profiles).length === 0;
43
- registry.profiles[name] = entry;
44
- if (isFirst) {
45
- registry.default = name;
46
- }
47
- saveProfiles(registry, configDir);
48
- return registry;
49
- }
50
- function removeProfile(name, configDir) {
51
- const registry = loadProfiles(configDir);
52
- if (!(name in registry.profiles)) {
53
- throw new Error(`Profile "${name}" does not exist`);
54
- }
55
- delete registry.profiles[name];
56
- if (registry.default === name) {
57
- const remaining = Object.keys(registry.profiles);
58
- registry.default = remaining.length > 0 ? remaining[0] : null;
59
- }
60
- saveProfiles(registry, configDir);
61
- return registry;
62
- }
63
- function getActiveProfile(overrideName, configDir) {
64
- const registry = loadProfiles(configDir);
65
- const name = overrideName ?? process.env.TABCTL_PROFILE ?? registry.default;
66
- if (!name || !(name in registry.profiles)) {
67
- return null;
68
- }
69
- return { name, profile: registry.profiles[name] };
70
- }
71
- function listProfiles(configDir) {
72
- const registry = loadProfiles(configDir);
73
- return Object.entries(registry.profiles).map(([name, profile]) => ({
74
- name,
75
- profile,
76
- isDefault: name === registry.default,
77
- }));
78
- }
@@ -1,8 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.DEV_BUILD = exports.DIRTY = exports.GIT_SHA = exports.VERSION = exports.BASE_VERSION = void 0;
4
- exports.BASE_VERSION = "0.5.3";
5
- exports.VERSION = "0.5.3";
6
- exports.GIT_SHA = "7d279446";
7
- exports.DIRTY = false;
8
- exports.DEV_BUILD = false;
@@ -1,132 +0,0 @@
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
- }