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.
- package/{cli → dist/cli}/lib/commands/index.js +4 -2
- package/dist/cli/lib/commands/meta.js +226 -0
- package/dist/cli/lib/commands/params-groups.js +40 -0
- package/dist/cli/lib/commands/params-move.js +44 -0
- package/{cli → dist/cli}/lib/commands/params.js +61 -125
- package/{cli/lib/commands/meta.js → dist/cli/lib/commands/setup.js} +26 -222
- package/{cli/lib/options.js → dist/cli/lib/options-commands.js} +3 -155
- package/dist/cli/lib/options-groups.js +41 -0
- package/dist/cli/lib/options.js +125 -0
- package/{cli → dist/cli}/lib/output.js +5 -4
- package/dist/cli/lib/policy-filter.js +202 -0
- package/dist/cli/lib/response.js +235 -0
- package/{cli → dist/cli}/lib/scope.js +3 -31
- package/dist/cli/tabctl.js +463 -0
- package/dist/extension/background.js +3398 -0
- package/dist/extension/lib/archive.js +444 -0
- package/dist/extension/lib/content.js +320 -0
- package/dist/extension/lib/deps.js +4 -0
- package/dist/extension/lib/groups.js +443 -0
- package/dist/extension/lib/inspect.js +316 -0
- package/dist/extension/lib/move.js +342 -0
- package/dist/extension/lib/screenshot.js +367 -0
- package/dist/extension/lib/tabs.js +395 -0
- package/dist/extension/lib/undo-handlers.js +439 -0
- package/{extension → dist/extension}/manifest.json +2 -2
- package/dist/host/host.js +124 -0
- package/{host/host.js → dist/host/lib/handlers.js} +84 -187
- package/{shared → dist/shared}/version.js +2 -2
- package/package.json +12 -10
- package/cli/tabctl.js +0 -841
- package/extension/background.js +0 -3372
- package/extension/manifest.template.json +0 -22
- /package/{cli → dist/cli}/lib/args.js +0 -0
- /package/{cli → dist/cli}/lib/client.js +0 -0
- /package/{cli → dist/cli}/lib/commands/list.js +0 -0
- /package/{cli → dist/cli}/lib/commands/profile.js +0 -0
- /package/{cli → dist/cli}/lib/constants.js +0 -0
- /package/{cli → dist/cli}/lib/help.js +0 -0
- /package/{cli → dist/cli}/lib/pagination.js +0 -0
- /package/{cli → dist/cli}/lib/policy.js +0 -0
- /package/{cli → dist/cli}/lib/report.js +0 -0
- /package/{cli → dist/cli}/lib/snapshot.js +0 -0
- /package/{cli → dist/cli}/lib/types.js +0 -0
- /package/{host → dist/host}/host.sh +0 -0
- /package/{host → dist/host}/lib/undo.js +0 -0
- /package/{shared → dist/shared}/config.js +0 -0
- /package/{shared → dist/shared}/extension-sync.js +0 -0
- /package/{shared → dist/shared}/profiles.js +0 -0
|
@@ -0,0 +1,395 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Tab operations — extracted from background.ts (pure structural refactor).
|
|
3
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
|
+
exports.getMostRecentFocusedWindowId = getMostRecentFocusedWindowId;
|
|
5
|
+
exports.normalizeUrl = normalizeUrl;
|
|
6
|
+
exports.normalizeTabIndex = normalizeTabIndex;
|
|
7
|
+
exports.resolveOpenWindow = resolveOpenWindow;
|
|
8
|
+
exports.focusTab = focusTab;
|
|
9
|
+
exports.refreshTabs = refreshTabs;
|
|
10
|
+
exports.openTabs = openTabs;
|
|
11
|
+
function getMostRecentFocusedWindowId(windows) {
|
|
12
|
+
let bestWindowId = null;
|
|
13
|
+
let bestFocusedAt = -Infinity;
|
|
14
|
+
for (const win of windows) {
|
|
15
|
+
for (const tab of win.tabs) {
|
|
16
|
+
const focusedAt = Number(tab.lastFocusedAt);
|
|
17
|
+
if (!Number.isFinite(focusedAt)) {
|
|
18
|
+
continue;
|
|
19
|
+
}
|
|
20
|
+
if (focusedAt > bestFocusedAt) {
|
|
21
|
+
bestFocusedAt = focusedAt;
|
|
22
|
+
bestWindowId = win.windowId;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return bestWindowId;
|
|
27
|
+
}
|
|
28
|
+
function normalizeUrl(rawUrl) {
|
|
29
|
+
if (!rawUrl || typeof rawUrl !== "string") {
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
let url;
|
|
33
|
+
try {
|
|
34
|
+
url = new URL(rawUrl);
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
if (url.protocol !== "http:" && url.protocol !== "https:") {
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
url.hash = "";
|
|
43
|
+
const dropKeys = new Set([
|
|
44
|
+
"fbclid",
|
|
45
|
+
"gclid",
|
|
46
|
+
"igshid",
|
|
47
|
+
"mc_cid",
|
|
48
|
+
"mc_eid",
|
|
49
|
+
"ref",
|
|
50
|
+
"ref_src",
|
|
51
|
+
"ref_url",
|
|
52
|
+
"utm_campaign",
|
|
53
|
+
"utm_content",
|
|
54
|
+
"utm_medium",
|
|
55
|
+
"utm_source",
|
|
56
|
+
"utm_term",
|
|
57
|
+
"utm_name",
|
|
58
|
+
"si",
|
|
59
|
+
]);
|
|
60
|
+
for (const key of Array.from(url.searchParams.keys())) {
|
|
61
|
+
if (key.startsWith("utm_") || dropKeys.has(key)) {
|
|
62
|
+
url.searchParams.delete(key);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
const search = url.searchParams.toString();
|
|
66
|
+
url.search = search ? `?${search}` : "";
|
|
67
|
+
return url.toString();
|
|
68
|
+
}
|
|
69
|
+
function normalizeTabIndex(value) {
|
|
70
|
+
const index = Number(value);
|
|
71
|
+
return Number.isFinite(index) ? index : null;
|
|
72
|
+
}
|
|
73
|
+
function matchIncludes(value, needle) {
|
|
74
|
+
if (!needle) {
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
77
|
+
return typeof value === "string" && value.toLowerCase().includes(needle);
|
|
78
|
+
}
|
|
79
|
+
function resolveOpenWindow(snapshot, params) {
|
|
80
|
+
const windows = snapshot.windows;
|
|
81
|
+
if (!windows.length) {
|
|
82
|
+
return { error: { message: "No windows available" } };
|
|
83
|
+
}
|
|
84
|
+
if (params.windowId != null) {
|
|
85
|
+
if (typeof params.windowId === "string") {
|
|
86
|
+
const normalized = params.windowId.trim().toLowerCase();
|
|
87
|
+
if (normalized === "active") {
|
|
88
|
+
const focused = windows.find((win) => win.focused);
|
|
89
|
+
if (focused) {
|
|
90
|
+
return { windowId: focused.windowId };
|
|
91
|
+
}
|
|
92
|
+
return { error: { message: "Active window not found" } };
|
|
93
|
+
}
|
|
94
|
+
if (normalized === "last-focused") {
|
|
95
|
+
const lastFocused = getMostRecentFocusedWindowId(windows);
|
|
96
|
+
if (lastFocused != null) {
|
|
97
|
+
return { windowId: lastFocused };
|
|
98
|
+
}
|
|
99
|
+
return { error: { message: "Last focused window not found" } };
|
|
100
|
+
}
|
|
101
|
+
if (normalized === "new") {
|
|
102
|
+
return { error: { message: "--window new is only supported by open" } };
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
const windowId = Number(params.windowId);
|
|
106
|
+
const found = windows.find((win) => win.windowId === windowId);
|
|
107
|
+
if (!found) {
|
|
108
|
+
return { error: { message: "Window not found" } };
|
|
109
|
+
}
|
|
110
|
+
return { windowId };
|
|
111
|
+
}
|
|
112
|
+
if (params.windowTabId != null) {
|
|
113
|
+
const tabId = Number(params.windowTabId);
|
|
114
|
+
const found = windows.find((win) => win.tabs.some((tab) => tab.tabId === tabId));
|
|
115
|
+
if (!found) {
|
|
116
|
+
return { error: { message: "Window not found for tab" } };
|
|
117
|
+
}
|
|
118
|
+
return { windowId: found.windowId };
|
|
119
|
+
}
|
|
120
|
+
let candidates = [...windows];
|
|
121
|
+
let filtered = false;
|
|
122
|
+
if (typeof params.afterGroupTitle === "string" && params.afterGroupTitle.trim()) {
|
|
123
|
+
const groupTitle = params.afterGroupTitle.trim();
|
|
124
|
+
candidates = candidates.filter((win) => win.groups.some((group) => group.title === groupTitle));
|
|
125
|
+
filtered = true;
|
|
126
|
+
}
|
|
127
|
+
if (typeof params.windowGroupTitle === "string" && params.windowGroupTitle.trim()) {
|
|
128
|
+
const groupTitle = params.windowGroupTitle.trim();
|
|
129
|
+
candidates = candidates.filter((win) => win.groups.some((group) => group.title === groupTitle));
|
|
130
|
+
filtered = true;
|
|
131
|
+
}
|
|
132
|
+
if (typeof params.windowUrl === "string" && params.windowUrl.trim()) {
|
|
133
|
+
const needle = params.windowUrl.trim().toLowerCase();
|
|
134
|
+
candidates = candidates.filter((win) => win.tabs.some((tab) => matchIncludes(tab.url, needle)));
|
|
135
|
+
filtered = true;
|
|
136
|
+
}
|
|
137
|
+
if (filtered) {
|
|
138
|
+
if (candidates.length === 1) {
|
|
139
|
+
return { windowId: candidates[0].windowId };
|
|
140
|
+
}
|
|
141
|
+
if (candidates.length === 0) {
|
|
142
|
+
return { error: { message: "No matching window found" } };
|
|
143
|
+
}
|
|
144
|
+
return { error: { message: "Multiple windows match selection. Provide --window to disambiguate." } };
|
|
145
|
+
}
|
|
146
|
+
const focused = windows.find((win) => win.focused);
|
|
147
|
+
if (focused) {
|
|
148
|
+
return { windowId: focused.windowId };
|
|
149
|
+
}
|
|
150
|
+
if (windows.length === 1) {
|
|
151
|
+
return { windowId: windows[0].windowId };
|
|
152
|
+
}
|
|
153
|
+
const lastFocused = getMostRecentFocusedWindowId(windows);
|
|
154
|
+
if (lastFocused != null) {
|
|
155
|
+
return { windowId: lastFocused };
|
|
156
|
+
}
|
|
157
|
+
return { error: { message: "Multiple windows available. Provide --window to target one." } };
|
|
158
|
+
}
|
|
159
|
+
async function focusTab(params) {
|
|
160
|
+
const tabIds = Array.isArray(params.tabIds) ? params.tabIds.map(Number) : [];
|
|
161
|
+
const tabId = Number.isFinite(params.tabId)
|
|
162
|
+
? Number(params.tabId)
|
|
163
|
+
: tabIds.length
|
|
164
|
+
? Number(tabIds[0])
|
|
165
|
+
: null;
|
|
166
|
+
if (!tabId) {
|
|
167
|
+
throw new Error("Missing tabId");
|
|
168
|
+
}
|
|
169
|
+
const tab = await chrome.tabs.get(tabId);
|
|
170
|
+
await chrome.windows.update(tab.windowId, { focused: true });
|
|
171
|
+
await chrome.tabs.update(tabId, { active: true });
|
|
172
|
+
return {
|
|
173
|
+
tabId,
|
|
174
|
+
windowId: tab.windowId,
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
async function refreshTabs(params) {
|
|
178
|
+
const tabId = Number.isFinite(params.tabId)
|
|
179
|
+
? Number(params.tabId)
|
|
180
|
+
: null;
|
|
181
|
+
if (!tabId) {
|
|
182
|
+
throw new Error("Missing tabId");
|
|
183
|
+
}
|
|
184
|
+
await chrome.tabs.reload(tabId);
|
|
185
|
+
return {
|
|
186
|
+
tabId,
|
|
187
|
+
summary: { refreshedTabs: 1 },
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
async function openTabs(params, deps) {
|
|
191
|
+
const urls = Array.isArray(params.urls)
|
|
192
|
+
? params.urls.map((url) => (typeof url === "string" ? url.trim() : "")).filter(Boolean)
|
|
193
|
+
: [];
|
|
194
|
+
const groupTitle = typeof params.groupTitle === "string" ? params.groupTitle.trim() : "";
|
|
195
|
+
const groupColor = typeof params.color === "string" ? params.color.trim() : "";
|
|
196
|
+
const afterGroupTitle = typeof params.afterGroupTitle === "string" ? params.afterGroupTitle.trim() : "";
|
|
197
|
+
const beforeTabId = Number.isFinite(params.beforeTabId) ? Number(params.beforeTabId) : null;
|
|
198
|
+
const afterTabId = Number.isFinite(params.afterTabId) ? Number(params.afterTabId) : null;
|
|
199
|
+
if (beforeTabId != null && afterTabId != null) {
|
|
200
|
+
throw new Error("Only one target position is allowed");
|
|
201
|
+
}
|
|
202
|
+
const newWindow = params.newWindow === true;
|
|
203
|
+
if (!urls.length && !newWindow) {
|
|
204
|
+
throw new Error("No URLs provided");
|
|
205
|
+
}
|
|
206
|
+
if (newWindow) {
|
|
207
|
+
if (afterGroupTitle || beforeTabId || afterTabId) {
|
|
208
|
+
throw new Error("Cannot use --before/--after with --new-window");
|
|
209
|
+
}
|
|
210
|
+
if (params.windowId != null || params.windowGroupTitle || params.windowTabId != null || params.windowUrl) {
|
|
211
|
+
throw new Error("Cannot combine --new-window with window selectors");
|
|
212
|
+
}
|
|
213
|
+
const created = [];
|
|
214
|
+
const skipped = [];
|
|
215
|
+
const createdWindow = await chrome.windows.create({ focused: false });
|
|
216
|
+
const windowId = createdWindow.id;
|
|
217
|
+
let seedTabs = createdWindow.tabs;
|
|
218
|
+
if (!seedTabs) {
|
|
219
|
+
seedTabs = await chrome.tabs.query({ windowId });
|
|
220
|
+
}
|
|
221
|
+
const seedTabId = seedTabs.find((tab) => typeof tab.id === "number")?.id ?? null;
|
|
222
|
+
for (const url of urls) {
|
|
223
|
+
try {
|
|
224
|
+
const tab = await chrome.tabs.create({ windowId, url, active: false });
|
|
225
|
+
created.push({
|
|
226
|
+
tabId: tab.id,
|
|
227
|
+
windowId: tab.windowId,
|
|
228
|
+
index: tab.index,
|
|
229
|
+
url: tab.url,
|
|
230
|
+
title: tab.title,
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
catch (error) {
|
|
234
|
+
skipped.push({ url, reason: "create_failed" });
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
if (!urls.length && seedTabs.length) {
|
|
238
|
+
const tab = seedTabs[0];
|
|
239
|
+
created.push({
|
|
240
|
+
tabId: tab.id,
|
|
241
|
+
windowId: tab.windowId,
|
|
242
|
+
index: tab.index,
|
|
243
|
+
url: tab.url,
|
|
244
|
+
title: tab.title,
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
if (seedTabId && created.length > 0 && urls.length > 0) {
|
|
248
|
+
try {
|
|
249
|
+
await chrome.tabs.remove(seedTabId);
|
|
250
|
+
}
|
|
251
|
+
catch (error) {
|
|
252
|
+
deps.log("Failed to remove seed tab", error);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
let groupId = null;
|
|
256
|
+
if (groupTitle && created.length > 0) {
|
|
257
|
+
try {
|
|
258
|
+
const tabIds = created.map((tab) => tab.tabId).filter((id) => typeof id === "number");
|
|
259
|
+
if (tabIds.length > 0) {
|
|
260
|
+
groupId = await chrome.tabs.group({ tabIds, createProperties: { windowId } });
|
|
261
|
+
const update = { title: groupTitle };
|
|
262
|
+
if (groupColor) {
|
|
263
|
+
update.color = groupColor;
|
|
264
|
+
}
|
|
265
|
+
await chrome.tabGroups.update(groupId, update);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
catch (error) {
|
|
269
|
+
deps.log("Failed to create group", error);
|
|
270
|
+
groupId = null;
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
return {
|
|
274
|
+
windowId,
|
|
275
|
+
groupId,
|
|
276
|
+
groupTitle: groupTitle || null,
|
|
277
|
+
afterGroupTitle: null,
|
|
278
|
+
insertIndex: null,
|
|
279
|
+
created,
|
|
280
|
+
skipped,
|
|
281
|
+
summary: {
|
|
282
|
+
createdTabs: created.length,
|
|
283
|
+
skippedUrls: skipped.length,
|
|
284
|
+
grouped: Boolean(groupId),
|
|
285
|
+
},
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
const snapshot = await deps.getTabSnapshot();
|
|
289
|
+
let openParams = params;
|
|
290
|
+
if (params.windowId == null && (beforeTabId != null || afterTabId != null)) {
|
|
291
|
+
const anchorId = beforeTabId != null ? beforeTabId : afterTabId;
|
|
292
|
+
const anchorWindow = snapshot.windows
|
|
293
|
+
.find((win) => win.tabs.some((tab) => tab.tabId === anchorId));
|
|
294
|
+
if (anchorWindow) {
|
|
295
|
+
openParams = { ...params, windowId: anchorWindow.windowId };
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
const selection = resolveOpenWindow(snapshot, openParams);
|
|
299
|
+
if (selection.error) {
|
|
300
|
+
throw selection.error;
|
|
301
|
+
}
|
|
302
|
+
const windowId = selection.windowId;
|
|
303
|
+
const windowSnapshot = snapshot.windows.find((win) => win.windowId === windowId);
|
|
304
|
+
if (!windowSnapshot) {
|
|
305
|
+
throw new Error("Window snapshot unavailable");
|
|
306
|
+
}
|
|
307
|
+
const created = [];
|
|
308
|
+
const skipped = [];
|
|
309
|
+
let insertIndex = null;
|
|
310
|
+
if (afterGroupTitle) {
|
|
311
|
+
const targetGroup = windowSnapshot.groups.find((group) => group.title === afterGroupTitle);
|
|
312
|
+
if (!targetGroup) {
|
|
313
|
+
throw new Error("Group not found in target window");
|
|
314
|
+
}
|
|
315
|
+
const groupTabs = windowSnapshot.tabs.filter((tab) => tab.groupId === targetGroup.groupId);
|
|
316
|
+
if (!groupTabs.length) {
|
|
317
|
+
throw new Error("Group has no tabs to anchor insertion");
|
|
318
|
+
}
|
|
319
|
+
const indices = groupTabs
|
|
320
|
+
.map((tab) => normalizeTabIndex(tab.index))
|
|
321
|
+
.filter((value) => value != null);
|
|
322
|
+
if (!indices.length) {
|
|
323
|
+
throw new Error("Group tabs missing indices");
|
|
324
|
+
}
|
|
325
|
+
insertIndex = Math.max(...indices) + 1;
|
|
326
|
+
}
|
|
327
|
+
if (beforeTabId != null || afterTabId != null) {
|
|
328
|
+
if (afterGroupTitle) {
|
|
329
|
+
throw new Error("Only one target position is allowed");
|
|
330
|
+
}
|
|
331
|
+
const anchorId = beforeTabId != null ? beforeTabId : afterTabId;
|
|
332
|
+
const anchorTab = windowSnapshot.tabs.find((tab) => tab.tabId === anchorId);
|
|
333
|
+
if (!anchorTab) {
|
|
334
|
+
throw new Error("Anchor tab not found in target window");
|
|
335
|
+
}
|
|
336
|
+
const anchorIndex = normalizeTabIndex(anchorTab.index);
|
|
337
|
+
if (!Number.isFinite(anchorIndex)) {
|
|
338
|
+
throw new Error("Anchor tab index unavailable");
|
|
339
|
+
}
|
|
340
|
+
insertIndex = beforeTabId != null ? anchorIndex : anchorIndex + 1;
|
|
341
|
+
}
|
|
342
|
+
let nextIndex = insertIndex;
|
|
343
|
+
for (const url of urls) {
|
|
344
|
+
try {
|
|
345
|
+
const createOptions = { windowId, url, active: false };
|
|
346
|
+
if (nextIndex != null) {
|
|
347
|
+
createOptions.index = nextIndex;
|
|
348
|
+
nextIndex += 1;
|
|
349
|
+
}
|
|
350
|
+
const tab = await chrome.tabs.create(createOptions);
|
|
351
|
+
created.push({
|
|
352
|
+
tabId: tab.id,
|
|
353
|
+
windowId: tab.windowId,
|
|
354
|
+
index: tab.index,
|
|
355
|
+
url: tab.url,
|
|
356
|
+
title: tab.title,
|
|
357
|
+
});
|
|
358
|
+
}
|
|
359
|
+
catch (error) {
|
|
360
|
+
skipped.push({ url, reason: "create_failed" });
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
let groupId = null;
|
|
364
|
+
if (groupTitle && created.length > 0) {
|
|
365
|
+
try {
|
|
366
|
+
const tabIds = created.map((tab) => tab.tabId).filter((id) => typeof id === "number");
|
|
367
|
+
if (tabIds.length > 0) {
|
|
368
|
+
groupId = await chrome.tabs.group({ tabIds, createProperties: { windowId } });
|
|
369
|
+
const update = { title: groupTitle };
|
|
370
|
+
if (groupColor) {
|
|
371
|
+
update.color = groupColor;
|
|
372
|
+
}
|
|
373
|
+
await chrome.tabGroups.update(groupId, update);
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
catch (error) {
|
|
377
|
+
deps.log("Failed to create group", error);
|
|
378
|
+
groupId = null;
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
return {
|
|
382
|
+
windowId,
|
|
383
|
+
groupId,
|
|
384
|
+
groupTitle: groupTitle || null,
|
|
385
|
+
afterGroupTitle: afterGroupTitle || null,
|
|
386
|
+
insertIndex,
|
|
387
|
+
created,
|
|
388
|
+
skipped,
|
|
389
|
+
summary: {
|
|
390
|
+
createdTabs: created.length,
|
|
391
|
+
skippedUrls: skipped.length,
|
|
392
|
+
grouped: Boolean(groupId),
|
|
393
|
+
},
|
|
394
|
+
};
|
|
395
|
+
}
|