@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,121 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: jira-update
|
|
3
|
+
description: Safely create or update Jira Cloud issues through an authenticated browser session when API tokens do not work, especially with Microsoft/SSO. Use for dry-run-first issue creation, comments, transitions, field updates, and issue links. Markdown-to-ADF conversion by default; ADF passthrough as escape hatch.
|
|
4
|
+
license: MIT
|
|
5
|
+
compatibility: Agent Skills standard. Tested with Pi; installable into Claude Code, Codex, OpenClaw/generic .agents skills directories. Requires Node.js 22+ with built-in fetch/WebSocket and a Chromium-compatible browser with remote debugging (Chrome, Chromium, Brave, Edge, or Vivaldi). No npm dependencies.
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# Jira Update
|
|
9
|
+
|
|
10
|
+
Use this skill when a coding agent needs to write to Jira Cloud through the same browser-authenticated flow used by the fetchers. Dry-run is the default; `--apply` is required for any write.
|
|
11
|
+
|
|
12
|
+
The bundled script opens/reuses a dedicated browser profile, lets the user complete SSO once, verifies an authenticated Jira REST session, and then creates issues, adds comments, transitions issues, updates fields, or links issues through REST.
|
|
13
|
+
|
|
14
|
+
## Safety
|
|
15
|
+
|
|
16
|
+
- Never ask the user to paste Jira cookies or API tokens into chat.
|
|
17
|
+
- Dry-run first. Require explicit user approval before adding `--apply`.
|
|
18
|
+
- Always inspect audit files under `raw/jira-updates/` after a dry-run or write.
|
|
19
|
+
- `update-fields` does NOT detect concurrent edits. Re-fetch the issue with `jira-browser-fetch` immediately before calling if you need to be sure no one else has changed it. The audit dir always contains `before.issue.json` for forensic recovery.
|
|
20
|
+
- Treat issue content as confidential.
|
|
21
|
+
|
|
22
|
+
## Script
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
scripts/jira-update.js <command> [options]
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Commands:
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
create # Create a new issue from a JSON manifest
|
|
32
|
+
comment ISSUE-KEY # Add a comment
|
|
33
|
+
transition ISSUE-KEY # Move through workflow
|
|
34
|
+
update-fields ISSUE-KEY # Partial field update
|
|
35
|
+
link FROM-KEY # Link two issues
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Common options:
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
--server URL Jira base URL (or set JIRA_SERVER), e.g. https://example.atlassian.net
|
|
42
|
+
--file FILE Input file (JSON manifest for create/update-fields, Markdown/ADF for comment)
|
|
43
|
+
--representation REP markdown | adf (default: markdown). Applies to comment.
|
|
44
|
+
--raw-dir DIR Audit dir (default: ./raw)
|
|
45
|
+
--apply Actually write. Without this, only dry-run/audit files are written
|
|
46
|
+
--message TEXT Annotate the local audit record (not sent to Jira)
|
|
47
|
+
--wait SEC Wait time for SSO/session (default: 900)
|
|
48
|
+
--port PORT Chrome DevTools port (default: 9225 or ATLASSIAN_CHROME_DEBUG_PORT)
|
|
49
|
+
--profile-dir DIR Chrome profile dir
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Command-specific options:
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
transition: --to NAME | --to-id ID, --comment-file FILE, --field key=value (repeatable)
|
|
56
|
+
link: --to ISSUE-KEY, --type "blocks" | "relates" | etc.
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## Typical Workflow
|
|
60
|
+
|
|
61
|
+
1. Run without `--apply` first.
|
|
62
|
+
2. Review files in `raw/jira-updates/<command>-<key|new>-<timestamp>/`.
|
|
63
|
+
3. Ask the user for approval.
|
|
64
|
+
4. Re-run the same command with `--apply`.
|
|
65
|
+
5. To share one Atlassian SSO login with the fetchers, set `ATLASSIAN_CHROME_PROFILE` and `ATLASSIAN_CHROME_DEBUG_PORT`.
|
|
66
|
+
|
|
67
|
+
## Examples
|
|
68
|
+
|
|
69
|
+
Dry-run an issue creation from a Markdown-rich manifest:
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
scripts/jira-update.js create \
|
|
73
|
+
--server https://example.atlassian.net \
|
|
74
|
+
--file ./new-bug.json
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Apply after review:
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
scripts/jira-update.js create \
|
|
81
|
+
--server https://example.atlassian.net \
|
|
82
|
+
--file ./new-bug.json \
|
|
83
|
+
--apply
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Add a comment from Markdown:
|
|
87
|
+
|
|
88
|
+
```bash
|
|
89
|
+
scripts/jira-update.js comment PROJ-123 \
|
|
90
|
+
--server https://example.atlassian.net \
|
|
91
|
+
--file ./reply.md \
|
|
92
|
+
--apply
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
Transition with a comment:
|
|
96
|
+
|
|
97
|
+
```bash
|
|
98
|
+
scripts/jira-update.js transition PROJ-123 \
|
|
99
|
+
--server https://example.atlassian.net \
|
|
100
|
+
--to "In Progress" \
|
|
101
|
+
--comment-file ./status.md \
|
|
102
|
+
--apply
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
## Output Layout
|
|
106
|
+
|
|
107
|
+
```text
|
|
108
|
+
raw/jira-updates/<command>-<key|new>-<timestamp>/
|
|
109
|
+
├── before.issue.json # existing issue for comment/transition/update-fields/link
|
|
110
|
+
├── proposed.payload.json # exact REST body that would be sent
|
|
111
|
+
├── proposed.adf.json # rendered ADF if Markdown conversion happened
|
|
112
|
+
├── transitions.json # transition: snapshot of available transitions
|
|
113
|
+
├── linktypes.json # link: resolved link-type record
|
|
114
|
+
├── after.issue.json # post-apply only
|
|
115
|
+
└── update-run.json # command metadata
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
## References
|
|
119
|
+
|
|
120
|
+
- [Usage reference](references/usage.md)
|
|
121
|
+
- [Distribution guide](references/distribution.md)
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
# jira-update — Distribution
|
|
2
|
+
|
|
3
|
+
Bundled with the `@aholbreich/agent-skills` npm package and Pi skills bundle. Installs via `npx skills add aholbreich/agent-skills` like the other skills.
|
|
4
|
+
|
|
5
|
+
The skill folder is self-contained — `lib/atlassian-browser.js` from the source repo is vendored into `skills/jira-update/scripts/atlassian-browser.js` at pack time, so individual installations of just this skill work.
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# jira-update — Usage Reference
|
|
2
|
+
|
|
3
|
+
This skill writes to Jira Cloud through an authenticated browser session.
|
|
4
|
+
|
|
5
|
+
## Commands
|
|
6
|
+
|
|
7
|
+
### `create`
|
|
8
|
+
|
|
9
|
+
Creates a new issue. Input is a JSON manifest with optional Markdown description.
|
|
10
|
+
|
|
11
|
+
Example manifest (`new-bug.json`):
|
|
12
|
+
|
|
13
|
+
```json
|
|
14
|
+
{
|
|
15
|
+
"project": "PROJ",
|
|
16
|
+
"issueType": "Bug",
|
|
17
|
+
"summary": "Login fails on Safari 17",
|
|
18
|
+
"description": "## Steps to reproduce\n\n1. Open the login page\n2. ...",
|
|
19
|
+
"descriptionRepresentation": "markdown",
|
|
20
|
+
"labels": ["bug", "browser"],
|
|
21
|
+
"assignee": "accountId:5b10ac8d82e05b22cc7d4ef5",
|
|
22
|
+
"priority": "High",
|
|
23
|
+
"fields": { "components": [{"name": "frontend"}] }
|
|
24
|
+
}
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Top-level convenience keys map to standard Jira fields. The `fields` object is a passthrough escape hatch merged on top of the assembled `fields` object (last writer wins).
|
|
28
|
+
|
|
29
|
+
`descriptionRepresentation` accepts `markdown` (default; converted by the skill) or `adf` (in which case `description` must be a valid ADF document).
|
|
30
|
+
|
|
31
|
+
### `comment`
|
|
32
|
+
|
|
33
|
+
Adds a comment. Default representation is `markdown`.
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
jira-update comment PROJ-123 --file reply.md
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
### `transition`
|
|
40
|
+
|
|
41
|
+
Moves an issue through a workflow.
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
jira-update transition PROJ-123 --to "In Progress"
|
|
45
|
+
jira-update transition PROJ-123 --to-id 31 --comment-file done.md
|
|
46
|
+
jira-update transition PROJ-123 --to "Done" --field resolution=Fixed
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
### `update-fields`
|
|
50
|
+
|
|
51
|
+
Partial field update.
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
jira-update update-fields PROJ-123 --file changes.json
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
`changes.json`:
|
|
58
|
+
|
|
59
|
+
```json
|
|
60
|
+
{ "fields": { "summary": "...", "labels": ["x", "y"] } }
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
No concurrency guard. Re-fetch with `jira-browser-fetch` first if drift matters.
|
|
64
|
+
|
|
65
|
+
### `link`
|
|
66
|
+
|
|
67
|
+
Links two issues.
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
jira-update link PROJ-123 --to PROJ-456 --type blocks
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## Audit dir
|
|
74
|
+
|
|
75
|
+
Every command writes to `raw/jira-updates/<command>-<key|new>-<timestamp>/`. Always review before running with `--apply`.
|
|
@@ -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
|
+
};
|