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,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);
@@ -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
- }