tabctl 0.5.3 → 0.6.0-alpha.10
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 +135 -35
- package/dist/extension/background.js +179 -3155
- package/dist/extension/lib/content.js +0 -115
- package/dist/extension/lib/screenshot.js +0 -93
- package/dist/extension/manifest.json +2 -2
- package/package.json +13 -5
- package/dist/cli/lib/args.js +0 -141
- package/dist/cli/lib/client.js +0 -83
- package/dist/cli/lib/commands/doctor.js +0 -134
- package/dist/cli/lib/commands/index.js +0 -51
- package/dist/cli/lib/commands/list.js +0 -159
- package/dist/cli/lib/commands/meta.js +0 -229
- package/dist/cli/lib/commands/params-groups.js +0 -48
- package/dist/cli/lib/commands/params-move.js +0 -44
- package/dist/cli/lib/commands/params.js +0 -314
- package/dist/cli/lib/commands/profile.js +0 -91
- package/dist/cli/lib/commands/setup.js +0 -294
- package/dist/cli/lib/constants.js +0 -30
- package/dist/cli/lib/help.js +0 -205
- package/dist/cli/lib/options-commands.js +0 -274
- package/dist/cli/lib/options-groups.js +0 -41
- package/dist/cli/lib/options.js +0 -125
- package/dist/cli/lib/output.js +0 -147
- package/dist/cli/lib/pagination.js +0 -55
- package/dist/cli/lib/policy-filter.js +0 -202
- package/dist/cli/lib/policy.js +0 -91
- package/dist/cli/lib/report.js +0 -61
- package/dist/cli/lib/response.js +0 -235
- package/dist/cli/lib/scope.js +0 -250
- package/dist/cli/lib/snapshot.js +0 -216
- package/dist/cli/lib/types.js +0 -2
- package/dist/cli/tabctl.js +0 -475
- package/dist/extension/lib/archive.js +0 -444
- package/dist/extension/lib/deps.js +0 -4
- package/dist/extension/lib/groups.js +0 -529
- package/dist/extension/lib/inspect.js +0 -252
- package/dist/extension/lib/move.js +0 -342
- package/dist/extension/lib/tabs.js +0 -456
- package/dist/extension/lib/undo-handlers.js +0 -447
- package/dist/host/host.bundle.js +0 -670
- package/dist/host/host.js +0 -143
- package/dist/host/host.sh +0 -5
- package/dist/host/launcher/go.mod +0 -3
- package/dist/host/launcher/main.go +0 -109
- package/dist/host/lib/handlers.js +0 -327
- package/dist/host/lib/undo.js +0 -60
- package/dist/shared/config.js +0 -134
- package/dist/shared/extension-sync.js +0 -170
- package/dist/shared/profiles.js +0 -78
- package/dist/shared/version.js +0 -8
- package/dist/shared/wrapper-health.js +0 -132
|
@@ -1,294 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
/**
|
|
3
|
-
* Setup command handler: browser profile configuration.
|
|
4
|
-
* Extracted from meta.ts for modularity.
|
|
5
|
-
*/
|
|
6
|
-
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
7
|
-
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
8
|
-
};
|
|
9
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
10
|
-
exports.resolveBrowser = resolveBrowser;
|
|
11
|
-
exports.resolveExtensionId = resolveExtensionId;
|
|
12
|
-
exports.resolveNodePath = resolveNodePath;
|
|
13
|
-
exports.resolveManifestDir = resolveManifestDir;
|
|
14
|
-
exports.writeWrapper = writeWrapper;
|
|
15
|
-
exports.runSetup = runSetup;
|
|
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"));
|
|
19
|
-
const constants_1 = require("../constants");
|
|
20
|
-
const output_1 = require("../output");
|
|
21
|
-
const profiles_1 = require("../../../shared/profiles");
|
|
22
|
-
const config_1 = require("../../../shared/config");
|
|
23
|
-
const extension_sync_1 = require("../../../shared/extension-sync");
|
|
24
|
-
function resolveBrowser(value) {
|
|
25
|
-
if (typeof value !== "string") {
|
|
26
|
-
return null;
|
|
27
|
-
}
|
|
28
|
-
const trimmed = value.trim().toLowerCase();
|
|
29
|
-
if (trimmed === "edge" || trimmed === "chrome") {
|
|
30
|
-
return trimmed;
|
|
31
|
-
}
|
|
32
|
-
return null;
|
|
33
|
-
}
|
|
34
|
-
function resolveExtensionId(options, required) {
|
|
35
|
-
const raw = typeof options["extension-id"] === "string"
|
|
36
|
-
? String(options["extension-id"])
|
|
37
|
-
: (process.env.TABCTL_EXTENSION_ID || "");
|
|
38
|
-
const value = raw.trim().toLowerCase();
|
|
39
|
-
if (!value) {
|
|
40
|
-
if (!required)
|
|
41
|
-
return null;
|
|
42
|
-
(0, output_1.errorOut)("Missing --extension-id (or TABCTL_EXTENSION_ID)");
|
|
43
|
-
}
|
|
44
|
-
if (!constants_1.EXTENSION_ID_PATTERN.test(value)) {
|
|
45
|
-
(0, output_1.errorOut)(`Extension ID looks unusual: ${raw}`);
|
|
46
|
-
}
|
|
47
|
-
return value;
|
|
48
|
-
}
|
|
49
|
-
function resolveNodePath(options) {
|
|
50
|
-
const raw = typeof options.node === "string"
|
|
51
|
-
? String(options.node)
|
|
52
|
-
: (process.env.TABCTL_NODE || process.execPath || "");
|
|
53
|
-
const value = raw.trim();
|
|
54
|
-
if (!value) {
|
|
55
|
-
(0, output_1.errorOut)("Node binary not found. Set --node or TABCTL_NODE.");
|
|
56
|
-
}
|
|
57
|
-
if (!node_path_1.default.isAbsolute(value)) {
|
|
58
|
-
(0, output_1.errorOut)(`Node path must be absolute: ${value}`);
|
|
59
|
-
}
|
|
60
|
-
if (process.platform !== "win32") {
|
|
61
|
-
try {
|
|
62
|
-
node_fs_1.default.accessSync(value, node_fs_1.default.constants.X_OK);
|
|
63
|
-
}
|
|
64
|
-
catch {
|
|
65
|
-
(0, output_1.errorOut)(`Node binary not executable: ${value}`);
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
else {
|
|
69
|
-
try {
|
|
70
|
-
node_fs_1.default.accessSync(value, node_fs_1.default.constants.R_OK);
|
|
71
|
-
}
|
|
72
|
-
catch {
|
|
73
|
-
(0, output_1.errorOut)(`Node binary not found: ${value}`);
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
return value;
|
|
77
|
-
}
|
|
78
|
-
function resolveHostPath(dataDir) {
|
|
79
|
-
// Sync host bundle to stable path so wrapper survives npm upgrades
|
|
80
|
-
try {
|
|
81
|
-
const result = (0, extension_sync_1.syncHost)(dataDir, { force: true });
|
|
82
|
-
return result.hostPath;
|
|
83
|
-
}
|
|
84
|
-
catch (err) {
|
|
85
|
-
const detail = err instanceof Error ? err.message : String(err);
|
|
86
|
-
(0, output_1.errorOut)(`Failed to resolve native host. Make sure the CLI is built (run: npm run build). Details: ${detail}`);
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
function resolveManifestDir(browser) {
|
|
90
|
-
const home = node_os_1.default.homedir();
|
|
91
|
-
if (!home) {
|
|
92
|
-
(0, output_1.errorOut)("Home directory not found.");
|
|
93
|
-
}
|
|
94
|
-
if (process.platform === "win32") {
|
|
95
|
-
// Windows: registry-based is preferred, but file-based works with --user-data-dir.
|
|
96
|
-
// For system-wide, we point to the per-user NativeMessagingHosts under LOCALAPPDATA.
|
|
97
|
-
const base = process.env.LOCALAPPDATA || node_path_1.default.join(home, "AppData", "Local");
|
|
98
|
-
if (browser === "edge") {
|
|
99
|
-
return node_path_1.default.join(base, "Microsoft", "Edge", "User Data", "NativeMessagingHosts");
|
|
100
|
-
}
|
|
101
|
-
return node_path_1.default.join(base, "Google", "Chrome", "User Data", "NativeMessagingHosts");
|
|
102
|
-
}
|
|
103
|
-
if (process.platform === "linux") {
|
|
104
|
-
if (browser === "edge") {
|
|
105
|
-
return node_path_1.default.join(home, ".config", "microsoft-edge", "NativeMessagingHosts");
|
|
106
|
-
}
|
|
107
|
-
return node_path_1.default.join(home, ".config", "google-chrome", "NativeMessagingHosts");
|
|
108
|
-
}
|
|
109
|
-
// macOS
|
|
110
|
-
if (browser === "edge") {
|
|
111
|
-
return node_path_1.default.join(home, "Library", "Application Support", "Microsoft Edge", "NativeMessagingHosts");
|
|
112
|
-
}
|
|
113
|
-
return node_path_1.default.join(home, "Library", "Application Support", "Google", "Chrome", "NativeMessagingHosts");
|
|
114
|
-
}
|
|
115
|
-
function writeWrapper(nodePath, hostPath, profileName, wrapperDir) {
|
|
116
|
-
node_fs_1.default.mkdirSync(wrapperDir, { recursive: true });
|
|
117
|
-
if (process.platform !== "win32") {
|
|
118
|
-
try {
|
|
119
|
-
node_fs_1.default.chmodSync(wrapperDir, 0o700);
|
|
120
|
-
}
|
|
121
|
-
catch { /* ignore */ }
|
|
122
|
-
}
|
|
123
|
-
if (process.platform === "win32") {
|
|
124
|
-
// Prefer the Go launcher binary from the platform package.
|
|
125
|
-
// Falls back to a .cmd wrapper if unavailable (dev/testing only —
|
|
126
|
-
// .cmd wrappers don't work for Chrome native messaging).
|
|
127
|
-
let exeSrc;
|
|
128
|
-
try {
|
|
129
|
-
exeSrc = require.resolve("tabctl-win32-x64/tabctl-host.exe");
|
|
130
|
-
}
|
|
131
|
-
catch {
|
|
132
|
-
// Not installed
|
|
133
|
-
}
|
|
134
|
-
if (exeSrc) {
|
|
135
|
-
const exeDst = node_path_1.default.join(wrapperDir, "tabctl-host.exe");
|
|
136
|
-
node_fs_1.default.copyFileSync(exeSrc, exeDst);
|
|
137
|
-
const cfgLines = [nodePath, hostPath];
|
|
138
|
-
if (profileName) {
|
|
139
|
-
cfgLines.push(`TABCTL_PROFILE=${profileName}`);
|
|
140
|
-
}
|
|
141
|
-
cfgLines.push("");
|
|
142
|
-
node_fs_1.default.writeFileSync(node_path_1.default.join(wrapperDir, "host-launcher.cfg"), cfgLines.join("\r\n"), "utf8");
|
|
143
|
-
return exeDst;
|
|
144
|
-
}
|
|
145
|
-
// Fallback: .cmd wrapper (won't work with Chrome native messaging)
|
|
146
|
-
const wrapperPath = node_path_1.default.join(wrapperDir, "tabctl-host.cmd");
|
|
147
|
-
const lines = ["@echo off"];
|
|
148
|
-
if (profileName) {
|
|
149
|
-
lines.push(`set TABCTL_PROFILE=${profileName}`);
|
|
150
|
-
}
|
|
151
|
-
lines.push(`"${nodePath}" "${hostPath}" %*`);
|
|
152
|
-
lines.push("");
|
|
153
|
-
node_fs_1.default.writeFileSync(wrapperPath, lines.join("\r\n"), "utf8");
|
|
154
|
-
return wrapperPath;
|
|
155
|
-
}
|
|
156
|
-
const wrapperPath = node_path_1.default.join(wrapperDir, "tabctl-host.sh");
|
|
157
|
-
const escapedNode = nodePath.replace(/"/g, "\\\"");
|
|
158
|
-
const escapedHost = hostPath.replace(/"/g, "\\\"");
|
|
159
|
-
const lines = [
|
|
160
|
-
"#!/usr/bin/env bash",
|
|
161
|
-
"set -euo pipefail",
|
|
162
|
-
];
|
|
163
|
-
if (profileName) {
|
|
164
|
-
lines.push(`export TABCTL_PROFILE="${profileName}"`);
|
|
165
|
-
}
|
|
166
|
-
lines.push(`exec \"${escapedNode}\" \"${escapedHost}\"`);
|
|
167
|
-
lines.push("");
|
|
168
|
-
const wrapper = lines.join("\n");
|
|
169
|
-
node_fs_1.default.writeFileSync(wrapperPath, wrapper, "utf8");
|
|
170
|
-
node_fs_1.default.chmodSync(wrapperPath, 0o700);
|
|
171
|
-
return wrapperPath;
|
|
172
|
-
}
|
|
173
|
-
function runSetup(options, prettyOutput) {
|
|
174
|
-
const browser = resolveBrowser(options.browser);
|
|
175
|
-
if (!browser) {
|
|
176
|
-
(0, output_1.errorOut)("Missing or invalid --browser (edge|chrome)");
|
|
177
|
-
}
|
|
178
|
-
const nodePath = resolveNodePath(options);
|
|
179
|
-
// Sync extension + host to stable paths (before extensionId so interactive mode can show it)
|
|
180
|
-
const config = (0, constants_1.resolveConfig)();
|
|
181
|
-
const hostPath = resolveHostPath(config.baseDataDir);
|
|
182
|
-
let extensionSync;
|
|
183
|
-
try {
|
|
184
|
-
extensionSync = (0, extension_sync_1.syncExtension)(config.baseDataDir, { force: true });
|
|
185
|
-
}
|
|
186
|
-
catch {
|
|
187
|
-
extensionSync = null;
|
|
188
|
-
}
|
|
189
|
-
// Resolve extension ID: explicit flag, derived from install path, or interactive prompt
|
|
190
|
-
let extensionId = resolveExtensionId(options, false);
|
|
191
|
-
if (!extensionId) {
|
|
192
|
-
// Auto-derive from the installed extension path (Chromium uses SHA256 of the path)
|
|
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"))) {
|
|
196
|
-
extensionId = (0, extension_sync_1.deriveExtensionId)(installedDir);
|
|
197
|
-
process.stderr.write(`Extension ID derived from: ${installedDir}\n`);
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
|
-
if (!extensionId) {
|
|
201
|
-
(0, output_1.errorOut)("Could not derive extension ID (extension not synced). Use --extension-id <id> or set TABCTL_EXTENSION_ID.");
|
|
202
|
-
}
|
|
203
|
-
// Profile name: --name flag or browser type
|
|
204
|
-
const profileName = typeof options.name === "string" && options.name.trim()
|
|
205
|
-
? options.name.trim().toLowerCase()
|
|
206
|
-
: browser;
|
|
207
|
-
try {
|
|
208
|
-
(0, profiles_1.validateProfileName)(profileName);
|
|
209
|
-
}
|
|
210
|
-
catch (err) {
|
|
211
|
-
(0, output_1.errorOut)(err.message);
|
|
212
|
-
}
|
|
213
|
-
// Profile data dir (use baseDataDir to avoid nesting under another profile)
|
|
214
|
-
const profileDataDir = node_path_1.default.join(config.baseDataDir, "profiles", profileName);
|
|
215
|
-
node_fs_1.default.mkdirSync(profileDataDir, { recursive: true });
|
|
216
|
-
// Write profile-specific wrapper
|
|
217
|
-
const wrapperPath = writeWrapper(nodePath, hostPath, profileName, profileDataDir);
|
|
218
|
-
// Resolve manifest directory: custom user-data-dir or system-wide
|
|
219
|
-
const rawUserDataDir = typeof options["user-data-dir"] === "string"
|
|
220
|
-
? options["user-data-dir"].trim()
|
|
221
|
-
: "";
|
|
222
|
-
const userDataDir = rawUserDataDir ? node_path_1.default.resolve(rawUserDataDir) : "";
|
|
223
|
-
const manifestDir = userDataDir
|
|
224
|
-
? node_path_1.default.join(userDataDir, "NativeMessagingHosts")
|
|
225
|
-
: resolveManifestDir(browser);
|
|
226
|
-
node_fs_1.default.mkdirSync(manifestDir, { recursive: true });
|
|
227
|
-
const manifestPath = node_path_1.default.join(manifestDir, `${constants_1.HOST_NAME}.json`);
|
|
228
|
-
const manifest = {
|
|
229
|
-
name: constants_1.HOST_NAME,
|
|
230
|
-
description: constants_1.HOST_DESCRIPTION,
|
|
231
|
-
path: wrapperPath,
|
|
232
|
-
type: "stdio",
|
|
233
|
-
allowed_origins: [`chrome-extension://${extensionId}/`],
|
|
234
|
-
};
|
|
235
|
-
node_fs_1.default.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2), "utf8");
|
|
236
|
-
// Register profile
|
|
237
|
-
const profileEntry = {
|
|
238
|
-
browser,
|
|
239
|
-
extensionId,
|
|
240
|
-
nodePath,
|
|
241
|
-
hostPath,
|
|
242
|
-
dataDir: profileDataDir,
|
|
243
|
-
};
|
|
244
|
-
if (userDataDir) {
|
|
245
|
-
profileEntry.userDataDir = userDataDir;
|
|
246
|
-
}
|
|
247
|
-
const registry = (0, profiles_1.addProfile)(profileName, profileEntry);
|
|
248
|
-
// Ensure printJson footer reflects the newly-created profile
|
|
249
|
-
(0, config_1.resetConfig)();
|
|
250
|
-
process.env.TABCTL_PROFILE = profileName;
|
|
251
|
-
(0, output_1.printJson)({
|
|
252
|
-
ok: true,
|
|
253
|
-
action: "setup",
|
|
254
|
-
data: {
|
|
255
|
-
profileName,
|
|
256
|
-
browser,
|
|
257
|
-
extensionId,
|
|
258
|
-
manifestPath,
|
|
259
|
-
hostPath,
|
|
260
|
-
nodePath,
|
|
261
|
-
wrapperPath,
|
|
262
|
-
dataDir: profileDataDir,
|
|
263
|
-
...(userDataDir ? { userDataDir } : {}),
|
|
264
|
-
isDefault: registry.default === profileName,
|
|
265
|
-
extensionDir: extensionSync?.extensionDir || null,
|
|
266
|
-
extensionSynced: extensionSync?.synced || false,
|
|
267
|
-
},
|
|
268
|
-
}, prettyOutput);
|
|
269
|
-
if (registry.default !== profileName) {
|
|
270
|
-
process.stderr.write([
|
|
271
|
-
"",
|
|
272
|
-
`Profile "${profileName}" created (current default: "${registry.default}").`,
|
|
273
|
-
` To use: tabctl --profile ${profileName} <command>`,
|
|
274
|
-
` To make default: tabctl profile-switch ${profileName}`,
|
|
275
|
-
"",
|
|
276
|
-
].join("\n"));
|
|
277
|
-
}
|
|
278
|
-
const extensionsUrl = browser === "edge" ? "edge://extensions" : "chrome://extensions";
|
|
279
|
-
const browserName = browser === "edge" ? "Edge" : "Chrome";
|
|
280
|
-
const extensionDir = extensionSync?.extensionDir || null;
|
|
281
|
-
const loadSteps = extensionDir
|
|
282
|
-
? [
|
|
283
|
-
`Load the extension: ${extensionsUrl} → Developer mode → Load unpacked`,
|
|
284
|
-
` Path: ${extensionDir}`,
|
|
285
|
-
process.platform === "darwin" ? " Tip: press Cmd+Shift+G in the file dialog to paste the path" : null,
|
|
286
|
-
].filter(Boolean).join("\n")
|
|
287
|
-
: `Load the extension: ${extensionsUrl} → Developer mode → Load unpacked`;
|
|
288
|
-
process.stderr.write([
|
|
289
|
-
loadSteps,
|
|
290
|
-
`Verify connection: tabctl --profile ${profileName} ping`,
|
|
291
|
-
`If ping fails, ensure the ${browserName} extension is active.`,
|
|
292
|
-
"",
|
|
293
|
-
].join("\n"));
|
|
294
|
-
}
|
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.SUPPORTED_SIGNAL_SET = exports.SUPPORTED_SIGNALS = exports.SKILL_REPO = exports.SKILL_NAME = exports.DEFAULT_PAGE_LIMIT = exports.GROUP_COLORS = exports.EXTENSION_ID_PATTERN = exports.HOST_DESCRIPTION = exports.HOST_NAME = exports.resolveConfig = exports.DIRTY = exports.GIT_SHA = exports.BASE_VERSION = exports.VERSION = void 0;
|
|
4
|
-
// Re-export version info from shared module
|
|
5
|
-
var version_1 = require("../../shared/version");
|
|
6
|
-
Object.defineProperty(exports, "VERSION", { enumerable: true, get: function () { return version_1.VERSION; } });
|
|
7
|
-
Object.defineProperty(exports, "BASE_VERSION", { enumerable: true, get: function () { return version_1.BASE_VERSION; } });
|
|
8
|
-
Object.defineProperty(exports, "GIT_SHA", { enumerable: true, get: function () { return version_1.GIT_SHA; } });
|
|
9
|
-
Object.defineProperty(exports, "DIRTY", { enumerable: true, get: function () { return version_1.DIRTY; } });
|
|
10
|
-
var config_1 = require("../../shared/config");
|
|
11
|
-
Object.defineProperty(exports, "resolveConfig", { enumerable: true, get: function () { return config_1.resolveConfig; } });
|
|
12
|
-
exports.HOST_NAME = "com.erwinkroon.tabctl";
|
|
13
|
-
exports.HOST_DESCRIPTION = "tabctl native host";
|
|
14
|
-
exports.EXTENSION_ID_PATTERN = /^[a-p]{32}$/;
|
|
15
|
-
exports.GROUP_COLORS = new Set([
|
|
16
|
-
"grey",
|
|
17
|
-
"blue",
|
|
18
|
-
"red",
|
|
19
|
-
"yellow",
|
|
20
|
-
"green",
|
|
21
|
-
"pink",
|
|
22
|
-
"purple",
|
|
23
|
-
"cyan",
|
|
24
|
-
"orange",
|
|
25
|
-
]);
|
|
26
|
-
exports.DEFAULT_PAGE_LIMIT = 100;
|
|
27
|
-
exports.SKILL_NAME = "tabctl";
|
|
28
|
-
exports.SKILL_REPO = process.env.TABCTL_SKILL_REPO || "https://github.com/ekroon/tabctl";
|
|
29
|
-
exports.SUPPORTED_SIGNALS = ["page-meta", "selector"];
|
|
30
|
-
exports.SUPPORTED_SIGNAL_SET = new Set(exports.SUPPORTED_SIGNALS);
|
package/dist/cli/lib/help.js
DELETED
|
@@ -1,205 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
/**
|
|
3
|
-
* Help generation using option groups from options.ts as the source of truth.
|
|
4
|
-
* This eliminates duplication by referencing option groups instead of repeating
|
|
5
|
-
* options for every command.
|
|
6
|
-
*/
|
|
7
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
8
|
-
exports.buildHelpData = buildHelpData;
|
|
9
|
-
exports.printHelp = printHelp;
|
|
10
|
-
const constants_1 = require("./constants");
|
|
11
|
-
const options_1 = require("./options");
|
|
12
|
-
const output_1 = require("./output");
|
|
13
|
-
// ============================================================================
|
|
14
|
-
// Help Data Generation
|
|
15
|
-
// ============================================================================
|
|
16
|
-
function formatOption(opt) {
|
|
17
|
-
if (opt.repeatable) {
|
|
18
|
-
return `${opt.flag} (repeatable)`;
|
|
19
|
-
}
|
|
20
|
-
return opt.flag;
|
|
21
|
-
}
|
|
22
|
-
/**
|
|
23
|
-
* Build structured help data from COMMANDS and OPTION_GROUPS.
|
|
24
|
-
*/
|
|
25
|
-
function buildHelpData(command) {
|
|
26
|
-
// Build option groups
|
|
27
|
-
const optionGroups = Object.entries(options_1.OPTION_GROUPS).map(([_key, group]) => ({
|
|
28
|
-
name: group.name,
|
|
29
|
-
description: group.description,
|
|
30
|
-
options: group.options.map(formatOption),
|
|
31
|
-
}));
|
|
32
|
-
optionGroups.push({
|
|
33
|
-
name: options_1.SCREENSHOT_OPTIONS.name,
|
|
34
|
-
description: options_1.SCREENSHOT_OPTIONS.description,
|
|
35
|
-
options: options_1.SCREENSHOT_OPTIONS.options.map(formatOption),
|
|
36
|
-
});
|
|
37
|
-
// Build commands list with their groups and specific options
|
|
38
|
-
const commands = Object.entries(options_1.COMMANDS)
|
|
39
|
-
.map(([name, meta]) => {
|
|
40
|
-
const cmd = {
|
|
41
|
-
name,
|
|
42
|
-
description: meta.description,
|
|
43
|
-
};
|
|
44
|
-
if (meta.groups && meta.groups.length > 0) {
|
|
45
|
-
cmd.groups = [...meta.groups];
|
|
46
|
-
}
|
|
47
|
-
if (meta.options && meta.options.length > 0) {
|
|
48
|
-
cmd.options = meta.options.map(formatOption);
|
|
49
|
-
}
|
|
50
|
-
return cmd;
|
|
51
|
-
});
|
|
52
|
-
const normalizedCommand = command ? normalizeHelpCommand(command) : undefined;
|
|
53
|
-
if (normalizedCommand && !options_1.COMMANDS[normalizedCommand]) {
|
|
54
|
-
(0, output_1.errorOut)(`Unknown command: ${normalizedCommand}`);
|
|
55
|
-
}
|
|
56
|
-
const filteredCommands = normalizedCommand
|
|
57
|
-
? commands.filter((entry) => entry.name === normalizedCommand)
|
|
58
|
-
: commands;
|
|
59
|
-
const filteredGroups = normalizedCommand
|
|
60
|
-
? optionGroups.filter((group) => {
|
|
61
|
-
const target = filteredCommands[0];
|
|
62
|
-
if (!target?.groups || target.groups.length === 0) {
|
|
63
|
-
return false;
|
|
64
|
-
}
|
|
65
|
-
const included = target.groups.some((groupKey) => {
|
|
66
|
-
const meta = options_1.OPTION_GROUPS[groupKey];
|
|
67
|
-
return meta?.name === group.name;
|
|
68
|
-
});
|
|
69
|
-
if (target.name === "screenshot" && group.name === options_1.SCREENSHOT_OPTIONS.name) {
|
|
70
|
-
return true;
|
|
71
|
-
}
|
|
72
|
-
return included;
|
|
73
|
-
})
|
|
74
|
-
: optionGroups;
|
|
75
|
-
// Global options
|
|
76
|
-
const globalOptions = ["--help", "--json", "--pretty"];
|
|
77
|
-
// Notes
|
|
78
|
-
const notes = [
|
|
79
|
-
"--before-group/--after-group only position tabs; use group-assign to move tabs into a group.",
|
|
80
|
-
"undo accepts a txid as a positional arg (or --txid) and supports --latest.",
|
|
81
|
-
"screenshot uses --out to write per-tab folders under the target directory.",
|
|
82
|
-
"Use selector attr href-url/src-url to resolve absolute http(s) links.",
|
|
83
|
-
];
|
|
84
|
-
return {
|
|
85
|
-
version: constants_1.VERSION,
|
|
86
|
-
usage: "tabctl <command> [options]",
|
|
87
|
-
commands: filteredCommands,
|
|
88
|
-
optionGroups: filteredGroups,
|
|
89
|
-
globalOptions,
|
|
90
|
-
notes,
|
|
91
|
-
};
|
|
92
|
-
}
|
|
93
|
-
function normalizeHelpCommand(command) {
|
|
94
|
-
const trimmed = command.trim();
|
|
95
|
-
if (!trimmed) {
|
|
96
|
-
return undefined;
|
|
97
|
-
}
|
|
98
|
-
if (trimmed === "groups" || trimmed === "group") {
|
|
99
|
-
return "group-list";
|
|
100
|
-
}
|
|
101
|
-
return trimmed;
|
|
102
|
-
}
|
|
103
|
-
// ============================================================================
|
|
104
|
-
// Text Output Formatting
|
|
105
|
-
// ============================================================================
|
|
106
|
-
/**
|
|
107
|
-
* Print help in human-readable text format.
|
|
108
|
-
*/
|
|
109
|
-
function printHelpText(data, command) {
|
|
110
|
-
const lines = [];
|
|
111
|
-
// Header
|
|
112
|
-
lines.push("tabctl - Edge tab management CLI");
|
|
113
|
-
lines.push(`Version: ${data.version}`);
|
|
114
|
-
lines.push("");
|
|
115
|
-
lines.push(`Usage: ${data.usage}`);
|
|
116
|
-
lines.push("");
|
|
117
|
-
if (!command) {
|
|
118
|
-
// Commands grouped by category
|
|
119
|
-
lines.push("Commands:");
|
|
120
|
-
const commandNames = data.commands.map((c) => c.name);
|
|
121
|
-
lines.push(` ${commandNames.join(", ")}`);
|
|
122
|
-
lines.push("");
|
|
123
|
-
}
|
|
124
|
-
// Option Groups
|
|
125
|
-
if (!command) {
|
|
126
|
-
lines.push("Option Groups:");
|
|
127
|
-
lines.push(" (Commands reference these groups; see command details below)");
|
|
128
|
-
lines.push("");
|
|
129
|
-
for (const group of data.optionGroups) {
|
|
130
|
-
lines.push(` [${group.name}] - ${group.description}`);
|
|
131
|
-
for (const opt of group.options) {
|
|
132
|
-
lines.push(` ${opt}`);
|
|
133
|
-
}
|
|
134
|
-
lines.push("");
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
else {
|
|
138
|
-
for (const group of data.optionGroups) {
|
|
139
|
-
lines.push(`Options (${group.name}):`);
|
|
140
|
-
for (const opt of group.options) {
|
|
141
|
-
lines.push(` ${opt}`);
|
|
142
|
-
}
|
|
143
|
-
lines.push("");
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
// Command Details
|
|
147
|
-
lines.push("Command Details:");
|
|
148
|
-
for (const cmd of data.commands) {
|
|
149
|
-
const parts = [` ${cmd.name}`];
|
|
150
|
-
if (cmd.description) {
|
|
151
|
-
parts.push(`- ${cmd.description}`);
|
|
152
|
-
}
|
|
153
|
-
lines.push(parts.join(" "));
|
|
154
|
-
// Show which groups the command uses
|
|
155
|
-
if (!command && cmd.groups && cmd.groups.length > 0) {
|
|
156
|
-
const groupRefs = cmd.groups.map((g) => {
|
|
157
|
-
const group = options_1.OPTION_GROUPS[g];
|
|
158
|
-
return group ? `[${group.name}]` : `[${g}]`;
|
|
159
|
-
});
|
|
160
|
-
lines.push(` Uses: ${groupRefs.join(", ")}`);
|
|
161
|
-
}
|
|
162
|
-
// Show command-specific options
|
|
163
|
-
if (cmd.options && cmd.options.length > 0) {
|
|
164
|
-
lines.push(" Options:");
|
|
165
|
-
for (const opt of cmd.options) {
|
|
166
|
-
lines.push(` ${opt}`);
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
|
-
lines.push("");
|
|
171
|
-
// Global Options
|
|
172
|
-
if (!command) {
|
|
173
|
-
lines.push("Global Options:");
|
|
174
|
-
for (const opt of data.globalOptions) {
|
|
175
|
-
lines.push(` ${opt}`);
|
|
176
|
-
}
|
|
177
|
-
lines.push("");
|
|
178
|
-
}
|
|
179
|
-
// Notes
|
|
180
|
-
if (!command) {
|
|
181
|
-
lines.push("Notes:");
|
|
182
|
-
for (const note of data.notes) {
|
|
183
|
-
lines.push(` ${note}`);
|
|
184
|
-
}
|
|
185
|
-
lines.push("");
|
|
186
|
-
}
|
|
187
|
-
// Policy location
|
|
188
|
-
lines.push("Policy: $XDG_CONFIG_HOME/tabctl/policy.json (or ~/.config/tabctl/policy.json)");
|
|
189
|
-
lines.push("Policy is enforced when the file exists; missing file means no policy.");
|
|
190
|
-
process.stdout.write(lines.join("\n") + "\n");
|
|
191
|
-
}
|
|
192
|
-
// ============================================================================
|
|
193
|
-
// Public API
|
|
194
|
-
// ============================================================================
|
|
195
|
-
/**
|
|
196
|
-
* Print help output in either JSON or text format.
|
|
197
|
-
*/
|
|
198
|
-
function printHelp(jsonOutput, command) {
|
|
199
|
-
const data = buildHelpData(command);
|
|
200
|
-
if (jsonOutput) {
|
|
201
|
-
(0, output_1.printJson)({ ok: true, data });
|
|
202
|
-
return;
|
|
203
|
-
}
|
|
204
|
-
printHelpText(data, command);
|
|
205
|
-
}
|