@cloud-cli/on 1.53.0 → 1.54.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/on.js CHANGED
@@ -255,13 +255,21 @@ var ye = class extends l {
255
255
  let t = await this.getJob(e);
256
256
  return t ? t.status !== "pending" && t.status !== "running" ? "not_active" : (await C.run("UPDATE jobs SET status = 'cancelled', updated_at = CURRENT_TIMESTAMP, finished_at = CURRENT_TIMESTAMP\n WHERE id = ? AND status IN ('pending', 'running');", [e]), (await this.getJob(e))?.status === "cancelled" ? "cancelled" : "not_active") : "not_found";
257
257
  }
258
- async restartJob(e) {
259
- let t = await this.getJob(e);
260
- if (!t) return;
261
- t.status === "running" && await C.run("UPDATE jobs SET status = 'cancelled', finished_at = CURRENT_TIMESTAMP WHERE id = ?;", [e]);
262
- let n = await C.get("SELECT active_revision FROM workflows WHERE id = ?", [t.workflow_id]), r = Number(n?.active_revision);
263
- if (!Number.isSafeInteger(r) || r < 1) throw Error(`Workflow ${t.workflow_id} has no active revision`);
264
- return (await C.get("INSERT INTO jobs (\n parentId, workflow_id, workflow_revision, required_tags, concurrency_key, payload,\n status, worker_id, report, started_at, finished_at\n )\n SELECT id, workflow_id, ?, required_tags, concurrency_key, payload,\n 'pending', NULL, NULL, NULL, NULL\n FROM jobs\n WHERE id = ?\n RETURNING *", [r, e])).id;
258
+ async restartJob(e, t = {}) {
259
+ let n = await this.getJob(e);
260
+ if (!n) return;
261
+ n.status === "running" && await C.run("UPDATE jobs SET status = 'cancelled', finished_at = CURRENT_TIMESTAMP WHERE id = ?;", [e]);
262
+ let r = await C.get("SELECT active_revision FROM workflows WHERE id = ?", [n.workflow_id]), i = Number(r?.active_revision);
263
+ if (!Number.isSafeInteger(i) || i < 1) throw Error(`Workflow ${n.workflow_id} has no active revision`);
264
+ let a = n.payload ? typeof n.payload == "string" ? JSON.parse(n.payload) : n.payload : { inputs: {} };
265
+ return a.inputs = {
266
+ ...a.inputs || {},
267
+ ...t
268
+ }, (await C.get("INSERT INTO jobs (\n parentId, workflow_id, workflow_revision, required_tags, concurrency_key, payload,\n status, worker_id, report, started_at, finished_at\n )\n SELECT id, workflow_id, ?, required_tags, concurrency_key, ?,\n 'pending', NULL, NULL, NULL, NULL\n FROM jobs\n WHERE id = ?\n RETURNING *", [
269
+ i,
270
+ JSON.stringify(a),
271
+ e
272
+ ])).id;
265
273
  }
266
274
  async isCancelled(e) {
267
275
  return (await C.get("SELECT status FROM jobs WHERE id = ?;", [+e]))?.status === "cancelled";
@@ -507,7 +515,7 @@ var je = class {
507
515
  type: "image/svg+xml",
508
516
  purpose: "any maskable"
509
517
  }]
510
- }), Ne = "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 512 512\">\n <rect width=\"512\" height=\"512\" rx=\"112\" fill=\"#030712\"/>\n <path d=\"M128 144h256v224H128z\" fill=\"#111827\" stroke=\"#6366f1\" stroke-width=\"24\"/>\n <path d=\"m174 218 62 38-62 38M260 300h78\" fill=\"none\" stroke=\"#f9fafb\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"24\"/>\n <circle cx=\"365\" cy=\"147\" r=\"47\" fill=\"#10b981\" stroke=\"#030712\" stroke-width=\"18\"/>\n</svg>", Pe = "self.addEventListener('install', () => self.skipWaiting());\nself.addEventListener('activate', (event) => event.waitUntil(self.clients.claim()));\nself.addEventListener('push', (event) => {\n const data = event.data?.json() || {};\n event.waitUntil(self.registration.showNotification(data.title || 'Runner Engine', {\n body: data.body || 'A job status changed.',\n icon: '/app-icon.svg',\n badge: '/app-icon.svg',\n data: { url: data.url || '/runs' },\n }));\n});\nself.addEventListener('notificationclick', (event) => {\n event.notification.close();\n const target = event.notification.data?.url || '/runs';\n event.waitUntil((async () => {\n const windows = await self.clients.matchAll({ type: 'window', includeUncontrolled: true });\n const existing = windows.find((client) => new URL(client.url).pathname === target);\n if (existing) return existing.focus();\n return self.clients.openWindow(target);\n })());\n});", Fe = "<!doctype html>\n<html lang=\"en\" class=\"dark\">\n <head>\n <meta charset=\"UTF-8\" />\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />\n <meta name=\"theme-color\" content=\"#030712\" />\n <meta name=\"mobile-web-app-capable\" content=\"yes\" />\n <link rel=\"manifest\" href=\"/manifest.webmanifest\" />\n <link rel=\"icon\" href=\"/app-icon.svg\" type=\"image/svg+xml\" />\n <link rel=\"component\" href=\"https://sodium.static.apphor.de/lucide-icon.html\" />\n <link rel=\"component\" href=\"https://sodium.static.apphor.de/code-block.html\" />\n <link rel=\"component\" href=\"/app-header.html\" />\n <title>Workflow Run</title>\n <script type=\"importmap\">\n {\n \"imports\": {\n \"@li3/\": \"https://cdn.li3.dev/@li3/\",\n \"ansi_up\": \"https://esm.sh/ansi_up@6.0.6\",\n \"marked\": \"https://cdn.jsdelivr.net/npm/marked@18.0.12/lib/marked.esm.js\"\n }\n }\n <\/script>\n <style>\n </style>\n </head>\n <body class=\"bg-gray-950 text-gray-100 min-h-screen p-4 md:p-6 font-sans\">\n <template app>\n <div class=\"max-w-7xl mx-auto space-y-4 py-6\">\n <app-header bind-title=\"report.workflowName\" bind-subtitle=\"'Run #' + report.jobId\" back=\"/runs\" back-label=\"Back to jobs\"></app-header>\n <div class=\"flex flex-wrap items-center justify-between gap-3 border-b border-gray-800 pb-4\">\n <p class=\"text-xs text-gray-400\">\n Started {{ report.startedAt }} · {{ timing }}\n <template if=\"report.workerId\"> · worker: {{ report.workerId }}</template>\n <template if=\"report.parentId\"> · from #{{ report.parentId }}</template>\n </p>\n <div class=\"flex items-center gap-2 shrink-0\">\n <span\n class=\"px-4 py-1.5 rounded-full text-sm font-semibold border\"\n bind-class=\"{\n 'bg-emerald-500/10.text-emerald-400.border-emerald-500/20': report.status === 'success',\n 'bg-rose-500/10.text-rose-400.border-rose-500/20': report.status === 'failed',\n 'bg-indigo-500/10.text-indigo-400.border-indigo-500/20': report.status === 'running',\n 'bg-amber-500/10.text-amber-400.border-amber-500/20': !['success', 'failed', 'running'].includes(report.status)\n }\"\n >\n {{ upper(report.status) }}\n </span>\n <template if=\"report.status === 'running'\">\n <button\n class=\"text-sm text-rose-300 px-4 py-1.5 border border-rose-500/50 hover:border-rose-400 rounded-full disabled:opacity-50\"\n on-click=\"cancelJob()\"\n bind-disabled.bool=\"cancelling\"\n >\n {{ cancelling ? 'Cancelling...' : 'Cancel' }}\n </button>\n </template>\n <template if=\"report.status !== 'running'\">\n <button\n class=\"text-sm text-white px-4 py-1.5 border border-gray-600 hover:border-gray-400 rounded-full\"\n on-click=\"restartJob()\"\n >\n Restart\n </button>\n </template>\n </div>\n </div>\n <template if=\"previousRuns.length\">\n <details class=\"rounded-xl border border-gray-800 bg-gray-900/70\">\n <summary class=\"cursor-pointer px-4 py-3 text-sm font-medium text-gray-300 outline-none hover:bg-gray-800 focus-visible:ring-2 focus-visible:ring-indigo-400/60\">\n Previous runs\n </summary>\n <ul class=\"divide-y divide-gray-800 border-t border-gray-800\">\n <template for=\"run of previousRuns\">\n <li>\n <a attr-href=\"'/runs/' + run.id\" class=\"flex items-center justify-between gap-3 px-4 py-3 text-sm outline-none hover:bg-gray-800 focus-visible:ring-2 focus-visible:ring-indigo-400/60\">\n <span><span class=\"font-mono text-indigo-300\">#{{ run.id }}</span><span class=\"ml-3 text-gray-400\">{{ run.startedAt }}</span></span>\n <span class=\"font-semibold uppercase text-xs text-gray-400\">{{ upper(run.status) }}</span>\n </a>\n </li>\n </template>\n </ul>\n </details>\n </template>\n\n <section class=\"rounded overflow-hidden border-b border-gray-800\">\n <h2 class=\"sr-only\">Execution Steps</h2>\n <template for=\"[step, index] of report.steps\">\n <details class=\"border border-b-0 border-gray-800 bg-gray-900/50 text-sm\">\n <summary\n class=\"flex flex-col md:flex-row md:items-center justify-between gap-2 p-3 bg-gray-800/40 border-b border-gray-800 cursor-pointer\"\n >\n <div class=\"flex items-center gap-3 min-w-0\">\n <lucide-icon\n class=\"flex h-7 w-7 shrink-0 items-center justify-center rounded-full bg-gray-700/70\"\n bind-class=\"{\n 'text-emerald-400': step.status === 'success',\n 'text-rose-400': step.status === 'failed',\n 'text-indigo-400.animate-spin': step.status === 'running',\n 'text-gray-400': !['success', 'failed', 'running'].includes(step.status)\n }\"\n bind-icon=\"\n step.status === 'success'\n ? 'circle-check'\n : step.status === 'failed'\n ? 'circle-x'\n : step.status === 'running'\n ? 'loader-circle'\n : 'circle'\n \"\n size=\"15\"\n ></lucide-icon>\n <template if=\"step.exitCode !== undefined && step.exitCode !== 0\">\n <span class=\"font-mono text-gray-500\">({{ step.exitCode }})</span>\n </template>\n <h3 class=\"font-semibold text-gray-200 truncate min-w-0 flex-1\">{{ step.name }}</h3>\n <div class=\"font-mono text-gray-400 shrink-0\">{{ step.durationMs }}ms</div>\n <span\n class=\"sr-only px-2.5 py-0.5 rounded-full font-medium\"\n bind-class=\"{\n 'text-emerald-400.bg-emerald-500/10': step.status === 'success',\n 'text-rose-400.bg-rose-500/10': step.status === 'failed',\n 'text-indigo-400.bg-indigo-500/10': step.status === 'running',\n 'text-gray-400.bg-gray-500/10': !['success', 'failed', 'running'].includes(step.status)\n }\"\n >\n {{ upper(step.status) }}\n </span>\n </div>\n </summary>\n <template if=\"report.canViewLogs\">\n <div class=\"relative p-4 bg-gray-950 font-mono text-gray-300 leading-relaxed overflow-auto w-full\">\n <template if=\"step.status === 'failed'\">\n <button type=\"button\" on-click=\"askAi(step, $event)\" aria-label=\"Get AI help for failed step\" title=\"Get AI help\" class=\"absolute right-3 top-3 z-10 inline-flex h-7 w-7 items-center justify-center rounded-lg bg-gray-900 text-indigo-300 outline-none hover:bg-indigo-500/10 focus-visible:ring-2 focus-visible:ring-indigo-400/60\">\n <lucide-icon bind-icon=\"aiLoading && aiStepId === step.id ? 'loader-circle' : 'sparkles'\" bind-class=\"aiLoading && aiStepId === step.id ? 'animate-spin' : ''\" size=\"16\"></lucide-icon>\n </button>\n </template>\n <pre class=\"whitespace-pre-wrap\" bind-innerhtml=\"stepLog(step)\"></pre>\n </div>\n </template>\n <template if=\"!report.canViewLogs\">\n <div class=\"p-4 bg-gray-950 text-sm text-gray-500\">Logs are available to authenticated users.</div>\n </template>\n </details>\n </template>\n </section>\n\n <details class=\"bg-gray-900 border border-gray-800 rounded-xl overflow-hidden\">\n <summary class=\"cursor-pointer px-4 py-3 text-xs font-semibold uppercase tracking-wider text-gray-400 outline-none transition hover:bg-gray-800 focus-visible:ring-2 focus-visible:ring-indigo-400/60\">\n Trigger Inputs\n </summary>\n <code-block\n language=\"json\"\n theme=\"atom-one-dark\"\n bind-source=\"inputsJson\"\n class=\"block max-h-[38rem] overflow-auto border-t border-gray-800 bg-gray-950 p-4\"\n ></code-block>\n </details>\n <template if=\"report.workflowSourceYaml\">\n <details class=\"bg-gray-900 border border-gray-800 rounded-xl overflow-hidden\">\n <summary class=\"cursor-pointer px-4 py-3 text-xs font-semibold uppercase tracking-wider text-gray-400 outline-none transition hover:bg-gray-800 focus-visible:ring-2 focus-visible:ring-indigo-400/60\">\n Workflow source YAML · revision {{ report.workflowRevision }}\n </summary>\n <code-block\n language=\"yaml\"\n theme=\"atom-one-dark\"\n bind-source=\"report.workflowSourceYaml\"\n class=\"block max-h-[38rem] overflow-auto border-t border-gray-800 bg-gray-950 p-4\"\n ></code-block>\n </details>\n </template>\n <template if=\"report.canViewLogs && aiStepId\">\n <aside class=\"fixed bottom-4 left-4 right-4 z-40 mt-4 flex max-h-[70vh] flex-col rounded-2xl border border-indigo-500/30 bg-gray-900/95 p-4 shadow-2xl backdrop-blur lg:left-auto lg:w-[28rem]\" aria-labelledby=\"ai-help-title\">\n <div class=\"flex items-start justify-between gap-3 border-b border-gray-800 pb-3\">\n <div><h2 id=\"ai-help-title\" class=\"font-semibold text-white\">AI help</h2><p class=\"mt-1 text-xs text-gray-500\">Ask about a failed step.</p></div>\n <lucide-icon icon=\"sparkles\" size=\"18\" class=\"text-indigo-300\"></lucide-icon>\n </div>\n <div class=\"min-h-0 flex-1 space-y-3 overflow-y-auto py-3\" aria-live=\"polite\">\n <p class=\"flex items-center gap-2 text-sm text-indigo-300\" class-hidden=\"!aiLoading\">\n <lucide-icon icon=\"loader-circle\" size=\"16\" class=\"animate-spin\"></lucide-icon>\n Thinking through the failed step...\n </p>\n <p class=\"text-sm text-gray-500\" class-hidden=\"aiMessages.length > 0\">Select the help icon beside a failed step to start.</p>\n <template for=\"message of aiMessages\">\n <div class=\"rounded-lg p-3 text-sm\" bind-class=\"message.role === 'assistant' ? 'bg-indigo-500/10.text-gray-200' : 'bg-gray-800.text-gray-300'\">\n <p class=\"mb-1 text-[10px] font-semibold uppercase tracking-wider text-gray-500\">{{ message.role }}</p>\n <template if=\"message.html\"><div class=\"ai-markdown text-sm leading-relaxed\" bind-innerhtml=\"message.html\"></div></template>\n <template if=\"!message.html\"><pre class=\"whitespace-pre-wrap font-sans leading-relaxed\">{{ message.content }}</pre></template>\n </div>\n </template>\n <p class=\"text-sm text-rose-300\" class-hidden=\"!aiError\">{{ aiError }}</p>\n </div>\n <form class=\"border-t border-gray-800 pt-3\" on-submit.prevent=\"askFollowup()\">\n <label class=\"sr-only\" for=\"ai-question\">Ask a follow-up question</label>\n <div class=\"flex gap-2\">\n <input id=\"ai-question\" bind-value=\"aiQuestion\" placeholder=\"Ask a follow-up...\" bind-disabled=\"!aiStepId || aiLoading\" class=\"min-w-0 flex-1 rounded-lg border border-gray-700 bg-gray-950 px-3 py-2 text-sm text-gray-200 outline-none placeholder:text-gray-600 focus:border-indigo-500 focus:ring-2 focus:ring-indigo-500/30 disabled:opacity-50\" />\n <button type=\"submit\" aria-label=\"Ask AI\" bind-disabled=\"!aiStepId || aiLoading\" class=\"inline-flex h-9 w-9 shrink-0 items-center justify-center rounded-lg bg-indigo-500 text-white outline-none hover:bg-indigo-400 focus-visible:ring-2 focus-visible:ring-indigo-400/60 disabled:opacity-50\"><lucide-icon icon=\"arrow-up\" size=\"16\"></lucide-icon></button>\n </div>\n </form>\n </aside>\n </template>\n <template if=\"report.canViewLogs && report.artifacts.length\">\n <details class=\"bg-gray-900 border border-gray-800 rounded-xl overflow-hidden\">\n <summary class=\"cursor-pointer px-4 py-3 text-xs font-semibold uppercase tracking-wider text-gray-400 outline-none transition hover:bg-gray-800 focus-visible:ring-2 focus-visible:ring-indigo-400/60\">\n Artifacts\n </summary>\n <ul class=\"divide-y divide-gray-800 border-t border-gray-800\">\n <template for=\"artifact of report.artifacts\">\n <li class=\"px-4 py-3\">\n <a\n attr-href=\"artifactUrl(artifact)\"\n class=\"font-mono text-sm text-indigo-300 hover:underline focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-400/60\"\n >{{ artifact }}</a>\n </li>\n </template>\n </ul>\n </details>\n </template>\n </div>\n\n <script setup>\n import { computed, onDestroy, onInit, ref } from '@li3/web';\n import { AnsiUp } from 'ansi_up';\n import { marked } from 'marked';\n\n export default function setup() {\n const jobId = window.location.pathname.split('/').filter(Boolean).pop() || '';\n const report = ref({ jobId });\n const previousRuns = ref([]);\n const aiMessages = ref([]);\n const aiStepId = ref('');\n const aiQuestion = ref('');\n const aiError = ref('');\n const aiLoading = ref(false);\n let previousRunsLoaded = false;\n const now = ref(Date.now());\n const ansiUp = new AnsiUp();\n ansiUp.use_classes = false;\n let eventSource;\n let refreshTimer;\n let clockTimer;\n let refreshing = false;\n let refreshPending = false;\n const cancelling = ref(false);\n\n const active = computed(() => ['pending', 'running'].includes(report.value.status));\n const inputsJson = computed(() => JSON.stringify(report.value.inputs || {}, null, 2));\n const timing = computed(() => {\n if (report.value.status === 'pending') return 'Waiting for a worker';\n if (report.value.status === 'running') {\n return `Running for ${Math.max(0, now.value - Date.parse(report.value.startedAt))}ms`;\n }\n return `Finished in ${report.value.durationMs}ms`;\n });\n\n const upper = (value) => String(value || '').toUpperCase();\n const artifactUrl = (path) => `/api/runs/${report.value.jobId}/artifacts/${encodeURIComponent(path)}`;\n const loadPreviousRuns = async () => {\n if (previousRunsLoaded || !report.value.parentId) return;\n const lineage = [];\n const visited = new Set();\n let parentId = report.value.parentId;\n while (parentId && !visited.has(String(parentId)) && lineage.length < 50) {\n visited.add(String(parentId));\n const response = await fetch(`/api/runs/${parentId}`, { headers: { accept: 'application/json' } });\n if (!response.ok) throw new Error(`Previous run request failed: ${response.status}`);\n const parent = await response.json();\n lineage.push({ id: parent.jobId, status: parent.status, startedAt: parent.startedAt });\n parentId = parent.parentId;\n }\n previousRuns.value = lineage;\n previousRunsLoaded = true;\n };\n const stepLog = (step) => {\n if (step.status === 'skipped') return '';\n if (step.status === 'running')\n return '<span class=\"text-indigo-400\">Step is running. Logs will appear after it finishes.</span>';\n if (step.status === 'pending') return '<span class=\"text-gray-500\">Waiting to run.</span>';\n if (step.logContent) return ansiUp.ansi_to_html(step.logContent);\n return '<span class=\"text-gray-500\">(No terminal log output recorded for this step)</span>';\n };\n const sanitizeAiHtml = (html) => {\n const parsed = new DOMParser().parseFromString(html, 'text/html');\n parsed.querySelectorAll('script,style,iframe,object,embed,form').forEach((node) => node.remove());\n parsed.querySelectorAll('*').forEach((element) => {\n [...element.attributes].forEach((attribute) => {\n if (attribute.name.toLowerCase().startsWith('on')) element.removeAttribute(attribute.name);\n if (['href', 'src'].includes(attribute.name.toLowerCase()) && /^(javascript|data):/i.test(attribute.value)) element.removeAttribute(attribute.name);\n });\n });\n return parsed.body.innerHTML;\n };\n const renderAiMarkdown = (content) => sanitizeAiHtml(marked.parse(content));\n const askAi = async (step, event) => {\n event.stopPropagation();\n aiStepId.value = step.id;\n aiMessages.value = [];\n aiQuestion.value = '';\n await requestAiHelp(step.id, 'Please explain why this step failed and what I should do next.');\n };\n const requestAiHelp = async (stepId, question) => {\n aiLoading.value = true;\n aiError.value = '';\n try {\n const response = await fetch(`/api/runs/${report.value.jobId}/ai-help`, {\n method: 'POST',\n headers: { accept: 'application/json', 'content-type': 'application/json' },\n body: JSON.stringify({ stepId, question, conversation: aiMessages.value }),\n });\n if (!response.ok) {\n const data = await response.json().catch(() => ({}));\n throw new Error(data.error || `AI help failed: ${response.status}`);\n }\n aiMessages.value = [...aiMessages.value, { role: 'user', content: question }, { role: 'assistant', content: '' }];\n const reader = response.body.getReader();\n const decoder = new TextDecoder();\n let buffer = '';\n while (true) {\n const chunk = await reader.read();\n if (chunk.done) break;\n buffer += decoder.decode(chunk.value, { stream: true });\n const events = buffer.split('\\n\\n');\n buffer = events.pop() || '';\n for (const event of events) {\n const line = event.split('\\n').find((value) => value.startsWith('data: '));\n if (!line) continue;\n const data = JSON.parse(line.slice(6));\n if (data.delta) {\n const messages = [...aiMessages.value];\n messages[messages.length - 1] = { ...messages[messages.length - 1], content: messages[messages.length - 1].content + data.delta };\n aiMessages.value = messages;\n }\n if (data.error) throw new Error(data.error);\n }\n }\n const messages = [...aiMessages.value];\n const answer = messages.at(-1);\n if (answer) messages[messages.length - 1] = { ...answer, html: renderAiMarkdown(answer.content) };\n aiMessages.value = messages;\n } catch (error) {\n aiError.value = error.message;\n } finally {\n aiLoading.value = false;\n }\n };\n const askFollowup = async () => {\n const question = aiQuestion.value.trim();\n if (!question || !aiStepId.value) return;\n aiQuestion.value = '';\n await requestAiHelp(aiStepId.value, question);\n };\n const restartJob = async () => {\n const response = await fetch(`/restart/${report.value.jobId}`, { method: 'POST' });\n if (response.ok) {\n const { id } = await response.json();\n location.href = `/runs/${id}`;\n }\n };\n const cancelJob = async () => {\n if (cancelling.value || report.value.status !== 'running') return;\n cancelling.value = true;\n try {\n const response = await fetch(`/api/jobs/${report.value.jobId}/cancel`, { method: 'POST' });\n if (!response.ok) throw new Error(`Cancel failed: ${response.status}`);\n await refreshRun();\n } catch (error) {\n console.error(error);\n } finally {\n cancelling.value = false;\n }\n };\n const refreshRun = async () => {\n if (refreshing) {\n refreshPending = true;\n return;\n }\n refreshing = true;\n\n try {\n const response = await fetch(`/api/runs/${jobId}`, {\n headers: { accept: 'application/json' },\n });\n if (!response.ok) throw new Error(`Run refresh failed: ${response.status}`);\n const previousStatus = report.value.status;\n const nextReport = await response.json();\n report.value = nextReport;\n document.title = `Run #${nextReport.jobId} - ${nextReport.workflowName}`;\n void loadPreviousRuns();\n if (\n ['pending', 'running'].includes(previousStatus) &&\n ['success', 'failed'].includes(nextReport.status) &&\n 'Notification' in window &&\n 'serviceWorker' in navigator &&\n Notification.permission === 'granted' &&\n localStorage.getItem('runner-notifications') === 'enabled'\n ) {\n const registration = await navigator.serviceWorker.ready;\n await registration.showNotification(\n nextReport.status === 'success'\n ? `Job #${nextReport.jobId} succeeded`\n : `Job #${nextReport.jobId} failed`,\n {\n body: `${nextReport.workflowName} finished with status ${nextReport.status}.`,\n icon: '/app-icon.svg',\n badge: '/app-icon.svg',\n tag: `runner-job-${nextReport.jobId}`,\n renotify: true,\n data: { url: `/runs/${nextReport.jobId}` },\n },\n );\n }\n } catch (error) {\n console.error(error);\n } finally {\n refreshing = false;\n if (refreshPending) {\n refreshPending = false;\n void refreshRun();\n }\n }\n };\n const handleJobChange = (event) => {\n const data = JSON.parse(event.data || '{}');\n if (!data.jobId || String(data.jobId) === String(report.value.jobId)) void refreshRun();\n };\n\n onInit(() => {\n void refreshRun();\n if ('serviceWorker' in navigator) {\n navigator.serviceWorker.register('/service-worker.js').catch((error) => {\n console.error('Service worker registration failed', error);\n });\n }\n eventSource = new EventSource('/api/events');\n eventSource.addEventListener('jobs.changed', handleJobChange);\n refreshTimer = setInterval(() => {\n if (active.value) void refreshRun();\n }, 60000);\n clockTimer = setInterval(() => {\n now.value = Date.now();\n }, 1000);\n });\n onDestroy(() => {\n eventSource?.close();\n clearInterval(refreshTimer);\n clearInterval(clockTimer);\n });\n\n return { report, previousRuns, aiMessages, aiStepId, aiQuestion, aiError, aiLoading, inputsJson, timing, upper, artifactUrl, stepLog, askAi, askFollowup, restartJob, cancelJob, cancelling };\n }\n <\/script>\n </template>\n\n <script type=\"module\">\n import '@li3/web';\n <\/script>\n <script src=\"https://cdn.tailwindcss.com\"><\/script>\n </body>\n</html>\n", Ie = /(?:^|[_-])auth(?:entication)?(?:$|[_-])|access[_-]?key|api[_-]?key|authorization|cookie|credential|passphrase|password|private[_-]?key|secret|session(?:id)?|signing[_-]?key|token/i;
518
+ }), Ne = "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 512 512\">\n <rect width=\"512\" height=\"512\" rx=\"112\" fill=\"#030712\"/>\n <path d=\"M128 144h256v224H128z\" fill=\"#111827\" stroke=\"#6366f1\" stroke-width=\"24\"/>\n <path d=\"m174 218 62 38-62 38M260 300h78\" fill=\"none\" stroke=\"#f9fafb\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"24\"/>\n <circle cx=\"365\" cy=\"147\" r=\"47\" fill=\"#10b981\" stroke=\"#030712\" stroke-width=\"18\"/>\n</svg>", Pe = "self.addEventListener('install', () => self.skipWaiting());\nself.addEventListener('activate', (event) => event.waitUntil(self.clients.claim()));\nself.addEventListener('push', (event) => {\n const data = event.data?.json() || {};\n event.waitUntil(self.registration.showNotification(data.title || 'Runner Engine', {\n body: data.body || 'A job status changed.',\n icon: '/app-icon.svg',\n badge: '/app-icon.svg',\n data: { url: data.url || '/runs' },\n }));\n});\nself.addEventListener('notificationclick', (event) => {\n event.notification.close();\n const target = event.notification.data?.url || '/runs';\n event.waitUntil((async () => {\n const windows = await self.clients.matchAll({ type: 'window', includeUncontrolled: true });\n const existing = windows.find((client) => new URL(client.url).pathname === target);\n if (existing) return existing.focus();\n return self.clients.openWindow(target);\n })());\n});", Fe = "<!doctype html>\n<html lang=\"en\" class=\"dark\">\n <head>\n <meta charset=\"UTF-8\" />\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />\n <meta name=\"theme-color\" content=\"#030712\" />\n <meta name=\"mobile-web-app-capable\" content=\"yes\" />\n <link rel=\"manifest\" href=\"/manifest.webmanifest\" />\n <link rel=\"icon\" href=\"/app-icon.svg\" type=\"image/svg+xml\" />\n <link rel=\"component\" href=\"https://sodium.static.apphor.de/lucide-icon.html\" />\n <link rel=\"component\" href=\"https://sodium.static.apphor.de/code-block.html\" />\n <link rel=\"component\" href=\"/app-header.html\" />\n <title>Workflow Run</title>\n <script type=\"importmap\">\n {\n \"imports\": {\n \"@li3/\": \"https://cdn.li3.dev/@li3/\",\n \"ansi_up\": \"https://esm.sh/ansi_up@6.0.6\",\n \"marked\": \"https://cdn.jsdelivr.net/npm/marked@18.0.12/lib/marked.esm.js\"\n }\n }\n <\/script>\n <style>\n </style>\n </head>\n <body class=\"bg-gray-950 text-gray-100 min-h-screen p-4 md:p-6 font-sans\">\n <template app>\n <div class=\"max-w-7xl mx-auto space-y-4 py-6\">\n <app-header bind-title=\"report.workflowName\" bind-subtitle=\"'Run #' + report.jobId\" back=\"/runs\" back-label=\"Back to jobs\"></app-header>\n <div class=\"flex flex-wrap items-center justify-between gap-3 border-b border-gray-800 pb-4\">\n <p class=\"text-xs text-gray-400\">\n Started {{ report.startedAt }} · {{ timing }}\n <template if=\"report.workerId\"> · worker: {{ report.workerId }}</template>\n <template if=\"report.parentId\"> · from #{{ report.parentId }}</template>\n </p>\n <div class=\"flex items-center gap-2 shrink-0\">\n <span\n class=\"px-4 py-1.5 rounded-full text-sm font-semibold border\"\n bind-class=\"{\n 'bg-emerald-500/10.text-emerald-400.border-emerald-500/20': report.status === 'success',\n 'bg-rose-500/10.text-rose-400.border-rose-500/20': report.status === 'failed',\n 'bg-indigo-500/10.text-indigo-400.border-indigo-500/20': report.status === 'running',\n 'bg-amber-500/10.text-amber-400.border-amber-500/20': !['success', 'failed', 'running'].includes(report.status)\n }\"\n >\n {{ upper(report.status) }}\n </span>\n <template if=\"report.status === 'running'\">\n <button\n class=\"text-sm text-rose-300 px-4 py-1.5 border border-rose-500/50 hover:border-rose-400 rounded-full disabled:opacity-50\"\n on-click=\"cancelJob()\"\n bind-disabled.bool=\"cancelling\"\n >\n {{ cancelling ? 'Cancelling...' : 'Cancel' }}\n </button>\n </template>\n <template if=\"report.status !== 'running'\">\n <div class=\"relative flex items-center gap-1\">\n <button class=\"text-sm text-white px-4 py-1.5 border border-gray-600 hover:border-gray-400 rounded-full\" on-click=\"restartJob()\">Restart</button>\n <details class=\"relative\">\n <summary class=\"cursor-pointer list-none rounded-full border border-gray-600 p-2 text-gray-300 outline-none hover:bg-gray-800 focus-visible:ring-2 focus-visible:ring-indigo-400/60\" aria-label=\"More restart options\">\n <lucide-icon icon=\"chevron-down\" size=\"14\"></lucide-icon>\n </summary>\n <div class=\"absolute right-0 z-20 mt-2 w-56 rounded-xl border border-gray-700 bg-gray-900 p-2 shadow-2xl\">\n <button type=\"button\" on-click=\"openManualRestart($event)\" class=\"w-full rounded-lg px-3 py-2 text-left text-sm text-gray-200 hover:bg-gray-800\">Restart with inputs</button>\n </div>\n </details>\n </div>\n </template>\n </div>\n </div>\n <template if=\"manualRestartOpen\">\n <div class=\"fixed inset-0 z-50 flex items-center justify-center bg-gray-950/80 p-4\" role=\"presentation\">\n <div class=\"w-full max-w-lg rounded-2xl border border-gray-700 bg-gray-900 p-5 shadow-2xl\" role=\"dialog\" aria-modal=\"true\" aria-labelledby=\"manual-restart-title\">\n <div class=\"flex items-start justify-between gap-3\"><div><h2 id=\"manual-restart-title\" class=\"text-lg font-semibold text-white\">Restart with inputs</h2><p class=\"mt-1 text-sm text-gray-400\">These values override the original trigger inputs for the new run.</p></div><button type=\"button\" on-click=\"closeManualRestart()\" aria-label=\"Close restart dialog\" class=\"rounded-lg p-2 text-gray-400 hover:bg-gray-800\"><lucide-icon icon=\"x\" size=\"17\"></lucide-icon></button></div>\n <div class=\"mt-5 space-y-3\">\n <template for=\"entry of manualInputs\">\n <div class=\"grid grid-cols-[1fr_1fr_auto] gap-2\"><input bind-value=\"entry.key\" placeholder=\"Key\" class=\"rounded-lg border border-gray-700 bg-gray-950 px-3 py-2 text-sm text-gray-200\" /><input bind-value=\"entry.value\" placeholder=\"Value\" class=\"rounded-lg border border-gray-700 bg-gray-950 px-3 py-2 text-sm text-gray-200\" /><button type=\"button\" on-click=\"removeManualInput(entry)\" aria-label=\"Remove input\" class=\"rounded-lg px-2 text-rose-300 hover:bg-rose-500/10\"><lucide-icon icon=\"x\" size=\"15\"></lucide-icon></button></div>\n </template>\n <button type=\"button\" on-click=\"addManualInput()\" class=\"text-sm text-indigo-300 hover:underline\">+ Add input</button>\n </div>\n <div class=\"mt-6 flex justify-end gap-2\"><button type=\"button\" on-click=\"closeManualRestart()\" class=\"rounded-lg border border-gray-700 px-3 py-2 text-sm text-gray-300 hover:bg-gray-800\">Cancel</button><button type=\"button\" on-click=\"restartWithInputs()\" bind-disabled=\"manualRestartLoading\" class=\"rounded-lg bg-indigo-500 px-3 py-2 text-sm font-semibold text-white hover:bg-indigo-400 disabled:opacity-50\">{{ manualRestartLoading ? 'Restarting...' : 'Restart' }}</button></div>\n </div>\n </div>\n </template>\n <template if=\"previousRuns.length\">\n <details class=\"rounded-xl border border-gray-800 bg-gray-900/70\">\n <summary class=\"cursor-pointer px-4 py-3 text-sm font-medium text-gray-300 outline-none hover:bg-gray-800 focus-visible:ring-2 focus-visible:ring-indigo-400/60\">\n Previous runs\n </summary>\n <ul class=\"divide-y divide-gray-800 border-t border-gray-800\">\n <template for=\"run of previousRuns\">\n <li>\n <a attr-href=\"'/runs/' + run.id\" class=\"flex items-center justify-between gap-3 px-4 py-3 text-sm outline-none hover:bg-gray-800 focus-visible:ring-2 focus-visible:ring-indigo-400/60\">\n <span><span class=\"font-mono text-indigo-300\">#{{ run.id }}</span><span class=\"ml-3 text-gray-400\">{{ run.startedAt }}</span></span>\n <span class=\"font-semibold uppercase text-xs text-gray-400\">{{ upper(run.status) }}</span>\n </a>\n </li>\n </template>\n </ul>\n </details>\n </template>\n\n <section class=\"rounded overflow-hidden border-b border-gray-800\">\n <h2 class=\"sr-only\">Execution Steps</h2>\n <template for=\"[step, index] of report.steps\">\n <details class=\"border border-b-0 border-gray-800 bg-gray-900/50 text-sm\">\n <summary\n class=\"flex flex-col md:flex-row md:items-center justify-between gap-2 p-3 bg-gray-800/40 border-b border-gray-800 cursor-pointer\"\n >\n <div class=\"flex items-center gap-3 min-w-0\">\n <lucide-icon\n class=\"flex h-7 w-7 shrink-0 items-center justify-center rounded-full bg-gray-700/70\"\n bind-class=\"{\n 'text-emerald-400': step.status === 'success',\n 'text-rose-400': step.status === 'failed',\n 'text-indigo-400.animate-spin': step.status === 'running',\n 'text-gray-400': !['success', 'failed', 'running'].includes(step.status)\n }\"\n bind-icon=\"\n step.status === 'success'\n ? 'circle-check'\n : step.status === 'failed'\n ? 'circle-x'\n : step.status === 'running'\n ? 'loader-circle'\n : 'circle'\n \"\n size=\"15\"\n ></lucide-icon>\n <template if=\"step.exitCode !== undefined && step.exitCode !== 0\">\n <span class=\"font-mono text-gray-500\">({{ step.exitCode }})</span>\n </template>\n <h3 class=\"font-semibold text-gray-200 truncate min-w-0 flex-1\">{{ step.name }}</h3>\n <div class=\"font-mono text-gray-400 shrink-0\">{{ step.durationMs }}ms</div>\n <span\n class=\"sr-only px-2.5 py-0.5 rounded-full font-medium\"\n bind-class=\"{\n 'text-emerald-400.bg-emerald-500/10': step.status === 'success',\n 'text-rose-400.bg-rose-500/10': step.status === 'failed',\n 'text-indigo-400.bg-indigo-500/10': step.status === 'running',\n 'text-gray-400.bg-gray-500/10': !['success', 'failed', 'running'].includes(step.status)\n }\"\n >\n {{ upper(step.status) }}\n </span>\n </div>\n </summary>\n <template if=\"report.canViewLogs\">\n <div class=\"relative p-4 bg-gray-950 font-mono text-gray-300 leading-relaxed overflow-auto w-full\">\n <template if=\"step.status === 'failed'\">\n <button type=\"button\" on-click=\"askAi(step, $event)\" aria-label=\"Get AI help for failed step\" title=\"Get AI help\" class=\"absolute right-3 top-3 z-10 inline-flex h-7 w-7 items-center justify-center rounded-lg bg-gray-900 text-indigo-300 outline-none hover:bg-indigo-500/10 focus-visible:ring-2 focus-visible:ring-indigo-400/60\">\n <lucide-icon bind-icon=\"aiLoading && aiStepId === step.id ? 'loader-circle' : 'sparkles'\" bind-class=\"aiLoading && aiStepId === step.id ? 'animate-spin' : ''\" size=\"16\"></lucide-icon>\n </button>\n </template>\n <pre class=\"whitespace-pre-wrap\" bind-innerhtml=\"stepLog(step)\"></pre>\n </div>\n </template>\n <template if=\"!report.canViewLogs\">\n <div class=\"p-4 bg-gray-950 text-sm text-gray-500\">Logs are available to authenticated users.</div>\n </template>\n </details>\n </template>\n </section>\n\n <details class=\"bg-gray-900 border border-gray-800 rounded-xl overflow-hidden\">\n <summary class=\"cursor-pointer px-4 py-3 text-xs font-semibold uppercase tracking-wider text-gray-400 outline-none transition hover:bg-gray-800 focus-visible:ring-2 focus-visible:ring-indigo-400/60\">\n Trigger Inputs\n </summary>\n <code-block\n language=\"json\"\n theme=\"atom-one-dark\"\n bind-source=\"inputsJson\"\n class=\"block max-h-[38rem] overflow-auto border-t border-gray-800 bg-gray-950 p-4\"\n ></code-block>\n </details>\n <template if=\"report.workflowSourceYaml\">\n <details class=\"bg-gray-900 border border-gray-800 rounded-xl overflow-hidden\">\n <summary class=\"cursor-pointer px-4 py-3 text-xs font-semibold uppercase tracking-wider text-gray-400 outline-none transition hover:bg-gray-800 focus-visible:ring-2 focus-visible:ring-indigo-400/60\">\n Workflow source YAML · revision {{ report.workflowRevision }}\n </summary>\n <code-block\n language=\"yaml\"\n theme=\"atom-one-dark\"\n bind-source=\"report.workflowSourceYaml\"\n class=\"block max-h-[38rem] overflow-auto border-t border-gray-800 bg-gray-950 p-4\"\n ></code-block>\n </details>\n </template>\n <template if=\"report.canViewLogs && aiStepId\">\n <aside class=\"fixed bottom-4 left-4 right-4 z-40 mt-4 flex max-h-[70vh] flex-col rounded-2xl border border-indigo-500/30 bg-gray-900/95 p-4 shadow-2xl backdrop-blur lg:left-auto lg:w-[28rem]\" aria-labelledby=\"ai-help-title\">\n <div class=\"flex items-start justify-between gap-3 border-b border-gray-800 pb-3\">\n <div><h2 id=\"ai-help-title\" class=\"font-semibold text-white\">AI help</h2><p class=\"mt-1 text-xs text-gray-500\">Ask about a failed step.</p></div>\n <lucide-icon icon=\"sparkles\" size=\"18\" class=\"text-indigo-300\"></lucide-icon>\n </div>\n <div class=\"min-h-0 flex-1 space-y-3 overflow-y-auto py-3\" aria-live=\"polite\">\n <p class=\"flex items-center gap-2 text-sm text-indigo-300\" class-hidden=\"!aiLoading\">\n <lucide-icon icon=\"loader-circle\" size=\"16\" class=\"animate-spin\"></lucide-icon>\n Thinking through the failed step...\n </p>\n <p class=\"text-sm text-gray-500\" class-hidden=\"aiMessages.length > 0\">Select the help icon beside a failed step to start.</p>\n <template for=\"message of aiMessages\">\n <div class=\"rounded-lg p-3 text-sm\" bind-class=\"message.role === 'assistant' ? 'bg-indigo-500/10.text-gray-200' : 'bg-gray-800.text-gray-300'\">\n <p class=\"mb-1 text-[10px] font-semibold uppercase tracking-wider text-gray-500\">{{ message.role }}</p>\n <template if=\"message.html\"><div class=\"ai-markdown text-sm leading-relaxed\" bind-innerhtml=\"message.html\"></div></template>\n <template if=\"!message.html\"><pre class=\"whitespace-pre-wrap font-sans leading-relaxed\">{{ message.content }}</pre></template>\n </div>\n </template>\n <p class=\"text-sm text-rose-300\" class-hidden=\"!aiError\">{{ aiError }}</p>\n </div>\n <form class=\"border-t border-gray-800 pt-3\" on-submit.prevent=\"askFollowup()\">\n <label class=\"sr-only\" for=\"ai-question\">Ask a follow-up question</label>\n <div class=\"flex gap-2\">\n <input id=\"ai-question\" bind-value=\"aiQuestion\" placeholder=\"Ask a follow-up...\" bind-disabled=\"!aiStepId || aiLoading\" class=\"min-w-0 flex-1 rounded-lg border border-gray-700 bg-gray-950 px-3 py-2 text-sm text-gray-200 outline-none placeholder:text-gray-600 focus:border-indigo-500 focus:ring-2 focus:ring-indigo-500/30 disabled:opacity-50\" />\n <button type=\"submit\" aria-label=\"Ask AI\" bind-disabled=\"!aiStepId || aiLoading\" class=\"inline-flex h-9 w-9 shrink-0 items-center justify-center rounded-lg bg-indigo-500 text-white outline-none hover:bg-indigo-400 focus-visible:ring-2 focus-visible:ring-indigo-400/60 disabled:opacity-50\"><lucide-icon icon=\"arrow-up\" size=\"16\"></lucide-icon></button>\n </div>\n </form>\n </aside>\n </template>\n <template if=\"report.canViewLogs && report.artifacts.length\">\n <details class=\"bg-gray-900 border border-gray-800 rounded-xl overflow-hidden\">\n <summary class=\"cursor-pointer px-4 py-3 text-xs font-semibold uppercase tracking-wider text-gray-400 outline-none transition hover:bg-gray-800 focus-visible:ring-2 focus-visible:ring-indigo-400/60\">\n Artifacts\n </summary>\n <ul class=\"divide-y divide-gray-800 border-t border-gray-800\">\n <template for=\"artifact of report.artifacts\">\n <li class=\"px-4 py-3\">\n <a\n attr-href=\"artifactUrl(artifact)\"\n class=\"font-mono text-sm text-indigo-300 hover:underline focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-400/60\"\n >{{ artifact }}</a>\n </li>\n </template>\n </ul>\n </details>\n </template>\n </div>\n\n <script setup>\n import { computed, onDestroy, onInit, ref } from '@li3/web';\n import { AnsiUp } from 'ansi_up';\n import { marked } from 'marked';\n\n export default function setup() {\n const jobId = window.location.pathname.split('/').filter(Boolean).pop() || '';\n const report = ref({ jobId });\n const previousRuns = ref([]);\n const aiMessages = ref([]);\n const aiStepId = ref('');\n const aiQuestion = ref('');\n const aiError = ref('');\n const aiLoading = ref(false);\n const manualRestartOpen = ref(false);\n const manualRestartLoading = ref(false);\n const manualInputs = ref([]);\n let previousRunsLoaded = false;\n const now = ref(Date.now());\n const ansiUp = new AnsiUp();\n ansiUp.use_classes = false;\n let eventSource;\n let refreshTimer;\n let clockTimer;\n let refreshing = false;\n let refreshPending = false;\n const cancelling = ref(false);\n\n const active = computed(() => ['pending', 'running'].includes(report.value.status));\n const inputsJson = computed(() => JSON.stringify(report.value.inputs || {}, null, 2));\n const timing = computed(() => {\n if (report.value.status === 'pending') return 'Waiting for a worker';\n if (report.value.status === 'running') {\n return `Running for ${Math.max(0, now.value - Date.parse(report.value.startedAt))}ms`;\n }\n return `Finished in ${report.value.durationMs}ms`;\n });\n\n const upper = (value) => String(value || '').toUpperCase();\n const artifactUrl = (path) => `/api/runs/${report.value.jobId}/artifacts/${encodeURIComponent(path)}`;\n const loadPreviousRuns = async () => {\n if (previousRunsLoaded || !report.value.parentId) return;\n const lineage = [];\n const visited = new Set();\n let parentId = report.value.parentId;\n while (parentId && !visited.has(String(parentId)) && lineage.length < 50) {\n visited.add(String(parentId));\n const response = await fetch(`/api/runs/${parentId}`, { headers: { accept: 'application/json' } });\n if (!response.ok) throw new Error(`Previous run request failed: ${response.status}`);\n const parent = await response.json();\n lineage.push({ id: parent.jobId, status: parent.status, startedAt: parent.startedAt });\n parentId = parent.parentId;\n }\n previousRuns.value = lineage;\n previousRunsLoaded = true;\n };\n const stepLog = (step) => {\n if (step.status === 'skipped') return '';\n if (step.status === 'running')\n return '<span class=\"text-indigo-400\">Step is running. Logs will appear after it finishes.</span>';\n if (step.status === 'pending') return '<span class=\"text-gray-500\">Waiting to run.</span>';\n if (step.logContent) return ansiUp.ansi_to_html(step.logContent);\n return '<span class=\"text-gray-500\">(No terminal log output recorded for this step)</span>';\n };\n const sanitizeAiHtml = (html) => {\n const parsed = new DOMParser().parseFromString(html, 'text/html');\n parsed.querySelectorAll('script,style,iframe,object,embed,form').forEach((node) => node.remove());\n parsed.querySelectorAll('*').forEach((element) => {\n [...element.attributes].forEach((attribute) => {\n if (attribute.name.toLowerCase().startsWith('on')) element.removeAttribute(attribute.name);\n if (['href', 'src'].includes(attribute.name.toLowerCase()) && /^(javascript|data):/i.test(attribute.value)) element.removeAttribute(attribute.name);\n });\n });\n return parsed.body.innerHTML;\n };\n const renderAiMarkdown = (content) => sanitizeAiHtml(marked.parse(content));\n const askAi = async (step, event) => {\n event.stopPropagation();\n aiStepId.value = step.id;\n aiMessages.value = [];\n aiQuestion.value = '';\n await requestAiHelp(step.id, 'Please explain why this step failed and what I should do next.');\n };\n const requestAiHelp = async (stepId, question) => {\n aiLoading.value = true;\n aiError.value = '';\n try {\n const response = await fetch(`/api/runs/${report.value.jobId}/ai-help`, {\n method: 'POST',\n headers: { accept: 'application/json', 'content-type': 'application/json' },\n body: JSON.stringify({ stepId, question, conversation: aiMessages.value }),\n });\n if (!response.ok) {\n const data = await response.json().catch(() => ({}));\n throw new Error(data.error || `AI help failed: ${response.status}`);\n }\n aiMessages.value = [...aiMessages.value, { role: 'user', content: question }, { role: 'assistant', content: '' }];\n const reader = response.body.getReader();\n const decoder = new TextDecoder();\n let buffer = '';\n while (true) {\n const chunk = await reader.read();\n if (chunk.done) break;\n buffer += decoder.decode(chunk.value, { stream: true });\n const events = buffer.split('\\n\\n');\n buffer = events.pop() || '';\n for (const event of events) {\n const line = event.split('\\n').find((value) => value.startsWith('data: '));\n if (!line) continue;\n const data = JSON.parse(line.slice(6));\n if (data.delta) {\n const messages = [...aiMessages.value];\n messages[messages.length - 1] = { ...messages[messages.length - 1], content: messages[messages.length - 1].content + data.delta };\n aiMessages.value = messages;\n }\n if (data.error) throw new Error(data.error);\n }\n }\n const messages = [...aiMessages.value];\n const answer = messages.at(-1);\n if (answer) messages[messages.length - 1] = { ...answer, html: renderAiMarkdown(answer.content) };\n aiMessages.value = messages;\n } catch (error) {\n aiError.value = error.message;\n } finally {\n aiLoading.value = false;\n }\n };\n const askFollowup = async () => {\n const question = aiQuestion.value.trim();\n if (!question || !aiStepId.value) return;\n aiQuestion.value = '';\n await requestAiHelp(aiStepId.value, question);\n };\n const restartJob = async () => {\n const response = await fetch(`/restart/${report.value.jobId}`, { method: 'POST' });\n if (response.ok) {\n const { id } = await response.json();\n location.href = `/runs/${id}`;\n }\n };\n const openManualRestart = (event) => {\n event.preventDefault();\n event.stopPropagation();\n manualInputs.value = Object.entries(report.value.inputs || {}).map(([key, value]) => ({ key, value: typeof value === 'string' ? value : JSON.stringify(value) }));\n if (!manualInputs.value.length) manualInputs.value = [{ key: '', value: '' }];\n manualRestartOpen.value = true;\n };\n const closeManualRestart = () => { manualRestartOpen.value = false; };\n const addManualInput = () => { manualInputs.value = [...manualInputs.value, { key: '', value: '' }]; };\n const removeManualInput = (entry) => { manualInputs.value = manualInputs.value.filter((item) => item !== entry); };\n const restartWithInputs = async () => {\n const inputs = Object.fromEntries(manualInputs.value.filter((entry) => entry.key.trim()).map((entry) => [entry.key.trim(), entry.value]));\n manualRestartLoading.value = true;\n try {\n const response = await fetch(`/restart/${report.value.jobId}`, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ inputs }) });\n if (!response.ok) throw new Error(`Restart failed: ${response.status}`);\n const { id } = await response.json();\n location.href = `/runs/${id}`;\n } catch (error) {\n aiError.value = error.message;\n } finally {\n manualRestartLoading.value = false;\n }\n };\n const cancelJob = async () => {\n if (cancelling.value || report.value.status !== 'running') return;\n cancelling.value = true;\n try {\n const response = await fetch(`/api/jobs/${report.value.jobId}/cancel`, { method: 'POST' });\n if (!response.ok) throw new Error(`Cancel failed: ${response.status}`);\n await refreshRun();\n } catch (error) {\n console.error(error);\n } finally {\n cancelling.value = false;\n }\n };\n const refreshRun = async () => {\n if (refreshing) {\n refreshPending = true;\n return;\n }\n refreshing = true;\n\n try {\n const response = await fetch(`/api/runs/${jobId}`, {\n headers: { accept: 'application/json' },\n });\n if (!response.ok) throw new Error(`Run refresh failed: ${response.status}`);\n const previousStatus = report.value.status;\n const nextReport = await response.json();\n report.value = nextReport;\n document.title = `Run #${nextReport.jobId} - ${nextReport.workflowName}`;\n void loadPreviousRuns();\n if (\n ['pending', 'running'].includes(previousStatus) &&\n ['success', 'failed'].includes(nextReport.status) &&\n 'Notification' in window &&\n 'serviceWorker' in navigator &&\n Notification.permission === 'granted' &&\n localStorage.getItem('runner-notifications') === 'enabled'\n ) {\n const registration = await navigator.serviceWorker.ready;\n await registration.showNotification(\n nextReport.status === 'success'\n ? `Job #${nextReport.jobId} succeeded`\n : `Job #${nextReport.jobId} failed`,\n {\n body: `${nextReport.workflowName} finished with status ${nextReport.status}.`,\n icon: '/app-icon.svg',\n badge: '/app-icon.svg',\n tag: `runner-job-${nextReport.jobId}`,\n renotify: true,\n data: { url: `/runs/${nextReport.jobId}` },\n },\n );\n }\n } catch (error) {\n console.error(error);\n } finally {\n refreshing = false;\n if (refreshPending) {\n refreshPending = false;\n void refreshRun();\n }\n }\n };\n const handleJobChange = (event) => {\n const data = JSON.parse(event.data || '{}');\n if (!data.jobId || String(data.jobId) === String(report.value.jobId)) void refreshRun();\n };\n\n onInit(() => {\n void refreshRun();\n if ('serviceWorker' in navigator) {\n navigator.serviceWorker.register('/service-worker.js').catch((error) => {\n console.error('Service worker registration failed', error);\n });\n }\n eventSource = new EventSource('/api/events');\n eventSource.addEventListener('jobs.changed', handleJobChange);\n refreshTimer = setInterval(() => {\n if (active.value) void refreshRun();\n }, 60000);\n clockTimer = setInterval(() => {\n now.value = Date.now();\n }, 1000);\n });\n onDestroy(() => {\n eventSource?.close();\n clearInterval(refreshTimer);\n clearInterval(clockTimer);\n });\n\n return { report, previousRuns, manualRestartOpen, manualRestartLoading, manualInputs, aiMessages, aiStepId, aiQuestion, aiError, aiLoading, inputsJson, timing, upper, artifactUrl, stepLog, askAi, askFollowup, restartJob, openManualRestart, closeManualRestart, addManualInput, removeManualInput, restartWithInputs, cancelJob, cancelling };\n }\n <\/script>\n </template>\n\n <script type=\"module\">\n import '@li3/web';\n <\/script>\n <script src=\"https://cdn.tailwindcss.com\"><\/script>\n </body>\n</html>\n", Ie = /(?:^|[_-])auth(?:entication)?(?:$|[_-])|access[_-]?key|api[_-]?key|authorization|cookie|credential|passphrase|password|private[_-]?key|secret|session(?:id)?|signing[_-]?key|token/i;
511
519
  function Le(e, t, n, r = [], i = !0, a) {
512
520
  let o = JSON.parse(e.payload), s = e.report ? JSON.parse(e.report) : ze(e, o, r), c = e.status;
513
521
  return {
package/dist/queue.d.ts CHANGED
@@ -21,7 +21,7 @@ export declare class QueueManager {
21
21
  finishJob(jobId: string | number, status: JobStatus): Promise<void>;
22
22
  completeJob(jobId: string | number, status: JobStatus, report: WorkflowExecutionReport): Promise<void>;
23
23
  cancelJob(jobId: string | number): Promise<'cancelled' | 'not_found' | 'not_active'>;
24
- restartJob(jobId: string | number): Promise<any>;
24
+ restartJob(jobId: string | number, manualInputs?: Record<string, unknown>): Promise<any>;
25
25
  /**
26
26
  * Checks if the current job has been marked for cancellation by another event
27
27
  */
@@ -1 +1 @@
1
- {"version":3,"file":"queue.d.ts","sourceRoot":"","sources":["../src/queue.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,uBAAuB,EAAE,UAAU,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAIvF,qBAAa,YAAY;IAGX,OAAO,CAAC,QAAQ;IAF5B,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAqB;gBAE7B,QAAQ,EAAE,MAAM;IAE9B,IAAI;IAKV;;;OAGG;IACG,OAAO,CAAC,UAAU,EAAE,MAAM,EAAE,gBAAgB,EAAE,MAAM,EAAE,OAAO,EAAE,UAAU,EAAE,YAAY,GAAE,MAAM,EAAO,EAAE,cAAc,CAAC,EAAE,MAAM;IAoBrI;;;OAGG;IACG,YAAY,CAAC,UAAU,GAAE,MAAM,EAAO,GAAG,OAAO,CAAC,SAAS,GAAG,IAAI,CAAC;IA6BlE,UAAU,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAOvD;;OAEG;IACG,SAAS,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,MAAM,EAAE,SAAS;IAInD,WAAW,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,uBAAuB,GAAG,OAAO,CAAC,IAAI,CAAC;IAUtG,SAAS,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,OAAO,CAAC,WAAW,GAAG,WAAW,GAAG,YAAY,CAAC;IAapF,UAAU,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM;IA+BvC;;OAEG;IACG,WAAW,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAKrD,cAAc;IAMd,YAAY;IAgDlB;;OAEG;IACG,MAAM,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,OAAO,CAAC,GAAG,CAAC;IAIlD;;OAEG;IACG,QAAQ,CAAC,KAAK,SAAK,EAAE,OAAO,CAAC,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,EAAE,UAAU,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,GAAG,EAAE,CAAC;IAkCrH;;OAEG;IACG,UAAU,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,MAAM,EAAE,uBAAuB,GAAG,OAAO,CAAC,IAAI,CAAC;IAOxF,2DAA2D;IACrD,WAAW,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAU5F;;OAEG;IACG,UAAU,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAUnE,eAAe,CAAC,IAAI,EAAE,UAAU,GAAG,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC;IAW7H,cAAc,CAAC,IAAI,EAAE,UAAU,GAAG,OAAO,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAK/G,aAAa,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,WAAW,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC;CAGlG"}
1
+ {"version":3,"file":"queue.d.ts","sourceRoot":"","sources":["../src/queue.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,uBAAuB,EAAE,UAAU,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAIvF,qBAAa,YAAY;IAGX,OAAO,CAAC,QAAQ;IAF5B,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAqB;gBAE7B,QAAQ,EAAE,MAAM;IAE9B,IAAI;IAKV;;;OAGG;IACG,OAAO,CAAC,UAAU,EAAE,MAAM,EAAE,gBAAgB,EAAE,MAAM,EAAE,OAAO,EAAE,UAAU,EAAE,YAAY,GAAE,MAAM,EAAO,EAAE,cAAc,CAAC,EAAE,MAAM;IAoBrI;;;OAGG;IACG,YAAY,CAAC,UAAU,GAAE,MAAM,EAAO,GAAG,OAAO,CAAC,SAAS,GAAG,IAAI,CAAC;IA6BlE,UAAU,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAOvD;;OAEG;IACG,SAAS,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,MAAM,EAAE,SAAS;IAInD,WAAW,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,uBAAuB,GAAG,OAAO,CAAC,IAAI,CAAC;IAUtG,SAAS,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,OAAO,CAAC,WAAW,GAAG,WAAW,GAAG,YAAY,CAAC;IAapF,UAAU,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,YAAY,GAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAM;IAiCnF;;OAEG;IACG,WAAW,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAKrD,cAAc;IAMd,YAAY;IAgDlB;;OAEG;IACG,MAAM,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,OAAO,CAAC,GAAG,CAAC;IAIlD;;OAEG;IACG,QAAQ,CAAC,KAAK,SAAK,EAAE,OAAO,CAAC,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,EAAE,UAAU,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,GAAG,EAAE,CAAC;IAkCrH;;OAEG;IACG,UAAU,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,MAAM,EAAE,uBAAuB,GAAG,OAAO,CAAC,IAAI,CAAC;IAOxF,2DAA2D;IACrD,WAAW,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAU5F;;OAEG;IACG,UAAU,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAUnE,eAAe,CAAC,IAAI,EAAE,UAAU,GAAG,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC;IAW7H,cAAc,CAAC,IAAI,EAAE,UAAU,GAAG,OAAO,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAK/G,aAAa,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,WAAW,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC;CAGlG"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cloud-cli/on",
3
- "version": "1.53.0",
3
+ "version": "1.54.0",
4
4
  "description": "CLI entry point for the on plugin ecosystem.",
5
5
  "type": "module",
6
6
  "packageManager": "pnpm@12.3.4",