tabctl 0.3.1 → 0.4.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/README.md +7 -5
- package/dist/cli/lib/client.js +3 -3
- package/dist/cli/lib/commands/index.js +2 -1
- package/dist/cli/lib/commands/meta.js +12 -12
- package/dist/cli/lib/commands/params-groups.js +8 -0
- package/dist/cli/lib/commands/params.js +6 -3
- package/dist/cli/lib/commands/setup.js +37 -136
- package/dist/cli/lib/options-commands.js +10 -1
- package/dist/cli/lib/policy-filter.js +1 -1
- package/dist/cli/lib/policy.js +3 -3
- package/dist/cli/lib/response.js +9 -9
- package/dist/cli/tabctl.js +5 -1
- package/dist/extension/background.js +426 -43
- package/dist/extension/lib/groups.js +89 -3
- package/dist/extension/lib/screenshot.js +2 -2
- package/dist/extension/lib/tabs.js +97 -36
- package/dist/extension/lib/undo-handlers.js +8 -0
- package/dist/extension/manifest.json +2 -2
- package/dist/host/host.bundle.js +45 -43
- package/dist/host/host.js +11 -11
- package/dist/host/lib/handlers.js +7 -5
- package/dist/host/lib/undo.js +6 -6
- package/dist/shared/config.js +19 -19
- package/dist/shared/extension-sync.js +20 -20
- package/dist/shared/profiles.js +5 -5
- package/dist/shared/version.js +2 -2
- package/package.json +7 -3
|
@@ -11,10 +11,15 @@ exports.listGroups = listGroups;
|
|
|
11
11
|
exports.groupUpdate = groupUpdate;
|
|
12
12
|
exports.groupUngroup = groupUngroup;
|
|
13
13
|
exports.groupAssign = groupAssign;
|
|
14
|
+
exports.groupGather = groupGather;
|
|
14
15
|
function getGroupTabs(windowSnapshot, groupId) {
|
|
15
16
|
return windowSnapshot.tabs
|
|
16
17
|
.filter((tab) => tab.groupId === groupId)
|
|
17
|
-
.sort((a, b) =>
|
|
18
|
+
.sort((a, b) => {
|
|
19
|
+
const ai = Number(a.index);
|
|
20
|
+
const bi = Number(b.index);
|
|
21
|
+
return (Number.isFinite(ai) ? ai : 0) - (Number.isFinite(bi) ? bi : 0);
|
|
22
|
+
});
|
|
18
23
|
}
|
|
19
24
|
function listGroupSummaries(snapshot, buildWindowLabels, windowId) {
|
|
20
25
|
const windowLabels = buildWindowLabels(snapshot);
|
|
@@ -83,8 +88,8 @@ function resolveGroupByTitle(snapshot, buildWindowLabels, groupTitle, windowId)
|
|
|
83
88
|
if (matches.length > 1) {
|
|
84
89
|
return {
|
|
85
90
|
error: {
|
|
86
|
-
message:
|
|
87
|
-
hint: "Use --window to
|
|
91
|
+
message: `Ambiguous group title: found ${matches.length} groups named "${groupTitle}". Use group-gather to merge duplicates, --group-id to target by ID, or --window to narrow scope.`,
|
|
92
|
+
hint: "Use group-gather to merge duplicates, --group-id to target by ID, or --window to narrow scope.",
|
|
88
93
|
matches: matches.map((match) => summarizeGroupMatch(match, windowLabels)),
|
|
89
94
|
availableGroups,
|
|
90
95
|
},
|
|
@@ -441,3 +446,84 @@ async function groupAssign(params, deps) {
|
|
|
441
446
|
txid: params.txid || null,
|
|
442
447
|
};
|
|
443
448
|
}
|
|
449
|
+
async function groupGather(params, deps) {
|
|
450
|
+
const snapshot = await deps.getTabSnapshot();
|
|
451
|
+
const windows = snapshot.windows;
|
|
452
|
+
const windowIdParam = params.windowId != null ? deps.resolveWindowIdFromParams(snapshot, params.windowId) : null;
|
|
453
|
+
if (windowIdParam && !windows.some((win) => win.windowId === windowIdParam)) {
|
|
454
|
+
throw new Error("Window not found");
|
|
455
|
+
}
|
|
456
|
+
const groupTitleFilter = typeof params.groupTitle === "string" ? params.groupTitle.trim() : "";
|
|
457
|
+
const merged = [];
|
|
458
|
+
const undoEntries = [];
|
|
459
|
+
for (const win of windows) {
|
|
460
|
+
if (windowIdParam && win.windowId !== windowIdParam)
|
|
461
|
+
continue;
|
|
462
|
+
const byTitle = new Map();
|
|
463
|
+
for (const group of win.groups) {
|
|
464
|
+
const title = typeof group.title === "string" ? group.title : "";
|
|
465
|
+
if (!title)
|
|
466
|
+
continue;
|
|
467
|
+
if (groupTitleFilter && title !== groupTitleFilter)
|
|
468
|
+
continue;
|
|
469
|
+
if (!byTitle.has(title))
|
|
470
|
+
byTitle.set(title, []);
|
|
471
|
+
byTitle.get(title).push(group);
|
|
472
|
+
}
|
|
473
|
+
for (const [title, titleGroups] of byTitle) {
|
|
474
|
+
if (titleGroups.length < 2)
|
|
475
|
+
continue;
|
|
476
|
+
const groupsWithIndex = titleGroups.map((g) => {
|
|
477
|
+
const tabs = win.tabs.filter((t) => t.groupId === g.groupId);
|
|
478
|
+
const minIndex = Math.min(...tabs.map((t) => {
|
|
479
|
+
const idx = Number(t.index);
|
|
480
|
+
return Number.isFinite(idx) ? idx : Infinity;
|
|
481
|
+
}));
|
|
482
|
+
return { group: g, tabs, minIndex };
|
|
483
|
+
});
|
|
484
|
+
groupsWithIndex.sort((a, b) => a.minIndex - b.minIndex);
|
|
485
|
+
const primary = groupsWithIndex[0];
|
|
486
|
+
const duplicates = groupsWithIndex.slice(1);
|
|
487
|
+
let movedTabs = 0;
|
|
488
|
+
for (const dup of duplicates) {
|
|
489
|
+
const tabIds = dup.tabs
|
|
490
|
+
.map((t) => t.tabId)
|
|
491
|
+
.filter((id) => typeof id === "number");
|
|
492
|
+
if (tabIds.length > 0) {
|
|
493
|
+
for (const tab of dup.tabs) {
|
|
494
|
+
undoEntries.push({
|
|
495
|
+
tabId: tab.tabId,
|
|
496
|
+
windowId: win.windowId,
|
|
497
|
+
index: tab.index,
|
|
498
|
+
groupId: tab.groupId,
|
|
499
|
+
groupTitle: tab.groupTitle,
|
|
500
|
+
groupColor: tab.groupColor,
|
|
501
|
+
groupCollapsed: dup.group.collapsed ?? null,
|
|
502
|
+
});
|
|
503
|
+
}
|
|
504
|
+
await chrome.tabs.group({ groupId: primary.group.groupId, tabIds });
|
|
505
|
+
movedTabs += tabIds.length;
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
merged.push({
|
|
509
|
+
windowId: win.windowId,
|
|
510
|
+
groupTitle: title,
|
|
511
|
+
primaryGroupId: primary.group.groupId,
|
|
512
|
+
mergedGroupCount: duplicates.length,
|
|
513
|
+
movedTabs,
|
|
514
|
+
});
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
return {
|
|
518
|
+
merged,
|
|
519
|
+
summary: {
|
|
520
|
+
mergedGroups: merged.reduce((sum, m) => sum + m.mergedGroupCount, 0),
|
|
521
|
+
movedTabs: merged.reduce((sum, m) => sum + m.movedTabs, 0),
|
|
522
|
+
},
|
|
523
|
+
undo: {
|
|
524
|
+
action: "group-gather",
|
|
525
|
+
tabs: undoEntries,
|
|
526
|
+
},
|
|
527
|
+
txid: params.txid || null,
|
|
528
|
+
};
|
|
529
|
+
}
|
|
@@ -15,7 +15,7 @@ exports.scrollToPosition = scrollToPosition;
|
|
|
15
15
|
exports.captureTabTiles = captureTabTiles;
|
|
16
16
|
exports.screenshotTabs = screenshotTabs;
|
|
17
17
|
exports.SCREENSHOT_TILE_MAX_DIM = 2000;
|
|
18
|
-
exports.SCREENSHOT_MAX_BYTES =
|
|
18
|
+
exports.SCREENSHOT_MAX_BYTES = 2_000_000;
|
|
19
19
|
exports.SCREENSHOT_QUALITY = 80;
|
|
20
20
|
exports.SCREENSHOT_SCROLL_DELAY_MS = 150;
|
|
21
21
|
exports.SCREENSHOT_CAPTURE_DELAY_MS = 350;
|
|
@@ -288,7 +288,7 @@ async function screenshotTabs(params, requestId, deps) {
|
|
|
288
288
|
const adjustedTileMaxDim = tileMaxDim < 50 ? 50 : tileMaxDim;
|
|
289
289
|
const maxBytesRaw = Number(params.maxBytes);
|
|
290
290
|
const maxBytes = Number.isFinite(maxBytesRaw) && maxBytesRaw > 0 ? Math.floor(maxBytesRaw) : exports.SCREENSHOT_MAX_BYTES;
|
|
291
|
-
const adjustedMaxBytes = maxBytes <
|
|
291
|
+
const adjustedMaxBytes = maxBytes < 50_000 ? 50_000 : maxBytes;
|
|
292
292
|
const progressEnabled = params.progress === true;
|
|
293
293
|
const tabs = selection.tabs;
|
|
294
294
|
const entries = [];
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
// Tab operations — extracted from background.ts (pure structural refactor).
|
|
3
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
4
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
5
|
+
};
|
|
3
6
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
7
|
exports.getMostRecentFocusedWindowId = getMostRecentFocusedWindowId;
|
|
5
8
|
exports.normalizeUrl = normalizeUrl;
|
|
@@ -8,6 +11,7 @@ exports.resolveOpenWindow = resolveOpenWindow;
|
|
|
8
11
|
exports.focusTab = focusTab;
|
|
9
12
|
exports.refreshTabs = refreshTabs;
|
|
10
13
|
exports.openTabs = openTabs;
|
|
14
|
+
const normalize_url_1 = __importDefault(require("normalize-url"));
|
|
11
15
|
function getMostRecentFocusedWindowId(windows) {
|
|
12
16
|
let bestWindowId = null;
|
|
13
17
|
let bestFocusedAt = -Infinity;
|
|
@@ -29,42 +33,26 @@ function normalizeUrl(rawUrl) {
|
|
|
29
33
|
if (!rawUrl || typeof rawUrl !== "string") {
|
|
30
34
|
return null;
|
|
31
35
|
}
|
|
32
|
-
let url;
|
|
33
36
|
try {
|
|
34
|
-
|
|
37
|
+
return (0, normalize_url_1.default)(rawUrl, {
|
|
38
|
+
stripHash: true,
|
|
39
|
+
removeQueryParameters: [
|
|
40
|
+
/^utm_\w+$/i,
|
|
41
|
+
"fbclid",
|
|
42
|
+
"gclid",
|
|
43
|
+
"igshid",
|
|
44
|
+
"mc_cid",
|
|
45
|
+
"mc_eid",
|
|
46
|
+
"ref",
|
|
47
|
+
"ref_src",
|
|
48
|
+
"ref_url",
|
|
49
|
+
"si",
|
|
50
|
+
],
|
|
51
|
+
});
|
|
35
52
|
}
|
|
36
53
|
catch {
|
|
37
54
|
return null;
|
|
38
55
|
}
|
|
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
56
|
}
|
|
69
57
|
function normalizeTabIndex(value) {
|
|
70
58
|
const index = Number(value);
|
|
@@ -200,6 +188,8 @@ async function openTabs(params, deps) {
|
|
|
200
188
|
throw new Error("Only one target position is allowed");
|
|
201
189
|
}
|
|
202
190
|
const newWindow = params.newWindow === true;
|
|
191
|
+
const forceNewGroup = params.newGroup === true;
|
|
192
|
+
const allowDuplicates = params.allowDuplicates === true;
|
|
203
193
|
if (!urls.length && !newWindow) {
|
|
204
194
|
throw new Error("No URLs provided");
|
|
205
195
|
}
|
|
@@ -287,6 +277,13 @@ async function openTabs(params, deps) {
|
|
|
287
277
|
}
|
|
288
278
|
const snapshot = await deps.getTabSnapshot();
|
|
289
279
|
let openParams = params;
|
|
280
|
+
// Auto-resolve window by group name when no explicit window selector is provided
|
|
281
|
+
if (groupTitle && !forceNewGroup && openParams.windowId == null && !openParams.windowGroupTitle && !openParams.windowTabId && !openParams.windowUrl) {
|
|
282
|
+
const groupWindows = snapshot.windows.filter((win) => win.groups.some((g) => g.title === groupTitle));
|
|
283
|
+
if (groupWindows.length === 1) {
|
|
284
|
+
openParams = { ...openParams, windowId: groupWindows[0].windowId };
|
|
285
|
+
}
|
|
286
|
+
}
|
|
290
287
|
if (params.windowId == null && (beforeTabId != null || afterTabId != null)) {
|
|
291
288
|
const anchorId = beforeTabId != null ? beforeTabId : afterTabId;
|
|
292
289
|
const anchorWindow = snapshot.windows
|
|
@@ -304,6 +301,25 @@ async function openTabs(params, deps) {
|
|
|
304
301
|
if (!windowSnapshot) {
|
|
305
302
|
throw new Error("Window snapshot unavailable");
|
|
306
303
|
}
|
|
304
|
+
// Resolve existing group for reuse
|
|
305
|
+
let existingGroupId = null;
|
|
306
|
+
const existingUrlSet = new Set();
|
|
307
|
+
if (groupTitle && !forceNewGroup) {
|
|
308
|
+
const matchingGroups = windowSnapshot.groups.filter((g) => g.title === groupTitle);
|
|
309
|
+
if (matchingGroups.length > 1) {
|
|
310
|
+
throw new Error(`Ambiguous group title "${groupTitle}": found ${matchingGroups.length} groups with the same name. Use --new-group to force a new group, group-gather to merge, or --group-id to target by ID.`);
|
|
311
|
+
}
|
|
312
|
+
if (matchingGroups.length === 1) {
|
|
313
|
+
existingGroupId = matchingGroups[0].groupId;
|
|
314
|
+
const existingTabs = windowSnapshot.tabs.filter((tab) => tab.groupId === existingGroupId);
|
|
315
|
+
for (const tab of existingTabs) {
|
|
316
|
+
const norm = normalizeUrl(tab.url);
|
|
317
|
+
if (norm) {
|
|
318
|
+
existingUrlSet.add(norm);
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
}
|
|
307
323
|
const created = [];
|
|
308
324
|
const skipped = [];
|
|
309
325
|
let insertIndex = null;
|
|
@@ -339,8 +355,25 @@ async function openTabs(params, deps) {
|
|
|
339
355
|
}
|
|
340
356
|
insertIndex = beforeTabId != null ? anchorIndex : anchorIndex + 1;
|
|
341
357
|
}
|
|
358
|
+
// Default insert position: append after existing group tabs
|
|
359
|
+
if (existingGroupId != null && insertIndex == null && beforeTabId == null && afterTabId == null) {
|
|
360
|
+
const groupTabs = windowSnapshot.tabs.filter((tab) => tab.groupId === existingGroupId);
|
|
361
|
+
const indices = groupTabs
|
|
362
|
+
.map((tab) => normalizeTabIndex(tab.index))
|
|
363
|
+
.filter((value) => value != null);
|
|
364
|
+
if (indices.length) {
|
|
365
|
+
insertIndex = Math.max(...indices) + 1;
|
|
366
|
+
}
|
|
367
|
+
}
|
|
342
368
|
let nextIndex = insertIndex;
|
|
343
369
|
for (const url of urls) {
|
|
370
|
+
if (!allowDuplicates && existingGroupId != null) {
|
|
371
|
+
const norm = normalizeUrl(url);
|
|
372
|
+
if (norm && existingUrlSet.has(norm)) {
|
|
373
|
+
skipped.push({ url, reason: "duplicate" });
|
|
374
|
+
continue;
|
|
375
|
+
}
|
|
376
|
+
}
|
|
344
377
|
try {
|
|
345
378
|
const createOptions = { windowId, url, active: false };
|
|
346
379
|
if (nextIndex != null) {
|
|
@@ -365,12 +398,18 @@ async function openTabs(params, deps) {
|
|
|
365
398
|
try {
|
|
366
399
|
const tabIds = created.map((tab) => tab.tabId).filter((id) => typeof id === "number");
|
|
367
400
|
if (tabIds.length > 0) {
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
401
|
+
if (existingGroupId != null) {
|
|
402
|
+
// Reuse existing group
|
|
403
|
+
groupId = await chrome.tabs.group({ groupId: existingGroupId, tabIds });
|
|
404
|
+
}
|
|
405
|
+
else {
|
|
406
|
+
groupId = await chrome.tabs.group({ tabIds, createProperties: { windowId } });
|
|
407
|
+
const update = { title: groupTitle };
|
|
408
|
+
if (groupColor) {
|
|
409
|
+
update.color = groupColor;
|
|
410
|
+
}
|
|
411
|
+
await chrome.tabGroups.update(groupId, update);
|
|
372
412
|
}
|
|
373
|
-
await chrome.tabGroups.update(groupId, update);
|
|
374
413
|
}
|
|
375
414
|
}
|
|
376
415
|
catch (error) {
|
|
@@ -378,6 +417,28 @@ async function openTabs(params, deps) {
|
|
|
378
417
|
groupId = null;
|
|
379
418
|
}
|
|
380
419
|
}
|
|
420
|
+
// All-dupes case: report existing group even when no new tabs were created
|
|
421
|
+
if (groupTitle && created.length === 0 && existingGroupId != null) {
|
|
422
|
+
groupId = existingGroupId;
|
|
423
|
+
}
|
|
424
|
+
// Reorder: groups before ungrouped tabs
|
|
425
|
+
try {
|
|
426
|
+
const freshTabs = await chrome.tabs.query({ windowId });
|
|
427
|
+
freshTabs.sort((a, b) => a.index - b.index);
|
|
428
|
+
const firstUngroupedIndex = freshTabs.findIndex(t => (t.groupId ?? -1) === -1);
|
|
429
|
+
if (firstUngroupedIndex >= 0) {
|
|
430
|
+
const groupedAfterUngrouped = freshTabs.filter((t, i) => i > firstUngroupedIndex && (t.groupId ?? -1) !== -1);
|
|
431
|
+
if (groupedAfterUngrouped.length > 0) {
|
|
432
|
+
const tabIdsToMove = groupedAfterUngrouped.map(t => t.id).filter(id => typeof id === "number");
|
|
433
|
+
if (tabIdsToMove.length > 0) {
|
|
434
|
+
await chrome.tabs.move(tabIdsToMove, { index: firstUngroupedIndex });
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
catch (err) {
|
|
440
|
+
deps.log("Failed to reorder groups before ungrouped tabs", err);
|
|
441
|
+
}
|
|
381
442
|
return {
|
|
382
443
|
windowId,
|
|
383
444
|
groupId,
|
|
@@ -4,6 +4,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
4
4
|
exports.undoGroupUpdate = undoGroupUpdate;
|
|
5
5
|
exports.undoGroupUngroup = undoGroupUngroup;
|
|
6
6
|
exports.undoGroupAssign = undoGroupAssign;
|
|
7
|
+
exports.undoGroupGather = undoGroupGather;
|
|
7
8
|
exports.undoMoveTab = undoMoveTab;
|
|
8
9
|
exports.undoMoveGroup = undoMoveGroup;
|
|
9
10
|
exports.undoMergeWindow = undoMergeWindow;
|
|
@@ -197,6 +198,10 @@ async function undoGroupAssign(undo, deps) {
|
|
|
197
198
|
const tabs = undo.tabs || [];
|
|
198
199
|
return await restoreTabsFromUndo(tabs, deps);
|
|
199
200
|
}
|
|
201
|
+
async function undoGroupGather(undo, deps) {
|
|
202
|
+
const tabs = undo.tabs || [];
|
|
203
|
+
return await restoreTabsFromUndo(tabs, deps);
|
|
204
|
+
}
|
|
200
205
|
async function undoMoveTab(undo, deps) {
|
|
201
206
|
const from = undo.from || {};
|
|
202
207
|
const entry = {
|
|
@@ -426,6 +431,9 @@ async function undoTransaction(params, deps) {
|
|
|
426
431
|
if (undo.action === "group-assign") {
|
|
427
432
|
return await undoGroupAssign(undo, deps);
|
|
428
433
|
}
|
|
434
|
+
if (undo.action === "group-gather") {
|
|
435
|
+
return await undoGroupGather(undo, deps);
|
|
436
|
+
}
|
|
429
437
|
if (undo.action === "move-tab") {
|
|
430
438
|
return await undoMoveTab(undo, deps);
|
|
431
439
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"manifest_version": 3,
|
|
3
3
|
"name": "Tab Control",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.4.0",
|
|
5
5
|
"description": "Archive and manage browser tabs with CLI support",
|
|
6
6
|
"permissions": [
|
|
7
7
|
"tabs",
|
|
@@ -19,5 +19,5 @@
|
|
|
19
19
|
"background": {
|
|
20
20
|
"service_worker": "background.js"
|
|
21
21
|
},
|
|
22
|
-
"version_name": "0.
|
|
22
|
+
"version_name": "0.4.0"
|
|
23
23
|
}
|
package/dist/host/host.bundle.js
CHANGED
|
@@ -17,28 +17,28 @@ var require_config = __commonJS({
|
|
|
17
17
|
exports2.resetConfig = resetConfig;
|
|
18
18
|
exports2.expandEnvVars = expandEnvVars;
|
|
19
19
|
exports2.resolveConfig = resolveConfig;
|
|
20
|
-
var
|
|
21
|
-
var
|
|
22
|
-
var
|
|
23
|
-
var
|
|
20
|
+
var node_os_1 = __importDefault2(require("node:os"));
|
|
21
|
+
var node_path_1 = __importDefault2(require("node:path"));
|
|
22
|
+
var node_crypto_12 = __importDefault2(require("node:crypto"));
|
|
23
|
+
var node_fs_12 = __importDefault2(require("node:fs"));
|
|
24
24
|
function defaultConfigBase() {
|
|
25
25
|
if (process.platform === "win32") {
|
|
26
|
-
return process.env.APPDATA ||
|
|
26
|
+
return process.env.APPDATA || node_path_1.default.join(node_os_1.default.homedir(), "AppData", "Roaming");
|
|
27
27
|
}
|
|
28
|
-
return
|
|
28
|
+
return node_path_1.default.join(node_os_1.default.homedir(), ".config");
|
|
29
29
|
}
|
|
30
30
|
function defaultStateBase() {
|
|
31
31
|
if (process.platform === "win32") {
|
|
32
|
-
return process.env.LOCALAPPDATA ||
|
|
32
|
+
return process.env.LOCALAPPDATA || node_path_1.default.join(node_os_1.default.homedir(), "AppData", "Local");
|
|
33
33
|
}
|
|
34
|
-
return
|
|
34
|
+
return node_path_1.default.join(node_os_1.default.homedir(), ".local", "state");
|
|
35
35
|
}
|
|
36
36
|
function resolveSocketPath(dataDir) {
|
|
37
37
|
if (process.platform === "win32") {
|
|
38
|
-
const hash =
|
|
38
|
+
const hash = node_crypto_12.default.createHash("sha256").update(dataDir).digest("hex").slice(0, 12);
|
|
39
39
|
return `\\\\.\\pipe\\tabctl-${hash}`;
|
|
40
40
|
}
|
|
41
|
-
return
|
|
41
|
+
return node_path_1.default.join(dataDir, "tabctl.sock");
|
|
42
42
|
}
|
|
43
43
|
var cached;
|
|
44
44
|
function resetConfig() {
|
|
@@ -53,23 +53,23 @@ var require_config = __commonJS({
|
|
|
53
53
|
function resolveConfig(profileName) {
|
|
54
54
|
if (!profileName && cached)
|
|
55
55
|
return cached;
|
|
56
|
-
const configDir = process.env.TABCTL_CONFIG_DIR ||
|
|
56
|
+
const configDir = process.env.TABCTL_CONFIG_DIR || node_path_1.default.join(process.env.XDG_CONFIG_HOME || defaultConfigBase(), "tabctl");
|
|
57
57
|
let fileConfig = {};
|
|
58
58
|
try {
|
|
59
|
-
const raw =
|
|
59
|
+
const raw = node_fs_12.default.readFileSync(node_path_1.default.join(configDir, "config.json"), "utf-8");
|
|
60
60
|
fileConfig = JSON.parse(raw);
|
|
61
61
|
} catch {
|
|
62
62
|
}
|
|
63
63
|
let dataDir;
|
|
64
64
|
if (typeof fileConfig.dataDir === "string" && fileConfig.dataDir) {
|
|
65
65
|
dataDir = expandEnvVars(fileConfig.dataDir);
|
|
66
|
-
if (!
|
|
66
|
+
if (!node_path_1.default.isAbsolute(dataDir)) {
|
|
67
67
|
throw new Error(`dataDir in config.json must be an absolute path (got: ${dataDir}). Use $HOME or full paths.`);
|
|
68
68
|
}
|
|
69
69
|
} else if (process.env.TABCTL_CONFIG_DIR) {
|
|
70
|
-
dataDir =
|
|
70
|
+
dataDir = node_path_1.default.join(configDir, "data");
|
|
71
71
|
} else {
|
|
72
|
-
dataDir =
|
|
72
|
+
dataDir = node_path_1.default.join(process.env.XDG_STATE_HOME || defaultStateBase(), "tabctl");
|
|
73
73
|
}
|
|
74
74
|
const baseDataDir = dataDir;
|
|
75
75
|
const explicitProfile = profileName;
|
|
@@ -77,7 +77,7 @@ var require_config = __commonJS({
|
|
|
77
77
|
let activeProfileName;
|
|
78
78
|
if (effectiveProfile) {
|
|
79
79
|
try {
|
|
80
|
-
const raw =
|
|
80
|
+
const raw = node_fs_12.default.readFileSync(node_path_1.default.join(configDir, "profiles.json"), "utf-8");
|
|
81
81
|
const registry = JSON.parse(raw);
|
|
82
82
|
const profile = registry.profiles[effectiveProfile];
|
|
83
83
|
if (profile) {
|
|
@@ -96,7 +96,7 @@ var require_config = __commonJS({
|
|
|
96
96
|
}
|
|
97
97
|
} else {
|
|
98
98
|
try {
|
|
99
|
-
const raw =
|
|
99
|
+
const raw = node_fs_12.default.readFileSync(node_path_1.default.join(configDir, "profiles.json"), "utf-8");
|
|
100
100
|
const registry = JSON.parse(raw);
|
|
101
101
|
if (registry.default && registry.profiles[registry.default]) {
|
|
102
102
|
dataDir = registry.profiles[registry.default].dataDir;
|
|
@@ -110,9 +110,9 @@ var require_config = __commonJS({
|
|
|
110
110
|
dataDir,
|
|
111
111
|
baseDataDir,
|
|
112
112
|
socketPath: process.env.TABCTL_SOCKET || resolveSocketPath(dataDir),
|
|
113
|
-
undoLog:
|
|
113
|
+
undoLog: node_path_1.default.join(dataDir, "undo.jsonl"),
|
|
114
114
|
wrapperDir: dataDir,
|
|
115
|
-
policyPath:
|
|
115
|
+
policyPath: node_path_1.default.join(configDir, "policy.json"),
|
|
116
116
|
activeProfileName
|
|
117
117
|
};
|
|
118
118
|
if (!profileName) {
|
|
@@ -129,8 +129,8 @@ var require_version = __commonJS({
|
|
|
129
129
|
"use strict";
|
|
130
130
|
Object.defineProperty(exports2, "__esModule", { value: true });
|
|
131
131
|
exports2.DIRTY = exports2.GIT_SHA = exports2.VERSION = exports2.BASE_VERSION = void 0;
|
|
132
|
-
exports2.BASE_VERSION = "0.
|
|
133
|
-
exports2.VERSION = "0.
|
|
132
|
+
exports2.BASE_VERSION = "0.4.0";
|
|
133
|
+
exports2.VERSION = "0.4.0";
|
|
134
134
|
exports2.GIT_SHA = null;
|
|
135
135
|
exports2.DIRTY = false;
|
|
136
136
|
}
|
|
@@ -149,18 +149,18 @@ var require_undo = __commonJS({
|
|
|
149
149
|
exports2.filterByRetention = filterByRetention;
|
|
150
150
|
exports2.findUndoRecord = findUndoRecord;
|
|
151
151
|
exports2.findLatestUndoRecord = findLatestUndoRecord;
|
|
152
|
-
var
|
|
153
|
-
var
|
|
152
|
+
var node_fs_12 = __importDefault2(require("node:fs"));
|
|
153
|
+
var node_path_1 = __importDefault2(require("node:path"));
|
|
154
154
|
var DEFAULT_RETENTION_DAYS = 30;
|
|
155
155
|
function appendUndoRecord(filePath, record) {
|
|
156
|
-
const dir =
|
|
157
|
-
|
|
158
|
-
|
|
156
|
+
const dir = node_path_1.default.dirname(filePath);
|
|
157
|
+
node_fs_12.default.mkdirSync(dir, { recursive: true, mode: 448 });
|
|
158
|
+
node_fs_12.default.appendFileSync(filePath, `${JSON.stringify(record)}
|
|
159
159
|
`, "utf8");
|
|
160
160
|
}
|
|
161
161
|
function readUndoRecords(filePath) {
|
|
162
162
|
try {
|
|
163
|
-
const content =
|
|
163
|
+
const content = node_fs_12.default.readFileSync(filePath, "utf8");
|
|
164
164
|
const lines = content.split("\n").filter(Boolean);
|
|
165
165
|
const records = [];
|
|
166
166
|
for (const line of lines) {
|
|
@@ -205,6 +205,7 @@ var require_handlers = __commonJS({
|
|
|
205
205
|
"dist/host/lib/handlers.js"(exports2) {
|
|
206
206
|
"use strict";
|
|
207
207
|
Object.defineProperty(exports2, "__esModule", { value: true });
|
|
208
|
+
exports2.LOCAL_ACTIONS = exports2.UNDO_ACTIONS = void 0;
|
|
208
209
|
exports2.respond = respond;
|
|
209
210
|
exports2.refreshTimeout = refreshTimeout;
|
|
210
211
|
exports2.forwardToExtension = forwardToExtension;
|
|
@@ -216,17 +217,18 @@ var require_handlers = __commonJS({
|
|
|
216
217
|
var MAX_RESPONSE_BYTES = 20 * 1024 * 1024;
|
|
217
218
|
var HISTORY_LIMIT_DEFAULT = 20;
|
|
218
219
|
var RETENTION_DAYS = 30;
|
|
219
|
-
|
|
220
|
+
exports2.UNDO_ACTIONS = /* @__PURE__ */ new Set([
|
|
220
221
|
"archive",
|
|
221
222
|
"close",
|
|
222
223
|
"group-update",
|
|
223
224
|
"group-ungroup",
|
|
224
225
|
"group-assign",
|
|
226
|
+
"group-gather",
|
|
225
227
|
"move-tab",
|
|
226
228
|
"move-group",
|
|
227
229
|
"merge-window"
|
|
228
230
|
]);
|
|
229
|
-
|
|
231
|
+
exports2.LOCAL_ACTIONS = /* @__PURE__ */ new Set(["history", "undo", "version"]);
|
|
230
232
|
function respond(socket, payload) {
|
|
231
233
|
const serialized = JSON.stringify(payload);
|
|
232
234
|
if (Buffer.byteLength(serialized, "utf8") > MAX_RESPONSE_BYTES) {
|
|
@@ -265,7 +267,7 @@ var require_handlers = __commonJS({
|
|
|
265
267
|
if (txid) {
|
|
266
268
|
params.txid = txid;
|
|
267
269
|
}
|
|
268
|
-
if (!LOCAL_ACTIONS.has(request.action)) {
|
|
270
|
+
if (!exports2.LOCAL_ACTIONS.has(request.action)) {
|
|
269
271
|
params.client = {
|
|
270
272
|
component: "host",
|
|
271
273
|
version: version_1.VERSION
|
|
@@ -362,7 +364,7 @@ var require_handlers = __commonJS({
|
|
|
362
364
|
});
|
|
363
365
|
return;
|
|
364
366
|
}
|
|
365
|
-
if (UNDO_ACTIONS.has(pendingRequest.action)) {
|
|
367
|
+
if (exports2.UNDO_ACTIONS.has(pendingRequest.action)) {
|
|
366
368
|
const record = {
|
|
367
369
|
txid: pendingRequest.txid,
|
|
368
370
|
createdAt: Date.now(),
|
|
@@ -518,7 +520,7 @@ var require_handlers = __commonJS({
|
|
|
518
520
|
}, { txid });
|
|
519
521
|
return;
|
|
520
522
|
}
|
|
521
|
-
if (UNDO_ACTIONS.has(action)) {
|
|
523
|
+
if (exports2.UNDO_ACTIONS.has(action)) {
|
|
522
524
|
const txid = deps2.createId("tx");
|
|
523
525
|
forwardToExtension(deps2, socket, request, { txid });
|
|
524
526
|
return;
|
|
@@ -533,9 +535,9 @@ var __importDefault = exports && exports.__importDefault || function(mod) {
|
|
|
533
535
|
return mod && mod.__esModule ? mod : { "default": mod };
|
|
534
536
|
};
|
|
535
537
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
536
|
-
var
|
|
537
|
-
var
|
|
538
|
-
var
|
|
538
|
+
var node_fs_1 = __importDefault(require("node:fs"));
|
|
539
|
+
var node_net_1 = __importDefault(require("node:net"));
|
|
540
|
+
var node_crypto_1 = __importDefault(require("node:crypto"));
|
|
539
541
|
var config_1 = require_config();
|
|
540
542
|
var handlers_1 = require_handlers();
|
|
541
543
|
var config;
|
|
@@ -555,10 +557,10 @@ function log(...args) {
|
|
|
555
557
|
`);
|
|
556
558
|
}
|
|
557
559
|
function ensureDir() {
|
|
558
|
-
|
|
560
|
+
node_fs_1.default.mkdirSync(SOCKET_DIR, { recursive: true, mode: 448 });
|
|
559
561
|
}
|
|
560
562
|
function createId(prefix) {
|
|
561
|
-
return `${prefix}-${Date.now()}-${
|
|
563
|
+
return `${prefix}-${Date.now()}-${node_crypto_1.default.randomBytes(4).toString("hex")}`;
|
|
562
564
|
}
|
|
563
565
|
function sendNative(message) {
|
|
564
566
|
const json = JSON.stringify(message);
|
|
@@ -601,10 +603,10 @@ process.stdin.on("end", () => {
|
|
|
601
603
|
});
|
|
602
604
|
function startSocketServer() {
|
|
603
605
|
ensureDir();
|
|
604
|
-
if (process.platform !== "win32" &&
|
|
605
|
-
|
|
606
|
+
if (process.platform !== "win32" && node_fs_1.default.existsSync(SOCKET_PATH)) {
|
|
607
|
+
node_fs_1.default.unlinkSync(SOCKET_PATH);
|
|
606
608
|
}
|
|
607
|
-
const server2 =
|
|
609
|
+
const server2 = node_net_1.default.createServer((socket) => {
|
|
608
610
|
socket.setEncoding("utf8");
|
|
609
611
|
let buffer = "";
|
|
610
612
|
socket.on("data", (data) => {
|
|
@@ -644,7 +646,7 @@ function startSocketServer() {
|
|
|
644
646
|
server2.listen(SOCKET_PATH, () => {
|
|
645
647
|
if (process.platform !== "win32") {
|
|
646
648
|
try {
|
|
647
|
-
|
|
649
|
+
node_fs_1.default.chmodSync(SOCKET_PATH, 384);
|
|
648
650
|
} catch {
|
|
649
651
|
}
|
|
650
652
|
}
|
|
@@ -654,8 +656,8 @@ function startSocketServer() {
|
|
|
654
656
|
}
|
|
655
657
|
function cleanupAndExit(code) {
|
|
656
658
|
try {
|
|
657
|
-
if (process.platform !== "win32" &&
|
|
658
|
-
|
|
659
|
+
if (process.platform !== "win32" && node_fs_1.default.existsSync(SOCKET_PATH)) {
|
|
660
|
+
node_fs_1.default.unlinkSync(SOCKET_PATH);
|
|
659
661
|
}
|
|
660
662
|
} catch {
|
|
661
663
|
}
|