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,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: [],