@aholbreich/agent-skills 0.9.0 → 1.0.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/CHANGELOG.md +14 -0
- package/README.md +51 -1
- package/bin/vendor.js +24 -0
- package/package.json +6 -3
- package/skills/bitbucket-browser-fetch/scripts/atlassian-browser.js +261 -0
- package/skills/bitbucket-browser-fetch/scripts/bitbucket-browser-fetch.js +16 -197
- package/skills/confluence-browser-fetch/scripts/atlassian-browser.js +261 -0
- package/skills/confluence-browser-fetch/scripts/confluence-browser-fetch.js +15 -210
- package/skills/confluence-update/scripts/atlassian-browser.js +261 -0
- package/skills/confluence-update/scripts/confluence-update.js +31 -224
- package/skills/jira-browser-fetch/scripts/atlassian-browser.js +261 -0
- package/skills/jira-browser-fetch/scripts/jira-browser-fetch.js +27 -230
- package/skills/jira-update/SKILL.md +121 -0
- package/skills/jira-update/references/distribution.md +5 -0
- package/skills/jira-update/references/usage.md +75 -0
- package/skills/jira-update/scripts/atlassian-browser.js +261 -0
- package/skills/jira-update/scripts/jira-update.js +404 -0
- package/skills/jira-update/scripts/lib.js +283 -0
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('node:fs');
|
|
4
|
+
const path = require('node:path');
|
|
5
|
+
const { spawn } = require('node:child_process');
|
|
6
|
+
|
|
7
|
+
const sleep = ms => new Promise(r => setTimeout(r, ms));
|
|
8
|
+
|
|
9
|
+
function isExecutable(file) {
|
|
10
|
+
try { fs.accessSync(file, fs.constants.X_OK); return true; } catch { return false; }
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function resolveBrowserCandidate(candidate) {
|
|
14
|
+
if (!candidate) return null;
|
|
15
|
+
if (candidate.includes(path.sep)) return isExecutable(candidate) ? candidate : null;
|
|
16
|
+
for (const dir of String(process.env.PATH || '').split(path.delimiter)) {
|
|
17
|
+
if (!dir) continue;
|
|
18
|
+
const full = path.join(dir, candidate);
|
|
19
|
+
if (isExecutable(full)) return full;
|
|
20
|
+
}
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function findBrowserExecutable() {
|
|
25
|
+
const candidates = [
|
|
26
|
+
process.env.CHROME,
|
|
27
|
+
process.env.CHROMIUM,
|
|
28
|
+
'google-chrome',
|
|
29
|
+
'google-chrome-stable',
|
|
30
|
+
'chromium',
|
|
31
|
+
'chromium-browser',
|
|
32
|
+
'brave-browser',
|
|
33
|
+
'brave',
|
|
34
|
+
'microsoft-edge',
|
|
35
|
+
'microsoft-edge-stable',
|
|
36
|
+
'vivaldi',
|
|
37
|
+
'vivaldi-stable',
|
|
38
|
+
'/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
|
|
39
|
+
'/Applications/Chromium.app/Contents/MacOS/Chromium',
|
|
40
|
+
'/Applications/Brave Browser.app/Contents/MacOS/Brave Browser',
|
|
41
|
+
'/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge',
|
|
42
|
+
'/Applications/Vivaldi.app/Contents/MacOS/Vivaldi',
|
|
43
|
+
];
|
|
44
|
+
for (const candidate of candidates) {
|
|
45
|
+
const resolved = resolveBrowserCandidate(candidate);
|
|
46
|
+
if (resolved) return resolved;
|
|
47
|
+
}
|
|
48
|
+
throw new Error('Could not find a Chromium-compatible browser. Install Chrome/Chromium/Brave/Edge/Vivaldi or set CHROME=/path/to/browser.');
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function connectCdp(wsUrl) {
|
|
52
|
+
return new Promise((resolve, reject) => {
|
|
53
|
+
const ws = new WebSocket(wsUrl);
|
|
54
|
+
let id = 0;
|
|
55
|
+
const pending = new Map();
|
|
56
|
+
const failTimer = setTimeout(() => reject(new Error('CDP websocket timeout')), 10000);
|
|
57
|
+
|
|
58
|
+
ws.addEventListener('open', () => {
|
|
59
|
+
clearTimeout(failTimer);
|
|
60
|
+
resolve({
|
|
61
|
+
send(method, params = {}) {
|
|
62
|
+
return new Promise((res, rej) => {
|
|
63
|
+
const msgId = ++id;
|
|
64
|
+
pending.set(msgId, { res, rej });
|
|
65
|
+
ws.send(JSON.stringify({ id: msgId, method, params }));
|
|
66
|
+
});
|
|
67
|
+
},
|
|
68
|
+
close() { try { ws.close(); } catch {} },
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
ws.addEventListener('message', ev => {
|
|
73
|
+
let data = ev.data;
|
|
74
|
+
if (typeof data !== 'string') data = Buffer.from(data).toString('utf8');
|
|
75
|
+
const msg = JSON.parse(data);
|
|
76
|
+
if (!msg.id || !pending.has(msg.id)) return;
|
|
77
|
+
const { res, rej } = pending.get(msg.id);
|
|
78
|
+
pending.delete(msg.id);
|
|
79
|
+
if (msg.error) rej(new Error(`${msg.error.message || 'CDP error'} ${JSON.stringify(msg.error)}`));
|
|
80
|
+
else res(msg.result);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
ws.addEventListener('error', err => reject(err));
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function createBrowserSession({ port, profileDir, waitSec, serverHost, verifySession, cookieUrls, userAgent }) {
|
|
88
|
+
if (!serverHost) throw new Error('createBrowserSession requires serverHost');
|
|
89
|
+
if (typeof verifySession !== 'function') throw new Error('createBrowserSession requires verifySession callback');
|
|
90
|
+
const ua = userAgent || 'agent-skills/1.0';
|
|
91
|
+
|
|
92
|
+
async function endpoint(pathname) {
|
|
93
|
+
const res = await fetch(`http://127.0.0.1:${port}${pathname}`);
|
|
94
|
+
if (!res.ok) throw new Error(`DevTools HTTP ${res.status} for ${pathname}`);
|
|
95
|
+
return res.json();
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
async function devtoolsReady() {
|
|
99
|
+
try { await endpoint('/json/version'); return true; } catch { return false; }
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
async function waitDevtools() {
|
|
103
|
+
for (let i = 0; i < 80; i++) {
|
|
104
|
+
if (await devtoolsReady()) return;
|
|
105
|
+
await sleep(250);
|
|
106
|
+
}
|
|
107
|
+
throw new Error('Chrome DevTools endpoint did not start');
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
async function openDevtoolsTab(url) {
|
|
111
|
+
if (!url) return false;
|
|
112
|
+
const endpointUrl = `http://127.0.0.1:${port}/json/new?${encodeURIComponent(url)}`;
|
|
113
|
+
for (const init of [{ method: 'PUT' }, {}]) {
|
|
114
|
+
try {
|
|
115
|
+
const res = await fetch(endpointUrl, init);
|
|
116
|
+
if (res.ok) { await sleep(500); return true; }
|
|
117
|
+
} catch {}
|
|
118
|
+
}
|
|
119
|
+
return false;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
async function hasDevtoolsTabForHost(url, pathPrefix) {
|
|
123
|
+
if (!url) return false;
|
|
124
|
+
const host = new URL(url).host;
|
|
125
|
+
const list = await endpoint('/json/list');
|
|
126
|
+
return list.some(t => t.type === 'page' && t.url && (() => {
|
|
127
|
+
try {
|
|
128
|
+
const tabUrl = new URL(t.url);
|
|
129
|
+
if (tabUrl.host !== host) return false;
|
|
130
|
+
if (pathPrefix && !tabUrl.pathname.startsWith(pathPrefix)) return false;
|
|
131
|
+
return true;
|
|
132
|
+
} catch { return false; }
|
|
133
|
+
})());
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function launchChrome(url) {
|
|
137
|
+
const browser = findBrowserExecutable();
|
|
138
|
+
const args = [
|
|
139
|
+
`--remote-debugging-port=${port}`,
|
|
140
|
+
'--remote-debugging-address=127.0.0.1',
|
|
141
|
+
'--remote-allow-origins=*',
|
|
142
|
+
`--user-data-dir=${profileDir}`,
|
|
143
|
+
'--no-first-run',
|
|
144
|
+
'--no-default-browser-check',
|
|
145
|
+
url,
|
|
146
|
+
];
|
|
147
|
+
console.log(`Launching browser: ${browser}`);
|
|
148
|
+
const child = spawn(browser, args, { detached: true, stdio: 'ignore' });
|
|
149
|
+
child.on('error', err => console.error(`Failed to launch browser ${browser}: ${err.message}`));
|
|
150
|
+
child.unref();
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
async function ensureBrowser(openUrl, { tabPathPrefix } = {}) {
|
|
154
|
+
if (!(await devtoolsReady())) {
|
|
155
|
+
console.log(`Opening Chromium-compatible browser with reusable profile: ${profileDir}`);
|
|
156
|
+
launchChrome(openUrl);
|
|
157
|
+
} else {
|
|
158
|
+
console.log(`Reusing Chrome DevTools on port ${port}`);
|
|
159
|
+
if (openUrl) {
|
|
160
|
+
const hasTab = await hasDevtoolsTabForHost(openUrl, tabPathPrefix);
|
|
161
|
+
if (hasTab) {
|
|
162
|
+
console.log(`Found existing tab for ${new URL(openUrl).host}; not opening another tab.`);
|
|
163
|
+
} else {
|
|
164
|
+
const opened = await openDevtoolsTab(openUrl);
|
|
165
|
+
if (opened) console.log(`Opened target URL in reused browser: ${openUrl}`);
|
|
166
|
+
else console.warn('Could not open target URL through DevTools; continuing with existing tabs.');
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
await waitDevtools();
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
async function getPageWsUrl() {
|
|
174
|
+
const list = await endpoint('/json/list');
|
|
175
|
+
const pages = list.filter(t => t.type === 'page' && t.webSocketDebuggerUrl);
|
|
176
|
+
const preferred = pages.find(t => (t.url || '').includes(serverHost)) || pages[0];
|
|
177
|
+
return preferred && preferred.webSocketDebuggerUrl;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
async function getCookieHeader() {
|
|
181
|
+
const wsUrl = await getPageWsUrl();
|
|
182
|
+
if (!wsUrl) return '';
|
|
183
|
+
const cdp = await connectCdp(wsUrl);
|
|
184
|
+
try {
|
|
185
|
+
await cdp.send('Network.enable');
|
|
186
|
+
const urls = (cookieUrls && cookieUrls.length) ? cookieUrls : [`https://${serverHost}/`];
|
|
187
|
+
const result = await cdp.send('Network.getCookies', { urls });
|
|
188
|
+
const cookies = (result.cookies || [])
|
|
189
|
+
.filter(c => c.domain && (c.domain === serverHost || c.domain.endsWith(`.${serverHost}`)))
|
|
190
|
+
.map(c => `${c.name}=${c.value}`);
|
|
191
|
+
return cookies.join('; ');
|
|
192
|
+
} finally {
|
|
193
|
+
cdp.close();
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
async function fetchText(url, cookie, options = {}) {
|
|
198
|
+
const method = options.method || 'GET';
|
|
199
|
+
const headers = {
|
|
200
|
+
Cookie: cookie,
|
|
201
|
+
Accept: options.accept || '*/*',
|
|
202
|
+
'User-Agent': ua,
|
|
203
|
+
};
|
|
204
|
+
if (options.body !== undefined && options.body !== null) headers['Content-Type'] = options.contentType || 'application/json';
|
|
205
|
+
const res = await fetch(url, { method, redirect: 'follow', headers, body: options.body ?? null });
|
|
206
|
+
return { status: res.status, contentType: res.headers.get('content-type') || '', text: await res.text() };
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
async function fetchJson(url, cookie, options = {}) {
|
|
210
|
+
const result = await fetchText(url, cookie, { ...options, accept: options.accept || 'application/json' });
|
|
211
|
+
let json = null;
|
|
212
|
+
try { json = JSON.parse(result.text); } catch {}
|
|
213
|
+
return { ...result, json };
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
async function getCookieWithWait(openUrl, { tabPathPrefix } = {}) {
|
|
217
|
+
await ensureBrowser(openUrl, { tabPathPrefix });
|
|
218
|
+
console.log(`If prompted in Chrome, complete SSO for: ${openUrl}`);
|
|
219
|
+
const deadline = Date.now() + waitSec * 1000;
|
|
220
|
+
let last = '';
|
|
221
|
+
while (Date.now() < deadline) {
|
|
222
|
+
try {
|
|
223
|
+
const cookie = await getCookieHeader();
|
|
224
|
+
const result = await verifySession(cookie);
|
|
225
|
+
if (result && result.ok) {
|
|
226
|
+
if (process.stdout.isTTY) process.stdout.write('\n');
|
|
227
|
+
console.log(`Authenticated session verified${result.url ? ` via ${result.url}` : ''}`);
|
|
228
|
+
return cookie;
|
|
229
|
+
}
|
|
230
|
+
last = (result && result.message) || 'session not yet verified';
|
|
231
|
+
} catch (e) { last = e.message; }
|
|
232
|
+
if (process.stdout.isTTY) {
|
|
233
|
+
process.stdout.write(`\r${new Date().toLocaleTimeString()} ${last.padEnd(120).slice(0, 120)}`);
|
|
234
|
+
}
|
|
235
|
+
await sleep(3000);
|
|
236
|
+
}
|
|
237
|
+
if (process.stdout.isTTY) process.stdout.write('\n');
|
|
238
|
+
throw new Error(`Could not verify authenticated session. Last result: ${last}`);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
return {
|
|
242
|
+
devtoolsReady,
|
|
243
|
+
waitDevtools,
|
|
244
|
+
openDevtoolsTab,
|
|
245
|
+
hasDevtoolsTabForHost,
|
|
246
|
+
launchChrome,
|
|
247
|
+
ensureBrowser,
|
|
248
|
+
getPageWsUrl,
|
|
249
|
+
getCookieHeader,
|
|
250
|
+
getCookieWithWait,
|
|
251
|
+
fetchText,
|
|
252
|
+
fetchJson,
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
module.exports = {
|
|
257
|
+
createBrowserSession,
|
|
258
|
+
findBrowserExecutable,
|
|
259
|
+
resolveBrowserCandidate,
|
|
260
|
+
connectCdp,
|
|
261
|
+
};
|
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
'use strict';
|
|
3
3
|
|
|
4
|
-
const fs = require('fs');
|
|
5
4
|
const fsp = require('fs/promises');
|
|
6
5
|
const os = require('os');
|
|
7
6
|
const path = require('path');
|
|
8
|
-
const {
|
|
7
|
+
const { createBrowserSession } = require('./atlassian-browser');
|
|
9
8
|
const {
|
|
10
9
|
parseSize,
|
|
11
10
|
formatBytes,
|
|
@@ -107,188 +106,29 @@ if (!opts.server) {
|
|
|
107
106
|
}
|
|
108
107
|
opts.rawDir = path.resolve(opts.rawDir);
|
|
109
108
|
|
|
110
|
-
const sleep = ms => new Promise(r => setTimeout(r, ms));
|
|
111
109
|
const issueKeyRe = /\b([A-Z][A-Z0-9]+)-(\d+)\b/g;
|
|
112
110
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
if (
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
for (let i = 0; i < 80; i++) {
|
|
125
|
-
if (await devtoolsReady()) return;
|
|
126
|
-
await sleep(250);
|
|
127
|
-
}
|
|
128
|
-
throw new Error('Chrome DevTools endpoint did not start');
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
async function openDevtoolsTab(url) {
|
|
132
|
-
if (!url) return false;
|
|
133
|
-
const endpointUrl = `http://127.0.0.1:${opts.port}/json/new?${encodeURIComponent(url)}`;
|
|
134
|
-
for (const init of [{ method: 'PUT' }, {}]) {
|
|
135
|
-
try {
|
|
136
|
-
const res = await fetch(endpointUrl, init);
|
|
137
|
-
if (res.ok) {
|
|
138
|
-
await sleep(500);
|
|
139
|
-
return true;
|
|
140
|
-
}
|
|
141
|
-
} catch {}
|
|
142
|
-
}
|
|
143
|
-
return false;
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
async function hasDevtoolsTabForHost(url) {
|
|
147
|
-
if (!url) return false;
|
|
148
|
-
const host = new URL(url).host;
|
|
149
|
-
const list = await endpoint('/json/list');
|
|
150
|
-
return list.some(t => t.type === 'page' && t.url && (() => {
|
|
151
|
-
try { return new URL(t.url).host === host; } catch { return false; }
|
|
152
|
-
})());
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
function isExecutable(file) {
|
|
156
|
-
try { fs.accessSync(file, fs.constants.X_OK); return true; } catch { return false; }
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
function resolveBrowserCandidate(candidate) {
|
|
160
|
-
if (!candidate) return null;
|
|
161
|
-
if (candidate.includes(path.sep)) return isExecutable(candidate) ? candidate : null;
|
|
162
|
-
for (const dir of String(process.env.PATH || '').split(path.delimiter)) {
|
|
163
|
-
if (!dir) continue;
|
|
164
|
-
const full = path.join(dir, candidate);
|
|
165
|
-
if (isExecutable(full)) return full;
|
|
166
|
-
}
|
|
167
|
-
return null;
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
function findBrowserExecutable() {
|
|
171
|
-
const candidates = [
|
|
172
|
-
process.env.CHROME,
|
|
173
|
-
process.env.CHROMIUM,
|
|
174
|
-
'google-chrome',
|
|
175
|
-
'google-chrome-stable',
|
|
176
|
-
'chromium',
|
|
177
|
-
'chromium-browser',
|
|
178
|
-
'brave-browser',
|
|
179
|
-
'brave',
|
|
180
|
-
'microsoft-edge',
|
|
181
|
-
'microsoft-edge-stable',
|
|
182
|
-
'vivaldi',
|
|
183
|
-
'vivaldi-stable',
|
|
184
|
-
'/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
|
|
185
|
-
'/Applications/Chromium.app/Contents/MacOS/Chromium',
|
|
186
|
-
'/Applications/Brave Browser.app/Contents/MacOS/Brave Browser',
|
|
187
|
-
'/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge',
|
|
188
|
-
'/Applications/Vivaldi.app/Contents/MacOS/Vivaldi',
|
|
189
|
-
];
|
|
190
|
-
for (const candidate of candidates) {
|
|
191
|
-
const resolved = resolveBrowserCandidate(candidate);
|
|
192
|
-
if (resolved) return resolved;
|
|
193
|
-
}
|
|
194
|
-
throw new Error('Could not find a Chromium-compatible browser. Install Chrome/Chromium/Brave/Edge/Vivaldi or set CHROME=/path/to/browser.');
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
function launchChrome(url) {
|
|
198
|
-
const browser = findBrowserExecutable();
|
|
199
|
-
const args = [
|
|
200
|
-
`--remote-debugging-port=${opts.port}`,
|
|
201
|
-
'--remote-debugging-address=127.0.0.1',
|
|
202
|
-
'--remote-allow-origins=*',
|
|
203
|
-
`--user-data-dir=${opts.profileDir}`,
|
|
204
|
-
'--no-first-run',
|
|
205
|
-
'--no-default-browser-check',
|
|
206
|
-
url,
|
|
207
|
-
];
|
|
208
|
-
console.log(`Launching browser: ${browser}`);
|
|
209
|
-
const child = spawn(browser, args, { detached: true, stdio: 'ignore' });
|
|
210
|
-
child.on('error', err => console.error(`Failed to launch browser ${browser}: ${err.message}`));
|
|
211
|
-
child.unref();
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
async function getPageWsUrl() {
|
|
215
|
-
const list = await endpoint('/json/list');
|
|
216
|
-
const pages = list.filter(t => t.type === 'page' && t.webSocketDebuggerUrl);
|
|
217
|
-
const host = new URL(opts.server).host;
|
|
218
|
-
const preferred = pages.find(t => (t.url || '').includes(host)) || pages[0];
|
|
219
|
-
return preferred && preferred.webSocketDebuggerUrl;
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
function connectCdp(wsUrl) {
|
|
223
|
-
return new Promise((resolve, reject) => {
|
|
224
|
-
const ws = new WebSocket(wsUrl);
|
|
225
|
-
let id = 0;
|
|
226
|
-
const pending = new Map();
|
|
227
|
-
const failTimer = setTimeout(() => reject(new Error('CDP websocket timeout')), 10000);
|
|
228
|
-
|
|
229
|
-
ws.addEventListener('open', () => {
|
|
230
|
-
clearTimeout(failTimer);
|
|
231
|
-
resolve({
|
|
232
|
-
send(method, params = {}) {
|
|
233
|
-
return new Promise((res, rej) => {
|
|
234
|
-
const msgId = ++id;
|
|
235
|
-
pending.set(msgId, { res, rej });
|
|
236
|
-
ws.send(JSON.stringify({ id: msgId, method, params }));
|
|
237
|
-
});
|
|
238
|
-
},
|
|
239
|
-
close() { try { ws.close(); } catch {} },
|
|
240
|
-
});
|
|
241
|
-
});
|
|
242
|
-
|
|
243
|
-
ws.addEventListener('message', ev => {
|
|
244
|
-
let data = ev.data;
|
|
245
|
-
if (typeof data !== 'string') data = Buffer.from(data).toString('utf8');
|
|
246
|
-
const msg = JSON.parse(data);
|
|
247
|
-
if (!msg.id || !pending.has(msg.id)) return;
|
|
248
|
-
const { res, rej } = pending.get(msg.id);
|
|
249
|
-
pending.delete(msg.id);
|
|
250
|
-
if (msg.error) rej(new Error(`${msg.error.message || 'CDP error'} ${JSON.stringify(msg.error)}`));
|
|
251
|
-
else res(msg.result);
|
|
252
|
-
});
|
|
253
|
-
|
|
254
|
-
ws.addEventListener('error', err => reject(err));
|
|
111
|
+
let session = null;
|
|
112
|
+
function getSession() {
|
|
113
|
+
if (session) return session;
|
|
114
|
+
session = createBrowserSession({
|
|
115
|
+
port: opts.port,
|
|
116
|
+
profileDir: opts.profileDir,
|
|
117
|
+
waitSec: opts.waitSec,
|
|
118
|
+
serverHost: new URL(opts.server).host,
|
|
119
|
+
cookieUrls: [`${opts.server}/`],
|
|
120
|
+
userAgent: 'jira-browser-fetch/1.0',
|
|
121
|
+
verifySession: cookie => verifyJiraSession(cookie),
|
|
255
122
|
});
|
|
123
|
+
return session;
|
|
256
124
|
}
|
|
257
125
|
|
|
258
|
-
async function
|
|
259
|
-
|
|
260
|
-
if (!wsUrl) return '';
|
|
261
|
-
const cdp = await connectCdp(wsUrl);
|
|
262
|
-
try {
|
|
263
|
-
await cdp.send('Network.enable');
|
|
264
|
-
const host = new URL(opts.server).host;
|
|
265
|
-
const result = await cdp.send('Network.getCookies', { urls: [`${opts.server}/`] });
|
|
266
|
-
const cookies = (result.cookies || [])
|
|
267
|
-
.filter(c => c.domain && (c.domain === host || c.domain.endsWith(`.${host}`)))
|
|
268
|
-
.map(c => `${c.name}=${c.value}`);
|
|
269
|
-
return cookies.join('; ');
|
|
270
|
-
} finally {
|
|
271
|
-
cdp.close();
|
|
272
|
-
}
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
async function fetchText(url, cookie, accept) {
|
|
276
|
-
const res = await fetch(url, {
|
|
277
|
-
redirect: 'follow',
|
|
278
|
-
headers: {
|
|
279
|
-
Cookie: cookie,
|
|
280
|
-
Accept: accept || '*/*',
|
|
281
|
-
'User-Agent': 'jira-browser-fetch/1.0',
|
|
282
|
-
},
|
|
283
|
-
});
|
|
284
|
-
return { status: res.status, contentType: res.headers.get('content-type') || '', text: await res.text() };
|
|
126
|
+
async function fetchTextAdapter(url, cookie, accept) {
|
|
127
|
+
return getSession().fetchText(url, cookie, { accept });
|
|
285
128
|
}
|
|
286
129
|
|
|
287
|
-
async function
|
|
288
|
-
|
|
289
|
-
let json = null;
|
|
290
|
-
try { json = JSON.parse(result.text); } catch {}
|
|
291
|
-
return { ...result, json };
|
|
130
|
+
async function fetchJsonAdapter(url, cookie, accept) {
|
|
131
|
+
return getSession().fetchJson(url, cookie, { accept: accept || 'application/json' });
|
|
292
132
|
}
|
|
293
133
|
|
|
294
134
|
async function verifyJiraSession(cookie) {
|
|
@@ -300,7 +140,7 @@ async function verifyJiraSession(cookie) {
|
|
|
300
140
|
];
|
|
301
141
|
|
|
302
142
|
for (const url of probes) {
|
|
303
|
-
const result = await
|
|
143
|
+
const result = await fetchJsonAdapter(url, cookie, 'application/json');
|
|
304
144
|
if (result.status === 200 && result.json && (result.json.accountId || result.json.name || result.json.key || result.json.displayName)) {
|
|
305
145
|
return { ok: true, url };
|
|
306
146
|
}
|
|
@@ -321,31 +161,8 @@ async function verifyJiraSession(cookie) {
|
|
|
321
161
|
return { ok: false, message: 'could not verify Jira session' };
|
|
322
162
|
}
|
|
323
163
|
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
console.log(`If prompted in Chrome, complete SSO for: ${openUrl || opts.server}`);
|
|
327
|
-
const deadline = Date.now() + opts.waitSec * 1000;
|
|
328
|
-
let last = '';
|
|
329
|
-
while (Date.now() < deadline) {
|
|
330
|
-
try {
|
|
331
|
-
const cookie = await getCookieHeader();
|
|
332
|
-
const session = await verifyJiraSession(cookie);
|
|
333
|
-
if (session.ok) {
|
|
334
|
-
if (process.stdout.isTTY) process.stdout.write('\n');
|
|
335
|
-
console.log(`Authenticated Jira session verified via ${session.url}`);
|
|
336
|
-
return cookie;
|
|
337
|
-
}
|
|
338
|
-
last = session.message;
|
|
339
|
-
} catch (e) { last = e.message; }
|
|
340
|
-
if (process.stdout.isTTY) {
|
|
341
|
-
process.stdout.write(`\r${new Date().toLocaleTimeString()} ${last.padEnd(120).slice(0, 120)}`);
|
|
342
|
-
} else if (Date.now() - deadline + opts.waitSec * 1000 < 4000) {
|
|
343
|
-
console.log(`Waiting up to ${opts.waitSec}s for Jira session...`);
|
|
344
|
-
}
|
|
345
|
-
await sleep(3000);
|
|
346
|
-
}
|
|
347
|
-
if (process.stdout.isTTY) process.stdout.write('\n');
|
|
348
|
-
throw new Error(`Could not verify authenticated Jira session. Last result: ${last}`);
|
|
164
|
+
function getCookieWithWait(openUrl) {
|
|
165
|
+
return getSession().getCookieWithWait(openUrl || `${opts.server}/`);
|
|
349
166
|
}
|
|
350
167
|
|
|
351
168
|
async function searchJql(jql) {
|
|
@@ -358,11 +175,11 @@ async function searchJql(jql) {
|
|
|
358
175
|
while (found.length < opts.maxSearchResults) {
|
|
359
176
|
const limit = Math.min(pageSize, opts.maxSearchResults - found.length);
|
|
360
177
|
const url = `${opts.server}/rest/api/3/search?jql=${encodeURIComponent(jql)}&fields=key&maxResults=${limit}&startAt=${startAt}`;
|
|
361
|
-
let result = await
|
|
178
|
+
let result = await fetchJsonAdapter(url, cookie, 'application/json');
|
|
362
179
|
|
|
363
180
|
if (result.status === 410 || result.status === 404 || !result.json || !Array.isArray(result.json.issues)) {
|
|
364
181
|
const newUrl = `${opts.server}/rest/api/3/search/jql?jql=${encodeURIComponent(jql)}&fields=key&maxResults=${limit}`;
|
|
365
|
-
result = await
|
|
182
|
+
result = await fetchJsonAdapter(newUrl, cookie, 'application/json');
|
|
366
183
|
}
|
|
367
184
|
|
|
368
185
|
if (result.status !== 200 || !result.json || !Array.isArray(result.json.issues)) {
|
|
@@ -384,7 +201,7 @@ async function fetchBacklogPageWithWait(url, cookie) {
|
|
|
384
201
|
let last = '';
|
|
385
202
|
while (Date.now() < deadline) {
|
|
386
203
|
try {
|
|
387
|
-
const result = await
|
|
204
|
+
const result = await fetchJsonAdapter(url, cookie, 'application/json');
|
|
388
205
|
if (result.status === 200 && result.json && Array.isArray(result.json.issues)) return result.json;
|
|
389
206
|
last = `HTTP ${result.status} ${(result.text || '').slice(0, 180).replace(/\s+/g, ' ')}`;
|
|
390
207
|
} catch (e) { last = e.message; }
|
|
@@ -528,26 +345,6 @@ async function downloadAttachments(issueJson, cookie, outDir) {
|
|
|
528
345
|
return manifest.length;
|
|
529
346
|
}
|
|
530
347
|
|
|
531
|
-
async function ensureBrowser(browseUrl) {
|
|
532
|
-
if (!(await devtoolsReady())) {
|
|
533
|
-
console.log(`Opening Chromium-compatible browser with reusable profile: ${opts.profileDir}`);
|
|
534
|
-
launchChrome(browseUrl);
|
|
535
|
-
} else {
|
|
536
|
-
console.log(`Reusing Chrome DevTools on port ${opts.port}`);
|
|
537
|
-
if (browseUrl) {
|
|
538
|
-
const hasTab = await hasDevtoolsTabForHost(browseUrl);
|
|
539
|
-
if (hasTab) {
|
|
540
|
-
console.log(`Found existing Jira/Atlassian tab for ${new URL(browseUrl).host}; not opening another tab.`);
|
|
541
|
-
} else {
|
|
542
|
-
const opened = await openDevtoolsTab(browseUrl);
|
|
543
|
-
if (opened) console.log(`Opened target URL in reused browser: ${browseUrl}`);
|
|
544
|
-
else console.warn(`Could not open target URL through DevTools; continuing with existing tabs.`);
|
|
545
|
-
}
|
|
546
|
-
}
|
|
547
|
-
}
|
|
548
|
-
await waitDevtools();
|
|
549
|
-
}
|
|
550
|
-
|
|
551
348
|
async function fetchIssue(issue) {
|
|
552
349
|
const outDir = path.join(opts.rawDir, issue);
|
|
553
350
|
await fsp.mkdir(outDir, { recursive: true });
|
|
@@ -559,7 +356,7 @@ async function fetchIssue(issue) {
|
|
|
559
356
|
|
|
560
357
|
const cookie = await getCookieWithWait(browseUrl);
|
|
561
358
|
|
|
562
|
-
const rest = await
|
|
359
|
+
const rest = await fetchJsonAdapter(restUrl, cookie, 'application/json');
|
|
563
360
|
if (rest.status !== 200 || !rest.json || rest.json.key !== issue) {
|
|
564
361
|
throw new Error(`Could not fetch ${issue}. HTTP ${rest.status}: ${(rest.text || '').slice(0, 300).replace(/\s+/g, ' ')}`);
|
|
565
362
|
}
|
|
@@ -569,19 +366,19 @@ async function fetchIssue(issue) {
|
|
|
569
366
|
|
|
570
367
|
let html = { status: 0, text: '' };
|
|
571
368
|
if (opts.html) {
|
|
572
|
-
html = await
|
|
369
|
+
html = await fetchTextAdapter(browseUrl, cookie, 'text/html');
|
|
573
370
|
await fsp.writeFile(path.join(outDir, 'issue.html'), html.text);
|
|
574
371
|
console.log(`Saved ${path.join(outDir, 'issue.html')} (HTTP ${html.status})`);
|
|
575
372
|
}
|
|
576
373
|
|
|
577
374
|
let xml = { status: 0, text: '' };
|
|
578
375
|
if (opts.xml) {
|
|
579
|
-
xml = await
|
|
376
|
+
xml = await fetchTextAdapter(xmlUrl, cookie, 'application/xml,text/xml,text/html');
|
|
580
377
|
await fsp.writeFile(path.join(outDir, 'issue.xml'), xml.text);
|
|
581
378
|
console.log(`Saved ${path.join(outDir, 'issue.xml')} (HTTP ${xml.status})`);
|
|
582
379
|
}
|
|
583
380
|
|
|
584
|
-
const remoteLinks = await
|
|
381
|
+
const remoteLinks = await fetchTextAdapter(remoteLinksUrl, cookie, 'application/json');
|
|
585
382
|
await fsp.writeFile(path.join(outDir, 'remotelinks.json'), remoteLinks.text);
|
|
586
383
|
console.log(`Saved ${path.join(outDir, 'remotelinks.json')} (HTTP ${remoteLinks.status})`);
|
|
587
384
|
|