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,320 @@
1
+ "use strict";
2
+ // Content script execution utilities — extracted from background.ts (pure structural refactor).
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ exports.SETTLE_POLL_INTERVAL_MS = exports.SETTLE_STABILITY_MS = void 0;
5
+ exports.isScriptableUrl = isScriptableUrl;
6
+ exports.delay = delay;
7
+ exports.executeWithTimeout = executeWithTimeout;
8
+ exports.isGitHubIssueOrPr = isGitHubIssueOrPr;
9
+ exports.detectGitHubState = detectGitHubState;
10
+ exports.extractPageMeta = extractPageMeta;
11
+ exports.extractSelectorSignal = extractSelectorSignal;
12
+ exports.waitForTabLoad = waitForTabLoad;
13
+ exports.waitForDomReady = waitForDomReady;
14
+ exports.waitForSettle = waitForSettle;
15
+ exports.waitForTabReady = waitForTabReady;
16
+ exports.SETTLE_STABILITY_MS = 500;
17
+ exports.SETTLE_POLL_INTERVAL_MS = 50;
18
+ function isScriptableUrl(url) {
19
+ return typeof url === "string" && (url.startsWith("http://") || url.startsWith("https://"));
20
+ }
21
+ function delay(ms) {
22
+ return new Promise((resolve) => setTimeout(resolve, ms));
23
+ }
24
+ async function executeWithTimeout(tabId, timeoutMs, func, args = []) {
25
+ const execPromise = chrome.scripting.executeScript({
26
+ target: { tabId },
27
+ func,
28
+ args,
29
+ });
30
+ const timeoutPromise = new Promise((resolve) => {
31
+ const handle = setTimeout(() => {
32
+ clearTimeout(handle);
33
+ resolve(null);
34
+ }, timeoutMs);
35
+ });
36
+ try {
37
+ const result = await Promise.race([execPromise, timeoutPromise]);
38
+ if (!result || !Array.isArray(result)) {
39
+ return null;
40
+ }
41
+ const [{ result: value }] = result;
42
+ return value ?? null;
43
+ }
44
+ catch {
45
+ return null;
46
+ }
47
+ }
48
+ function isGitHubIssueOrPr(url) {
49
+ if (!url) {
50
+ return false;
51
+ }
52
+ return /^https:\/\/github\.com\/[^/]+\/[^/]+\/(issues|pull)\/\d+/.test(url);
53
+ }
54
+ async function detectGitHubState(tabId, timeoutMs) {
55
+ const result = await executeWithTimeout(tabId, timeoutMs, () => {
56
+ const stateEl = document.querySelector(".gh-header-meta .State") ||
57
+ document.querySelector(".State") ||
58
+ document.querySelector(".js-issue-state");
59
+ if (!stateEl) {
60
+ return null;
61
+ }
62
+ const text = (stateEl.textContent || "").trim().toLowerCase();
63
+ if (text.includes("merged")) {
64
+ return "merged";
65
+ }
66
+ if (text.includes("closed")) {
67
+ return "closed";
68
+ }
69
+ if (text.includes("open")) {
70
+ return "open";
71
+ }
72
+ return null;
73
+ });
74
+ return typeof result === "string" ? result : null;
75
+ }
76
+ async function extractPageMeta(tabId, timeoutMs, descriptionMaxLength) {
77
+ const result = await executeWithTimeout(tabId, timeoutMs, () => {
78
+ const pickContent = (selector) => {
79
+ const el = document.querySelector(selector);
80
+ if (!el) {
81
+ return "";
82
+ }
83
+ const content = el.getAttribute("content") || el.textContent || "";
84
+ return content.trim();
85
+ };
86
+ const description = pickContent("meta[name='description']") ||
87
+ pickContent("meta[property='og:description']") ||
88
+ pickContent("meta[name='twitter:description']");
89
+ const h1 = document.querySelector("h1");
90
+ const h1Text = h1 ? h1.textContent?.trim() : "";
91
+ return {
92
+ description: description.replace(/\s+/g, " ").trim(),
93
+ h1: (h1Text || "").replace(/\s+/g, " ").trim(),
94
+ };
95
+ });
96
+ if (!result || typeof result !== "object") {
97
+ return null;
98
+ }
99
+ const meta = result;
100
+ return {
101
+ description: (meta.description || "").slice(0, descriptionMaxLength),
102
+ h1: (meta.h1 || "").slice(0, descriptionMaxLength),
103
+ };
104
+ }
105
+ async function extractSelectorSignal(tabId, specs, timeoutMs, selectorValueMaxLength) {
106
+ if (!specs.length) {
107
+ return null;
108
+ }
109
+ const result = await executeWithTimeout(tabId, timeoutMs, (rawSpecs, maxLen) => {
110
+ const values = {};
111
+ const missing = [];
112
+ const errors = {};
113
+ const hints = {};
114
+ for (const raw of rawSpecs) {
115
+ const selector = typeof raw.selector === "string" ? raw.selector : "";
116
+ if (!selector) {
117
+ continue;
118
+ }
119
+ const name = typeof raw.name === "string" && raw.name ? raw.name : selector;
120
+ const attr = typeof raw.attr === "string" ? raw.attr : "text";
121
+ const all = Boolean(raw.all);
122
+ const text = typeof raw.text === "string" ? raw.text.trim() : "";
123
+ const textMode = typeof raw.textMode === "string" ? raw.textMode.trim().toLowerCase() : "";
124
+ const normalizedTextMode = textMode === "includes" ? "contains" : textMode;
125
+ const textModes = new Set(["", "contains", "exact", "starts-with"]);
126
+ if (!textModes.has(normalizedTextMode)) {
127
+ errors[name] = `Unsupported textMode: ${textMode || "unknown"}`;
128
+ hints[name] = "Use textMode: contains | exact | starts-with";
129
+ continue;
130
+ }
131
+ try {
132
+ const elements = Array.from(document.querySelectorAll(selector));
133
+ if (!elements.length) {
134
+ missing.push(name);
135
+ if (selector.includes(":contains(")) {
136
+ hints[name] = "CSS :contains() is not supported; use selector text filters or a different selector.";
137
+ }
138
+ else {
139
+ hints[name] = "No matches found; capture a screenshot for context or adjust the selector.";
140
+ }
141
+ continue;
142
+ }
143
+ const matchesText = (el) => {
144
+ if (!text) {
145
+ return true;
146
+ }
147
+ const content = (el.textContent || "").replace(/\s+/g, " ").trim();
148
+ if (normalizedTextMode === "exact") {
149
+ return content === text;
150
+ }
151
+ if (normalizedTextMode === "starts-with") {
152
+ return content.startsWith(text);
153
+ }
154
+ return content.includes(text);
155
+ };
156
+ const filtered = text ? elements.filter(matchesText) : elements;
157
+ if (!filtered.length) {
158
+ missing.push(name);
159
+ hints[name] = "Selector matched elements, but none matched the text filter; capture a screenshot for context or adjust text/textMode.";
160
+ continue;
161
+ }
162
+ const getValue = (el) => {
163
+ let value = "";
164
+ if (attr === "text") {
165
+ value = el.textContent || "";
166
+ }
167
+ else if (attr === "href-url" || attr === "src-url") {
168
+ const rawValue = el.getAttribute(attr === "href-url" ? "href" : "src") || "";
169
+ if (!rawValue) {
170
+ value = "";
171
+ }
172
+ else {
173
+ try {
174
+ const resolved = new URL(rawValue, document.baseURI);
175
+ if (resolved.protocol === "http:" || resolved.protocol === "https:") {
176
+ value = resolved.toString();
177
+ }
178
+ else {
179
+ value = "";
180
+ }
181
+ }
182
+ catch {
183
+ value = "";
184
+ }
185
+ }
186
+ }
187
+ else {
188
+ value = el.getAttribute(attr) || "";
189
+ }
190
+ return value.replace(/\s+/g, " ").trim().slice(0, maxLen);
191
+ };
192
+ if (all) {
193
+ values[name] = filtered.map(getValue).filter((val) => val.length > 0);
194
+ }
195
+ else {
196
+ values[name] = getValue(filtered[0]);
197
+ }
198
+ }
199
+ catch (err) {
200
+ const message = err instanceof Error ? err.message : "selector_error";
201
+ errors[name] = message;
202
+ if (selector.includes(":contains(")) {
203
+ hints[name] = "CSS :contains() is not supported; use selector text filters or a different selector.";
204
+ }
205
+ else {
206
+ hints[name] = "Selector failed to evaluate; capture a screenshot for context or adjust the selector.";
207
+ }
208
+ }
209
+ }
210
+ return { values, missing, errors, hints };
211
+ }, [specs, selectorValueMaxLength]);
212
+ if (!result || typeof result !== "object") {
213
+ return null;
214
+ }
215
+ return result;
216
+ }
217
+ function waitForTabLoad(tabId, timeoutMs) {
218
+ return new Promise((resolve) => {
219
+ let settled = false;
220
+ const done = () => {
221
+ if (settled) {
222
+ return;
223
+ }
224
+ settled = true;
225
+ chrome.tabs.onUpdated.removeListener(onUpdated);
226
+ resolve();
227
+ };
228
+ const onUpdated = (updatedTabId, info) => {
229
+ if (updatedTabId === tabId && info.status === "complete") {
230
+ done();
231
+ }
232
+ };
233
+ chrome.tabs.onUpdated.addListener(onUpdated);
234
+ chrome.tabs.get(tabId).then((tab) => {
235
+ if (tab.status === "complete") {
236
+ done();
237
+ }
238
+ }).catch(() => {
239
+ done();
240
+ });
241
+ setTimeout(done, timeoutMs);
242
+ });
243
+ }
244
+ async function waitForDomReady(tabId, timeoutMs) {
245
+ const result = await executeWithTimeout(tabId, timeoutMs, () => {
246
+ if (document.readyState === "interactive" || document.readyState === "complete") {
247
+ return true;
248
+ }
249
+ return new Promise((resolve) => {
250
+ const onReady = () => {
251
+ document.removeEventListener("DOMContentLoaded", onReady);
252
+ resolve(true);
253
+ };
254
+ document.addEventListener("DOMContentLoaded", onReady, { once: true });
255
+ setTimeout(() => {
256
+ document.removeEventListener("DOMContentLoaded", onReady);
257
+ resolve(false);
258
+ }, Math.max(0, timeoutMs - 50));
259
+ });
260
+ });
261
+ if (result === null) {
262
+ await delay(Math.min(200, Math.max(50, Math.floor(timeoutMs / 10))));
263
+ }
264
+ }
265
+ async function waitForSettle(tabId, timeoutMs) {
266
+ const startTime = Date.now();
267
+ let lastUrl = "";
268
+ let lastTitle = "";
269
+ let stableStart = Date.now();
270
+ while (Date.now() - startTime < timeoutMs) {
271
+ const tab = await chrome.tabs.get(tabId).catch(() => null);
272
+ if (!tab)
273
+ return;
274
+ const currentUrl = tab.url || "";
275
+ const currentTitle = tab.title || "";
276
+ // Reset stability timer if URL or title changed
277
+ if (currentUrl !== lastUrl || currentTitle !== lastTitle) {
278
+ lastUrl = currentUrl;
279
+ lastTitle = currentTitle;
280
+ stableStart = Date.now();
281
+ }
282
+ else if (isScriptableUrl(currentUrl) &&
283
+ tab.status === "complete" &&
284
+ Date.now() - stableStart >= exports.SETTLE_STABILITY_MS) {
285
+ // Page is loaded, URL is valid, and stable for long enough
286
+ return;
287
+ }
288
+ await delay(exports.SETTLE_POLL_INTERVAL_MS);
289
+ }
290
+ // Timeout reached, continue anyway
291
+ }
292
+ async function waitForTabReady(tabId, params, fallbackTimeoutMs) {
293
+ const waitFor = typeof params.waitFor === "string" ? params.waitFor.trim().toLowerCase() : "";
294
+ if (!waitFor || waitFor === "none") {
295
+ return;
296
+ }
297
+ const timeoutRaw = Number(params.waitTimeoutMs);
298
+ const timeoutMs = Number.isFinite(timeoutRaw) && timeoutRaw > 0 ? Math.floor(timeoutRaw) : fallbackTimeoutMs;
299
+ // settle mode handles its own URL checking, so skip the early return
300
+ if (waitFor === "settle") {
301
+ await waitForSettle(tabId, timeoutMs);
302
+ return;
303
+ }
304
+ try {
305
+ const tab = await chrome.tabs.get(tabId);
306
+ if (!isScriptableUrl(tab.url)) {
307
+ return;
308
+ }
309
+ }
310
+ catch {
311
+ return;
312
+ }
313
+ if (waitFor === "load") {
314
+ await waitForTabLoad(tabId, timeoutMs);
315
+ return;
316
+ }
317
+ if (waitFor === "dom") {
318
+ await waitForDomReady(tabId, timeoutMs);
319
+ }
320
+ }
@@ -0,0 +1,4 @@
1
+ "use strict";
2
+ // Shared dependency interface for extension modules.
3
+ // Each module uses Pick<ExtensionDeps, ...> to declare its actual requirements.
4
+ Object.defineProperty(exports, "__esModule", { value: true });