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,443 @@
1
+ "use strict";
2
+ // Group management — extracted from background.ts (pure structural refactor).
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ exports.getGroupTabs = getGroupTabs;
5
+ exports.listGroupSummaries = listGroupSummaries;
6
+ exports.summarizeGroupMatch = summarizeGroupMatch;
7
+ exports.findGroupMatches = findGroupMatches;
8
+ exports.resolveGroupByTitle = resolveGroupByTitle;
9
+ exports.resolveGroupById = resolveGroupById;
10
+ exports.listGroups = listGroups;
11
+ exports.groupUpdate = groupUpdate;
12
+ exports.groupUngroup = groupUngroup;
13
+ exports.groupAssign = groupAssign;
14
+ function getGroupTabs(windowSnapshot, groupId) {
15
+ return windowSnapshot.tabs
16
+ .filter((tab) => tab.groupId === groupId)
17
+ .sort((a, b) => (Number(a.index) || 0) - (Number(b.index) || 0));
18
+ }
19
+ function listGroupSummaries(snapshot, buildWindowLabels, windowId) {
20
+ const windowLabels = buildWindowLabels(snapshot);
21
+ const summaries = [];
22
+ const windows = snapshot.windows;
23
+ for (const win of windows) {
24
+ if (windowId && win.windowId !== windowId) {
25
+ continue;
26
+ }
27
+ for (const group of win.groups) {
28
+ summaries.push({
29
+ windowId: win.windowId,
30
+ windowLabel: windowLabels.get(win.windowId) ?? null,
31
+ groupId: group.groupId,
32
+ title: typeof group.title === "string" ? group.title : null,
33
+ });
34
+ }
35
+ }
36
+ return summaries;
37
+ }
38
+ function summarizeGroupMatch(match, windowLabels) {
39
+ return {
40
+ windowId: match.windowId,
41
+ windowLabel: windowLabels.get(match.windowId) ?? null,
42
+ groupId: match.group.groupId,
43
+ title: typeof match.group.title === "string" ? match.group.title : null,
44
+ };
45
+ }
46
+ function findGroupMatches(snapshot, groupTitle, windowId) {
47
+ const matches = [];
48
+ const windows = snapshot.windows;
49
+ for (const win of windows) {
50
+ if (windowId && win.windowId !== windowId) {
51
+ continue;
52
+ }
53
+ for (const group of win.groups) {
54
+ if (group.title === groupTitle) {
55
+ matches.push({
56
+ windowId: win.windowId,
57
+ group,
58
+ tabs: getGroupTabs(win, group.groupId),
59
+ });
60
+ }
61
+ }
62
+ }
63
+ return matches;
64
+ }
65
+ function resolveGroupByTitle(snapshot, buildWindowLabels, groupTitle, windowId) {
66
+ const windowLabels = buildWindowLabels(snapshot);
67
+ const allMatches = findGroupMatches(snapshot, groupTitle);
68
+ const matches = windowId ? allMatches.filter((match) => match.windowId === windowId) : allMatches;
69
+ const availableGroups = listGroupSummaries(snapshot, buildWindowLabels);
70
+ if (matches.length === 0) {
71
+ const message = windowId && allMatches.length > 0
72
+ ? "Group title not found in specified window"
73
+ : "No matching group title found";
74
+ return {
75
+ error: {
76
+ message,
77
+ hint: "Use tabctl group-list to see existing groups.",
78
+ matches: allMatches.map((match) => summarizeGroupMatch(match, windowLabels)),
79
+ availableGroups,
80
+ },
81
+ };
82
+ }
83
+ if (matches.length > 1) {
84
+ return {
85
+ error: {
86
+ message: "Group title is ambiguous. Provide a windowId.",
87
+ hint: "Use --window to disambiguate group titles.",
88
+ matches: matches.map((match) => summarizeGroupMatch(match, windowLabels)),
89
+ availableGroups,
90
+ },
91
+ };
92
+ }
93
+ return { match: matches[0] };
94
+ }
95
+ function resolveGroupById(snapshot, buildWindowLabels, groupId) {
96
+ const windows = snapshot.windows;
97
+ const matches = [];
98
+ for (const win of windows) {
99
+ const group = win.groups.find((entry) => entry.groupId === groupId);
100
+ if (group) {
101
+ matches.push({
102
+ windowId: win.windowId,
103
+ group,
104
+ tabs: getGroupTabs(win, groupId),
105
+ });
106
+ }
107
+ }
108
+ if (matches.length === 0) {
109
+ return {
110
+ error: {
111
+ message: "Group not found",
112
+ hint: "Use tabctl group-list to see existing groups.",
113
+ availableGroups: listGroupSummaries(snapshot, buildWindowLabels),
114
+ },
115
+ };
116
+ }
117
+ if (matches.length > 1) {
118
+ const windowLabels = buildWindowLabels(snapshot);
119
+ return {
120
+ error: {
121
+ message: "Group id is ambiguous. Provide a windowId.",
122
+ hint: "Use --window to disambiguate group ids.",
123
+ matches: matches.map((match) => summarizeGroupMatch(match, windowLabels)),
124
+ availableGroups: listGroupSummaries(snapshot, buildWindowLabels),
125
+ },
126
+ };
127
+ }
128
+ return { match: matches[0] };
129
+ }
130
+ async function listGroups(params, deps) {
131
+ const snapshot = await deps.getTabSnapshot();
132
+ const windows = snapshot.windows;
133
+ const windowLabels = deps.buildWindowLabels(snapshot);
134
+ const windowIdParam = params.windowId != null ? deps.resolveWindowIdFromParams(snapshot, params.windowId) : null;
135
+ if (windowIdParam && !windows.some((win) => win.windowId === windowIdParam)) {
136
+ throw new Error("Window not found");
137
+ }
138
+ const groups = [];
139
+ for (const win of windows) {
140
+ if (windowIdParam && win.windowId !== windowIdParam) {
141
+ continue;
142
+ }
143
+ const counts = new Map();
144
+ for (const tab of win.tabs) {
145
+ const groupId = tab.groupId;
146
+ if (typeof groupId === "number" && groupId !== -1) {
147
+ counts.set(groupId, (counts.get(groupId) || 0) + 1);
148
+ }
149
+ }
150
+ for (const group of win.groups) {
151
+ const groupId = group.groupId;
152
+ groups.push({
153
+ windowId: win.windowId,
154
+ windowLabel: windowLabels.get(win.windowId) ?? null,
155
+ groupId,
156
+ title: group.title ?? null,
157
+ color: group.color ?? null,
158
+ collapsed: group.collapsed ?? null,
159
+ tabCount: counts.get(groupId) || 0,
160
+ });
161
+ }
162
+ }
163
+ return { groups };
164
+ }
165
+ async function groupUpdate(params, deps) {
166
+ const groupId = Number.isFinite(params.groupId) ? Number(params.groupId) : null;
167
+ const groupTitle = typeof params.groupTitle === "string" ? params.groupTitle.trim() : "";
168
+ if (!groupId && !groupTitle) {
169
+ throw new Error("Missing group identifier");
170
+ }
171
+ const snapshot = await deps.getTabSnapshot();
172
+ const windows = snapshot.windows;
173
+ const windowIdParam = params.windowId != null ? deps.resolveWindowIdFromParams(snapshot, params.windowId) : null;
174
+ if (windowIdParam && !windows.some((win) => win.windowId === windowIdParam)) {
175
+ throw new Error("Window not found");
176
+ }
177
+ let match;
178
+ if (groupId != null) {
179
+ const resolved = resolveGroupById(snapshot, deps.buildWindowLabels, groupId);
180
+ if (resolved.error) {
181
+ throw resolved.error;
182
+ }
183
+ match = resolved.match;
184
+ if (windowIdParam && windowIdParam !== match.windowId) {
185
+ throw new Error("Group is not in the specified window");
186
+ }
187
+ }
188
+ else {
189
+ const resolved = resolveGroupByTitle(snapshot, deps.buildWindowLabels, groupTitle, windowIdParam || undefined);
190
+ if (resolved.error) {
191
+ throw resolved.error;
192
+ }
193
+ match = resolved.match;
194
+ }
195
+ const update = {};
196
+ if (typeof params.title === "string") {
197
+ update.title = params.title;
198
+ }
199
+ if (typeof params.color === "string" && params.color.trim()) {
200
+ update.color = params.color.trim();
201
+ }
202
+ if (typeof params.collapsed === "boolean") {
203
+ update.collapsed = params.collapsed;
204
+ }
205
+ if (!Object.keys(update).length) {
206
+ throw new Error("Missing group update fields");
207
+ }
208
+ const updated = await chrome.tabGroups.update(match.group.groupId, update);
209
+ return {
210
+ groupId: updated.id,
211
+ windowId: updated.windowId,
212
+ title: updated.title,
213
+ color: updated.color,
214
+ collapsed: updated.collapsed,
215
+ undo: {
216
+ action: "group-update",
217
+ groupId: updated.id,
218
+ windowId: match.windowId,
219
+ previous: {
220
+ title: match.group.title ?? null,
221
+ color: match.group.color ?? null,
222
+ collapsed: match.group.collapsed ?? null,
223
+ },
224
+ },
225
+ txid: params.txid || null,
226
+ };
227
+ }
228
+ async function groupUngroup(params, deps) {
229
+ const groupId = Number.isFinite(params.groupId) ? Number(params.groupId) : null;
230
+ const groupTitle = typeof params.groupTitle === "string" ? params.groupTitle.trim() : "";
231
+ if (!groupId && !groupTitle) {
232
+ throw new Error("Missing group identifier");
233
+ }
234
+ const snapshot = await deps.getTabSnapshot();
235
+ const windows = snapshot.windows;
236
+ const windowIdParam = params.windowId != null ? deps.resolveWindowIdFromParams(snapshot, params.windowId) : null;
237
+ if (windowIdParam && !windows.some((win) => win.windowId === windowIdParam)) {
238
+ throw new Error("Window not found");
239
+ }
240
+ let match;
241
+ if (groupId != null) {
242
+ const resolved = resolveGroupById(snapshot, deps.buildWindowLabels, groupId);
243
+ if (resolved.error) {
244
+ throw resolved.error;
245
+ }
246
+ match = resolved.match;
247
+ if (windowIdParam && windowIdParam !== match.windowId) {
248
+ throw new Error("Group is not in the specified window");
249
+ }
250
+ }
251
+ else {
252
+ const resolved = resolveGroupByTitle(snapshot, deps.buildWindowLabels, groupTitle, windowIdParam || undefined);
253
+ if (resolved.error) {
254
+ throw resolved.error;
255
+ }
256
+ match = resolved.match;
257
+ }
258
+ const undoTabs = match.tabs
259
+ .map((tab) => ({
260
+ tabId: tab.tabId,
261
+ windowId: tab.windowId,
262
+ index: tab.index,
263
+ groupId: tab.groupId,
264
+ groupTitle: tab.groupTitle,
265
+ groupColor: tab.groupColor,
266
+ groupCollapsed: match.group.collapsed ?? null,
267
+ }))
268
+ .filter((tab) => typeof tab.tabId === "number");
269
+ const tabIds = match.tabs
270
+ .map((tab) => tab.tabId)
271
+ .filter((tabId) => typeof tabId === "number");
272
+ if (tabIds.length) {
273
+ await chrome.tabs.ungroup(tabIds);
274
+ }
275
+ return {
276
+ groupId: match.group.groupId,
277
+ groupTitle: match.group.title || null,
278
+ windowId: match.windowId,
279
+ summary: {
280
+ ungroupedTabs: tabIds.length,
281
+ },
282
+ undo: {
283
+ action: "group-ungroup",
284
+ groupId: match.group.groupId,
285
+ windowId: match.windowId,
286
+ groupTitle: match.group.title || null,
287
+ groupColor: match.group.color || null,
288
+ groupCollapsed: match.group.collapsed ?? null,
289
+ tabs: undoTabs,
290
+ },
291
+ txid: params.txid || null,
292
+ };
293
+ }
294
+ async function groupAssign(params, deps) {
295
+ const rawTabIds = Array.isArray(params.tabIds) ? params.tabIds.map(Number) : [];
296
+ const tabIds = rawTabIds.filter((id) => Number.isFinite(id));
297
+ if (!tabIds.length) {
298
+ throw new Error("Missing tabIds");
299
+ }
300
+ const groupId = Number.isFinite(params.groupId) ? Number(params.groupId) : null;
301
+ const groupTitle = typeof params.groupTitle === "string" ? params.groupTitle.trim() : "";
302
+ if (!groupId && !groupTitle) {
303
+ throw new Error("Missing group identifier");
304
+ }
305
+ const snapshot = await deps.getTabSnapshot();
306
+ const windows = snapshot.windows;
307
+ const windowIdParam = params.windowId != null ? deps.resolveWindowIdFromParams(snapshot, params.windowId) : null;
308
+ if (windowIdParam && !windows.some((win) => win.windowId === windowIdParam)) {
309
+ throw new Error("Window not found");
310
+ }
311
+ const tabIndex = new Map();
312
+ for (const win of windows) {
313
+ for (const tab of win.tabs) {
314
+ if (typeof tab.tabId === "number") {
315
+ tabIndex.set(tab.tabId, { tab, windowId: win.windowId });
316
+ }
317
+ }
318
+ }
319
+ const skipped = [];
320
+ const resolvedTabIds = [];
321
+ const sourceWindows = new Set();
322
+ const undoTabs = [];
323
+ for (const tabId of tabIds) {
324
+ const entry = tabIndex.get(tabId);
325
+ if (!entry) {
326
+ skipped.push({ tabId, reason: "not_found" });
327
+ continue;
328
+ }
329
+ resolvedTabIds.push(tabId);
330
+ sourceWindows.add(entry.windowId);
331
+ const tab = entry.tab;
332
+ undoTabs.push({
333
+ tabId,
334
+ windowId: entry.windowId,
335
+ index: tab.index,
336
+ groupId: tab.groupId,
337
+ groupTitle: tab.groupTitle,
338
+ groupColor: tab.groupColor,
339
+ groupCollapsed: tab.groupCollapsed ?? null,
340
+ });
341
+ }
342
+ if (!resolvedTabIds.length) {
343
+ throw new Error("No matching tabs found");
344
+ }
345
+ let targetGroupId = null;
346
+ let targetWindowId = null;
347
+ let targetTitle = null;
348
+ let created = false;
349
+ if (groupId != null) {
350
+ const resolved = resolveGroupById(snapshot, deps.buildWindowLabels, groupId);
351
+ if (resolved.error) {
352
+ throw resolved.error;
353
+ }
354
+ const match = resolved.match;
355
+ targetGroupId = match.group.groupId;
356
+ targetWindowId = match.windowId;
357
+ targetTitle = typeof match.group.title === "string" ? match.group.title : null;
358
+ if (windowIdParam && windowIdParam !== targetWindowId) {
359
+ throw new Error("Group is not in the specified window");
360
+ }
361
+ }
362
+ else {
363
+ const resolved = resolveGroupByTitle(snapshot, deps.buildWindowLabels, groupTitle, windowIdParam || undefined);
364
+ if (resolved.error) {
365
+ const error = resolved.error;
366
+ if (error.message === "No matching group title found" && params.create === true) {
367
+ targetWindowId = windowIdParam || (sourceWindows.size === 1 ? Array.from(sourceWindows)[0] : null);
368
+ if (!targetWindowId) {
369
+ throw new Error("Multiple source windows. Provide --window to create a new group.");
370
+ }
371
+ targetTitle = groupTitle;
372
+ created = true;
373
+ }
374
+ else {
375
+ throw error;
376
+ }
377
+ }
378
+ else {
379
+ const match = resolved.match;
380
+ targetGroupId = match.group.groupId;
381
+ targetWindowId = match.windowId;
382
+ targetTitle = typeof match.group.title === "string" && match.group.title ? match.group.title : groupTitle;
383
+ }
384
+ }
385
+ if (!targetWindowId) {
386
+ throw new Error("Target window not found");
387
+ }
388
+ const moveIds = resolvedTabIds.filter((tabId) => {
389
+ const entry = tabIndex.get(tabId);
390
+ return entry && entry.windowId !== targetWindowId;
391
+ });
392
+ if (moveIds.length > 0) {
393
+ await chrome.tabs.move(moveIds, { windowId: targetWindowId, index: -1 });
394
+ }
395
+ let assignedGroupId = targetGroupId;
396
+ if (targetGroupId != null) {
397
+ await chrome.tabs.group({ groupId: targetGroupId, tabIds: resolvedTabIds });
398
+ }
399
+ else {
400
+ assignedGroupId = await chrome.tabs.group({ tabIds: resolvedTabIds, createProperties: { windowId: targetWindowId } });
401
+ const update = {};
402
+ if (targetTitle) {
403
+ update.title = targetTitle;
404
+ }
405
+ if (typeof params.color === "string" && params.color.trim()) {
406
+ update.color = params.color.trim();
407
+ }
408
+ if (typeof params.collapsed === "boolean") {
409
+ update.collapsed = params.collapsed;
410
+ }
411
+ if (Object.keys(update).length > 0) {
412
+ try {
413
+ await chrome.tabGroups.update(assignedGroupId, update);
414
+ }
415
+ catch (error) {
416
+ deps.log("Failed to update group", error);
417
+ }
418
+ }
419
+ created = true;
420
+ }
421
+ return {
422
+ groupId: assignedGroupId,
423
+ groupTitle: targetTitle || groupTitle || null,
424
+ windowId: targetWindowId,
425
+ created,
426
+ summary: {
427
+ movedTabs: moveIds.length,
428
+ groupedTabs: resolvedTabIds.length,
429
+ skippedTabs: skipped.length,
430
+ },
431
+ skipped,
432
+ undo: {
433
+ action: "group-assign",
434
+ groupId: assignedGroupId,
435
+ groupTitle: targetTitle || groupTitle || null,
436
+ groupColor: typeof params.color === "string" && params.color.trim() ? params.color.trim() : null,
437
+ groupCollapsed: typeof params.collapsed === "boolean" ? params.collapsed : null,
438
+ created,
439
+ tabs: undoTabs,
440
+ },
441
+ txid: params.txid || null,
442
+ };
443
+ }