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.
package/cli/tabctl.js ADDED
@@ -0,0 +1,841 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ var __importDefault = (this && this.__importDefault) || function (mod) {
4
+ return (mod && mod.__esModule) ? mod : { "default": mod };
5
+ };
6
+ Object.defineProperty(exports, "__esModule", { value: true });
7
+ const fs_1 = __importDefault(require("fs"));
8
+ const path_1 = __importDefault(require("path"));
9
+ const report_1 = require("./lib/report");
10
+ const policy_1 = require("./lib/policy");
11
+ const constants_1 = require("./lib/constants");
12
+ const output_1 = require("./lib/output");
13
+ const args_1 = require("./lib/args");
14
+ const client_1 = require("./lib/client");
15
+ const scope_1 = require("./lib/scope");
16
+ const pagination_1 = require("./lib/pagination");
17
+ const snapshot_1 = require("./lib/snapshot");
18
+ const help_1 = require("./lib/help");
19
+ const commands_1 = require("./lib/commands");
20
+ const commands_2 = require("./lib/commands");
21
+ const createId = client_1.createRequestId;
22
+ async function main() {
23
+ (0, output_1.setupStdoutErrorHandling)();
24
+ let { command, options, warnings } = (0, args_1.parseArgs)(process.argv.slice(2));
25
+ if (typeof options.profile === "string" && options.profile) {
26
+ process.env.TABCTL_PROFILE = options.profile;
27
+ }
28
+ if (command === "dedupe" && options.close) {
29
+ (0, output_1.errorOut)("dedupe does not support --close; use --confirm or close --apply <analysisId>.");
30
+ }
31
+ const prettyOutput = options.pretty !== false;
32
+ if (command === "groups" || command === "group") {
33
+ command = "group-list";
34
+ }
35
+ if (command === "profile" || command === "profiles") {
36
+ command = "profile-list";
37
+ }
38
+ if (command === "list" && options.groups === true) {
39
+ command = "group-list";
40
+ }
41
+ if (options.format && command !== "report" && command !== "screenshot") {
42
+ (0, output_1.errorOut)("Unknown option: --format");
43
+ }
44
+ if (Object.prototype.hasOwnProperty.call(options, "policy")) {
45
+ (0, output_1.errorOut)("Custom policy path is not supported. Use XDG_CONFIG_HOME/tabctl/policy.json.");
46
+ }
47
+ if (command === "refresh") {
48
+ const tabValues = Array.isArray(options.tab)
49
+ ? options.tab.map((value) => String(value).trim()).filter(Boolean)
50
+ : [];
51
+ if (tabValues.length === 0) {
52
+ (0, output_1.errorOut)("refresh requires --tab");
53
+ }
54
+ if (tabValues.length > 1) {
55
+ (0, output_1.errorOut)("refresh requires a single --tab");
56
+ }
57
+ }
58
+ if (command === "open" && options.color && !options.group) {
59
+ (0, output_1.errorOut)("--color requires --group");
60
+ }
61
+ if (command === "undo") {
62
+ if (options.txid && options._.length > 0) {
63
+ (0, output_1.errorOut)("undo requires a single txid (use positional arg or --txid)");
64
+ }
65
+ if (options.latest === true && options._.length > 0) {
66
+ (0, output_1.errorOut)("undo --latest cannot be combined with a txid");
67
+ }
68
+ if (options.latest === true && options.txid) {
69
+ (0, output_1.errorOut)("undo --latest cannot be combined with --txid");
70
+ }
71
+ if (options.txid && options._.length === 0) {
72
+ options._ = [String(options.txid)];
73
+ }
74
+ }
75
+ if (command === "inspect") {
76
+ const selectorCount = Array.isArray(options.selector) ? options.selector.length : 0;
77
+ if (selectorCount > 0) {
78
+ const signalList = (0, args_1.normalizeSignals)(options.signal);
79
+ if (!signalList.includes("selector")) {
80
+ signalList.push("selector");
81
+ }
82
+ options.signal = signalList;
83
+ }
84
+ const signalList = (0, args_1.normalizeSignals)(options.signal);
85
+ if (signalList.length > 0) {
86
+ (0, args_1.validateSignals)(signalList);
87
+ options.signal = signalList;
88
+ }
89
+ }
90
+ if (command === "screenshot") {
91
+ const mode = options.mode != null ? String(options.mode).trim().toLowerCase() : "viewport";
92
+ const format = options.format != null ? String(options.format).trim().toLowerCase() : "png";
93
+ if (mode !== "viewport" && mode !== "full") {
94
+ (0, output_1.errorOut)("Invalid --mode value (use viewport or full)");
95
+ }
96
+ if (format !== "png" && format !== "jpeg") {
97
+ (0, output_1.errorOut)("Invalid --format value (use png or jpeg)");
98
+ }
99
+ if (format === "jpeg" && options.quality == null) {
100
+ options.quality = 80;
101
+ }
102
+ const qualityRaw = options.quality != null ? Number(options.quality) : null;
103
+ if (qualityRaw != null && (!Number.isFinite(qualityRaw) || qualityRaw < 0 || qualityRaw > 100)) {
104
+ (0, output_1.errorOut)("Invalid --quality value (use 0-100)");
105
+ }
106
+ if (qualityRaw != null && format !== "jpeg") {
107
+ (0, output_1.errorOut)("--quality requires --format jpeg");
108
+ }
109
+ const tileMaxDimRaw = options["tile-max-dim"] != null ? Number(options["tile-max-dim"]) : null;
110
+ if (tileMaxDimRaw != null && (!Number.isFinite(tileMaxDimRaw) || tileMaxDimRaw <= 0)) {
111
+ (0, output_1.errorOut)("Invalid --tile-max-dim value");
112
+ }
113
+ const maxBytesRaw = options["max-bytes"] != null ? Number(options["max-bytes"]) : null;
114
+ if (maxBytesRaw != null && (!Number.isFinite(maxBytesRaw) || maxBytesRaw <= 0)) {
115
+ (0, output_1.errorOut)("Invalid --max-bytes value");
116
+ }
117
+ if (mode === "viewport" && options["tile-max-dim"] != null) {
118
+ (0, output_1.errorOut)("--tile-max-dim requires --mode full");
119
+ }
120
+ if (mode === "viewport" && options["max-bytes"] != null) {
121
+ (0, output_1.errorOut)("--max-bytes requires --mode full");
122
+ }
123
+ if (options.mode == null) {
124
+ options.mode = "viewport";
125
+ }
126
+ if (options.format == null) {
127
+ options.format = "png";
128
+ }
129
+ }
130
+ const policyContext = (0, policy_1.loadPolicy)();
131
+ const policySummary = (0, policy_1.summarizePolicy)(policyContext.policy, policyContext.path);
132
+ const policyEnabled = policyContext.policy !== null;
133
+ const enforcePolicy = policyEnabled;
134
+ const includeWindowTitle = options["window-title"] === true;
135
+ const includeStale = options["include-stale"] === true;
136
+ let policySnapshot = null;
137
+ const getPolicySnapshot = async () => {
138
+ if (!policySnapshot) {
139
+ policySnapshot = await (0, client_1.fetchSnapshot)();
140
+ }
141
+ return policySnapshot;
142
+ };
143
+ if (!command || command === "help" || options.help) {
144
+ const helpTarget = command === "help"
145
+ ? (options._.length > 0 ? String(options._[0]) : undefined)
146
+ : command;
147
+ (0, help_1.printHelp)(options.json === true, helpTarget);
148
+ return;
149
+ }
150
+ if (warnings.length > 0) {
151
+ for (const warning of warnings) {
152
+ process.stderr.write(`[tabctl] warning: ${warning}\n`);
153
+ }
154
+ }
155
+ if (command === "skill") {
156
+ (0, commands_1.runSkillInstall)(options, prettyOutput);
157
+ return;
158
+ }
159
+ if (command === "setup") {
160
+ await (0, commands_1.runSetup)(options, prettyOutput);
161
+ return;
162
+ }
163
+ if (command === "version") {
164
+ (0, commands_1.runVersion)(prettyOutput);
165
+ return;
166
+ }
167
+ if (command === "policy") {
168
+ (0, commands_1.runPolicy)(options, policyContext, prettyOutput);
169
+ return;
170
+ }
171
+ if (command === "ping") {
172
+ await (0, commands_1.runPing)(prettyOutput);
173
+ return;
174
+ }
175
+ if (command === "history") {
176
+ await (0, commands_1.runHistory)(options, prettyOutput);
177
+ return;
178
+ }
179
+ if (command === "undo") {
180
+ await (0, commands_1.runUndo)(options, prettyOutput);
181
+ return;
182
+ }
183
+ if (command === "profile-list") {
184
+ (0, commands_1.runProfileList)(options, prettyOutput);
185
+ return;
186
+ }
187
+ if (command === "profile-show") {
188
+ (0, commands_1.runProfileShow)(options, prettyOutput);
189
+ return;
190
+ }
191
+ if (command === "profile-switch") {
192
+ (0, commands_1.runProfileSwitch)(options, prettyOutput);
193
+ return;
194
+ }
195
+ if (command === "profile-remove") {
196
+ (0, commands_1.runProfileRemove)(options, prettyOutput);
197
+ return;
198
+ }
199
+ if (command === "list") {
200
+ await (0, commands_1.runList)(options, policyContext, policySummary, prettyOutput);
201
+ return;
202
+ }
203
+ if (command === "group-list") {
204
+ await (0, commands_1.runGroupList)(options, policyContext, policySummary, prettyOutput);
205
+ return;
206
+ }
207
+ let dedupeMode = false;
208
+ if (command === "close" && options["dry-run"]) {
209
+ command = "analyze";
210
+ }
211
+ if (command === "dedupe") {
212
+ dedupeMode = true;
213
+ command = "analyze";
214
+ }
215
+ let action = command;
216
+ let params = {};
217
+ let policyInfo = null;
218
+ let earlyResponse = null;
219
+ switch (command) {
220
+ case "analyze":
221
+ action = "analyze";
222
+ params = (0, commands_2.buildAnalyzeParams)(options);
223
+ break;
224
+ case "inspect":
225
+ action = "inspect";
226
+ params = (0, commands_2.buildInspectParams)(options);
227
+ break;
228
+ case "focus":
229
+ action = "focus";
230
+ params = (0, commands_2.buildFocusParams)(options);
231
+ break;
232
+ case "refresh":
233
+ action = "refresh";
234
+ params = (0, commands_2.buildRefreshParams)(options);
235
+ break;
236
+ case "open":
237
+ action = "open";
238
+ params = (0, commands_2.buildOpenParams)(options);
239
+ break;
240
+ case "group-update":
241
+ action = "group-update";
242
+ params = (0, commands_2.buildGroupUpdateParams)(options);
243
+ break;
244
+ case "group-ungroup":
245
+ action = "group-ungroup";
246
+ params = (0, commands_2.buildGroupUngroupParams)(options);
247
+ break;
248
+ case "group-assign":
249
+ action = "group-assign";
250
+ params = (0, commands_2.buildGroupAssignParams)(options);
251
+ break;
252
+ case "move-tab":
253
+ action = "move-tab";
254
+ params = (0, commands_2.buildMoveTabParams)(options);
255
+ break;
256
+ case "move-group":
257
+ action = "move-group";
258
+ params = (0, commands_2.buildMoveGroupParams)(options);
259
+ break;
260
+ case "merge-window":
261
+ action = "merge-window";
262
+ params = (0, commands_2.buildMergeWindowParams)(options);
263
+ break;
264
+ case "archive":
265
+ action = "archive";
266
+ params = (0, commands_2.buildArchiveParams)(options);
267
+ break;
268
+ case "close":
269
+ action = "close";
270
+ params = (0, commands_2.buildCloseParams)(options);
271
+ break;
272
+ case "report":
273
+ action = "report";
274
+ params = (0, commands_2.buildReportParams)(options);
275
+ break;
276
+ case "screenshot":
277
+ action = "screenshot";
278
+ params = (0, commands_2.buildScreenshotParams)(options);
279
+ break;
280
+ default:
281
+ (0, output_1.errorOut)(`Unknown command: ${command}`);
282
+ }
283
+ if (command === "analyze") {
284
+ const tabIds = params.tabIds;
285
+ const windowId = params.windowId;
286
+ const hasScope = (Array.isArray(tabIds) && tabIds.length > 0)
287
+ || Boolean(params.groupTitle)
288
+ || Number.isFinite(params.groupId)
289
+ || (typeof windowId === "number" && Number.isFinite(windowId))
290
+ || (typeof windowId === "string" && windowId.length > 0)
291
+ || params.all === true;
292
+ if (!hasScope) {
293
+ params = { ...params, all: true };
294
+ }
295
+ }
296
+ if (command === "merge-window") {
297
+ const fromWindowId = params.fromWindowId;
298
+ const toWindowId = params.toWindowId;
299
+ if (!Number.isFinite(fromWindowId) || !Number.isFinite(toWindowId)) {
300
+ (0, output_1.errorOut)("merge-window requires --from and --to window ids");
301
+ }
302
+ if (fromWindowId === toWindowId) {
303
+ (0, output_1.errorOut)("merge-window --from and --to cannot be the same window");
304
+ }
305
+ if (params.closeSource && !params.confirmed) {
306
+ (0, output_1.errorOut)("merge-window --close-source requires --confirm");
307
+ }
308
+ }
309
+ if (enforcePolicy && ["analyze", "inspect", "report", "screenshot", "close", "archive", "focus", "refresh", "move-tab", "move-group", "group-assign", "group-update", "group-ungroup", "merge-window"].includes(command)) {
310
+ if (command === "close" && options.apply) {
311
+ (0, output_1.errorOut)("Policy blocks close --apply; use explicit tab targets.");
312
+ }
313
+ const snapshot = await getPolicySnapshot();
314
+ if (!snapshot) {
315
+ (0, output_1.errorOut)("Failed to load tabs for policy evaluation");
316
+ }
317
+ const selection = (0, scope_1.selectTabsFromSnapshot)(snapshot, params);
318
+ if (selection.error) {
319
+ (0, output_1.printJson)({ ok: false, error: selection.error }, prettyOutput);
320
+ process.exit(1);
321
+ }
322
+ const selectedTabs = selection.tabs;
323
+ const eligibleTabs = selectedTabs.filter((tab) => (0, policy_1.evaluateTab)(tab, policyContext.policy).eligible);
324
+ const protectedTabs = selectedTabs.filter((tab) => !(0, policy_1.evaluateTab)(tab, policyContext.policy).eligible);
325
+ const eligibleIds = eligibleTabs.map((tab) => tab.tabId).filter((id) => typeof id === "number");
326
+ if (command === "focus" || command === "refresh") {
327
+ if (!eligibleIds.length) {
328
+ (0, output_1.errorOut)(`Tab is protected by policy and cannot be ${command === "focus" ? "focused" : "refreshed"} via CLI`);
329
+ }
330
+ params = {
331
+ tabId: eligibleIds[0],
332
+ };
333
+ }
334
+ else if (command === "close" || command === "archive") {
335
+ if (!eligibleIds.length) {
336
+ earlyResponse = {
337
+ ok: true,
338
+ action: command,
339
+ data: {
340
+ summary: { eligible: 0, protected: protectedTabs.length },
341
+ protected: protectedTabs.map((tab) => ({
342
+ tabId: tab.tabId,
343
+ windowId: tab.windowId,
344
+ groupId: tab.groupId,
345
+ groupTitle: tab.groupTitle,
346
+ title: tab.title,
347
+ url: tab.url,
348
+ pinned: tab.pinned,
349
+ })),
350
+ policy: policySummary,
351
+ },
352
+ };
353
+ }
354
+ else if (command === "close") {
355
+ params = {
356
+ mode: "direct",
357
+ confirmed: true,
358
+ tabIds: eligibleIds,
359
+ };
360
+ }
361
+ else if (command === "archive") {
362
+ params = {
363
+ tabIds: eligibleIds,
364
+ };
365
+ }
366
+ policyInfo = {
367
+ protected: protectedTabs.map((tab) => ({
368
+ tabId: tab.tabId,
369
+ windowId: tab.windowId,
370
+ groupId: tab.groupId,
371
+ groupTitle: tab.groupTitle,
372
+ title: tab.title,
373
+ url: tab.url,
374
+ pinned: tab.pinned,
375
+ })),
376
+ };
377
+ }
378
+ else if (command === "move-tab" || command === "move-group" || command === "group-assign") {
379
+ if (!eligibleIds.length || (command === "move-group" && protectedTabs.length > 0)) {
380
+ earlyResponse = {
381
+ ok: true,
382
+ action: command,
383
+ data: {
384
+ summary: { eligible: eligibleIds.length, protected: protectedTabs.length },
385
+ protected: protectedTabs.map((tab) => ({
386
+ tabId: tab.tabId,
387
+ windowId: tab.windowId,
388
+ groupId: tab.groupId,
389
+ groupTitle: tab.groupTitle,
390
+ title: tab.title,
391
+ url: tab.url,
392
+ pinned: tab.pinned,
393
+ })),
394
+ policy: policySummary,
395
+ },
396
+ };
397
+ }
398
+ else if (command === "move-tab" || command === "group-assign") {
399
+ params = {
400
+ ...params,
401
+ tabId: eligibleIds[0],
402
+ tabIds: eligibleIds,
403
+ };
404
+ }
405
+ policyInfo = {
406
+ protected: protectedTabs.map((tab) => ({
407
+ tabId: tab.tabId,
408
+ windowId: tab.windowId,
409
+ groupId: tab.groupId,
410
+ groupTitle: tab.groupTitle,
411
+ title: tab.title,
412
+ url: tab.url,
413
+ pinned: tab.pinned,
414
+ })),
415
+ };
416
+ }
417
+ else if (command === "merge-window") {
418
+ if (!eligibleIds.length) {
419
+ earlyResponse = {
420
+ ok: true,
421
+ action: command,
422
+ data: {
423
+ summary: { eligible: 0, protected: protectedTabs.length },
424
+ protected: protectedTabs.map((tab) => ({
425
+ tabId: tab.tabId,
426
+ windowId: tab.windowId,
427
+ groupId: tab.groupId,
428
+ groupTitle: tab.groupTitle,
429
+ title: tab.title,
430
+ url: tab.url,
431
+ pinned: tab.pinned,
432
+ })),
433
+ policy: policySummary,
434
+ },
435
+ };
436
+ }
437
+ params = {
438
+ ...params,
439
+ tabIds: eligibleIds,
440
+ };
441
+ policyInfo = {
442
+ protected: protectedTabs.map((tab) => ({
443
+ tabId: tab.tabId,
444
+ windowId: tab.windowId,
445
+ groupId: tab.groupId,
446
+ groupTitle: tab.groupTitle,
447
+ title: tab.title,
448
+ url: tab.url,
449
+ pinned: tab.pinned,
450
+ })),
451
+ };
452
+ }
453
+ else if (command === "group-update" || command === "group-ungroup") {
454
+ if (!eligibleIds.length || protectedTabs.length > 0) {
455
+ earlyResponse = {
456
+ ok: true,
457
+ action: command,
458
+ data: {
459
+ summary: { eligible: eligibleIds.length, protected: protectedTabs.length },
460
+ protected: protectedTabs.map((tab) => ({
461
+ tabId: tab.tabId,
462
+ windowId: tab.windowId,
463
+ groupId: tab.groupId,
464
+ groupTitle: tab.groupTitle,
465
+ title: tab.title,
466
+ url: tab.url,
467
+ pinned: tab.pinned,
468
+ })),
469
+ policy: policySummary,
470
+ },
471
+ };
472
+ }
473
+ policyInfo = {
474
+ protected: protectedTabs.map((tab) => ({
475
+ tabId: tab.tabId,
476
+ windowId: tab.windowId,
477
+ groupId: tab.groupId,
478
+ groupTitle: tab.groupTitle,
479
+ title: tab.title,
480
+ url: tab.url,
481
+ pinned: tab.pinned,
482
+ })),
483
+ };
484
+ }
485
+ else {
486
+ if (!eligibleIds.length) {
487
+ const generatedAt = Date.now();
488
+ if (command === "analyze") {
489
+ earlyResponse = {
490
+ ok: true,
491
+ action: command,
492
+ data: {
493
+ generatedAt,
494
+ staleDays: params.staleDays || 0,
495
+ totals: { tabs: 0, analyzed: 0, candidates: 0 },
496
+ meta: { durationMs: 0, githubChecked: 0, githubTotal: 0, githubMatched: 0, githubTimeoutMs: params.githubTimeoutMs || 0 },
497
+ candidates: [],
498
+ analysisId: null,
499
+ policy: policySummary,
500
+ },
501
+ };
502
+ }
503
+ else if (command === "screenshot") {
504
+ earlyResponse = {
505
+ ok: true,
506
+ action: command,
507
+ data: {
508
+ generatedAt,
509
+ entries: [],
510
+ totals: { tabs: 0, tiles: 0 },
511
+ meta: {
512
+ durationMs: 0,
513
+ mode: params.mode || "viewport",
514
+ format: params.format || "png",
515
+ tileMaxDim: params.tileMaxDim || null,
516
+ maxBytes: params.maxBytes || null,
517
+ },
518
+ policy: policySummary,
519
+ },
520
+ };
521
+ }
522
+ else {
523
+ earlyResponse = {
524
+ ok: true,
525
+ action: command,
526
+ data: {
527
+ generatedAt,
528
+ entries: [],
529
+ totals: { tabs: 0, signals: 0, tasks: 0 },
530
+ meta: { durationMs: 0, signalTimeoutMs: params.signalTimeoutMs || 0, selectorCount: 0 },
531
+ policy: policySummary,
532
+ },
533
+ };
534
+ }
535
+ }
536
+ else {
537
+ params = {
538
+ ...params,
539
+ tabIds: eligibleIds,
540
+ };
541
+ }
542
+ }
543
+ }
544
+ const request = {
545
+ id: createId(),
546
+ action,
547
+ params,
548
+ client: {
549
+ component: "cli",
550
+ version: constants_1.VERSION,
551
+ baseVersion: constants_1.BASE_VERSION,
552
+ gitSha: constants_1.GIT_SHA,
553
+ dirty: constants_1.DIRTY,
554
+ },
555
+ };
556
+ let response;
557
+ const showProgress = options.progress === true;
558
+ const startedAt = Date.now();
559
+ let progressTimer = null;
560
+ if (showProgress) {
561
+ progressTimer = setInterval(() => {
562
+ const elapsed = Math.round((Date.now() - startedAt) / 1000);
563
+ process.stderr.write(`[tabctl] waiting ${elapsed}s...\n`);
564
+ }, 2000);
565
+ }
566
+ const onProgress = showProgress
567
+ ? (message) => {
568
+ const data = message.data;
569
+ if (data?.phase === "github") {
570
+ const processed = data.processed;
571
+ const total = data.total;
572
+ const matched = data.matched;
573
+ process.stderr.write(`[tabctl] github ${processed}/${total} (matched ${matched})\n`);
574
+ }
575
+ if (data?.phase === "inspect") {
576
+ const processed = data.processed;
577
+ const total = data.total;
578
+ const signalId = data.signalId;
579
+ process.stderr.write(`[tabctl] inspect ${processed}/${total} (${signalId})\n`);
580
+ }
581
+ if (data?.phase === "screenshot") {
582
+ const processed = data.processed;
583
+ const total = data.total;
584
+ process.stderr.write(`[tabctl] screenshot ${processed}/${total}\n`);
585
+ }
586
+ }
587
+ : undefined;
588
+ if (earlyResponse) {
589
+ response = earlyResponse;
590
+ }
591
+ else {
592
+ try {
593
+ response = await (0, client_1.sendRequest)(request, onProgress);
594
+ }
595
+ catch (error) {
596
+ if (progressTimer) {
597
+ clearInterval(progressTimer);
598
+ }
599
+ const message = error instanceof Error ? error.message : "Unknown error";
600
+ (0, output_1.errorOut)(`Failed to connect to host: ${message}`);
601
+ return;
602
+ }
603
+ finally {
604
+ if (progressTimer) {
605
+ clearInterval(progressTimer);
606
+ }
607
+ }
608
+ }
609
+ if (progressTimer) {
610
+ clearInterval(progressTimer);
611
+ }
612
+ if (!response) {
613
+ (0, output_1.errorOut)("No response received");
614
+ }
615
+ if (!response.ok) {
616
+ (0, output_1.printJson)(response, prettyOutput);
617
+ process.exit(1);
618
+ }
619
+ if (response.data && typeof response.data === "object") {
620
+ const data = response.data;
621
+ if ((command === "inspect" || command === "report" || command === "screenshot") && Array.isArray(data.entries)) {
622
+ let snapshot = null;
623
+ if (policyEnabled) {
624
+ snapshot = await getPolicySnapshot();
625
+ }
626
+ const tabIndex = snapshot ? (0, snapshot_1.buildTabIndex)(snapshot) : null;
627
+ const annotated = data.entries.map((entry) => {
628
+ const tab = tabIndex?.get(entry.tabId) || entry;
629
+ const { eligible, protectedReasons } = (0, policy_1.evaluateTab)(tab, policyContext.policy);
630
+ return {
631
+ ...entry,
632
+ eligible,
633
+ protectedReasons,
634
+ };
635
+ }).filter((entry) => entry.eligible !== false);
636
+ const scope = (0, scope_1.resolveScopeFlags)(options);
637
+ const allScope = options.all === true || !scope.hasScope;
638
+ const scopeArgs = (0, scope_1.buildScopeArgs)(options, allScope);
639
+ const pagination = (0, pagination_1.resolvePagination)(options, annotated.length, command, scopeArgs);
640
+ const start = pagination.offset;
641
+ const end = pagination.offset + pagination.limit;
642
+ data.entries = annotated.slice(start, end);
643
+ if (pagination.page) {
644
+ data.page = pagination.page;
645
+ }
646
+ }
647
+ if (command === "analyze" && Array.isArray(data.candidates)) {
648
+ let snapshot = null;
649
+ if (policyEnabled || includeWindowTitle) {
650
+ snapshot = await getPolicySnapshot();
651
+ }
652
+ const tabIndex = snapshot ? (0, snapshot_1.buildTabIndex)(snapshot) : null;
653
+ const windowTitleIndex = snapshot && includeWindowTitle
654
+ ? (0, snapshot_1.buildWindowTitleIndex)(snapshot, policyContext.policy)
655
+ : null;
656
+ data.candidates = data.candidates.map((candidate) => {
657
+ const tab = tabIndex?.get(candidate.tabId) || candidate;
658
+ const { eligible, protectedReasons } = (0, policy_1.evaluateTab)(tab, policyContext.policy);
659
+ const windowTitle = includeWindowTitle
660
+ ? (windowTitleIndex?.get(candidate.windowId) ?? null)
661
+ : undefined;
662
+ return {
663
+ ...candidate,
664
+ eligible,
665
+ protectedReasons,
666
+ ...(includeWindowTitle ? { windowTitle } : {}),
667
+ };
668
+ }).filter((candidate) => candidate.eligible !== false);
669
+ }
670
+ data.policy = policySummary;
671
+ if (policyInfo) {
672
+ data.policyInfo = policyInfo;
673
+ }
674
+ }
675
+ else if (response.ok) {
676
+ response.policy = policySummary;
677
+ }
678
+ if (dedupeMode) {
679
+ if (!response.ok) {
680
+ (0, output_1.printJson)(response, prettyOutput);
681
+ return;
682
+ }
683
+ const data = response.data || {};
684
+ const candidates = Array.isArray(data.candidates) ? data.candidates : [];
685
+ const planned = candidates.filter((candidate) => {
686
+ const reasons = Array.isArray(candidate.reasons) ? candidate.reasons : [];
687
+ const hasDuplicate = reasons.some((reason) => reason.type === "duplicate" || reason.type === "closed_issue");
688
+ const hasStale = reasons.some((reason) => reason.type === "stale");
689
+ return hasDuplicate || (includeStale && hasStale);
690
+ });
691
+ const planTabIds = [];
692
+ const expectedUrls = {};
693
+ for (const candidate of planned) {
694
+ const tabId = candidate.tabId;
695
+ if (!Number.isFinite(tabId)) {
696
+ continue;
697
+ }
698
+ if (!planTabIds.includes(tabId)) {
699
+ planTabIds.push(tabId);
700
+ }
701
+ if (typeof candidate.url === "string") {
702
+ expectedUrls[String(tabId)] = candidate.url;
703
+ }
704
+ }
705
+ let closeData = null;
706
+ if (options.confirm === true && planTabIds.length > 0) {
707
+ const closeResponse = await (0, client_1.sendRequest)({
708
+ id: createId(),
709
+ action: "close",
710
+ params: {
711
+ mode: "direct",
712
+ confirmed: true,
713
+ tabIds: planTabIds,
714
+ expectedUrls,
715
+ },
716
+ });
717
+ if (!closeResponse.ok) {
718
+ (0, output_1.printJson)(closeResponse, prettyOutput);
719
+ return;
720
+ }
721
+ closeData = closeResponse.data || {};
722
+ closeData.policy = policySummary;
723
+ if (policyInfo) {
724
+ closeData.policyInfo = policyInfo;
725
+ }
726
+ }
727
+ const closeSummary = closeData?.summary;
728
+ const closedTabs = Number(closeSummary?.closedTabs ?? 0);
729
+ const skippedTabs = Number(closeSummary?.skippedTabs ?? 0);
730
+ const output = {
731
+ ok: true,
732
+ action: "dedupe",
733
+ data: {
734
+ analysisId: data.analysisId || null,
735
+ summary: {
736
+ candidates: candidates.length,
737
+ planned: planTabIds.length,
738
+ closed: Number.isFinite(closedTabs) ? closedTabs : 0,
739
+ skipped: Number.isFinite(skippedTabs) ? skippedTabs : 0,
740
+ },
741
+ plan: {
742
+ tabIds: planTabIds,
743
+ candidates: planned,
744
+ },
745
+ close: closeData,
746
+ nextCommand: options.confirm === true
747
+ ? null
748
+ : (planTabIds.length > 0 && data.analysisId ? `tabctl close --apply ${data.analysisId} --confirm` : null),
749
+ policy: data.policy,
750
+ policyInfo: data.policyInfo,
751
+ },
752
+ };
753
+ (0, output_1.printJson)(output, prettyOutput);
754
+ return;
755
+ }
756
+ if (command === "report") {
757
+ const format = options.format || "json";
758
+ const data = response.data;
759
+ const entries = data?.entries || [];
760
+ const generatedAt = data?.generatedAt;
761
+ const page = data && "page" in data ? data.page : undefined;
762
+ let content = "";
763
+ if (format === "json") {
764
+ content = JSON.stringify({ generatedAt, entries }, null, 2);
765
+ }
766
+ else if (format === "csv") {
767
+ content = (0, report_1.renderCsv)(entries);
768
+ }
769
+ else if (format === "md") {
770
+ content = (0, report_1.renderMarkdown)(entries, generatedAt);
771
+ }
772
+ else {
773
+ (0, output_1.errorOut)(`Unknown report format: ${format}`);
774
+ }
775
+ if (options.out) {
776
+ fs_1.default.writeFileSync(String(options.out), content, "utf8");
777
+ (0, output_1.printJson)({ ok: true, data: { writtenTo: options.out, format, count: entries.length, ...(page ? { page } : {}) } }, prettyOutput);
778
+ return;
779
+ }
780
+ if (format === "json") {
781
+ (0, output_1.printJson)({ ok: true, data: { format, entries, ...(page ? { page } : {}) } }, prettyOutput);
782
+ return;
783
+ }
784
+ (0, output_1.printJson)({ ok: true, data: { format, entries, content, ...(page ? { page } : {}) } }, prettyOutput);
785
+ return;
786
+ }
787
+ if (command === "screenshot") {
788
+ const data = response.data;
789
+ const entries = data?.entries || [];
790
+ const page = data && "page" in data ? data.page : undefined;
791
+ const outDir = options.out
792
+ ? String(options.out)
793
+ : path_1.default.join(process.cwd(), ".tabctl", "screenshots", String(Date.now()));
794
+ fs_1.default.mkdirSync(outDir, { recursive: true });
795
+ let filesWritten = 0;
796
+ const sanitized = entries.map((entry) => {
797
+ const tabId = entry.tabId;
798
+ const tabDir = path_1.default.join(outDir, String(tabId ?? "unknown"));
799
+ fs_1.default.mkdirSync(tabDir, { recursive: true });
800
+ const tiles = Array.isArray(entry.tiles) ? entry.tiles : [];
801
+ const sanitizedTiles = tiles.map((tile) => {
802
+ const rawUrl = tile.dataUrl;
803
+ const { dataUrl: _ignored, ...rest } = tile;
804
+ if (!rawUrl) {
805
+ return { ...rest, path: null, error: "missing_data" };
806
+ }
807
+ const match = rawUrl.match(/^data:(image\/png|image\/jpeg);base64,(.+)$/);
808
+ if (!match) {
809
+ return { ...rest, path: null, error: "invalid_data_url" };
810
+ }
811
+ const mime = match[1];
812
+ const base64 = match[2];
813
+ const ext = mime === "image/jpeg" ? "jpg" : "png";
814
+ const index = Number.isFinite(tile.index) ? Number(tile.index) + 1 : filesWritten + 1;
815
+ const total = Number.isFinite(tile.total) ? Number(tile.total) : null;
816
+ const suffix = total && total > 1 ? `-of-${total}` : "";
817
+ const filename = `screenshot-${index}${suffix}.${ext}`;
818
+ const filePath = path_1.default.join(tabDir, filename);
819
+ const buffer = Buffer.from(base64, "base64");
820
+ fs_1.default.writeFileSync(filePath, buffer);
821
+ filesWritten += 1;
822
+ return {
823
+ ...rest,
824
+ path: filePath,
825
+ bytes: buffer.length,
826
+ };
827
+ });
828
+ return {
829
+ ...entry,
830
+ tiles: sanitizedTiles,
831
+ };
832
+ });
833
+ (0, output_1.printJson)({ ok: true, data: { writtenTo: outDir, files: filesWritten, entries: sanitized, ...(page ? { page } : {}) } }, prettyOutput);
834
+ return;
835
+ }
836
+ (0, output_1.emitVersionWarnings)(response, command);
837
+ (0, output_1.printJson)(response, prettyOutput);
838
+ }
839
+ main().catch((error) => {
840
+ (0, output_1.errorOut)(error.message || "Unknown error");
841
+ });