@sickr/replay 0.1.0 → 0.2.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/dist/cli.js +134 -15
- package/dist/hookConfig.js +23 -0
- package/dist/render.js +48 -7
- package/dist/share.js +9 -1
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -1,19 +1,58 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { pathToFileURL } from 'node:url';
|
|
3
|
-
import { readFileSync, writeFileSync, mkdirSync, existsSync, readdirSync, statSync } from 'node:fs';
|
|
3
|
+
import { readFileSync, writeFileSync, mkdirSync, existsSync, readdirSync, statSync, unlinkSync } from 'node:fs';
|
|
4
4
|
import { homedir } from 'node:os';
|
|
5
5
|
import { join } from 'node:path';
|
|
6
6
|
import { spawn } from 'node:child_process';
|
|
7
7
|
import { appendEvent, loadRun, runsDir, latestRunId } from './recorder.js';
|
|
8
|
-
import { mergeHooks } from './hookConfig.js';
|
|
8
|
+
import { mergeHooks, removeHooks } from './hookConfig.js';
|
|
9
9
|
import { renderRunHtml } from './render.js';
|
|
10
|
-
import { buildSharePayload, publish } from './share.js';
|
|
10
|
+
import { buildSharePayload, publish, PublishError } from './share.js';
|
|
11
11
|
const REPLAY_ENDPOINT = process.env.SICKR_REPLAY_ENDPOINT ?? 'https://sickr.ai/api/replay';
|
|
12
|
-
const COMMANDS = ['init', 'record', 'open', 'list', 'share'];
|
|
12
|
+
const COMMANDS = ['init', 'record', 'open', 'list', 'share', 'stop', 'clear', 'help'];
|
|
13
13
|
export function parseCommand(argv) {
|
|
14
14
|
const c = argv[0];
|
|
15
15
|
return c && COMMANDS.includes(c) ? c : null;
|
|
16
16
|
}
|
|
17
|
+
export const HELP = `SICKR Replay — audit & replay what your AI coding agent did.
|
|
18
|
+
|
|
19
|
+
Records your Claude Code session (prompts, edits, commands) to a local, redacted
|
|
20
|
+
timeline you can replay — and optionally share as a public link.
|
|
21
|
+
|
|
22
|
+
Why: a durable record of every agent action. If your agent (Claude or Codex)
|
|
23
|
+
loses context or can't reload a past chat, the replay log helps you — and it —
|
|
24
|
+
recall what was just done.
|
|
25
|
+
|
|
26
|
+
Usage: npx @sickr/replay <command> [options]
|
|
27
|
+
|
|
28
|
+
Commands:
|
|
29
|
+
init Install the Claude Code recording hooks in this project and
|
|
30
|
+
start capturing runs to ~/.sickr/runs (secrets redacted).
|
|
31
|
+
open [run] Render a run to a local HTML timeline and open it in your
|
|
32
|
+
browser. 100% local — nothing is uploaded.
|
|
33
|
+
share [run] Redact and publish ONE run to a public sickr.ai/r/<id> link
|
|
34
|
+
(shows a preview and asks first). Links expire after 24h.
|
|
35
|
+
--open also open the published link in your browser
|
|
36
|
+
--yes skip the confirmation prompt
|
|
37
|
+
list List recorded runs, newest first.
|
|
38
|
+
stop Stop recording — removes SICKR's hooks from this project.
|
|
39
|
+
Your recorded runs are kept; run \`init\` to start again.
|
|
40
|
+
clear Delete all local runs in ~/.sickr/runs (asks first).
|
|
41
|
+
help Show this help.
|
|
42
|
+
|
|
43
|
+
────────────────────────────────────────────────────────────────────
|
|
44
|
+
This tool audits ONE agent on ONE machine. SICKR governs your whole team.
|
|
45
|
+
|
|
46
|
+
· Gates & approvals — plan sign-off, review, merge and validation checks
|
|
47
|
+
that work HOLDS at until they pass.
|
|
48
|
+
· Humans + agents on one board — agents are first-class teammates with
|
|
49
|
+
roles, capacity and accountability, not a side channel.
|
|
50
|
+
· A full, signed-off audit trail across every actor and every change.
|
|
51
|
+
· Runs 24/7 — produce as much work as you like; the team handles it.
|
|
52
|
+
|
|
53
|
+
Free tier available · bring your own Claude or Codex subscription.
|
|
54
|
+
→ https://sickr.ai
|
|
55
|
+
`;
|
|
17
56
|
export function currentRunId(cc) {
|
|
18
57
|
return String(cc.session_id ?? 'session');
|
|
19
58
|
}
|
|
@@ -38,6 +77,52 @@ function handleInit() {
|
|
|
38
77
|
`Runs are recorded locally to ${runsDir()} (secrets redacted).\n` +
|
|
39
78
|
`Use Claude Code as normal, then: npx @sickr/replay open\n`);
|
|
40
79
|
}
|
|
80
|
+
/** Stop recording: remove SICKR's hooks from this project's settings, keep runs. */
|
|
81
|
+
export function handleStop() {
|
|
82
|
+
const settingsPath = join(process.cwd(), '.claude', 'settings.json');
|
|
83
|
+
if (!existsSync(settingsPath)) {
|
|
84
|
+
process.stdout.write('sickr: no .claude/settings.json here — not recording in this project.\n');
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
let settings;
|
|
88
|
+
try {
|
|
89
|
+
settings = JSON.parse(readFileSync(settingsPath, 'utf8'));
|
|
90
|
+
}
|
|
91
|
+
catch {
|
|
92
|
+
process.stderr.write(`sickr: could not parse ${settingsPath}; left it unchanged.\n`);
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
writeFileSync(settingsPath, JSON.stringify(removeHooks(settings), null, 2) + '\n');
|
|
96
|
+
process.stdout.write('sickr: recording stopped — removed SICKR hooks from .claude/settings.json.\n' +
|
|
97
|
+
'Your recorded runs are kept. Run `npx @sickr/replay init` to start again.\n');
|
|
98
|
+
}
|
|
99
|
+
/** Delete all local runs. Destructive — confirms unless `yes` is set. */
|
|
100
|
+
export async function handleClear(yes) {
|
|
101
|
+
const dir = runsDir();
|
|
102
|
+
const files = existsSync(dir) ? readdirSync(dir).filter((f) => f.endsWith('.ndjson')) : [];
|
|
103
|
+
if (files.length === 0) {
|
|
104
|
+
process.stdout.write('sickr: no local runs to clear.\n');
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
if (!yes) {
|
|
108
|
+
if (!process.stdin.isTTY) {
|
|
109
|
+
process.stderr.write(`sickr: ${files.length} run(s) in ${dir}. Re-run with --yes to delete them.\n`);
|
|
110
|
+
process.exit(1);
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
process.stdout.write(`Delete ${files.length} recorded run(s) from ${dir}? This cannot be undone. [y/N] `);
|
|
114
|
+
const answer = await new Promise((resolve) => {
|
|
115
|
+
process.stdin.once('data', (d) => resolve(d.toString().trim().toLowerCase()));
|
|
116
|
+
});
|
|
117
|
+
if (answer !== 'y' && answer !== 'yes') {
|
|
118
|
+
process.stdout.write('sickr: cancelled.\n');
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
for (const f of files)
|
|
123
|
+
unlinkSync(join(dir, f));
|
|
124
|
+
process.stdout.write(`sickr: cleared ${files.length} run(s).\n`);
|
|
125
|
+
}
|
|
41
126
|
function openInBrowser(file) {
|
|
42
127
|
const cmd = process.platform === 'win32' ? 'cmd' : process.platform === 'darwin' ? 'open' : 'xdg-open';
|
|
43
128
|
const args = process.platform === 'win32' ? ['/c', 'start', '', file] : [file];
|
|
@@ -70,7 +155,7 @@ function handleList() {
|
|
|
70
155
|
.sort((a, b) => statSync(join(dir, b)).mtimeMs - statSync(join(dir, a)).mtimeMs)
|
|
71
156
|
.forEach((f) => process.stdout.write(`${f.replace(/\.ndjson$/, '')}\t${statSync(join(dir, f)).mtime.toISOString()}\n`));
|
|
72
157
|
}
|
|
73
|
-
async function handleShare(runId, yes) {
|
|
158
|
+
async function handleShare(runId, yes, open) {
|
|
74
159
|
const id = runId ?? latestRunId();
|
|
75
160
|
if (!id) {
|
|
76
161
|
process.stderr.write('sickr: no runs to share. Use Claude Code first.\n');
|
|
@@ -79,9 +164,8 @@ async function handleShare(runId, yes) {
|
|
|
79
164
|
}
|
|
80
165
|
const run = loadRun(id);
|
|
81
166
|
const payload = buildSharePayload(run);
|
|
82
|
-
process.stdout.write(`sickr: about to publish run "${id}" (${payload.run.events.length} events, secrets already redacted) to ${REPLAY_ENDPOINT}\n`
|
|
83
|
-
|
|
84
|
-
process.stdout.write(` · ${e.label}: ${e.detail || ''}\n`);
|
|
167
|
+
process.stdout.write(`sickr: about to publish run "${id}" (${payload.run.events.length} events, secrets already redacted) to ${REPLAY_ENDPOINT}\n` +
|
|
168
|
+
`sickr: tip — run \`npx @sickr/replay open ${id}\` to review the full timeline locally before sharing.\n`);
|
|
85
169
|
if (!yes) {
|
|
86
170
|
if (!process.stdin.isTTY) {
|
|
87
171
|
process.stderr.write('sickr: re-run with --yes to publish non-interactively.\n');
|
|
@@ -97,8 +181,34 @@ async function handleShare(runId, yes) {
|
|
|
97
181
|
return;
|
|
98
182
|
}
|
|
99
183
|
}
|
|
100
|
-
|
|
184
|
+
let url;
|
|
185
|
+
try {
|
|
186
|
+
({ url } = await publish(payload, REPLAY_ENDPOINT));
|
|
187
|
+
}
|
|
188
|
+
catch (err) {
|
|
189
|
+
if (err instanceof PublishError && err.status === 429) {
|
|
190
|
+
process.stdout.write('sickr: rate-limited — you can publish about once every 10s. Waiting to retry once...\n');
|
|
191
|
+
await new Promise((r) => setTimeout(r, 11_000));
|
|
192
|
+
try {
|
|
193
|
+
({ url } = await publish(payload, REPLAY_ENDPOINT));
|
|
194
|
+
}
|
|
195
|
+
catch (retryErr) {
|
|
196
|
+
if (retryErr instanceof PublishError && retryErr.status === 429) {
|
|
197
|
+
process.stderr.write('sickr: still rate-limited. Give it a minute and run `share` again.\n');
|
|
198
|
+
process.exit(1);
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
throw retryErr;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
else {
|
|
205
|
+
throw err;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
101
208
|
process.stdout.write(`sickr: published → ${url}\n`);
|
|
209
|
+
process.stdout.write('sickr: this link expires in 24h.\n');
|
|
210
|
+
if (open)
|
|
211
|
+
openInBrowser(url);
|
|
102
212
|
}
|
|
103
213
|
async function readStdin() {
|
|
104
214
|
const chunks = [];
|
|
@@ -108,7 +218,12 @@ async function readStdin() {
|
|
|
108
218
|
}
|
|
109
219
|
async function main() {
|
|
110
220
|
const argv = process.argv.slice(2);
|
|
221
|
+
if (argv.length === 0 || argv[0] === 'help' || argv.includes('--help') || argv.includes('-h')) {
|
|
222
|
+
process.stdout.write(HELP);
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
111
225
|
const cmd = parseCommand(argv);
|
|
226
|
+
const rest = argv.slice(1);
|
|
112
227
|
switch (cmd) {
|
|
113
228
|
case 'record':
|
|
114
229
|
handleRecord(await readStdin());
|
|
@@ -122,14 +237,18 @@ async function main() {
|
|
|
122
237
|
case 'list':
|
|
123
238
|
handleList();
|
|
124
239
|
return;
|
|
125
|
-
case '
|
|
126
|
-
|
|
127
|
-
|
|
240
|
+
case 'stop':
|
|
241
|
+
handleStop();
|
|
242
|
+
return;
|
|
243
|
+
case 'clear':
|
|
244
|
+
await handleClear(rest.includes('--yes') || rest.includes('-y'));
|
|
245
|
+
return;
|
|
246
|
+
case 'share':
|
|
247
|
+
await handleShare(rest.find((a) => !a.startsWith('-')), rest.includes('--yes') || rest.includes('-y'), rest.includes('--open'));
|
|
128
248
|
return;
|
|
129
|
-
}
|
|
130
249
|
default:
|
|
131
|
-
process.stderr.write('
|
|
132
|
-
process.exit(
|
|
250
|
+
process.stderr.write('sickr: unknown command. Run `npx @sickr/replay help`.\n');
|
|
251
|
+
process.exit(1);
|
|
133
252
|
}
|
|
134
253
|
}
|
|
135
254
|
const invokedDirectly = process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href;
|
package/dist/hookConfig.js
CHANGED
|
@@ -17,3 +17,26 @@ export function mergeHooks(settings, binPath) {
|
|
|
17
17
|
}
|
|
18
18
|
return next;
|
|
19
19
|
}
|
|
20
|
+
/**
|
|
21
|
+
* Remove the SICKR recording hooks from a Claude Code settings object — the
|
|
22
|
+
* inverse of mergeHooks. Unrelated hooks are preserved; an event left with no
|
|
23
|
+
* hooks is dropped so settings.json stays clean.
|
|
24
|
+
*/
|
|
25
|
+
export function removeHooks(settings) {
|
|
26
|
+
const next = { ...(settings ?? {}) };
|
|
27
|
+
if (!next.hooks)
|
|
28
|
+
return next;
|
|
29
|
+
const hooks = { ...next.hooks };
|
|
30
|
+
for (const ev of EVENTS) {
|
|
31
|
+
const groups = Array.isArray(hooks[ev]) ? hooks[ev] : undefined;
|
|
32
|
+
if (!groups)
|
|
33
|
+
continue;
|
|
34
|
+
const kept = groups.filter((g) => !JSON.stringify(g).includes(TAG));
|
|
35
|
+
if (kept.length > 0)
|
|
36
|
+
hooks[ev] = kept;
|
|
37
|
+
else
|
|
38
|
+
delete hooks[ev];
|
|
39
|
+
}
|
|
40
|
+
next.hooks = hooks;
|
|
41
|
+
return next;
|
|
42
|
+
}
|
package/dist/render.js
CHANGED
|
@@ -2,6 +2,35 @@ const PLASMA = '#34e0ff';
|
|
|
2
2
|
function esc(s) {
|
|
3
3
|
return s.replace(/[&<>"']/g, (c) => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c]));
|
|
4
4
|
}
|
|
5
|
+
// Inline Sickr wordmark (mirrors src/components/SickrLogo.tsx) — static ids.
|
|
6
|
+
function wordmark() {
|
|
7
|
+
return `<svg viewBox="0 0 1105 395" role="img" aria-label="Sickr" class="logo" shape-rendering="geometricPrecision">
|
|
8
|
+
<defs>
|
|
9
|
+
<linearGradient id="lg-silver" x1="0" y1="0" x2="1" y2="1">
|
|
10
|
+
<stop offset="0%" stop-color="#ffffff"/><stop offset="36%" stop-color="#f6f8fb"/>
|
|
11
|
+
<stop offset="58%" stop-color="#c7cedb"/><stop offset="78%" stop-color="#ffffff"/><stop offset="100%" stop-color="#dde3ee"/>
|
|
12
|
+
</linearGradient>
|
|
13
|
+
<filter id="lg-fx" x="-10%" y="-12%" width="120%" height="124%">
|
|
14
|
+
<feGaussianBlur stdDeviation="5" result="b"/>
|
|
15
|
+
<feColorMatrix in="b" type="matrix" values="0 0 0 0 0.20 0 0 0 0 0.88 0 0 0 0 1 0 0 0 0.5 0" result="g"/>
|
|
16
|
+
<feMerge><feMergeNode in="g"/><feMergeNode in="SourceGraphic"/></feMerge>
|
|
17
|
+
</filter>
|
|
18
|
+
<mask id="lg-s" maskUnits="userSpaceOnUse"><rect width="1105" height="395" fill="white"/><polygon points="101,258 270,145 202,204 161,213" fill="black"/></mask>
|
|
19
|
+
<mask id="lg-i" maskUnits="userSpaceOnUse"><rect width="1105" height="395" fill="white"/><polygon points="361,260 396,229 396,252 361,283" fill="black"/></mask>
|
|
20
|
+
<mask id="lg-c" maskUnits="userSpaceOnUse"><rect width="1105" height="395" fill="white"/><polygon points="458,272 493,244 504,262 469,290" fill="black"/></mask>
|
|
21
|
+
<mask id="lg-r" maskUnits="userSpaceOnUse"><rect width="1105" height="395" fill="white"/><polygon points="888,266 920,238 920,261 888,289" fill="black"/></mask>
|
|
22
|
+
</defs>
|
|
23
|
+
<g fill="url(#lg-silver)" filter="url(#lg-fx)">
|
|
24
|
+
<path mask="url(#lg-s)" d="M 70 324 L 110 296 L 221 296 C 243 296 258 282 258 260 C 258 238 247 223 224 207 L 244 188 C 272 204 286 227 286 258 C 286 297 260 324 228 324 Z M 136 213 C 109 199 96 179 96 153 C 96 109 122 77 156 77 L 296 77 L 262 106 L 158 106 C 138 106 124 125 124 153 C 124 171 134 184 157 195 Z"/>
|
|
25
|
+
<polygon points="268,146 200,203 109,256 162,211"/>
|
|
26
|
+
<circle cx="378" cy="116" r="19"/>
|
|
27
|
+
<path mask="url(#lg-i)" d="M 366 182 L 395 155 L 395 299 L 366 324 Z"/>
|
|
28
|
+
<path mask="url(#lg-c)" d="M 623 153 L 587 182 L 530 182 C 504 182 486 204 486 234 C 486 264 504 285 529 285 L 625 285 L 591 314 L 520 314 C 482 314 457 282 457 237 C 457 191 482 153 518 153 Z"/>
|
|
29
|
+
<path d="M 684 98 L 714 70 L 714 200 L 684 227 Z M 684 254 L 796 151 L 837 151 L 714 265 L 714 294 L 684 320 Z M 764 238 L 841 313 L 802 313 L 744 256 Z"/>
|
|
30
|
+
<path mask="url(#lg-r)" d="M 889 321 L 889 193 C 889 168 906 153 927 153 L 1034 153 L 998 182 L 934 182 C 924 182 918 189 918 199 L 918 295 Z"/>
|
|
31
|
+
</g>
|
|
32
|
+
</svg>`;
|
|
33
|
+
}
|
|
5
34
|
function dot(kind) {
|
|
6
35
|
return kind === 'tool' ? PLASMA : kind === 'prompt' ? '#8b95a7' : '#5f6b80';
|
|
7
36
|
}
|
|
@@ -20,11 +49,15 @@ export function renderRunHtml(run) {
|
|
|
20
49
|
return `<!doctype html>
|
|
21
50
|
<html lang="en"><head><meta charset="UTF-8"/><meta name="viewport" content="width=device-width,initial-scale=1"/>
|
|
22
51
|
<title>SICKR Replay — ${esc(run.id)}</title>
|
|
52
|
+
<link rel="preconnect" href="https://fonts.googleapis.com"/>
|
|
53
|
+
<link href="https://fonts.googleapis.com/css2?family=Chakra+Petch:wght@600;700&family=Sora:wght@300;400;600&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet"/>
|
|
23
54
|
<style>
|
|
24
55
|
body{margin:0;background:#06080d;color:#e7ecf3;font-family:Sora,system-ui,Arial,sans-serif;padding:40px 20px}
|
|
25
56
|
.wrap{max-width:820px;margin:0 auto}
|
|
26
|
-
.
|
|
27
|
-
.
|
|
57
|
+
.bar{display:flex;align-items:center;justify-content:space-between;gap:16px;margin-bottom:24px}
|
|
58
|
+
.logo{height:30px;width:auto;display:block}
|
|
59
|
+
.bar a{font-family:"JetBrains Mono",monospace;font-size:11px;letter-spacing:.16em;text-transform:uppercase;color:${PLASMA};text-decoration:none}
|
|
60
|
+
.meta{font-family:"JetBrains Mono",ui-monospace,monospace;font-size:12px;color:#5f6b80;margin:0 0 28px}
|
|
28
61
|
ol{list-style:none;margin:0;padding:0;position:relative}
|
|
29
62
|
ol::before{content:"";position:absolute;left:6px;top:6px;bottom:6px;width:1px;background:linear-gradient(${PLASMA},transparent)}
|
|
30
63
|
li{position:relative;padding:0 0 18px 28px}
|
|
@@ -33,13 +66,21 @@ export function renderRunHtml(run) {
|
|
|
33
66
|
.kind{font-family:"JetBrains Mono",monospace;font-size:10px;text-transform:uppercase;letter-spacing:.12em;color:${PLASMA};margin-left:6px}
|
|
34
67
|
.detail{font-family:"JetBrains Mono",monospace;font-size:12.5px;color:#cdd5e1;margin-top:4px;white-space:pre-wrap;word-break:break-word}
|
|
35
68
|
.time{font-family:"JetBrains Mono",monospace;font-size:11px;color:#5f6b80;margin-top:3px}
|
|
36
|
-
.cta{margin-top:36px;border
|
|
37
|
-
.cta
|
|
69
|
+
.cta{margin-top:36px;border:1px solid #1b2435;border-radius:12px;background:rgba(8,12,20,.55);padding:22px}
|
|
70
|
+
.cta h2{font-family:"Chakra Petch",sans-serif;font-weight:700;font-size:18px;color:#fff;margin:0 0 8px}
|
|
71
|
+
.cta p{font-size:14px;line-height:1.6;color:#9aa6b6;margin:0 0 16px}
|
|
72
|
+
.btn{display:inline-flex;align-items:center;gap:8px;background:${PLASMA};color:#06080d;font-family:"JetBrains Mono",monospace;font-size:12px;font-weight:600;text-transform:uppercase;letter-spacing:.08em;padding:11px 16px;border-radius:6px;text-decoration:none;box-shadow:0 0 24px rgba(52,224,255,.3)}
|
|
73
|
+
.share-hint{margin-top:14px;font-family:"JetBrains Mono",monospace;font-size:11px;color:#5f6b80}
|
|
38
74
|
</style></head>
|
|
39
75
|
<body><div class="wrap">
|
|
40
|
-
<div class="
|
|
41
|
-
<div class="meta">replay · ${esc(run.id)} · ${esc(run.cwd || '')} · ${run.events.length} events</div>
|
|
76
|
+
<div class="bar">${wordmark()}<a href="https://sickr.ai">sickr.ai →</a></div>
|
|
77
|
+
<div class="meta">local replay · ${esc(run.id)} · ${esc(run.cwd || '')} · ${run.events.length} events</div>
|
|
42
78
|
<ol>${events}</ol>
|
|
43
|
-
<div class="cta">
|
|
79
|
+
<div class="cta">
|
|
80
|
+
<h2>This is one agent, on your machine.</h2>
|
|
81
|
+
<p>SICKR governs your whole team — gates, approvals, multi-agent hand-offs and a full, signed-off audit trail across humans and agents. Free tier · bring your own Claude or Codex.</p>
|
|
82
|
+
<a class="btn" href="https://sickr.ai">Explore SICKR →</a>
|
|
83
|
+
<div class="share-hint">Tip: <code>npx @sickr/replay share</code> publishes this run to a public link.</div>
|
|
84
|
+
</div>
|
|
44
85
|
</div></body></html>`;
|
|
45
86
|
}
|
package/dist/share.js
CHANGED
|
@@ -2,6 +2,14 @@
|
|
|
2
2
|
export function buildSharePayload(run) {
|
|
3
3
|
return { run: { cwd: run.cwd, startedAt: run.startedAt, events: run.events } };
|
|
4
4
|
}
|
|
5
|
+
export class PublishError extends Error {
|
|
6
|
+
status;
|
|
7
|
+
constructor(status) {
|
|
8
|
+
super(`publish failed: ${status}`);
|
|
9
|
+
this.status = status;
|
|
10
|
+
this.name = 'PublishError';
|
|
11
|
+
}
|
|
12
|
+
}
|
|
5
13
|
export async function publish(payload, endpoint) {
|
|
6
14
|
const res = await fetch(endpoint, {
|
|
7
15
|
method: 'POST',
|
|
@@ -9,6 +17,6 @@ export async function publish(payload, endpoint) {
|
|
|
9
17
|
body: JSON.stringify(payload),
|
|
10
18
|
});
|
|
11
19
|
if (!res.ok)
|
|
12
|
-
throw new
|
|
20
|
+
throw new PublishError(res.status);
|
|
13
21
|
return (await res.json());
|
|
14
22
|
}
|
package/package.json
CHANGED