tabctl 0.1.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.
@@ -0,0 +1,205 @@
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
+ }
@@ -0,0 +1,408 @@
1
+ "use strict";
2
+ /**
3
+ * Central source of truth for CLI options and command metadata.
4
+ * This eliminates duplication between argument parsing and help display.
5
+ */
6
+ Object.defineProperty(exports, "__esModule", { value: true });
7
+ exports.COMMANDS = exports.SCREENSHOT_OPTIONS = exports.OPTION_GROUPS = void 0;
8
+ exports.getBooleanFlags = getBooleanFlags;
9
+ exports.getAllowedFlags = getAllowedFlags;
10
+ exports.getCommandAllowedFlags = getCommandAllowedFlags;
11
+ exports.getCommandGroups = getCommandGroups;
12
+ exports.getCommandOptions = getCommandOptions;
13
+ exports.commandSupportsGroup = commandSupportsGroup;
14
+ const GLOBAL_FLAGS = ["help", "json", "pretty"];
15
+ // ============================================================================
16
+ // Option Group Definitions
17
+ // ============================================================================
18
+ exports.OPTION_GROUPS = {
19
+ scope: {
20
+ name: "Scope Options",
21
+ description: "Filter which tabs/groups to operate on",
22
+ options: [
23
+ { flag: "--tab <id>", desc: "Target specific tab(s) by ID", repeatable: true },
24
+ { flag: "--group <name>", desc: "Target tabs in group by title" },
25
+ { flag: "--group-id <id>", desc: "Target group by ID (use -1 for ungrouped)" },
26
+ { flag: "--ungrouped", desc: "Alias for --group-id -1" },
27
+ { flag: "--window <id|active|last-focused>", desc: "Target tabs in specific window" },
28
+ { flag: "--all", desc: "Target all eligible tabs" },
29
+ ],
30
+ },
31
+ pagination: {
32
+ name: "Pagination Options",
33
+ description: "Control result paging (default limit: 100)",
34
+ options: [
35
+ { flag: "--limit <n>", desc: "Maximum items to return" },
36
+ { flag: "--offset <n>", desc: "Skip first n items" },
37
+ { flag: "--no-page", desc: "Disable pagination, return all results" },
38
+ ],
39
+ },
40
+ };
41
+ exports.SCREENSHOT_OPTIONS = {
42
+ name: "Screenshot Options",
43
+ description: "Control screenshot capture",
44
+ options: [
45
+ { flag: "--mode viewport|full", desc: "Capture mode" },
46
+ { flag: "--format png|jpeg", desc: "Image format" },
47
+ { flag: "--quality <n>", desc: "JPEG quality (0-100)" },
48
+ { flag: "--tile-max-dim <px>", desc: "Max tile dimension in pixels" },
49
+ { flag: "--max-bytes <n>", desc: "Max bytes per tile" },
50
+ { flag: "--wait-for load|dom|settle|none", desc: "Wait for page readiness before capture" },
51
+ { flag: "--wait-timeout-ms <ms>", desc: "Timeout for page readiness wait" },
52
+ { flag: "--out <dir>", desc: "Write files to directory" },
53
+ { flag: "--progress", desc: "Show progress during capture" },
54
+ ],
55
+ };
56
+ // ============================================================================
57
+ // Command Metadata
58
+ // ============================================================================
59
+ exports.COMMANDS = {
60
+ help: {
61
+ description: "Show help information",
62
+ },
63
+ list: {
64
+ description: "List browser tabs",
65
+ groups: ["scope", "pagination"],
66
+ options: [
67
+ { flag: "--groups", desc: "Alias for group-list command" },
68
+ ],
69
+ },
70
+ analyze: {
71
+ description: "Analyze tabs for duplicates and stale content",
72
+ groups: ["scope"],
73
+ options: [
74
+ { flag: "--stale-days <n>", desc: "Days threshold for stale tabs" },
75
+ { flag: "--github", desc: "Enable GitHub PR/issue status checking" },
76
+ { flag: "--github-concurrency <n>", desc: "Max concurrent GitHub API requests" },
77
+ { flag: "--github-timeout-ms <ms>", desc: "Timeout for GitHub API requests" },
78
+ { flag: "--window-title", desc: "Include active window title in output" },
79
+ { flag: "--progress", desc: "Show progress during analysis" },
80
+ ],
81
+ },
82
+ dedupe: {
83
+ description: "Interactively deduplicate tabs",
84
+ groups: ["scope"],
85
+ options: [
86
+ { flag: "--stale-days <n>", desc: "Days threshold for stale tabs" },
87
+ { flag: "--github", desc: "Enable GitHub PR/issue status checking" },
88
+ { flag: "--github-concurrency <n>", desc: "Max concurrent GitHub API requests" },
89
+ { flag: "--github-timeout-ms <ms>", desc: "Timeout for GitHub API requests" },
90
+ { flag: "--include-stale", desc: "Include stale tabs in deduplication" },
91
+ { flag: "--window-title", desc: "Include active window title in output" },
92
+ { flag: "--progress", desc: "Show progress during analysis" },
93
+ { flag: "--confirm", desc: "Execute changes without prompting" },
94
+ ],
95
+ },
96
+ inspect: {
97
+ description: "Extract signals from tab content",
98
+ groups: ["scope", "pagination"],
99
+ options: [
100
+ { flag: "--signal-config <path>", desc: "Path to signal configuration file" },
101
+ { flag: "--signal <id>", desc: "Signal ID to extract", repeatable: true },
102
+ { flag: "--selector <name=css|json>", desc: "Custom selector definition (attr: href-url/src-url supported; text/textMode supported)", repeatable: true },
103
+ { flag: "--selector-attr <attr>", desc: "Default selector attr (text|href|src|href-url|src-url)" },
104
+ { flag: "--signal-concurrency <n>", desc: "Max concurrent signal extractions" },
105
+ { flag: "--signal-timeout-ms <ms>", desc: "Timeout for signal extraction" },
106
+ { flag: "--wait-for load|dom|settle|none", desc: "Wait for page readiness before inspection" },
107
+ { flag: "--wait-timeout-ms <ms>", desc: "Timeout for page readiness wait" },
108
+ { flag: "--progress", desc: "Show progress during inspection" },
109
+ ],
110
+ },
111
+ screenshot: {
112
+ description: "Capture screenshots from tabs",
113
+ groups: ["scope"],
114
+ options: exports.SCREENSHOT_OPTIONS.options,
115
+ },
116
+ focus: {
117
+ description: "Focus a specific tab",
118
+ options: [
119
+ { flag: "--tab <id>", desc: "Tab ID to focus" },
120
+ ],
121
+ },
122
+ refresh: {
123
+ description: "Refresh a specific tab",
124
+ options: [
125
+ { flag: "--tab <id>", desc: "Tab ID to refresh" },
126
+ ],
127
+ },
128
+ open: {
129
+ description: "Open new tabs with URLs",
130
+ options: [
131
+ { flag: "--url <url>", desc: "URL to open", repeatable: true },
132
+ { flag: "--group <name>", desc: "Add tabs to group by name" },
133
+ { flag: "--color <name>", desc: "Group color (if creating)" },
134
+ { flag: "--before-tab <id>", desc: "Position before this tab" },
135
+ { flag: "--after-tab <id>", desc: "Position after this tab" },
136
+ { flag: "--after-group <name>", desc: "Position after this group" },
137
+ { flag: "--window <id|active|last-focused|new>", desc: "Target window ID" },
138
+ { flag: "--new-window", desc: "Open in new window" },
139
+ { flag: "--window-group <name>", desc: "Find window containing group" },
140
+ { flag: "--window-tab <id>", desc: "Find window containing tab" },
141
+ { flag: "--window-url <substring>", desc: "Find window containing URL" },
142
+ ],
143
+ },
144
+ "group-list": {
145
+ description: "List tab groups",
146
+ groups: ["scope", "pagination"],
147
+ },
148
+ group: {
149
+ description: "Alias for group-list",
150
+ aliases: ["group-list"],
151
+ groups: ["scope", "pagination"],
152
+ },
153
+ "group-update": {
154
+ description: "Update tab group properties",
155
+ options: [
156
+ { flag: "--group <name>", desc: "Target group by title" },
157
+ { flag: "--group-id <id>", desc: "Target group by ID" },
158
+ { flag: "--window <id|active|last-focused>", desc: "Target window ID" },
159
+ { flag: "--title <name>", desc: "New group title" },
160
+ { flag: "--color <name>", desc: "New group color" },
161
+ { flag: "--collapsed", desc: "Collapse the group" },
162
+ { flag: "--expanded", desc: "Expand the group" },
163
+ ],
164
+ },
165
+ "group-ungroup": {
166
+ description: "Remove tabs from a group",
167
+ options: [
168
+ { flag: "--group <name>", desc: "Target group by title" },
169
+ { flag: "--group-id <id>", desc: "Target group by ID" },
170
+ { flag: "--window <id|active|last-focused>", desc: "Target window ID" },
171
+ ],
172
+ },
173
+ "group-assign": {
174
+ description: "Assign tabs to a group",
175
+ options: [
176
+ { flag: "--tab <id>", desc: "Tab ID(s) to assign", repeatable: true },
177
+ { flag: "--group <name>", desc: "Target group by title" },
178
+ { flag: "--group-id <id>", desc: "Target group by ID" },
179
+ { flag: "--window <id|active|last-focused>", desc: "Target window ID" },
180
+ { flag: "--create", desc: "Create group if not exists" },
181
+ { flag: "--color <name>", desc: "Group color (if creating)" },
182
+ { flag: "--collapsed", desc: "Collapse group after assign" },
183
+ { flag: "--expanded", desc: "Expand group after assign" },
184
+ ],
185
+ },
186
+ "move-tab": {
187
+ description: "Move a tab to a new position",
188
+ options: [
189
+ { flag: "--tab <id>", desc: "Tab ID to move" },
190
+ { flag: "--before-tab <id>", desc: "Position before this tab" },
191
+ { flag: "--after-tab <id>", desc: "Position after this tab" },
192
+ { flag: "--before-group <name>", desc: "Position before this group" },
193
+ { flag: "--after-group <name>", desc: "Position after this group" },
194
+ { flag: "--window <id|active|last-focused>", desc: "Target window ID" },
195
+ { flag: "--new-window", desc: "Move to new window" },
196
+ ],
197
+ },
198
+ "move-group": {
199
+ description: "Move a tab group to a new position",
200
+ options: [
201
+ { flag: "--group <name>", desc: "Target group by title" },
202
+ { flag: "--group-id <id>", desc: "Target group by ID" },
203
+ { flag: "--before-tab <id>", desc: "Position before this tab" },
204
+ { flag: "--after-tab <id>", desc: "Position after this tab" },
205
+ { flag: "--before-group <name>", desc: "Position before this group" },
206
+ { flag: "--after-group <name>", desc: "Position after this group" },
207
+ { flag: "--window <id|active|last-focused>", desc: "Target window ID" },
208
+ { flag: "--new-window", desc: "Move to new window" },
209
+ ],
210
+ },
211
+ "merge-window": {
212
+ description: "Merge tabs from one window to another",
213
+ options: [
214
+ { flag: "--from <id>", desc: "Source window ID" },
215
+ { flag: "--to <id>", desc: "Destination window ID" },
216
+ { flag: "--close-source", desc: "Close source window after merge" },
217
+ { flag: "--confirm", desc: "Execute without prompting" },
218
+ ],
219
+ },
220
+ setup: {
221
+ description: "Configure tabctl connection",
222
+ options: [
223
+ { flag: "--browser edge|chrome", desc: "Browser type" },
224
+ { flag: "--extension-id <id>", desc: "Extension ID to connect to" },
225
+ { flag: "--node <path>", desc: "Path to Node.js executable" },
226
+ { flag: "--name <name>", desc: "Profile name (default: browser type)" },
227
+ { flag: "--user-data-dir <path>", desc: "Chrome/Edge user data directory for custom profiles" },
228
+ ],
229
+ },
230
+ policy: {
231
+ description: "Manage browser policies",
232
+ options: [
233
+ { flag: "--init", desc: "Initialize policy configuration" },
234
+ ],
235
+ },
236
+ archive: {
237
+ description: "Archive tabs to storage",
238
+ groups: ["scope"],
239
+ },
240
+ close: {
241
+ description: "Close tabs",
242
+ options: [
243
+ { flag: "--apply <analysisId>", desc: "Apply analysis results" },
244
+ { flag: "--tab <id>", desc: "Tab ID(s) to close", repeatable: true },
245
+ { flag: "--group <name>", desc: "Close tabs in group by title" },
246
+ { flag: "--group-id <id>", desc: "Close tabs in group by ID" },
247
+ { flag: "--ungrouped", desc: "Close ungrouped tabs" },
248
+ { flag: "--window <id|active|last-focused>", desc: "Target window ID" },
249
+ { flag: "--confirm", desc: "Execute without prompting" },
250
+ { flag: "--dry-run", desc: "Show what would be closed" },
251
+ ],
252
+ },
253
+ report: {
254
+ description: "Generate tab reports",
255
+ groups: ["scope", "pagination"],
256
+ options: [
257
+ { flag: "--format json|md|csv", desc: "Output format" },
258
+ { flag: "--out <path>", desc: "Output file path" },
259
+ ],
260
+ },
261
+ undo: {
262
+ description: "Undo a previous operation",
263
+ options: [
264
+ { flag: "<txid>", desc: "Transaction ID (positional)" },
265
+ { flag: "--txid <id>", desc: "Transaction ID" },
266
+ { flag: "--latest", desc: "Undo most recent transaction" },
267
+ ],
268
+ },
269
+ history: {
270
+ description: "Show operation history",
271
+ options: [
272
+ { flag: "--limit <n>", desc: "Maximum entries to show" },
273
+ ],
274
+ },
275
+ skill: {
276
+ description: "Show AI agent skill documentation",
277
+ options: [
278
+ { flag: "--agent <name>", desc: "Target agent(s)", repeatable: true },
279
+ { flag: "--global", desc: "Show global skill info" },
280
+ ],
281
+ },
282
+ "profile-list": {
283
+ description: "List configured profiles",
284
+ },
285
+ profile: {
286
+ description: "Alias for profile-list",
287
+ aliases: ["profile-list"],
288
+ },
289
+ "profile-show": {
290
+ description: "Show active profile details",
291
+ },
292
+ "profile-switch": {
293
+ description: "Switch the default profile",
294
+ options: [
295
+ { flag: "<name>", desc: "Profile name (positional)" },
296
+ ],
297
+ },
298
+ "profile-remove": {
299
+ description: "Remove a profile",
300
+ options: [
301
+ { flag: "<name>", desc: "Profile name (positional)" },
302
+ ],
303
+ },
304
+ version: {
305
+ description: "Show version information",
306
+ },
307
+ ping: {
308
+ description: "Test connection to browser extension",
309
+ },
310
+ };
311
+ // ============================================================================
312
+ // Helper Functions
313
+ // ============================================================================
314
+ /**
315
+ * Get all allowed flags for argument parsing validation.
316
+ */
317
+ function getBooleanFlags() {
318
+ const flags = new Set();
319
+ GLOBAL_FLAGS.forEach((flag) => flags.add(flag));
320
+ const addFromOptions = (options) => {
321
+ for (const opt of options) {
322
+ if (!/^--[a-z-]+$/.test(opt.flag.trim())) {
323
+ continue;
324
+ }
325
+ const match = opt.flag.match(/^--([a-z-]+)/);
326
+ if (match) {
327
+ flags.add(match[1]);
328
+ }
329
+ }
330
+ };
331
+ for (const group of Object.values(exports.OPTION_GROUPS)) {
332
+ addFromOptions(group.options);
333
+ }
334
+ for (const cmd of Object.values(exports.COMMANDS)) {
335
+ if (cmd.options) {
336
+ addFromOptions(cmd.options);
337
+ }
338
+ }
339
+ return flags;
340
+ }
341
+ function getAllowedFlags() {
342
+ const flags = new Set();
343
+ for (const flag of getBooleanFlags()) {
344
+ flags.add(flag);
345
+ }
346
+ // Add value flags from option groups
347
+ for (const group of Object.values(exports.OPTION_GROUPS)) {
348
+ for (const opt of group.options) {
349
+ const match = opt.flag.match(/^--([a-z-]+)/);
350
+ if (match)
351
+ flags.add(match[1]);
352
+ }
353
+ }
354
+ // Add value flags from command options
355
+ for (const cmd of Object.values(exports.COMMANDS)) {
356
+ for (const opt of cmd.options || []) {
357
+ const match = opt.flag.match(/^--([a-z-]+)/);
358
+ if (match)
359
+ flags.add(match[1]);
360
+ }
361
+ }
362
+ flags.add("profile");
363
+ return flags;
364
+ }
365
+ function getCommandAllowedFlags(command) {
366
+ const flags = new Set(GLOBAL_FLAGS);
367
+ flags.add("profile");
368
+ const meta = exports.COMMANDS[command];
369
+ const addOptions = (options) => {
370
+ if (!options) {
371
+ return;
372
+ }
373
+ for (const opt of options) {
374
+ const match = opt.flag.match(/^--([a-z-]+)/);
375
+ if (match) {
376
+ flags.add(match[1]);
377
+ }
378
+ }
379
+ };
380
+ if (meta?.groups) {
381
+ for (const groupKey of meta.groups) {
382
+ addOptions(exports.OPTION_GROUPS[groupKey]?.options);
383
+ }
384
+ }
385
+ addOptions(meta?.options);
386
+ return flags;
387
+ }
388
+ /**
389
+ * Get the option groups for a command.
390
+ */
391
+ function getCommandGroups(command) {
392
+ const meta = exports.COMMANDS[command];
393
+ return meta?.groups ? [...meta.groups] : [];
394
+ }
395
+ /**
396
+ * Get command-specific options (not from groups).
397
+ */
398
+ function getCommandOptions(command) {
399
+ const meta = exports.COMMANDS[command];
400
+ return meta?.options ? [...meta.options] : [];
401
+ }
402
+ /**
403
+ * Check if a command supports a specific option group.
404
+ */
405
+ function commandSupportsGroup(command, group) {
406
+ const meta = exports.COMMANDS[command];
407
+ return meta?.groups?.includes(group) ?? false;
408
+ }
@@ -0,0 +1,64 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.printJson = printJson;
4
+ exports.errorOut = errorOut;
5
+ exports.setupStdoutErrorHandling = setupStdoutErrorHandling;
6
+ exports.emitVersionWarnings = emitVersionWarnings;
7
+ const version_1 = require("../../shared/version");
8
+ const profiles_1 = require("../../shared/profiles");
9
+ function printJson(payload, pretty = true) {
10
+ try {
11
+ const active = (0, profiles_1.getActiveProfile)();
12
+ if (active) {
13
+ payload.profile = active.name;
14
+ payload.browser = active.profile.browser;
15
+ }
16
+ }
17
+ catch {
18
+ // Don't let profile errors break output
19
+ }
20
+ const output = pretty ? JSON.stringify(payload, null, 2) : JSON.stringify(payload);
21
+ process.stdout.write(`${output}\n`);
22
+ }
23
+ function errorOut(message) {
24
+ const hints = {
25
+ "Unknown option: --format": "Use --json for JSON output. --format is only for report.",
26
+ };
27
+ const hint = hints[message];
28
+ if (hint) {
29
+ printJson({ ok: false, error: { message, hint } });
30
+ }
31
+ else {
32
+ printJson({ ok: false, error: { message } });
33
+ }
34
+ process.exit(1);
35
+ throw new Error(message);
36
+ }
37
+ function setupStdoutErrorHandling() {
38
+ process.stdout.on("error", (error) => {
39
+ if (error.code === "EPIPE") {
40
+ process.exit(0);
41
+ }
42
+ throw error;
43
+ });
44
+ }
45
+ function emitVersionWarnings(response, fallbackAction) {
46
+ const hostVersion = typeof response.version === "string" ? response.version : null;
47
+ if (hostVersion && hostVersion !== version_1.VERSION) {
48
+ process.stderr.write(`[tabctl] version mismatch: cli ${version_1.VERSION}, host ${hostVersion}\n`);
49
+ }
50
+ const data = response.data;
51
+ const extensionVersion = data && typeof data.extensionVersion === "string" ? data.extensionVersion : null;
52
+ const extensionComponent = data && typeof data.extensionComponent === "string" ? data.extensionComponent : null;
53
+ if (extensionVersion && hostVersion && extensionVersion !== hostVersion) {
54
+ process.stderr.write(`[tabctl] version mismatch: host ${hostVersion}, extension ${extensionVersion}\n`);
55
+ }
56
+ if (extensionComponent && extensionComponent !== "extension") {
57
+ process.stderr.write(`[tabctl] unexpected extension component: ${extensionComponent}\n`);
58
+ }
59
+ const action = response.action || fallbackAction;
60
+ const extensionExpected = !["history", "version"].includes(action);
61
+ if (extensionExpected && !extensionVersion) {
62
+ process.stderr.write("[tabctl] extension version unavailable; reload the extension to validate version match\n");
63
+ }
64
+ }