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,202 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.applyPolicyFilter = applyPolicyFilter;
|
|
4
|
+
const policy_1 = require("./policy");
|
|
5
|
+
const scope_1 = require("./scope");
|
|
6
|
+
function mapProtectedTab(tab) {
|
|
7
|
+
return {
|
|
8
|
+
tabId: tab.tabId,
|
|
9
|
+
windowId: tab.windowId,
|
|
10
|
+
groupId: tab.groupId,
|
|
11
|
+
groupTitle: tab.groupTitle,
|
|
12
|
+
title: tab.title,
|
|
13
|
+
url: tab.url,
|
|
14
|
+
pinned: tab.pinned,
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
function applyPolicyFilter(command, params, snapshot, policyContext, policySummary) {
|
|
18
|
+
const policy = policyContext.policy;
|
|
19
|
+
const selection = (0, scope_1.selectTabsFromSnapshot)(snapshot, params);
|
|
20
|
+
if (selection.error) {
|
|
21
|
+
return {
|
|
22
|
+
params,
|
|
23
|
+
policyInfo: null,
|
|
24
|
+
earlyResponse: {
|
|
25
|
+
ok: false,
|
|
26
|
+
error: selection.error,
|
|
27
|
+
},
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
const selectedTabs = selection.tabs;
|
|
31
|
+
const eligibleTabs = selectedTabs.filter((tab) => (0, policy_1.evaluateTab)(tab, policy).eligible);
|
|
32
|
+
const protectedTabs = selectedTabs.filter((tab) => !(0, policy_1.evaluateTab)(tab, policy).eligible);
|
|
33
|
+
const eligibleIds = eligibleTabs.map((tab) => tab.tabId).filter((id) => typeof id === "number");
|
|
34
|
+
let earlyResponse = null;
|
|
35
|
+
let policyInfo = null;
|
|
36
|
+
let newParams = params;
|
|
37
|
+
if (command === "focus" || command === "refresh") {
|
|
38
|
+
if (!eligibleIds.length) {
|
|
39
|
+
return {
|
|
40
|
+
params,
|
|
41
|
+
policyInfo: null,
|
|
42
|
+
earlyResponse: {
|
|
43
|
+
ok: false,
|
|
44
|
+
error: { message: `Tab is protected by policy and cannot be ${command === "focus" ? "focused" : "refreshed"} via CLI` },
|
|
45
|
+
},
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
newParams = {
|
|
49
|
+
tabId: eligibleIds[0],
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
else if (command === "close" || command === "archive") {
|
|
53
|
+
if (!eligibleIds.length) {
|
|
54
|
+
earlyResponse = {
|
|
55
|
+
ok: true,
|
|
56
|
+
action: command,
|
|
57
|
+
data: {
|
|
58
|
+
summary: { eligible: 0, protected: protectedTabs.length },
|
|
59
|
+
protected: protectedTabs.map(mapProtectedTab),
|
|
60
|
+
policy: policySummary,
|
|
61
|
+
},
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
else if (command === "close") {
|
|
65
|
+
newParams = {
|
|
66
|
+
mode: "direct",
|
|
67
|
+
confirmed: true,
|
|
68
|
+
tabIds: eligibleIds,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
else if (command === "archive") {
|
|
72
|
+
newParams = {
|
|
73
|
+
tabIds: eligibleIds,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
policyInfo = {
|
|
77
|
+
protected: protectedTabs.map(mapProtectedTab),
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
else if (command === "move-tab" || command === "move-group" || command === "group-assign") {
|
|
81
|
+
if (!eligibleIds.length || (command === "move-group" && protectedTabs.length > 0)) {
|
|
82
|
+
earlyResponse = {
|
|
83
|
+
ok: true,
|
|
84
|
+
action: command,
|
|
85
|
+
data: {
|
|
86
|
+
summary: { eligible: eligibleIds.length, protected: protectedTabs.length },
|
|
87
|
+
protected: protectedTabs.map(mapProtectedTab),
|
|
88
|
+
policy: policySummary,
|
|
89
|
+
},
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
else if (command === "move-tab" || command === "group-assign") {
|
|
93
|
+
newParams = {
|
|
94
|
+
...params,
|
|
95
|
+
tabId: eligibleIds[0],
|
|
96
|
+
tabIds: eligibleIds,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
policyInfo = {
|
|
100
|
+
protected: protectedTabs.map(mapProtectedTab),
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
else if (command === "merge-window") {
|
|
104
|
+
if (!eligibleIds.length) {
|
|
105
|
+
earlyResponse = {
|
|
106
|
+
ok: true,
|
|
107
|
+
action: command,
|
|
108
|
+
data: {
|
|
109
|
+
summary: { eligible: 0, protected: protectedTabs.length },
|
|
110
|
+
protected: protectedTabs.map(mapProtectedTab),
|
|
111
|
+
policy: policySummary,
|
|
112
|
+
},
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
newParams = {
|
|
116
|
+
...params,
|
|
117
|
+
tabIds: eligibleIds,
|
|
118
|
+
};
|
|
119
|
+
policyInfo = {
|
|
120
|
+
protected: protectedTabs.map(mapProtectedTab),
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
else if (command === "group-update" || command === "group-ungroup") {
|
|
124
|
+
if (!eligibleIds.length || protectedTabs.length > 0) {
|
|
125
|
+
earlyResponse = {
|
|
126
|
+
ok: true,
|
|
127
|
+
action: command,
|
|
128
|
+
data: {
|
|
129
|
+
summary: { eligible: eligibleIds.length, protected: protectedTabs.length },
|
|
130
|
+
protected: protectedTabs.map(mapProtectedTab),
|
|
131
|
+
policy: policySummary,
|
|
132
|
+
},
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
policyInfo = {
|
|
136
|
+
protected: protectedTabs.map(mapProtectedTab),
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
else {
|
|
140
|
+
if (!eligibleIds.length) {
|
|
141
|
+
const generatedAt = Date.now();
|
|
142
|
+
if (command === "analyze") {
|
|
143
|
+
earlyResponse = {
|
|
144
|
+
ok: true,
|
|
145
|
+
action: command,
|
|
146
|
+
data: {
|
|
147
|
+
generatedAt,
|
|
148
|
+
staleDays: params.staleDays || 0,
|
|
149
|
+
totals: { tabs: 0, analyzed: 0, candidates: 0 },
|
|
150
|
+
meta: { durationMs: 0, githubChecked: 0, githubTotal: 0, githubMatched: 0, githubTimeoutMs: params.githubTimeoutMs || 0 },
|
|
151
|
+
candidates: [],
|
|
152
|
+
analysisId: null,
|
|
153
|
+
policy: policySummary,
|
|
154
|
+
},
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
else if (command === "screenshot") {
|
|
158
|
+
earlyResponse = {
|
|
159
|
+
ok: true,
|
|
160
|
+
action: command,
|
|
161
|
+
data: {
|
|
162
|
+
generatedAt,
|
|
163
|
+
entries: [],
|
|
164
|
+
totals: { tabs: 0, tiles: 0 },
|
|
165
|
+
meta: {
|
|
166
|
+
durationMs: 0,
|
|
167
|
+
mode: params.mode || "viewport",
|
|
168
|
+
format: params.format || "png",
|
|
169
|
+
tileMaxDim: params.tileMaxDim || null,
|
|
170
|
+
maxBytes: params.maxBytes || null,
|
|
171
|
+
},
|
|
172
|
+
policy: policySummary,
|
|
173
|
+
},
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
else {
|
|
177
|
+
earlyResponse = {
|
|
178
|
+
ok: true,
|
|
179
|
+
action: command,
|
|
180
|
+
data: {
|
|
181
|
+
generatedAt,
|
|
182
|
+
entries: [],
|
|
183
|
+
totals: { tabs: 0, signals: 0, tasks: 0 },
|
|
184
|
+
meta: { durationMs: 0, signalTimeoutMs: params.signalTimeoutMs || 0, selectorCount: 0 },
|
|
185
|
+
policy: policySummary,
|
|
186
|
+
},
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
else {
|
|
191
|
+
newParams = {
|
|
192
|
+
...params,
|
|
193
|
+
tabIds: eligibleIds,
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
return {
|
|
198
|
+
params: newParams,
|
|
199
|
+
policyInfo,
|
|
200
|
+
earlyResponse,
|
|
201
|
+
};
|
|
202
|
+
}
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.annotateEntries = annotateEntries;
|
|
7
|
+
exports.annotateCandidates = annotateCandidates;
|
|
8
|
+
exports.buildDedupeOutput = buildDedupeOutput;
|
|
9
|
+
exports.extractDedupePlan = extractDedupePlan;
|
|
10
|
+
exports.formatReport = formatReport;
|
|
11
|
+
exports.writeScreenshots = writeScreenshots;
|
|
12
|
+
const fs_1 = __importDefault(require("fs"));
|
|
13
|
+
const path_1 = __importDefault(require("path"));
|
|
14
|
+
const report_1 = require("./report");
|
|
15
|
+
const policy_1 = require("./policy");
|
|
16
|
+
const output_1 = require("./output");
|
|
17
|
+
const scope_1 = require("./scope");
|
|
18
|
+
const pagination_1 = require("./pagination");
|
|
19
|
+
const snapshot_1 = require("./snapshot");
|
|
20
|
+
/**
|
|
21
|
+
* Annotate inspect/report/screenshot entries with policy info and apply pagination.
|
|
22
|
+
*/
|
|
23
|
+
function annotateEntries(data, options, command, policyEnabled, policy, snapshot) {
|
|
24
|
+
const entries = data.entries;
|
|
25
|
+
const tabIndex = snapshot ? (0, snapshot_1.buildTabIndex)(snapshot) : null;
|
|
26
|
+
const annotated = entries.map((entry) => {
|
|
27
|
+
const tab = tabIndex?.get(entry.tabId) || entry;
|
|
28
|
+
const { eligible, protectedReasons } = (0, policy_1.evaluateTab)(tab, policy);
|
|
29
|
+
return {
|
|
30
|
+
...entry,
|
|
31
|
+
eligible,
|
|
32
|
+
protectedReasons,
|
|
33
|
+
};
|
|
34
|
+
}).filter((entry) => entry.eligible !== false);
|
|
35
|
+
const scope = (0, scope_1.resolveScopeFlags)(options);
|
|
36
|
+
const allScope = options.all === true || !scope.hasScope;
|
|
37
|
+
const scopeArgs = (0, scope_1.buildScopeArgs)(options, allScope);
|
|
38
|
+
const pagination = (0, pagination_1.resolvePagination)(options, annotated.length, command, scopeArgs);
|
|
39
|
+
const start = pagination.offset;
|
|
40
|
+
const end = pagination.offset + pagination.limit;
|
|
41
|
+
data.entries = annotated.slice(start, end);
|
|
42
|
+
if (pagination.page) {
|
|
43
|
+
data.page = pagination.page;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Annotate analyze candidates with policy info and optional window titles.
|
|
48
|
+
*/
|
|
49
|
+
function annotateCandidates(data, policy, includeWindowTitle, snapshot) {
|
|
50
|
+
const candidates = data.candidates;
|
|
51
|
+
const tabIndex = snapshot ? (0, snapshot_1.buildTabIndex)(snapshot) : null;
|
|
52
|
+
const windowTitleIndex = snapshot && includeWindowTitle
|
|
53
|
+
? (0, snapshot_1.buildWindowTitleIndex)(snapshot, policy)
|
|
54
|
+
: null;
|
|
55
|
+
data.candidates = candidates.map((candidate) => {
|
|
56
|
+
const tab = tabIndex?.get(candidate.tabId) || candidate;
|
|
57
|
+
const { eligible, protectedReasons } = (0, policy_1.evaluateTab)(tab, policy);
|
|
58
|
+
const windowTitle = includeWindowTitle
|
|
59
|
+
? (windowTitleIndex?.get(candidate.windowId) ?? null)
|
|
60
|
+
: undefined;
|
|
61
|
+
return {
|
|
62
|
+
...candidate,
|
|
63
|
+
eligible,
|
|
64
|
+
protectedReasons,
|
|
65
|
+
...(includeWindowTitle ? { windowTitle } : {}),
|
|
66
|
+
};
|
|
67
|
+
}).filter((candidate) => candidate.eligible !== false);
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Build dedupe output from analyze response, optionally closing tabs.
|
|
71
|
+
*/
|
|
72
|
+
function buildDedupeOutput(response, includeStale, closeData, confirmed) {
|
|
73
|
+
const data = response.data || {};
|
|
74
|
+
const candidates = Array.isArray(data.candidates) ? data.candidates : [];
|
|
75
|
+
const planned = candidates.filter((candidate) => {
|
|
76
|
+
const reasons = Array.isArray(candidate.reasons) ? candidate.reasons : [];
|
|
77
|
+
const hasDuplicate = reasons.some((reason) => reason.type === "duplicate" || reason.type === "closed_issue");
|
|
78
|
+
const hasStale = reasons.some((reason) => reason.type === "stale");
|
|
79
|
+
return hasDuplicate || (includeStale && hasStale);
|
|
80
|
+
});
|
|
81
|
+
const planTabIds = [];
|
|
82
|
+
const expectedUrls = {};
|
|
83
|
+
for (const candidate of planned) {
|
|
84
|
+
const tabId = candidate.tabId;
|
|
85
|
+
if (!Number.isFinite(tabId)) {
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
if (!planTabIds.includes(tabId)) {
|
|
89
|
+
planTabIds.push(tabId);
|
|
90
|
+
}
|
|
91
|
+
if (typeof candidate.url === "string") {
|
|
92
|
+
expectedUrls[String(tabId)] = candidate.url;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
const closeSummary = closeData?.summary;
|
|
96
|
+
const closedTabs = Number(closeSummary?.closedTabs ?? 0);
|
|
97
|
+
const skippedTabs = Number(closeSummary?.skippedTabs ?? 0);
|
|
98
|
+
return {
|
|
99
|
+
ok: true,
|
|
100
|
+
action: "dedupe",
|
|
101
|
+
data: {
|
|
102
|
+
analysisId: data.analysisId || null,
|
|
103
|
+
summary: {
|
|
104
|
+
candidates: candidates.length,
|
|
105
|
+
planned: planTabIds.length,
|
|
106
|
+
closed: Number.isFinite(closedTabs) ? closedTabs : 0,
|
|
107
|
+
skipped: Number.isFinite(skippedTabs) ? skippedTabs : 0,
|
|
108
|
+
},
|
|
109
|
+
plan: {
|
|
110
|
+
tabIds: planTabIds,
|
|
111
|
+
candidates: planned,
|
|
112
|
+
},
|
|
113
|
+
close: closeData,
|
|
114
|
+
nextCommand: confirmed
|
|
115
|
+
? null
|
|
116
|
+
: (planTabIds.length > 0 && data.analysisId ? `tabctl close --apply ${data.analysisId} --confirm` : null),
|
|
117
|
+
policy: data.policy,
|
|
118
|
+
policyInfo: data.policyInfo,
|
|
119
|
+
},
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Extract dedupe plan tab IDs and expected URLs from analyze response.
|
|
124
|
+
*/
|
|
125
|
+
function extractDedupePlan(response, includeStale) {
|
|
126
|
+
const data = response.data || {};
|
|
127
|
+
const candidates = Array.isArray(data.candidates) ? data.candidates : [];
|
|
128
|
+
const planned = candidates.filter((candidate) => {
|
|
129
|
+
const reasons = Array.isArray(candidate.reasons) ? candidate.reasons : [];
|
|
130
|
+
const hasDuplicate = reasons.some((reason) => reason.type === "duplicate" || reason.type === "closed_issue");
|
|
131
|
+
const hasStale = reasons.some((reason) => reason.type === "stale");
|
|
132
|
+
return hasDuplicate || (includeStale && hasStale);
|
|
133
|
+
});
|
|
134
|
+
const planTabIds = [];
|
|
135
|
+
const expectedUrls = {};
|
|
136
|
+
for (const candidate of planned) {
|
|
137
|
+
const tabId = candidate.tabId;
|
|
138
|
+
if (!Number.isFinite(tabId)) {
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
if (!planTabIds.includes(tabId)) {
|
|
142
|
+
planTabIds.push(tabId);
|
|
143
|
+
}
|
|
144
|
+
if (typeof candidate.url === "string") {
|
|
145
|
+
expectedUrls[String(tabId)] = candidate.url;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
return { planTabIds, expectedUrls };
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Format report output based on format option.
|
|
152
|
+
*/
|
|
153
|
+
function formatReport(response, options, prettyOutput) {
|
|
154
|
+
const format = options.format || "json";
|
|
155
|
+
const data = response.data;
|
|
156
|
+
const entries = data?.entries || [];
|
|
157
|
+
const generatedAt = data?.generatedAt;
|
|
158
|
+
const page = data && "page" in data ? data.page : undefined;
|
|
159
|
+
let content = "";
|
|
160
|
+
if (format === "json") {
|
|
161
|
+
content = JSON.stringify({ generatedAt, entries }, null, 2);
|
|
162
|
+
}
|
|
163
|
+
else if (format === "csv") {
|
|
164
|
+
content = (0, report_1.renderCsv)(entries);
|
|
165
|
+
}
|
|
166
|
+
else if (format === "md") {
|
|
167
|
+
content = (0, report_1.renderMarkdown)(entries, generatedAt);
|
|
168
|
+
}
|
|
169
|
+
else {
|
|
170
|
+
(0, output_1.errorOut)(`Unknown report format: ${format}`);
|
|
171
|
+
}
|
|
172
|
+
if (options.out) {
|
|
173
|
+
fs_1.default.writeFileSync(String(options.out), content, "utf8");
|
|
174
|
+
(0, output_1.printJson)({ ok: true, data: { writtenTo: options.out, format, count: entries.length, ...(page ? { page } : {}) } }, prettyOutput);
|
|
175
|
+
return { printed: true };
|
|
176
|
+
}
|
|
177
|
+
if (format === "json") {
|
|
178
|
+
(0, output_1.printJson)({ ok: true, data: { format, entries, ...(page ? { page } : {}) } }, prettyOutput);
|
|
179
|
+
return { printed: true };
|
|
180
|
+
}
|
|
181
|
+
(0, output_1.printJson)({ ok: true, data: { format, entries, content, ...(page ? { page } : {}) } }, prettyOutput);
|
|
182
|
+
return { printed: true };
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* Write screenshot tiles to disk and return sanitized output.
|
|
186
|
+
*/
|
|
187
|
+
function writeScreenshots(response, options, prettyOutput) {
|
|
188
|
+
const data = response.data;
|
|
189
|
+
const entries = data?.entries || [];
|
|
190
|
+
const page = data && "page" in data ? data.page : undefined;
|
|
191
|
+
const outDir = options.out
|
|
192
|
+
? String(options.out)
|
|
193
|
+
: path_1.default.join(process.cwd(), ".tabctl", "screenshots", String(Date.now()));
|
|
194
|
+
fs_1.default.mkdirSync(outDir, { recursive: true });
|
|
195
|
+
let filesWritten = 0;
|
|
196
|
+
const sanitized = entries.map((entry) => {
|
|
197
|
+
const tabId = entry.tabId;
|
|
198
|
+
const tabDir = path_1.default.join(outDir, String(tabId ?? "unknown"));
|
|
199
|
+
fs_1.default.mkdirSync(tabDir, { recursive: true });
|
|
200
|
+
const tiles = Array.isArray(entry.tiles) ? entry.tiles : [];
|
|
201
|
+
const sanitizedTiles = tiles.map((tile) => {
|
|
202
|
+
const rawUrl = tile.dataUrl;
|
|
203
|
+
const { dataUrl: _ignored, ...rest } = tile;
|
|
204
|
+
if (!rawUrl) {
|
|
205
|
+
return { ...rest, path: null, error: "missing_data" };
|
|
206
|
+
}
|
|
207
|
+
const match = rawUrl.match(/^data:(image\/png|image\/jpeg);base64,(.+)$/);
|
|
208
|
+
if (!match) {
|
|
209
|
+
return { ...rest, path: null, error: "invalid_data_url" };
|
|
210
|
+
}
|
|
211
|
+
const mime = match[1];
|
|
212
|
+
const base64 = match[2];
|
|
213
|
+
const ext = mime === "image/jpeg" ? "jpg" : "png";
|
|
214
|
+
const index = Number.isFinite(tile.index) ? Number(tile.index) + 1 : filesWritten + 1;
|
|
215
|
+
const total = Number.isFinite(tile.total) ? Number(tile.total) : null;
|
|
216
|
+
const suffix = total && total > 1 ? `-of-${total}` : "";
|
|
217
|
+
const filename = `screenshot-${index}${suffix}.${ext}`;
|
|
218
|
+
const filePath = path_1.default.join(tabDir, filename);
|
|
219
|
+
const buffer = Buffer.from(base64, "base64");
|
|
220
|
+
fs_1.default.writeFileSync(filePath, buffer);
|
|
221
|
+
filesWritten += 1;
|
|
222
|
+
return {
|
|
223
|
+
...rest,
|
|
224
|
+
path: filePath,
|
|
225
|
+
bytes: buffer.length,
|
|
226
|
+
};
|
|
227
|
+
});
|
|
228
|
+
return {
|
|
229
|
+
...entry,
|
|
230
|
+
tiles: sanitizedTiles,
|
|
231
|
+
};
|
|
232
|
+
});
|
|
233
|
+
(0, output_1.printJson)({ ok: true, data: { writtenTo: outDir, files: filesWritten, entries: sanitized, ...(page ? { page } : {}) } }, prettyOutput);
|
|
234
|
+
return { printed: true };
|
|
235
|
+
}
|
|
@@ -8,6 +8,7 @@ exports.selectTabsFromSnapshot = selectTabsFromSnapshot;
|
|
|
8
8
|
exports.resolveWindowIdFromSnapshot = resolveWindowIdFromSnapshot;
|
|
9
9
|
exports.filterGroupsByScope = filterGroupsByScope;
|
|
10
10
|
const output_1 = require("./output");
|
|
11
|
+
const snapshot_1 = require("./snapshot");
|
|
11
12
|
function formatCliArgValue(value) {
|
|
12
13
|
const raw = String(value);
|
|
13
14
|
if (!raw) {
|
|
@@ -104,35 +105,6 @@ function extractScopeParams(options) {
|
|
|
104
105
|
all: options.all === true,
|
|
105
106
|
};
|
|
106
107
|
}
|
|
107
|
-
// Helper to build window label index (needed by selectTabsFromSnapshot)
|
|
108
|
-
function buildWindowLabelIndex(snapshot) {
|
|
109
|
-
const windowLabels = new Map();
|
|
110
|
-
const windows = snapshot.windows || [];
|
|
111
|
-
windows.forEach((win, index) => {
|
|
112
|
-
const windowId = win.windowId;
|
|
113
|
-
if (typeof windowId === "number") {
|
|
114
|
-
windowLabels.set(windowId, `W${index + 1}`);
|
|
115
|
-
}
|
|
116
|
-
});
|
|
117
|
-
return windowLabels;
|
|
118
|
-
}
|
|
119
|
-
// Helper to list group summaries (needed by selectTabsFromSnapshot)
|
|
120
|
-
function listGroupSummaries(snapshot, windowLabels) {
|
|
121
|
-
const windows = snapshot.windows || [];
|
|
122
|
-
const summaries = [];
|
|
123
|
-
for (const win of windows) {
|
|
124
|
-
const groups = win.groups || [];
|
|
125
|
-
for (const group of groups) {
|
|
126
|
-
summaries.push({
|
|
127
|
-
windowId: win.windowId,
|
|
128
|
-
windowLabel: windowLabels.get(win.windowId) ?? null,
|
|
129
|
-
groupId: group.groupId,
|
|
130
|
-
title: typeof group.title === "string" ? group.title : null,
|
|
131
|
-
});
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
return summaries;
|
|
135
|
-
}
|
|
136
108
|
function selectTabsFromSnapshot(snapshot, params) {
|
|
137
109
|
const windows = snapshot.windows || [];
|
|
138
110
|
const allTabs = windows.flatMap((win) => win.tabs || []);
|
|
@@ -145,7 +117,7 @@ function selectTabsFromSnapshot(snapshot, params) {
|
|
|
145
117
|
return { tabs: allTabs.filter((tab) => tab.groupId === groupId) };
|
|
146
118
|
}
|
|
147
119
|
if (params.groupTitle) {
|
|
148
|
-
const windowLabels = buildWindowLabelIndex(snapshot);
|
|
120
|
+
const windowLabels = (0, snapshot_1.buildWindowLabelIndex)(snapshot);
|
|
149
121
|
const matches = [];
|
|
150
122
|
for (const win of windows) {
|
|
151
123
|
const groups = win.groups || [];
|
|
@@ -159,7 +131,7 @@ function selectTabsFromSnapshot(snapshot, params) {
|
|
|
159
131
|
}
|
|
160
132
|
}
|
|
161
133
|
}
|
|
162
|
-
const availableGroups = listGroupSummaries(snapshot, windowLabels);
|
|
134
|
+
const availableGroups = (0, snapshot_1.listGroupSummaries)(snapshot, windowLabels);
|
|
163
135
|
if (matches.length === 0) {
|
|
164
136
|
return {
|
|
165
137
|
tabs: [],
|