tabctl 0.1.4 → 0.2.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 (48) hide show
  1. package/{cli → dist/cli}/lib/commands/index.js +4 -2
  2. package/dist/cli/lib/commands/meta.js +226 -0
  3. package/dist/cli/lib/commands/params-groups.js +40 -0
  4. package/dist/cli/lib/commands/params-move.js +44 -0
  5. package/{cli → dist/cli}/lib/commands/params.js +61 -125
  6. package/{cli/lib/commands/meta.js → dist/cli/lib/commands/setup.js} +26 -222
  7. package/{cli/lib/options.js → dist/cli/lib/options-commands.js} +3 -155
  8. package/dist/cli/lib/options-groups.js +41 -0
  9. package/dist/cli/lib/options.js +125 -0
  10. package/{cli → dist/cli}/lib/output.js +5 -4
  11. package/dist/cli/lib/policy-filter.js +202 -0
  12. package/dist/cli/lib/response.js +235 -0
  13. package/{cli → dist/cli}/lib/scope.js +3 -31
  14. package/dist/cli/tabctl.js +463 -0
  15. package/dist/extension/background.js +3398 -0
  16. package/dist/extension/lib/archive.js +444 -0
  17. package/dist/extension/lib/content.js +320 -0
  18. package/dist/extension/lib/deps.js +4 -0
  19. package/dist/extension/lib/groups.js +443 -0
  20. package/dist/extension/lib/inspect.js +316 -0
  21. package/dist/extension/lib/move.js +342 -0
  22. package/dist/extension/lib/screenshot.js +367 -0
  23. package/dist/extension/lib/tabs.js +395 -0
  24. package/dist/extension/lib/undo-handlers.js +439 -0
  25. package/{extension → dist/extension}/manifest.json +2 -2
  26. package/dist/host/host.js +124 -0
  27. package/{host/host.js → dist/host/lib/handlers.js} +84 -187
  28. package/{shared → dist/shared}/version.js +2 -2
  29. package/package.json +12 -10
  30. package/cli/tabctl.js +0 -841
  31. package/extension/background.js +0 -3372
  32. package/extension/manifest.template.json +0 -22
  33. /package/{cli → dist/cli}/lib/args.js +0 -0
  34. /package/{cli → dist/cli}/lib/client.js +0 -0
  35. /package/{cli → dist/cli}/lib/commands/list.js +0 -0
  36. /package/{cli → dist/cli}/lib/commands/profile.js +0 -0
  37. /package/{cli → dist/cli}/lib/constants.js +0 -0
  38. /package/{cli → dist/cli}/lib/help.js +0 -0
  39. /package/{cli → dist/cli}/lib/pagination.js +0 -0
  40. /package/{cli → dist/cli}/lib/policy.js +0 -0
  41. /package/{cli → dist/cli}/lib/report.js +0 -0
  42. /package/{cli → dist/cli}/lib/snapshot.js +0 -0
  43. /package/{cli → dist/cli}/lib/types.js +0 -0
  44. /package/{host → dist/host}/host.sh +0 -0
  45. /package/{host → dist/host}/lib/undo.js +0 -0
  46. /package/{shared → dist/shared}/config.js +0 -0
  47. /package/{shared → dist/shared}/extension-sync.js +0 -0
  48. /package/{shared → dist/shared}/profiles.js +0 -0
@@ -0,0 +1,444 @@
1
+ "use strict";
2
+ // Archive, close, and merge operations — extracted from background.ts (pure structural refactor).
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ exports.ensureArchiveWindow = ensureArchiveWindow;
5
+ exports.archiveTabs = archiveTabs;
6
+ exports.getTabsByIds = getTabsByIds;
7
+ exports.closeTabs = closeTabs;
8
+ exports.mergeWindow = mergeWindow;
9
+ async function ensureArchiveWindow(deps) {
10
+ const archiveWindowId = await deps.getArchiveWindowId();
11
+ if (archiveWindowId) {
12
+ try {
13
+ await chrome.windows.get(archiveWindowId);
14
+ return archiveWindowId;
15
+ }
16
+ catch {
17
+ await deps.setArchiveWindowId(null);
18
+ }
19
+ }
20
+ const created = await chrome.windows.create({ focused: false });
21
+ await deps.setArchiveWindowId(created.id);
22
+ return created.id;
23
+ }
24
+ async function archiveTabs(params, deps) {
25
+ const snapshot = await deps.getTabSnapshot();
26
+ const windowLabels = deps.buildWindowLabels(snapshot);
27
+ let windowsToProcess = snapshot.windows;
28
+ if (params.windowId) {
29
+ const resolvedWindowId = deps.resolveWindowIdFromParams(snapshot, params.windowId);
30
+ windowsToProcess = resolvedWindowId != null
31
+ ? windowsToProcess.filter((win) => win.windowId === resolvedWindowId)
32
+ : [];
33
+ }
34
+ else if (!params.all) {
35
+ const focused = windowsToProcess.find((win) => win.focused);
36
+ windowsToProcess = focused ? [focused] : [];
37
+ }
38
+ if (params.groupTitle || params.groupId || params.tabIds) {
39
+ const selected = deps.selectTabsByScope(snapshot, params);
40
+ if (selected.error) {
41
+ throw selected.error;
42
+ }
43
+ windowsToProcess = snapshot.windows
44
+ .map((win) => ({
45
+ windowId: win.windowId,
46
+ focused: win.focused,
47
+ state: win.state,
48
+ tabs: win.tabs.filter((tab) => selected.tabs.some((sel) => sel.tabId === tab.tabId)),
49
+ groups: win.groups,
50
+ }))
51
+ .filter((win) => win.tabs.length > 0);
52
+ }
53
+ if (windowsToProcess.length === 0) {
54
+ return {
55
+ txid: params.txid || null,
56
+ summary: { movedTabs: 0, movedGroups: 0, skippedTabs: 0 },
57
+ archiveWindowId: null,
58
+ skipped: [],
59
+ undo: { action: "archive", tabs: [] },
60
+ };
61
+ }
62
+ const archiveWindowId = await ensureArchiveWindow(deps);
63
+ const undoTabs = [];
64
+ const skipped = [];
65
+ let movedGroups = 0;
66
+ let movedTabs = 0;
67
+ for (const window of windowsToProcess) {
68
+ const groupsById = new Map();
69
+ for (const group of window.groups) {
70
+ groupsById.set(group.groupId, group);
71
+ }
72
+ const groupedTabs = new Map();
73
+ const ungroupedTabs = [];
74
+ for (const tab of window.tabs) {
75
+ if (tab.tabId == null) {
76
+ continue;
77
+ }
78
+ if (tab.groupId === -1) {
79
+ ungroupedTabs.push(tab);
80
+ }
81
+ else {
82
+ if (!groupedTabs.has(tab.groupId)) {
83
+ groupedTabs.set(tab.groupId, []);
84
+ }
85
+ groupedTabs.get(tab.groupId)?.push(tab);
86
+ }
87
+ }
88
+ const windowLabel = windowLabels.get(window.windowId) || `W${window.windowId}`;
89
+ const plans = [];
90
+ for (const [groupId, tabs] of groupedTabs.entries()) {
91
+ const group = groupsById.get(groupId) || null;
92
+ plans.push({
93
+ windowId: window.windowId,
94
+ windowLabel,
95
+ group,
96
+ tabs,
97
+ isUngrouped: false,
98
+ });
99
+ }
100
+ if (ungroupedTabs.length > 0) {
101
+ plans.push({
102
+ windowId: window.windowId,
103
+ windowLabel,
104
+ group: null,
105
+ tabs: ungroupedTabs,
106
+ isUngrouped: true,
107
+ });
108
+ }
109
+ for (const plan of plans) {
110
+ const tabIds = plan.tabs.map((tab) => tab.tabId);
111
+ if (tabIds.length === 0) {
112
+ continue;
113
+ }
114
+ const tabById = new Map();
115
+ for (const tab of plan.tabs) {
116
+ tabById.set(tab.tabId, tab);
117
+ }
118
+ let moved;
119
+ try {
120
+ moved = await chrome.tabs.move(tabIds, { windowId: archiveWindowId, index: -1 });
121
+ }
122
+ catch (error) {
123
+ for (const tabId of tabIds) {
124
+ skipped.push({ tabId, reason: "move_failed" });
125
+ }
126
+ deps.log("Failed to move tabs", error);
127
+ continue;
128
+ }
129
+ const movedList = Array.isArray(moved) ? moved : [moved];
130
+ const movedIds = movedList.map((tab) => tab.id);
131
+ movedTabs += movedIds.length;
132
+ for (const movedId of movedIds) {
133
+ const tab = tabById.get(movedId);
134
+ if (!tab) {
135
+ continue;
136
+ }
137
+ undoTabs.push({
138
+ tabId: tab.tabId,
139
+ url: tab.url,
140
+ title: tab.title,
141
+ pinned: tab.pinned,
142
+ active: tab.active,
143
+ from: {
144
+ windowId: tab.windowId,
145
+ index: tab.index,
146
+ groupId: tab.groupId,
147
+ groupTitle: plan.group ? plan.group.title : null,
148
+ groupColor: plan.group ? plan.group.color : null,
149
+ groupCollapsed: plan.group ? plan.group.collapsed : null,
150
+ },
151
+ });
152
+ }
153
+ const titleBase = plan.group && plan.group.title
154
+ ? plan.group.title
155
+ : plan.isUngrouped
156
+ ? "Ungrouped"
157
+ : "Group";
158
+ const archiveTitle = `${plan.windowLabel} - ${titleBase}`;
159
+ const groupColor = plan.group && plan.group.color
160
+ ? plan.group.color
161
+ : "grey";
162
+ try {
163
+ const newGroupId = await chrome.tabs.group({ tabIds: movedIds, createProperties: { windowId: archiveWindowId } });
164
+ await chrome.tabGroups.update(newGroupId, { title: archiveTitle, color: groupColor });
165
+ movedGroups += 1;
166
+ }
167
+ catch (error) {
168
+ deps.log("Failed to group archived tabs", error);
169
+ }
170
+ }
171
+ }
172
+ return {
173
+ txid: params.txid || null,
174
+ summary: {
175
+ movedTabs,
176
+ movedGroups,
177
+ skippedTabs: skipped.length,
178
+ },
179
+ archiveWindowId,
180
+ skipped,
181
+ undo: {
182
+ action: "archive",
183
+ tabs: undoTabs,
184
+ },
185
+ };
186
+ }
187
+ async function getTabsByIds(tabIds) {
188
+ const results = [];
189
+ for (const tabId of tabIds) {
190
+ try {
191
+ const tab = await chrome.tabs.get(tabId);
192
+ results.push(tab);
193
+ }
194
+ catch {
195
+ results.push(null);
196
+ }
197
+ }
198
+ return results;
199
+ }
200
+ async function closeTabs(params, deps) {
201
+ const mode = params.mode || "direct";
202
+ if (mode === "direct" && !params.confirmed) {
203
+ throw new Error("Direct close requires confirmation");
204
+ }
205
+ let tabIds = params.tabIds || [];
206
+ if (!tabIds.length && (params.groupTitle || params.groupId || params.windowId)) {
207
+ const snapshot = await deps.getTabSnapshot();
208
+ const selection = deps.selectTabsByScope(snapshot, params);
209
+ if (selection.error) {
210
+ throw selection.error;
211
+ }
212
+ tabIds = selection.tabs.map((tab) => tab.tabId);
213
+ }
214
+ if (!tabIds.length) {
215
+ return {
216
+ txid: params.txid || null,
217
+ summary: { closedTabs: 0, skippedTabs: 0 },
218
+ skipped: [],
219
+ undo: { action: "close", tabs: [] },
220
+ };
221
+ }
222
+ const expectedUrls = params.expectedUrls || {};
223
+ const tabInfos = await getTabsByIds(tabIds);
224
+ const validTabs = [];
225
+ const skipped = [];
226
+ const groups = await chrome.tabGroups.query({});
227
+ const groupById = new Map(groups.map((group) => [group.id, group]));
228
+ for (let i = 0; i < tabIds.length; i += 1) {
229
+ const tabId = tabIds[i];
230
+ const tab = tabInfos[i];
231
+ if (!tab) {
232
+ skipped.push({ tabId, reason: "not_found" });
233
+ continue;
234
+ }
235
+ const expected = expectedUrls[String(tabId)];
236
+ if (expected && tab.url !== expected) {
237
+ skipped.push({ tabId, reason: "url_mismatch" });
238
+ continue;
239
+ }
240
+ const group = tab.groupId !== -1 ? groupById.get(tab.groupId) : null;
241
+ validTabs.push({
242
+ tabId,
243
+ url: tab.url,
244
+ title: tab.title,
245
+ pinned: tab.pinned,
246
+ active: tab.active,
247
+ from: {
248
+ windowId: tab.windowId,
249
+ index: tab.index,
250
+ groupId: tab.groupId,
251
+ groupTitle: group ? group.title : null,
252
+ groupColor: group ? group.color : null,
253
+ groupCollapsed: group ? group.collapsed : null,
254
+ },
255
+ });
256
+ }
257
+ if (validTabs.length > 0) {
258
+ await chrome.tabs.remove(validTabs.map((tab) => tab.tabId));
259
+ }
260
+ return {
261
+ txid: params.txid || null,
262
+ summary: {
263
+ closedTabs: validTabs.length,
264
+ skippedTabs: skipped.length,
265
+ },
266
+ skipped,
267
+ undo: {
268
+ action: "close",
269
+ tabs: validTabs.map((tab) => ({
270
+ url: tab.url,
271
+ title: tab.title,
272
+ pinned: tab.pinned,
273
+ active: tab.active,
274
+ from: tab.from,
275
+ })),
276
+ },
277
+ };
278
+ }
279
+ async function mergeWindow(params, deps) {
280
+ const fromWindowId = Number.isFinite(params.fromWindowId)
281
+ ? Number(params.fromWindowId)
282
+ : Number(params.windowId);
283
+ const toWindowId = Number.isFinite(params.toWindowId) ? Number(params.toWindowId) : null;
284
+ if (!Number.isFinite(fromWindowId) || !Number.isFinite(toWindowId)) {
285
+ throw new Error("Missing source or target window id");
286
+ }
287
+ if (fromWindowId === toWindowId) {
288
+ throw new Error("Source and target windows must differ");
289
+ }
290
+ const snapshot = await deps.getTabSnapshot();
291
+ const windows = snapshot.windows;
292
+ const sourceWindow = windows.find((win) => win.windowId === fromWindowId);
293
+ if (!sourceWindow) {
294
+ throw new Error("Source window not found");
295
+ }
296
+ const targetWindow = windows.find((win) => win.windowId === toWindowId);
297
+ if (!targetWindow) {
298
+ throw new Error("Target window not found");
299
+ }
300
+ const rawTabIds = Array.isArray(params.tabIds) ? params.tabIds.map(Number) : [];
301
+ const tabIdSet = new Set(rawTabIds.filter((id) => Number.isFinite(id)));
302
+ const skipped = [];
303
+ let selectedTabs = sourceWindow.tabs;
304
+ if (tabIdSet.size > 0) {
305
+ const sourceTabIds = new Set(sourceWindow.tabs.map((tab) => tab.tabId).filter((id) => typeof id === "number"));
306
+ for (const tabId of tabIdSet) {
307
+ if (!sourceTabIds.has(tabId)) {
308
+ skipped.push({ tabId, reason: "not_in_source" });
309
+ }
310
+ }
311
+ selectedTabs = sourceWindow.tabs.filter((tab) => tabIdSet.has(tab.tabId));
312
+ }
313
+ if (selectedTabs.length === 0) {
314
+ return {
315
+ fromWindowId,
316
+ toWindowId,
317
+ summary: { movedTabs: 0, movedGroups: 0, skippedTabs: skipped.length, closedSource: false },
318
+ skipped,
319
+ groups: [],
320
+ undo: {
321
+ action: "merge-window",
322
+ fromWindowId,
323
+ toWindowId,
324
+ closedSource: false,
325
+ tabs: [],
326
+ },
327
+ };
328
+ }
329
+ const orderedTabs = [...selectedTabs].sort((a, b) => {
330
+ const aIndex = Number(a.index);
331
+ const bIndex = Number(b.index);
332
+ if (!Number.isFinite(aIndex) && !Number.isFinite(bIndex)) {
333
+ return 0;
334
+ }
335
+ if (!Number.isFinite(aIndex)) {
336
+ return 1;
337
+ }
338
+ if (!Number.isFinite(bIndex)) {
339
+ return -1;
340
+ }
341
+ return aIndex - bIndex;
342
+ });
343
+ const groupById = new Map();
344
+ for (const group of sourceWindow.groups) {
345
+ groupById.set(group.groupId, group);
346
+ }
347
+ const plans = [];
348
+ let currentPlan = null;
349
+ for (const tab of orderedTabs) {
350
+ const rawGroupId = tab.groupId;
351
+ const groupId = typeof rawGroupId === "number" && rawGroupId !== -1 ? rawGroupId : null;
352
+ if (!currentPlan || currentPlan.groupId !== groupId) {
353
+ currentPlan = { groupId, tabs: [] };
354
+ plans.push(currentPlan);
355
+ }
356
+ currentPlan.tabs.push(tab);
357
+ }
358
+ let movedTabs = 0;
359
+ let movedGroups = 0;
360
+ const groups = [];
361
+ const undoTabs = [];
362
+ for (const plan of plans) {
363
+ const tabIds = plan.tabs.map((tab) => tab.tabId).filter((id) => typeof id === "number");
364
+ if (!tabIds.length) {
365
+ continue;
366
+ }
367
+ let moved;
368
+ try {
369
+ moved = await chrome.tabs.move(tabIds, { windowId: toWindowId, index: -1 });
370
+ }
371
+ catch (error) {
372
+ for (const tabId of tabIds) {
373
+ skipped.push({ tabId, reason: "move_failed" });
374
+ }
375
+ deps.log("Failed to move tabs", error);
376
+ continue;
377
+ }
378
+ const movedList = Array.isArray(moved) ? moved : [moved];
379
+ const movedIds = movedList.map((tab) => tab.id).filter((id) => typeof id === "number");
380
+ movedTabs += movedIds.length;
381
+ for (const entry of plan.tabs) {
382
+ if (typeof entry.tabId !== "number") {
383
+ continue;
384
+ }
385
+ const meta = groupById.get(entry.groupId);
386
+ undoTabs.push({
387
+ tabId: entry.tabId,
388
+ windowId: entry.windowId,
389
+ index: entry.index,
390
+ groupId: entry.groupId,
391
+ groupTitle: entry.groupTitle,
392
+ groupColor: entry.groupColor,
393
+ groupCollapsed: meta ? meta.collapsed : null,
394
+ });
395
+ }
396
+ if (plan.groupId != null && movedIds.length > 0) {
397
+ movedGroups += 1;
398
+ let newGroupId = null;
399
+ try {
400
+ newGroupId = await chrome.tabs.group({ tabIds: movedIds, createProperties: { windowId: toWindowId } });
401
+ const meta = groupById.get(plan.groupId);
402
+ if (meta) {
403
+ await chrome.tabGroups.update(newGroupId, {
404
+ title: meta.title || "",
405
+ color: meta.color || "grey",
406
+ collapsed: meta.collapsed || false,
407
+ });
408
+ }
409
+ }
410
+ catch (error) {
411
+ deps.log("Failed to regroup tabs", error);
412
+ }
413
+ groups.push({ sourceGroupId: plan.groupId, newGroupId });
414
+ }
415
+ }
416
+ let closedSource = false;
417
+ if (params.closeSource === true) {
418
+ try {
419
+ const remainingTabs = await chrome.tabs.query({ windowId: fromWindowId });
420
+ if (remainingTabs.length === 0) {
421
+ await chrome.windows.remove(fromWindowId);
422
+ closedSource = true;
423
+ }
424
+ }
425
+ catch (error) {
426
+ deps.log("Failed to close source window", error);
427
+ }
428
+ }
429
+ return {
430
+ fromWindowId,
431
+ toWindowId,
432
+ summary: { movedTabs, movedGroups, skippedTabs: skipped.length, closedSource },
433
+ skipped,
434
+ groups,
435
+ undo: {
436
+ action: "merge-window",
437
+ fromWindowId,
438
+ toWindowId,
439
+ closedSource,
440
+ tabs: undoTabs,
441
+ },
442
+ txid: params.txid || null,
443
+ };
444
+ }