@cloud-cli/on 1.8.1 → 1.9.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/on.js CHANGED
@@ -63,20 +63,22 @@ async function le(e) {
63
63
  return t(o.workflows) ? (ce(o.database), o) : (console.warn(`⚠️ Warning: Workflows directory '${o.workflows}' not found.`), null);
64
64
  }
65
65
  function ue(e, t) {
66
- let n = process.env;
66
+ let n = process.env, r = Number(e.port || t.port || n.PORT || 11235), i = e.tags ?? (n.RUNNER_TAGS ? n.RUNNER_TAGS.split(",") : []);
67
67
  return {
68
- port: Number(e.port || t.port || n.PORT || 11235),
68
+ port: r,
69
69
  adminToken: e.adminToken ?? n.RUNNER_ADMIN_SECRET ?? "",
70
70
  database: e.database ?? t.database ?? n.RUNNER_DATABASE_URL ?? "",
71
71
  workflows: e.workflows ?? t.workflows ?? n.RUNNER_WORKFLOWS ?? "on/",
72
72
  workers: Number(e.workers ?? t.workers ?? n.RUNNER_WORKERS ?? 5),
73
+ serverUrl: e.serverUrl ?? n.RUNNER_SERVER_URL ?? `http://127.0.0.1:${r}`,
74
+ tags: i.map((e) => e.trim()).filter(Boolean),
73
75
  storagePath: e.storagePath ?? n.RUNNER_TMP ?? "/tmp/workspaces",
74
76
  env: e.env ?? {},
75
- reporters: e.reporters ?? []
77
+ plugins: e.plugins ?? []
76
78
  };
77
79
  }
78
80
  function de() {
79
- console.log("\n🏃 Runner CLI 🏃\n\nUsage:\n npx -y @cloud-cli/on <command> [options]\n pnpm dlx -y @cloud-cli/on <command> [options]\n\nCommands:\n start Runs both Webhook Ingress Server and Workers (Default)\n start-server Runs Webhook Ingress Server only (API Gateway mode)\n start-workers Runs Worker Polling loops only (Scalable Worker mode)\n validate Parses and validates workflow YAML files without running\n\nOptions:\n -c, --config Path to runner.config.mjs (default: ./runner.config.mjs, env: RUNNER_CONFIG_PATH)\n -d, --database SQLite Database URL (env: RUNNER_DATABASE_URL)\n -w, --workflows Path to where your workflows are defined (default: on/, env: RUNNER_WORKFLOWS_PATH)\n -p, --port Port for Webhook Ingress Server (default: 11235, env: PORT)\n -k, --workers Number of worker thread loops to spawn (default: 5, env: RUNNER_WORKERS)\n -h, --help Show this help message\n ");
81
+ console.log("\n🏃 Runner CLI 🏃\n\nUsage:\n npx -y @cloud-cli/on <command> [options]\n pnpm dlx -y @cloud-cli/on <command> [options]\n\nCommands:\n start Runs both Webhook Ingress Server and Workers (Default)\n start-server Runs Webhook Ingress Server only (API Gateway mode)\n start-workers Runs event-driven workers (Scalable Worker mode)\n validate Parses and validates workflow YAML files without running\n\nOptions:\n -c, --config Path to runner.config.mjs (default: ./runner.config.mjs, env: RUNNER_CONFIG_PATH)\n -d, --database SQLite Database URL (env: RUNNER_DATABASE_URL)\n -w, --workflows Path to where your workflows are defined (default: on/, env: RUNNER_WORKFLOWS_PATH)\n -p, --port Port for Webhook Ingress Server (default: 11235, env: PORT)\n -k, --workers Maximum concurrent jobs (default: 5, env: RUNNER_WORKERS)\n Worker tags (comma-separated env: RUNNER_TAGS)\n Webhook server URL (env: RUNNER_SERVER_URL)\n -h, --help Show this help message\n ");
80
82
  }
81
83
  async function fe() {
82
84
  let { values: e, positionals: t } = l({
@@ -4462,8 +4464,11 @@ var pi = class {
4462
4464
  JSON.stringify(t)
4463
4465
  ]);
4464
4466
  }
4465
- async claimNextJob() {
4466
- return await S.get("\n UPDATE jobs\n SET\n status = 'running',\n worker_id = ?,\n started_at = CURRENT_TIMESTAMP\n WHERE id = (\n SELECT id FROM jobs\n WHERE status = 'pending'\n ORDER BY created_at ASC\n LIMIT 1\n )\n RETURNING *;\n ", [this.workerId]) || null;
4467
+ async claimNextJob(e = []) {
4468
+ return await S.get("\n UPDATE jobs\n SET\n status = 'running',\n worker_id = ?,\n started_at = CURRENT_TIMESTAMP\n WHERE id = (\n SELECT id FROM jobs\n WHERE status = 'pending'\n AND NOT EXISTS (\n SELECT 1\n FROM json_each(COALESCE(json_extract(jobs.payload, '$.tags'), '[]')) AS required_tag\n WHERE required_tag.value NOT IN (SELECT value FROM json_each(?))\n )\n ORDER BY created_at ASC\n LIMIT 1\n )\n RETURNING *;\n ", [this.workerId, JSON.stringify(e)]) || null;
4469
+ }
4470
+ async releaseJob(e) {
4471
+ await S.run("UPDATE jobs SET status = 'pending', worker_id = NULL, started_at = NULL WHERE id = ? AND status = 'running';", [e]);
4467
4472
  }
4468
4473
  async finishJob(e, t) {
4469
4474
  await S.run("UPDATE jobs SET status = ?, finished_at = CURRENT_TIMESTAMP WHERE id = ?;", [t, e]);
@@ -7949,7 +7954,7 @@ var Mo = class {
7949
7954
  owner: c,
7950
7955
  repo: l,
7951
7956
  clone_url: n.repository?.clone_url,
7952
- commit_sha: n.after || n.head_commit?.id,
7957
+ commit_sha: n.after || n.head_commit?.id || n.pull_request?.head?.sha,
7953
7958
  author: n.pusher?.name || n.sender?.login,
7954
7959
  action: n.action,
7955
7960
  raw: n,
@@ -7977,29 +7982,7 @@ var Mo = class {
7977
7982
  inputs: e
7978
7983
  };
7979
7984
  }
7980
- };
7981
- //#endregion
7982
- //#region src/html-state.ts
7983
- function No(e) {
7984
- return JSON.stringify(e).replace(/[<>&\u2028\u2029]/g, (e) => `\\u${e.charCodeAt(0).toString(16).padStart(4, "0")}`);
7985
- }
7986
- //#endregion
7987
- //#region src/reporters/html.reporter.html?raw
7988
- var Po = "<!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 <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 }\n }\n <\/script>\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-5xl mx-auto space-y-4\">\n <header class=\"flex flex-col md:flex-row md:items-center justify-between border-b border-gray-800 pb-6 gap-4\">\n <div class=\"min-w-0\">\n <a href=\"/runs\" class=\"text-xs text-indigo-400 hover:underline mb-1 inline-block\">Back to Dashboard</a>\n <h1 class=\"text-2xl font-bold text-white flex flex-wrap items-center gap-3\">\n {{ report.workflowName }}\n <span class=\"text-sm font-mono text-gray-500\">#{{ report.jobId }}</span>\n <template if=\"report.parentId\">\n <a attr-href=\"'/runs/' + report.parentId\" class=\"text-sm font-mono text-indigo-400 hover:underline\">\n from #{{ report.parentId }}\n </a>\n </template>\n </h1>\n <p class=\"text-xs text-gray-400 mt-1\">Started {{ report.startedAt }} · {{ timing }}</p>\n </div>\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 <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 </div>\n </header>\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 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 <div class=\"flex flex-wrap items-center gap-3 min-w-0\">\n <span class=\"font-mono text-gray-500\">\n #{{ index + 1 }}<template if=\"step.exitCode !== undefined && step.exitCode !== 0\"> ({{ step.exitCode }})</template>\n </span>\n <h3 class=\"font-semibold text-gray-200 break-words\">{{ step.name }}</h3>\n <span\n class=\"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 <div class=\"font-mono text-gray-400 shrink-0\">{{ step.durationMs }}ms</div>\n </summary>\n <div class=\"p-4 bg-gray-950 font-mono text-gray-300 leading-relaxed overflow-auto w-full\">\n <pre class=\"whitespace-pre-wrap\" bind-innerhtml=\"stepLog(step)\"></pre>\n </div>\n </details>\n </template>\n </section>\n\n <details class=\"bg-gray-900 border border-gray-800 rounded-xl p-4 overflow-auto max-h-[400px]\">\n <summary class=\"cursor-pointer\">\n <h2 class=\"inline text-xs font-semibold text-gray-400 uppercase tracking-wider\">Trigger Inputs</h2>\n </summary>\n <pre class=\"font-mono text-xs text-gray-200 bg-gray-950 p-3 mt-3 rounded-lg overflow-x-auto\">{{ inputsJson }}</pre>\n </details>\n </div>\n\n <script setup>\n import { computed, onInit, ref } from '@li3/web';\n import { AnsiUp } from 'ansi_up';\n\n export default function setup() {\n const report = ref({});\n const ansiUp = new AnsiUp();\n ansiUp.use_classes = 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') return `Running for ${report.value.durationMs}ms`;\n return `Finished in ${report.value.durationMs}ms`;\n });\n\n const upper = (value) => String(value || '').toUpperCase();\n const stepLog = (step) => {\n if (step.status === 'skipped') return '';\n if (step.status === 'running') 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 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\n onInit(() => {\n document.title = `Run #${report.value.jobId} - ${report.value.workflowName}`;\n if (active.value) setTimeout(() => location.reload(), 3000);\n });\n\n return { report, inputsJson, timing, upper, stepLog, restartJob };\n }\n <\/script>\n <script state>__REPORT_STATE__<\/script>\n </template>\n\n <script type=\"module\">import '@li3/web';<\/script>\n <script src=\"https://cdn.tailwindcss.com\"><\/script>\n</body>\n</html>\n", Fo = "__REPORT_STATE__", Io = class {
7989
- name = "html";
7990
- outputDir;
7991
- constructor(e) {
7992
- this.outputDir = e.outputDir;
7993
- }
7994
- async report(t) {
7995
- e.mkdirSync(this.outputDir, { recursive: !0 });
7996
- let n = this.generateHtml(t), r = a.join(this.outputDir, `run-${t.jobId}.html`);
7997
- e.writeFileSync(r, n, "utf-8"), console.log(`📊 HTML Execution Report generated: ${r}`);
7998
- }
7999
- generateHtml(e) {
8000
- return Po.replace(Fo, () => No({ report: e }));
8001
- }
8002
- }, Lo = class e {
7985
+ }, No = class e {
8003
7986
  static async from(t) {
8004
7987
  let n = o(t) ? c("/", t) : s(process.cwd(), c("/", t)), r = (await _(n, { withFileTypes: !0 })).filter((e) => e.isFile() && (e.name.endsWith(".yml") || e.name.endsWith(".yaml"))).map((e) => s(n, e.name)), i = [], a = new di(t);
8005
7988
  for (let t of r) i.push(...await e.loadFile(t, a));
@@ -8024,7 +8007,8 @@ var Po = "<!DOCTYPE html>\n<html lang=\"en\" class=\"dark\">\n<head>\n <meta ch
8024
8007
  },
8025
8008
  concurrency: t.concurrency,
8026
8009
  steps: t.steps,
8027
- env: t.env
8010
+ env: t.env,
8011
+ tags: Array.isArray(t.tags) ? [...new Set(t.tags.filter((e) => typeof e == "string").map((e) => e.trim()).filter(Boolean))] : void 0
8028
8012
  });
8029
8013
  }
8030
8014
  } catch (t) {
@@ -8032,10 +8016,15 @@ var Po = "<!DOCTYPE html>\n<html lang=\"en\" class=\"dark\">\n<head>\n <meta ch
8032
8016
  }
8033
8017
  return n;
8034
8018
  }
8035
- }, Ro = "<!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 <title>Workflow Engine Dashboard</title>\n <script type=\"importmap\">\n { \"imports\": { \"@li3/\": \"https://cdn.li3.dev/@li3/\" } }\n <\/script>\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 <main class=\"max-w-6xl mx-auto space-y-6\">\n <header class=\"flex flex-col sm:flex-row sm:items-center justify-between gap-3 border-b border-gray-800 pb-4\">\n <div>\n <h1 class=\"text-2xl font-bold text-white\">Runner Engine Status</h1>\n <p class=\"text-xs text-gray-400\">Live Job Queue &amp; Execution Traces</p>\n </div>\n <div class=\"flex items-center gap-3 text-xs font-mono\">\n <span class=\"text-gray-500\">Updated {{ lastUpdated }}</span>\n <span\n class=\"px-3 py-1 rounded-full border\"\n bind-class=\"{\n 'bg-emerald-500/10.text-emerald-400.border-emerald-500/20': !refreshError,\n 'bg-rose-500/10.text-rose-400.border-rose-500/20': refreshError\n }\"\n >\n {{ refreshError ? 'Refresh failed' : 'System operational' }}\n </span>\n </div>\n </header>\n\n <section class=\"bg-gray-900 border border-gray-800 rounded-xl overflow-hidden hidden md:block\">\n <table class=\"w-full text-left text-sm\">\n <thead class=\"bg-gray-800/50 text-gray-400 text-xs uppercase font-mono border-b border-gray-800\">\n <tr>\n <th class=\"py-3 px-4\">Job ID</th>\n <th class=\"py-3 px-4\">Workflow</th>\n <th class=\"py-3 px-4\">Status</th>\n <th class=\"py-3 px-4 hidden xl:table-cell\">Worker</th>\n <th class=\"py-3 px-4 hidden lg:table-cell\">Created At</th>\n <th class=\"py-3 px-4 text-right\">Action</th>\n </tr>\n </thead>\n <tbody>\n <template for=\"job of jobs\">\n <tr class=\"border-b border-gray-800 hover:bg-gray-800/40 transition\">\n <td class=\"py-3 px-4 font-mono text-indigo-400\">\n <a attr-href=\"'/runs/' + job.id\" class=\"hover:underline\">#{{ job.id }}</a>\n </td>\n <td class=\"py-3 px-4 font-medium text-white\">{{ job.workflowId }}</td>\n <td class=\"py-3 px-4\">\n <span\n class=\"px-2.5 py-0.5 rounded-full text-xs font-semibold\"\n bind-class=\"{\n 'bg-emerald-500/10.text-emerald-400': job.status === 'success',\n 'bg-rose-500/10.text-rose-400': job.status === 'failed',\n 'bg-indigo-500/10.text-indigo-400.animate-pulse': job.status === 'running',\n 'bg-gray-500/10.text-gray-400': !['success', 'failed', 'running'].includes(job.status)\n }\"\n >\n {{ upper(job.status) }}\n </span>\n </td>\n <td class=\"py-3 px-4 hidden xl:table-cell text-xs font-mono text-gray-400\">{{ job.workerId || '-' }}</td>\n <td class=\"py-3 px-4 hidden lg:table-cell text-xs text-gray-400\">{{ job.createdAt }}</td>\n <td class=\"py-3 px-4 text-right whitespace-nowrap\">\n <a attr-href=\"'/runs/' + job.id\" class=\"text-xs bg-gray-800 hover:bg-gray-700 text-gray-200 px-3 py-1 rounded border border-gray-700\">View Trace</a>\n </td>\n </tr>\n </template>\n </tbody>\n </table>\n <template if=\"!jobs.length\">\n <div class=\"p-8 text-center text-gray-500\">No jobs recorded yet.</div>\n </template>\n </section>\n\n <section class=\"md:hidden bg-gray-900 border border-gray-800 rounded-xl divide-y divide-gray-800 overflow-hidden\">\n <template for=\"job of jobs\">\n <article class=\"p-4 bg-gray-900/60\">\n <div class=\"flex items-start justify-between gap-3\">\n <div class=\"min-w-0\">\n <div class=\"flex items-center gap-2\">\n <span\n class=\"w-2.5 h-2.5 rounded-full shrink-0\"\n bind-class=\"{\n 'bg-emerald-400': job.status === 'success',\n 'bg-rose-400': job.status === 'failed',\n 'bg-indigo-400.animate-pulse': job.status === 'running',\n 'bg-gray-500': !['success', 'failed', 'running'].includes(job.status)\n }\"\n ></span>\n <h2 class=\"font-medium text-white truncate\">{{ job.workflowId }}</h2>\n </div>\n <p class=\"mt-1 pl-[18px] text-xs font-mono text-gray-400\">#{{ job.id }} · {{ upper(job.status) }}</p>\n </div>\n <a attr-href=\"'/runs/' + job.id\" class=\"shrink-0 text-xs bg-gray-800 hover:bg-gray-700 text-gray-300 px-2.5 py-1 rounded border border-gray-700\">Trace</a>\n </div>\n </article>\n </template>\n <template if=\"!jobs.length\">\n <div class=\"p-8 text-center text-gray-500\">No jobs recorded yet.</div>\n </template>\n </section>\n\n <template if=\"hasMore\">\n <div class=\"flex justify-center\">\n <button\n class=\"px-4 py-2 text-sm text-gray-200 bg-gray-800 hover:bg-gray-700 border border-gray-700 rounded-lg\"\n on-click=\"loadMore()\"\n >\n {{ loadingMore ? 'Loading...' : 'Load more' }}\n </button>\n </div>\n </template>\n </main>\n\n <script setup>\n import { onDestroy, onInit, ref } from '@li3/web';\n\n export default function setup() {\n const jobs = ref([]);\n const lastUpdated = ref('now');\n const refreshError = ref(false);\n const hasMore = ref(false);\n const loadingMore = ref(false);\n let refreshTimer;\n let refreshing = false;\n\n const upper = (value) => String(value || '').toUpperCase();\n const terminalStatuses = new Set(['success', 'failed', 'cancelled']);\n const refreshJobs = async () => {\n if (refreshing) return;\n refreshing = true;\n\n try {\n const activeIds = jobs.value\n .filter((job) => !terminalStatuses.has(job.status))\n .map((job) => Number(job.id));\n const knownIds = jobs.value.map((job) => Number(job.id));\n const afterId = activeIds.length\n ? Math.max(0, Math.min(...activeIds) - 1)\n : knownIds.length\n ? Math.max(...knownIds)\n : null;\n const url = afterId === null ? '/api/jobs?limit=500' : `/api/jobs?afterId=${afterId}&limit=500`;\n const response = await fetch(url, { headers: { accept: 'application/json' } });\n if (!response.ok) throw new Error(`Dashboard refresh failed: ${response.status}`);\n\n const data = await response.json();\n const merged = new Map(jobs.value.map((job) => [job.id, job]));\n for (const job of data.jobs) merged.set(job.id, job);\n jobs.value = Array.from(merged.values()).sort((a, b) => Number(b.id) - Number(a.id));\n lastUpdated.value = new Date().toLocaleTimeString();\n refreshError.value = false;\n } catch (error) {\n console.error(error);\n refreshError.value = true;\n } finally {\n refreshing = false;\n }\n };\n\n const loadMore = async () => {\n if (loadingMore.value || !hasMore.value || !jobs.value.length) return;\n loadingMore.value = true;\n\n try {\n const beforeId = Math.min(...jobs.value.map((job) => Number(job.id)));\n const response = await fetch(`/api/jobs?beforeId=${beforeId}`, {\n headers: { accept: 'application/json' },\n });\n if (!response.ok) throw new Error(`Loading older jobs failed: ${response.status}`);\n\n const data = await response.json();\n const merged = new Map(jobs.value.map((job) => [job.id, job]));\n for (const job of data.jobs) merged.set(job.id, job);\n jobs.value = Array.from(merged.values()).sort((a, b) => Number(b.id) - Number(a.id));\n hasMore.value = data.hasMore;\n } catch (error) {\n console.error(error);\n refreshError.value = true;\n } finally {\n loadingMore.value = false;\n }\n };\n\n onInit(() => {\n refreshTimer = setInterval(refreshJobs, 5000);\n });\n onDestroy(() => clearInterval(refreshTimer));\n\n return { jobs, lastUpdated, refreshError, hasMore, loadingMore, loadMore, upper };\n }\n <\/script>\n <script state>__DASHBOARD_STATE__<\/script>\n </template>\n\n <script type=\"module\">import '@li3/web';<\/script>\n <script src=\"https://cdn.tailwindcss.com\"><\/script>\n</body>\n</html>\n";
8019
+ }, Po = "<!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 <title>Workflow Engine Dashboard</title>\n <script type=\"importmap\">\n { \"imports\": { \"@li3/\": \"https://cdn.li3.dev/@li3/\" } }\n <\/script>\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 <main class=\"max-w-6xl mx-auto space-y-6\">\n <header class=\"flex flex-col sm:flex-row sm:items-center justify-between gap-3 border-b border-gray-800 pb-4\">\n <div>\n <h1 class=\"text-2xl font-bold text-white\">Runner Engine Status</h1>\n <p class=\"text-xs text-gray-400\">Live Job Queue &amp; Execution Traces</p>\n </div>\n <div class=\"flex items-center gap-3 text-xs font-mono\">\n <span class=\"text-gray-500\">Updated {{ lastUpdated }}</span>\n <span\n class=\"px-3 py-1 rounded-full border\"\n bind-class=\"{\n 'bg-emerald-500/10.text-emerald-400.border-emerald-500/20': !refreshError,\n 'bg-rose-500/10.text-rose-400.border-rose-500/20': refreshError\n }\"\n >\n {{ refreshError ? 'Refresh failed' : 'System operational' }}\n </span>\n </div>\n </header>\n\n <section class=\"bg-gray-900 border border-gray-800 rounded-xl overflow-hidden hidden md:block\">\n <table class=\"w-full text-left text-sm\">\n <thead class=\"bg-gray-800/50 text-gray-400 text-xs uppercase font-mono border-b border-gray-800\">\n <tr>\n <th class=\"py-3 px-4\">Job ID</th>\n <th class=\"py-3 px-4\">Workflow</th>\n <th class=\"py-3 px-4\">Status</th>\n <th class=\"py-3 px-4 hidden xl:table-cell\">Worker</th>\n <th class=\"py-3 px-4 hidden lg:table-cell\">Created At</th>\n <th class=\"py-3 px-4 text-right\">Action</th>\n </tr>\n </thead>\n <tbody>\n <template for=\"job of jobs\">\n <tr class=\"border-b border-gray-800 hover:bg-gray-800/40 transition\">\n <td class=\"py-3 px-4 font-mono text-indigo-400\">\n <a attr-href=\"'/runs/' + job.id\" class=\"hover:underline\">#{{ job.id }}</a>\n </td>\n <td class=\"py-3 px-4 font-medium text-white\">{{ job.workflowId }}</td>\n <td class=\"py-3 px-4\">\n <span\n class=\"px-2.5 py-0.5 rounded-full text-xs font-semibold\"\n bind-class=\"{\n 'bg-emerald-500/10.text-emerald-400': job.status === 'success',\n 'bg-rose-500/10.text-rose-400': job.status === 'failed',\n 'bg-indigo-500/10.text-indigo-400.animate-pulse': job.status === 'running',\n 'bg-gray-500/10.text-gray-400': !['success', 'failed', 'running'].includes(job.status)\n }\"\n >\n {{ upper(job.status) }}\n </span>\n </td>\n <td class=\"py-3 px-4 hidden xl:table-cell text-xs font-mono text-gray-400\">{{ job.workerId || '-' }}</td>\n <td class=\"py-3 px-4 hidden lg:table-cell text-xs text-gray-400\">{{ job.createdAt }}</td>\n <td class=\"py-3 px-4 text-right whitespace-nowrap\">\n <a attr-href=\"'/runs/' + job.id\" class=\"text-xs bg-gray-800 hover:bg-gray-700 text-gray-200 px-3 py-1 rounded border border-gray-700\">View Trace</a>\n </td>\n </tr>\n </template>\n </tbody>\n </table>\n <template if=\"!jobs.length\">\n <div class=\"p-8 text-center text-gray-500\">No jobs recorded yet.</div>\n </template>\n </section>\n\n <section class=\"md:hidden bg-gray-900 border border-gray-800 rounded-xl divide-y divide-gray-800 overflow-hidden\">\n <template for=\"job of jobs\">\n <article class=\"p-4 bg-gray-900/60\">\n <div class=\"flex items-start justify-between gap-3\">\n <div class=\"min-w-0\">\n <div class=\"flex items-center gap-2\">\n <span\n class=\"w-2.5 h-2.5 rounded-full shrink-0\"\n bind-class=\"{\n 'bg-emerald-400': job.status === 'success',\n 'bg-rose-400': job.status === 'failed',\n 'bg-indigo-400.animate-pulse': job.status === 'running',\n 'bg-gray-500': !['success', 'failed', 'running'].includes(job.status)\n }\"\n ></span>\n <h2 class=\"font-medium text-white truncate\">{{ job.workflowId }}</h2>\n </div>\n <p class=\"mt-1 pl-[18px] text-xs font-mono text-gray-400\">#{{ job.id }} · {{ upper(job.status) }}</p>\n </div>\n <a attr-href=\"'/runs/' + job.id\" class=\"shrink-0 text-xs bg-gray-800 hover:bg-gray-700 text-gray-300 px-2.5 py-1 rounded border border-gray-700\">Trace</a>\n </div>\n </article>\n </template>\n <template if=\"!jobs.length\">\n <div class=\"p-8 text-center text-gray-500\">No jobs recorded yet.</div>\n </template>\n </section>\n\n <template if=\"hasMore\">\n <div class=\"flex justify-center\">\n <button\n class=\"px-4 py-2 text-sm text-gray-200 bg-gray-800 hover:bg-gray-700 border border-gray-700 rounded-lg\"\n on-click=\"loadMore()\"\n >\n {{ loadingMore ? 'Loading...' : 'Load more' }}\n </button>\n </div>\n </template>\n </main>\n\n <script setup>\n import { onDestroy, onInit, ref } from '@li3/web';\n\n export default function setup() {\n const jobs = ref([]);\n const lastUpdated = ref('now');\n const refreshError = ref(false);\n const hasMore = ref(false);\n const loadingMore = ref(false);\n let refreshTimer;\n let eventSource;\n let refreshing = false;\n let refreshPending = false;\n\n const upper = (value) => String(value || '').toUpperCase();\n const terminalStatuses = new Set(['success', 'failed', 'cancelled']);\n const refreshJobs = async () => {\n if (refreshing) {\n refreshPending = true;\n return;\n }\n refreshing = true;\n\n try {\n const activeIds = jobs.value\n .filter((job) => !terminalStatuses.has(job.status))\n .map((job) => Number(job.id));\n const knownIds = jobs.value.map((job) => Number(job.id));\n const afterId = activeIds.length\n ? Math.max(0, Math.min(...activeIds) - 1)\n : knownIds.length\n ? Math.max(...knownIds)\n : null;\n const url = afterId === null ? '/api/jobs?limit=500' : `/api/jobs?afterId=${afterId}&limit=500`;\n const response = await fetch(url, { headers: { accept: 'application/json' } });\n if (!response.ok) throw new Error(`Dashboard refresh failed: ${response.status}`);\n\n const data = await response.json();\n const merged = new Map(jobs.value.map((job) => [job.id, job]));\n for (const job of data.jobs) merged.set(job.id, job);\n jobs.value = Array.from(merged.values()).sort((a, b) => Number(b.id) - Number(a.id));\n lastUpdated.value = new Date().toLocaleTimeString();\n refreshError.value = false;\n } catch (error) {\n console.error(error);\n refreshError.value = true;\n } finally {\n refreshing = false;\n if (refreshPending) {\n refreshPending = false;\n void refreshJobs();\n }\n }\n };\n\n const loadMore = async () => {\n if (loadingMore.value || !hasMore.value || !jobs.value.length) return;\n loadingMore.value = true;\n\n try {\n const beforeId = Math.min(...jobs.value.map((job) => Number(job.id)));\n const response = await fetch(`/api/jobs?beforeId=${beforeId}`, {\n headers: { accept: 'application/json' },\n });\n if (!response.ok) throw new Error(`Loading older jobs failed: ${response.status}`);\n\n const data = await response.json();\n const merged = new Map(jobs.value.map((job) => [job.id, job]));\n for (const job of data.jobs) merged.set(job.id, job);\n jobs.value = Array.from(merged.values()).sort((a, b) => Number(b.id) - Number(a.id));\n hasMore.value = data.hasMore;\n } catch (error) {\n console.error(error);\n refreshError.value = true;\n } finally {\n loadingMore.value = false;\n }\n };\n\n onInit(() => {\n eventSource = new EventSource('/api/events');\n eventSource.addEventListener('jobs.available', refreshJobs);\n eventSource.addEventListener('jobs.changed', refreshJobs);\n eventSource.onerror = () => {\n refreshError.value = true;\n };\n refreshTimer = setInterval(refreshJobs, 60000);\n });\n onDestroy(() => {\n eventSource?.close();\n clearInterval(refreshTimer);\n });\n\n return { jobs, lastUpdated, refreshError, hasMore, loadingMore, loadMore, upper };\n }\n <\/script>\n <script state>__DASHBOARD_STATE__<\/script>\n </template>\n\n <script type=\"module\">import '@li3/web';<\/script>\n <script src=\"https://cdn.tailwindcss.com\"><\/script>\n</body>\n</html>\n";
8020
+ //#endregion
8021
+ //#region src/html-state.ts
8022
+ function Fo(e) {
8023
+ return JSON.stringify(e).replace(/[<>&\u2028\u2029]/g, (e) => `\\u${e.charCodeAt(0).toString(16).padStart(4, "0")}`);
8024
+ }
8036
8025
  //#endregion
8037
8026
  //#region src/dashboard.ts
8038
- function zo(e) {
8027
+ function Io(e) {
8039
8028
  return e.map((e) => ({
8040
8029
  id: e.id,
8041
8030
  workflowId: e.workflow_id,
@@ -8045,27 +8034,145 @@ function zo(e) {
8045
8034
  updatedAt: e.updated_at
8046
8035
  }));
8047
8036
  }
8048
- function Bo(e, t = !1) {
8049
- return Ro.replace("__DASHBOARD_STATE__", () => No({
8037
+ function Lo(e, t = !1) {
8038
+ return Po.replace("__DASHBOARD_STATE__", () => Fo({
8050
8039
  jobs: e,
8051
8040
  hasMore: t
8052
8041
  }));
8053
8042
  }
8054
8043
  //#endregion
8044
+ //#region src/events.ts
8045
+ var Ro = class {
8046
+ clients = /* @__PURE__ */ new Map();
8047
+ nextId = 1;
8048
+ maxClients = 1e3;
8049
+ subscribe(e, t) {
8050
+ if (this.clients.size >= this.maxClients) {
8051
+ t.writeHead(503, {
8052
+ "Content-Type": "application/json",
8053
+ "Retry-After": "5"
8054
+ }), t.end(JSON.stringify({ error: "Too many event stream clients" }));
8055
+ return;
8056
+ }
8057
+ t.writeHead(200, {
8058
+ "Cache-Control": "no-cache, no-transform",
8059
+ Connection: "keep-alive",
8060
+ "Content-Type": "text/event-stream",
8061
+ "X-Accel-Buffering": "no"
8062
+ }), t.write("retry: 2000\n\n");
8063
+ let n = setInterval(() => t.write(": heartbeat\n\n"), 3e4);
8064
+ n.unref(), this.clients.set(t, n), e.once("close", () => {
8065
+ clearInterval(n), this.clients.delete(t);
8066
+ });
8067
+ }
8068
+ publish(e, t = {}) {
8069
+ let n = `id: ${this.nextId++}\nevent: ${e}\ndata: ${JSON.stringify(t)}\n\n`;
8070
+ for (let [e, t] of this.clients) e.write(n) || (clearInterval(t), this.clients.delete(e), e.end());
8071
+ }
8072
+ close() {
8073
+ for (let [e, t] of this.clients) clearInterval(t), e.end();
8074
+ this.clients.clear();
8075
+ }
8076
+ };
8077
+ async function zo(e, t, n, r) {
8078
+ let i = await fetch(new URL("/api/events", e), {
8079
+ headers: { Accept: "text/event-stream" },
8080
+ signal: t
8081
+ });
8082
+ if (!i.ok || !i.body) throw Error(`Event stream failed with HTTP ${i.status}`);
8083
+ r?.();
8084
+ let a = new TextDecoder(), o = "";
8085
+ for await (let e of i.body) {
8086
+ o += a.decode(e, { stream: !0 }).replaceAll("\r\n", "\n");
8087
+ let t = o.indexOf("\n\n");
8088
+ for (; t !== -1;) {
8089
+ let e = o.slice(0, t);
8090
+ o = o.slice(t + 2), t = o.indexOf("\n\n");
8091
+ let r = e.match(/^event: (.+)$/m)?.[1];
8092
+ if (!r) continue;
8093
+ let i = e.match(/^data: (.*)$/m)?.[1];
8094
+ n(r, i ? JSON.parse(i) : {});
8095
+ }
8096
+ }
8097
+ }
8098
+ //#endregion
8099
+ //#region src/run.html?raw
8100
+ var Bo = "<!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 <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 }\n }\n <\/script>\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-5xl mx-auto space-y-4\">\n <header class=\"flex flex-col md:flex-row md:items-center justify-between border-b border-gray-800 pb-6 gap-4\">\n <div class=\"min-w-0\">\n <a href=\"/runs\" class=\"text-xs text-indigo-400 hover:underline mb-1 inline-block\">Back to Dashboard</a>\n <h1 class=\"text-2xl font-bold text-white flex flex-wrap items-center gap-3\">\n {{ report.workflowName }}\n <span class=\"text-sm font-mono text-gray-500\">#{{ report.jobId }}</span>\n <template if=\"report.parentId\">\n <a attr-href=\"'/runs/' + report.parentId\" class=\"text-sm font-mono text-indigo-400 hover:underline\">\n from #{{ report.parentId }}\n </a>\n </template>\n </h1>\n <p class=\"text-xs text-gray-400 mt-1\">Started {{ report.startedAt }} · {{ timing }}</p>\n </div>\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 <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 </div>\n </header>\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 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 <div class=\"flex flex-wrap items-center gap-3 min-w-0\">\n <span class=\"font-mono text-gray-500\">\n #{{ index + 1 }}<template if=\"step.exitCode !== undefined && step.exitCode !== 0\"> ({{ step.exitCode }})</template>\n </span>\n <h3 class=\"font-semibold text-gray-200 break-words\">{{ step.name }}</h3>\n <span\n class=\"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 <div class=\"font-mono text-gray-400 shrink-0\">{{ step.durationMs }}ms</div>\n </summary>\n <div class=\"p-4 bg-gray-950 font-mono text-gray-300 leading-relaxed overflow-auto w-full\">\n <pre class=\"whitespace-pre-wrap\" bind-innerhtml=\"stepLog(step)\"></pre>\n </div>\n </details>\n </template>\n </section>\n\n <details class=\"bg-gray-900 border border-gray-800 rounded-xl p-4 overflow-auto max-h-[400px]\">\n <summary class=\"cursor-pointer\">\n <h2 class=\"inline text-xs font-semibold text-gray-400 uppercase tracking-wider\">Trigger Inputs</h2>\n </summary>\n <pre class=\"font-mono text-xs text-gray-200 bg-gray-950 p-3 mt-3 rounded-lg overflow-x-auto\">{{ inputsJson }}</pre>\n </details>\n </div>\n\n <script setup>\n import { computed, onDestroy, onInit, ref } from '@li3/web';\n import { AnsiUp } from 'ansi_up';\n\n export default function setup() {\n const report = ref({});\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\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 stepLog = (step) => {\n if (step.status === 'skipped') return '';\n if (step.status === 'running') 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 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 refreshRun = async () => {\n if (refreshing) {\n refreshPending = true;\n return;\n }\n refreshing = true;\n\n try {\n const response = await fetch(`/api/runs/${report.value.jobId}`, {\n headers: { accept: 'application/json' },\n });\n if (!response.ok) throw new Error(`Run refresh failed: ${response.status}`);\n report.value = await response.json();\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 document.title = `Run #${report.value.jobId} - ${report.value.workflowName}`;\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, inputsJson, timing, upper, stepLog, restartJob };\n }\n <\/script>\n <script state>__REPORT_STATE__<\/script>\n </template>\n\n <script type=\"module\">import '@li3/web';<\/script>\n <script src=\"https://cdn.tailwindcss.com\"><\/script>\n</body>\n</html>\n", Vo = /(?:^|[_-])auth(?:entication)?(?:$|[_-])|access[_-]?key|api[_-]?key|authorization|cookie|credential|passphrase|password|private[_-]?key|secret|session(?:id)?|signing[_-]?key|token/i;
8101
+ function Ho(e, t, n) {
8102
+ let r = JSON.parse(e.payload), i = e.report ? JSON.parse(e.report) : Wo(e, r), a = e.status;
8103
+ return {
8104
+ jobId: String(e.id),
8105
+ parentId: String(i.parentId || e.parentId || ""),
8106
+ workflowName: n(i.workflowName),
8107
+ status: a,
8108
+ durationMs: a === "running" ? Math.max(0, Date.now() - Date.parse(i.startedAt)) : i.durationMs,
8109
+ startedAt: i.startedAt,
8110
+ finishedAt: i.finishedAt,
8111
+ inputs: Go(i.inputs || {}, n),
8112
+ steps: (i.steps || []).map((e) => {
8113
+ let r = Object.hasOwn(t, e.id) && typeof t[e.id] == "string" ? t[e.id] : "";
8114
+ return {
8115
+ id: e.id,
8116
+ name: n(e.name),
8117
+ status: e.status,
8118
+ durationMs: e.durationMs,
8119
+ exitCode: e.exitCode,
8120
+ startedAt: e.startedAt,
8121
+ finishedAt: e.finishedAt,
8122
+ error: e.error ? n(e.error) : void 0,
8123
+ outputs: Go(e.outputs || {}, n),
8124
+ logContent: e.status === "running" || e.status === "pending" ? "" : n(r || e.logContent || "")
8125
+ };
8126
+ }),
8127
+ artifacts: (i.artifacts || []).map(n)
8128
+ };
8129
+ }
8130
+ function Uo(e) {
8131
+ return Bo.replace("__REPORT_STATE__", () => Fo({ report: e }));
8132
+ }
8133
+ function Wo(e, t) {
8134
+ let n = e.started_at || e.created_at;
8135
+ return {
8136
+ jobId: String(e.id),
8137
+ parentId: String(e.parentId || ""),
8138
+ workflowName: e.workflow_id,
8139
+ status: e.status,
8140
+ durationMs: e.status === "running" ? Math.max(0, Date.now() - Date.parse(n)) : 0,
8141
+ startedAt: n,
8142
+ inputs: t.inputs || {},
8143
+ environment: {},
8144
+ steps: (t.steps || []).map((e, t) => ({
8145
+ id: e.id || `step-${t}`,
8146
+ name: e.name || e.id || `step-${t}`,
8147
+ status: "pending",
8148
+ durationMs: 0,
8149
+ outputs: {},
8150
+ logContent: ""
8151
+ })),
8152
+ artifacts: [],
8153
+ rerunToken: ""
8154
+ };
8155
+ }
8156
+ function Go(e, t) {
8157
+ return typeof e == "string" ? t(e) : Array.isArray(e) ? e.map((e) => Go(e, t)) : !e || typeof e != "object" ? e : Object.fromEntries(Object.entries(e).filter(([e]) => e.toLowerCase() !== "raw" && !Vo.test(e)).map(([e, n]) => [e, Go(n, t)]));
8158
+ }
8159
+ //#endregion
8055
8160
  //#region src/server.ts
8056
- var Vo = 50, Ho = 500, Uo = class e {
8161
+ var Ko = 50, qo = 500, Jo = class e {
8057
8162
  server;
8058
8163
  preprocessors = /* @__PURE__ */ new Map();
8059
8164
  workflows = [];
8060
8165
  queue;
8061
8166
  secrets;
8062
8167
  adminToken;
8168
+ events = new Ro();
8169
+ workflowsLoaded;
8063
8170
  static async withPort(t) {
8064
8171
  let { port: n, ...r } = t;
8065
8172
  return new e(r).listen(n);
8066
8173
  }
8067
8174
  constructor(e) {
8068
- this.queue = e.queue, this.secrets = e.secrets, this.adminToken = e.adminToken, Lo.from(e.config.workflows).then((t) => {
8175
+ this.queue = e.queue, this.secrets = e.secrets, this.adminToken = e.adminToken, this.workflowsLoaded = No.from(e.config.workflows).then((t) => {
8069
8176
  this.workflows = t, console.log(`✅ Loaded ${t.length} workflow(s) from ${e.config.workflows}`);
8070
8177
  }), this.registerPreprocessor(new Mo()), this.server = f.createServer((e, t) => this.handleRequest(e, t));
8071
8178
  }
@@ -8076,12 +8183,18 @@ var Vo = 50, Ho = 500, Uo = class e {
8076
8183
  let n = new p(e.url || "/", `${e.headers["x-forwarded-proto"] || "http"}://${e.headers["x-forwarded-host"] || e.headers.host}`);
8077
8184
  if (e.method === "GET" && (n.pathname === "/runs" || n.pathname === "/")) return this.renderDashboard(t);
8078
8185
  if (e.method === "GET" && n.pathname === "/api/jobs") {
8079
- let e = n.searchParams.get("afterId"), r = n.searchParams.get("beforeId"), i = n.searchParams.get("limit"), a = e === null ? void 0 : Number(e), o = r === null ? void 0 : Number(r), s = i === null ? Vo : Number(i);
8080
- return a !== void 0 && (!Number.isSafeInteger(a) || a < 0) ? (t.writeHead(400, { "Content-Type": "application/json; charset=utf-8" }), t.end(JSON.stringify({ error: "afterId must be a non-negative integer" }))) : o !== void 0 && (!Number.isSafeInteger(o) || o < 1) ? (t.writeHead(400, { "Content-Type": "application/json; charset=utf-8" }), t.end(JSON.stringify({ error: "beforeId must be a positive integer" }))) : !Number.isSafeInteger(s) || s < 1 || s > Ho ? (t.writeHead(400, { "Content-Type": "application/json; charset=utf-8" }), t.end(JSON.stringify({ error: `limit must be an integer from 1 to ${Ho}` }))) : this.renderDashboardJobs(t, s, a, o);
8186
+ let e = n.searchParams.get("afterId"), r = n.searchParams.get("beforeId"), i = n.searchParams.get("limit"), a = e === null ? void 0 : Number(e), o = r === null ? void 0 : Number(r), s = i === null ? Ko : Number(i);
8187
+ return a !== void 0 && (!Number.isSafeInteger(a) || a < 0) ? (t.writeHead(400, { "Content-Type": "application/json; charset=utf-8" }), t.end(JSON.stringify({ error: "afterId must be a non-negative integer" }))) : o !== void 0 && (!Number.isSafeInteger(o) || o < 1) ? (t.writeHead(400, { "Content-Type": "application/json; charset=utf-8" }), t.end(JSON.stringify({ error: "beforeId must be a positive integer" }))) : !Number.isSafeInteger(s) || s < 1 || s > qo ? (t.writeHead(400, { "Content-Type": "application/json; charset=utf-8" }), t.end(JSON.stringify({ error: `limit must be an integer from 1 to ${qo}` }))) : this.renderDashboardJobs(t, s, a, o);
8081
8188
  }
8189
+ if (e.method === "GET" && n.pathname === "/api/events") return this.events.subscribe(e, t);
8190
+ if (e.method === "POST" && n.pathname === "/api/events") return this.handleWorkerEvent(e, t);
8082
8191
  if (e.method === "GET" && n.pathname.startsWith("/runs/")) {
8083
8192
  let e = n.pathname.replace("/runs/", "");
8084
- return this.renderRunDetails(e, t);
8193
+ return this.renderRunDetails(e, t, "html");
8194
+ }
8195
+ if (e.method === "GET" && n.pathname.startsWith("/api/runs/")) {
8196
+ let e = n.pathname.replace("/api/runs/", "");
8197
+ return this.renderRunDetails(e, t, "json");
8085
8198
  }
8086
8199
  if (e.method === "POST" && n.pathname.startsWith("/restart/")) {
8087
8200
  let e = n.pathname.replace("/restart/", "");
@@ -8103,7 +8216,7 @@ var Vo = 50, Ho = 500, Uo = class e {
8103
8216
  n.writeHead(401, { "Content-Type": "application/json" }), n.end(JSON.stringify({ error: "Invalid HMAC signature or authentication failed" }));
8104
8217
  return;
8105
8218
  }
8106
- this.matchWorkflows(e, o), n.writeHead(202, { "Content-Type": "application/json" }), n.end(JSON.stringify({ message: "OK" }));
8219
+ await this.workflowsLoaded, await this.matchWorkflows(e, o), n.writeHead(202, { "Content-Type": "application/json" }), n.end(JSON.stringify({ message: "OK" }));
8107
8220
  } catch (e) {
8108
8221
  console.error("❌ Webhook Ingress Error:", e), n.writeHead(500, { "Content-Type": "application/json" }), n.end(JSON.stringify({
8109
8222
  error: "Internal Ingress Error",
@@ -8169,21 +8282,35 @@ var Vo = 50, Ho = 500, Uo = class e {
8169
8282
  workflowId: n.id,
8170
8283
  env: n.env,
8171
8284
  steps: n.steps,
8172
- inputs: t
8285
+ inputs: t,
8286
+ tags: n.tags
8173
8287
  };
8174
- await this.queue.enqueue(n.id, a, i);
8288
+ await this.queue.enqueue(n.id, a, i), this.events.publish("jobs.available", { tags: n.tags || [] });
8175
8289
  }
8176
8290
  }
8177
8291
  async handleSecretReload(e, t) {
8178
8292
  if (e.headers.authorization !== `Bearer ${this.adminToken}`) return t.writeHead(403, { "Content-Type": "application/json" }), t.end(JSON.stringify({ error: "Unauthorized" }));
8179
8293
  this.secrets.reload(), console.log("🔄 SecretStore reloaded successfully without downtime!"), t.writeHead(200, { "Content-Type": "application/json" }), t.end(JSON.stringify({ message: "Secrets reloaded successfully" }));
8180
8294
  }
8295
+ async handleWorkerEvent(e, t) {
8296
+ if (!this.adminToken || e.headers.authorization !== `Bearer ${this.adminToken}`) return t.writeHead(403, { "Content-Type": "application/json" }), t.end(JSON.stringify({ error: "Unauthorized" }));
8297
+ let { rawBuffer: n } = await this.readRequest(e, t);
8298
+ if (!n || t.headersSent) return;
8299
+ let r;
8300
+ try {
8301
+ let e = n.length ? JSON.parse(n.toString("utf8")) : {}, t = Number(e.jobId);
8302
+ Number.isSafeInteger(t) && t > 0 && (r = t);
8303
+ } catch {
8304
+ return t.writeHead(400, { "Content-Type": "application/json" }), t.end(JSON.stringify({ error: "Invalid event payload" }));
8305
+ }
8306
+ this.events.publish("jobs.changed", r ? { jobId: r } : {}), t.writeHead(202).end();
8307
+ }
8181
8308
  async renderDashboard(e) {
8182
- let t = await this.queue.listJobs(51), n = zo(t.slice(0, Vo)), r = this.secrets.redactText(Bo(n, t.length > Vo));
8309
+ let t = await this.queue.listJobs(51), n = Io(t.slice(0, Ko)), r = this.secrets.redactText(Lo(n, t.length > Ko));
8183
8310
  e.writeHead(200, { "Content-Type": "text/html; charset=utf-8" }), e.end(r);
8184
8311
  }
8185
8312
  async renderDashboardJobs(e, t, n, r) {
8186
- let i = await this.queue.listJobs(t + 1, n, r), a = zo(i.slice(0, t)), o = this.secrets.redactText(JSON.stringify({
8313
+ let i = await this.queue.listJobs(t + 1, n, r), a = Io(i.slice(0, t)), o = this.secrets.redactText(JSON.stringify({
8187
8314
  jobs: a,
8188
8315
  hasMore: i.length > t
8189
8316
  }));
@@ -8192,49 +8319,23 @@ var Vo = 50, Ho = 500, Uo = class e {
8192
8319
  "Content-Type": "application/json; charset=utf-8"
8193
8320
  }), e.end(o);
8194
8321
  }
8195
- async renderRunDetails(e, t) {
8196
- let n = await this.queue.getJob(e);
8197
- if (!n) return t.writeHead(404, { "Content-Type": "text/html; charset=utf-8" }), t.end("<h1>404 - Report Not Found</h1>");
8198
- let r = n.report ? JSON.parse(n.report) : this.buildPendingReport(n);
8199
- r.status = n.status, r.status === "running" && (r.durationMs = Math.max(0, Date.now() - Date.parse(r.startedAt)));
8200
- let i = await this.queue.getJobLogs(e);
8201
- r.steps = (r.steps || []).map((e) => ({
8202
- ...e,
8203
- logContent: e.status === "running" || e.status === "pending" ? "" : i[e.id] || ""
8204
- }));
8205
- let a = new Io({ outputDir: "" }).generateHtml(r), o = this.secrets.redactText(a);
8206
- t.writeHead(200, { "Content-Type": "text/html; charset=utf-8" }), t.end(o);
8207
- }
8208
- buildPendingReport(e) {
8209
- let t = JSON.parse(e.payload), n = e.started_at || e.created_at;
8210
- return {
8211
- jobId: String(e.id),
8212
- parentId: String(e.parentId || ""),
8213
- workflowName: e.workflow_id,
8214
- status: e.status,
8215
- durationMs: e.status === "running" ? Math.max(0, Date.now() - Date.parse(n)) : 0,
8216
- startedAt: n,
8217
- inputs: t.inputs || {},
8218
- environment: t.env || {},
8219
- steps: (t.steps || []).map((e, t) => ({
8220
- id: e.id || `step-${t}`,
8221
- name: e.name || e.id || `step-${t}`,
8222
- status: "pending",
8223
- durationMs: 0,
8224
- outputs: {},
8225
- logContent: ""
8226
- })),
8227
- artifacts: [],
8228
- rerunToken: JSON.stringify({
8229
- jobId: e.id,
8230
- payload: t
8231
- })
8232
- };
8322
+ async renderRunDetails(e, t, n) {
8323
+ let r = await this.queue.getJob(e);
8324
+ if (!r) {
8325
+ let e = n === "json" ? "application/json; charset=utf-8" : "text/html; charset=utf-8";
8326
+ return t.writeHead(404, { "Content-Type": e }), t.end(n === "json" ? JSON.stringify({ error: "Run not found" }) : "<h1>404 - Run Not Found</h1>");
8327
+ }
8328
+ let i = Ho(r, await this.queue.getJobLogs(e), (e) => this.secrets.redactText(e));
8329
+ if (n === "json") return t.writeHead(200, {
8330
+ "Cache-Control": "no-store",
8331
+ "Content-Type": "application/json; charset=utf-8"
8332
+ }), t.end(JSON.stringify(i));
8333
+ t.writeHead(200, { "Content-Type": "text/html; charset=utf-8" }), t.end(Uo(i));
8233
8334
  }
8234
8335
  async handleRestartJob(e, t) {
8235
8336
  let n = await this.queue.restartJob(e);
8236
8337
  if (n) {
8237
- t.writeHead(201, { "Content-Type": "application/json" }), t.end(JSON.stringify({ id: n }));
8338
+ this.events.publish("jobs.available"), t.writeHead(201, { "Content-Type": "application/json" }), t.end(JSON.stringify({ id: n }));
8238
8339
  return;
8239
8340
  }
8240
8341
  t.writeHead(404, { "Content-Type": "application/json" }), t.end(JSON.stringify({ error: "Job not found" }));
@@ -8247,13 +8348,13 @@ var Vo = 50, Ho = 500, Uo = class e {
8247
8348
  });
8248
8349
  }
8249
8350
  async stop() {
8250
- return new Promise((e) => {
8351
+ return this.events.close(), new Promise((e) => {
8251
8352
  this.server.close(() => {
8252
8353
  console.log("🌐 Webhook Ingress Server stopped listening."), e();
8253
8354
  });
8254
8355
  });
8255
8356
  }
8256
- }, Wo = d(v), Go = class {
8357
+ }, Yo = d(v), Xo = class {
8257
8358
  name = "systemd";
8258
8359
  async isSupported() {
8259
8360
  try {
@@ -8376,7 +8477,7 @@ var Vo = 50, Ho = 500, Uo = class e {
8376
8477
  cancel: async () => {
8377
8478
  d = !0;
8378
8479
  try {
8379
- t.image && await Wo(`docker stop -t 2 ${o}`).catch(() => {}), await Wo(`systemctl stop ${o}.service`).catch(() => {});
8480
+ t.image && await Yo(`docker stop -t 2 ${o}`).catch(() => {}), await Yo(`systemctl stop ${o}.service`).catch(() => {});
8380
8481
  } catch {}
8381
8482
  },
8382
8483
  logFilePath: i
@@ -8386,7 +8487,7 @@ var Vo = 50, Ho = 500, Uo = class e {
8386
8487
  let t = await g(e, "utf-8"), n = t.includes("Running as unit: ") ? t.indexOf("\n") + 1 : 0, r = t.includes("Finished with result: ") ? t.lastIndexOf("Finished with result: ") : t.length;
8387
8488
  return t.slice(n, r);
8388
8489
  }
8389
- }, Ko = class {
8490
+ }, Zo = class {
8390
8491
  name = "standard-process";
8391
8492
  async isSupported() {
8392
8493
  return !0;
@@ -8501,13 +8602,13 @@ var Vo = 50, Ho = 500, Uo = class e {
8501
8602
  };
8502
8603
  //#endregion
8503
8604
  //#region src/drivers/index.ts
8504
- async function qo() {
8505
- let e = new Go();
8506
- return await e.isSupported() ? (console.log("⚡ Selected Execution Driver: Systemd (cgroups enabled)"), e) : (console.log("📦 Selected Execution Driver: Standard Process (Fallback)"), new Ko());
8605
+ async function Qo() {
8606
+ let e = new Xo();
8607
+ return await e.isSupported() ? (console.log("⚡ Selected Execution Driver: Systemd (cgroups enabled)"), e) : (console.log("📦 Selected Execution Driver: Standard Process (Fallback)"), new Zo());
8507
8608
  }
8508
8609
  //#endregion
8509
8610
  //#region src/signals.ts
8510
- async function Jo(e) {
8611
+ async function $o(e) {
8511
8612
  let t = !1;
8512
8613
  async function n(n) {
8513
8614
  if (!t) {
@@ -8515,7 +8616,7 @@ async function Jo(e) {
8515
8616
  console.error("⚠️ Graceful shutdown timed out after 10s. Forcing exit!"), process.exit(1);
8516
8617
  }, 1e4).unref();
8517
8618
  try {
8518
- Xo.isStopping = !0, e.length > 0 && (console.log("⚙️ Waiting for active worker jobs to drain..."), await Promise.allSettled(e)), console.log("✨ Engine stopped cleanly. Goodbye!"), process.exit(0);
8619
+ os(), e.length > 0 && (console.log("⚙️ Waiting for active worker jobs to drain..."), await Promise.allSettled(e)), console.log("✨ Engine stopped cleanly. Goodbye!"), process.exit(0);
8519
8620
  } catch (e) {
8520
8621
  console.error("❌ Error during graceful shutdown:", e.message), process.exit(1);
8521
8622
  }
@@ -8524,35 +8625,126 @@ async function Jo(e) {
8524
8625
  process.on("SIGINT", () => n("SIGINT")), process.on("SIGTERM", () => n("SIGTERM"));
8525
8626
  }
8526
8627
  //#endregion
8527
- //#region src/worker.ts
8528
- var Yo = !!process.env.DEBUG, Xo = { isStopping: !1 };
8529
- function Zo(e, t, n, r) {
8530
- Xo.isStopping = !1;
8531
- let i = Array.from({ length: e }, (e, i) => Qo(`worker-${i + 1}`, t, n, r));
8532
- return Jo(i), i;
8533
- }
8534
- async function Qo(e, t, n, r) {
8535
- let i = await qo();
8536
- for (console.log(`[${e}] 🚀 Worker started. Driver: ${i.name}`); !Xo.isStopping;) try {
8537
- let a = await t.claimNextJob();
8538
- if (!a) {
8539
- await new Promise((e) => setTimeout(e, 2e3));
8540
- continue;
8628
+ //#region src/plugins/manager.ts
8629
+ var es = class {
8630
+ plugins;
8631
+ constructor(e = []) {
8632
+ this.plugins = e;
8633
+ }
8634
+ register(e) {
8635
+ console.log(`🔌 Registered Plugin: ${e.name}`), this.plugins.push(e);
8636
+ }
8637
+ async triggerWorkflowStart(e) {
8638
+ for (let t of this.plugins) if (t.onWorkflowStart) try {
8639
+ await t.onWorkflowStart(e);
8640
+ } catch (e) {
8641
+ console.error(`[Plugin Error] ${t.name}.onWorkflowStart:`, e);
8642
+ }
8643
+ }
8644
+ async triggerWorkflowFinish(e, t) {
8645
+ for (let n of this.plugins) if (n.onWorkflowFinish) try {
8646
+ await n.onWorkflowFinish(e, t);
8647
+ } catch (e) {
8648
+ console.error(`[Plugin Error] ${n.name}.onWorkflowFinish:`, e);
8649
+ }
8650
+ }
8651
+ }, ts = !!process.env.DEBUG, ns = { isStopping: !1 }, rs = /* @__PURE__ */ new Set(), is = null, as = null;
8652
+ function os() {
8653
+ ns.isStopping = !0, is?.abort(), as?.();
8654
+ }
8655
+ function ss(e, t, n, r) {
8656
+ ns.isStopping = !1;
8657
+ let i = [cs(e, t, n, r)];
8658
+ return $o(i), i;
8659
+ }
8660
+ async function cs(e, t, n, r) {
8661
+ let i = await Qo(), a = /* @__PURE__ */ new Set(), o = 0, s = null, c = () => {
8662
+ o++, s?.();
8663
+ };
8664
+ as = c, is = new AbortController();
8665
+ let l = ls(r, is.signal, c);
8666
+ for (console.log(`🚀 Worker scheduler started. Driver: ${i.name}. Concurrency: ${e}. Tags: ${r.tags.join(", ") || "(none)"}`); !ns.isStopping;) {
8667
+ let l = o;
8668
+ try {
8669
+ for (; a.size < e && !ns.isStopping;) {
8670
+ let e = await t.claimNextJob(r.tags);
8671
+ if (!e) break;
8672
+ if (ns.isStopping) {
8673
+ await t.releaseJob(e.id);
8674
+ break;
8675
+ }
8676
+ let o = `worker-${e.id}`, s;
8677
+ s = (async () => {
8678
+ fs(r, e.id), await ps({
8679
+ workerId: o,
8680
+ job: e,
8681
+ queue: t,
8682
+ secrets: n,
8683
+ config: r,
8684
+ driver: i
8685
+ });
8686
+ })().catch((e) => console.error(`[${o}] ⚠️ Worker execution error:`, e)).finally(() => {
8687
+ a.delete(s), fs(r, e.id), c();
8688
+ }), a.add(s);
8689
+ }
8690
+ } catch (e) {
8691
+ console.error("⚠️ Worker scheduler claim error:", e), setTimeout(c, 5e3).unref();
8541
8692
  }
8542
- await $o({
8543
- workerId: e,
8544
- job: a,
8545
- queue: t,
8546
- secrets: n,
8547
- config: r,
8548
- driver: i
8693
+ !ns.isStopping && o === l && (await us(l, () => o, (e) => s = e), s = null);
8694
+ }
8695
+ is.abort(), await Promise.allSettled(a), await l, as = null, console.log("🛑 Worker scheduler stopped cleanly.");
8696
+ }
8697
+ async function ls(e, t, n) {
8698
+ let r = 1e3;
8699
+ for (; !t.aborted;) {
8700
+ try {
8701
+ await zo(e.serverUrl, t, (t, r) => {
8702
+ t === "jobs.available" && (Array.isArray(r.tags) ? r.tags.filter((e) => typeof e == "string") : []).every((t) => e.tags.includes(t)) && n();
8703
+ }, n), r = 1e3;
8704
+ } catch (e) {
8705
+ if (t.aborted || e?.name === "AbortError") break;
8706
+ console.error(`⚠️ Worker event stream disconnected: ${e.message}`);
8707
+ }
8708
+ await ds(r, t), r = Math.min(r * 2, 3e4);
8709
+ }
8710
+ }
8711
+ function us(e, t, n) {
8712
+ return new Promise((r) => {
8713
+ if (t() !== e) return r();
8714
+ let i = setTimeout(r, 6e4);
8715
+ n(() => {
8716
+ clearTimeout(i), r();
8549
8717
  });
8550
- } catch (t) {
8551
- console.error(`[${e}] ⚠️ Worker execution loop error:`, t), await new Promise((e) => setTimeout(e, 5e3));
8718
+ });
8719
+ }
8720
+ function ds(e, t) {
8721
+ return new Promise((n) => {
8722
+ if (t.aborted) return n();
8723
+ let r = () => {
8724
+ clearTimeout(i), n();
8725
+ }, i = setTimeout(() => {
8726
+ t.removeEventListener("abort", r), n();
8727
+ }, e);
8728
+ t.addEventListener("abort", r, { once: !0 });
8729
+ });
8730
+ }
8731
+ async function fs(e, t) {
8732
+ if (e.adminToken) try {
8733
+ let n = await fetch(new URL("/api/events", e.serverUrl), {
8734
+ method: "POST",
8735
+ headers: {
8736
+ Authorization: `Bearer ${e.adminToken}`,
8737
+ "Content-Type": "application/json"
8738
+ },
8739
+ body: JSON.stringify({ jobId: t }),
8740
+ signal: AbortSignal.timeout(5e3)
8741
+ });
8742
+ !n.ok && ts && console.error(`Failed to publish job status event: HTTP ${n.status}`);
8743
+ } catch (e) {
8744
+ ts && console.error("Failed to publish job status event:", e);
8552
8745
  }
8553
- console.log(`[${e}] 🛑 Worker loop stopped cleanly.`);
8554
8746
  }
8555
- async function $o(e) {
8747
+ async function ps(e) {
8556
8748
  let { workerId: t, job: n, config: r, secrets: i, queue: o } = e;
8557
8749
  console.log(`\n[${t}] 📦 Claimed Job #${n.id} (Workflow: ${n.workflow_id})`);
8558
8750
  let s = typeof n.payload == "string" ? JSON.parse(n.payload) : n.payload, c = s.steps || [], l = s.inputs || {}, u = Date.now(), d = a.join(r.storagePath, `job-${n.id}`), f = a.join(d, "logs"), p = a.join(d, "wd"), m = {
@@ -8569,20 +8761,27 @@ async function $o(e) {
8569
8761
  durationMs: 0,
8570
8762
  outputs: {},
8571
8763
  logContent: ""
8572
- })), g = os(n, "running", u, l, m.env, h, s);
8573
- await o.saveReport(n.id, g);
8574
- let { cancelled: _, failed: v } = await es({
8764
+ })), g = bs(n, "running", u, l, m.env, h, s);
8765
+ await o.saveReport(n.id, g), fs(r, n.id);
8766
+ let _ = new es(r.plugins), v = {
8767
+ jobId: String(n.id),
8768
+ workflowName: n.workflow_id,
8769
+ inputs: l,
8770
+ runUrl: new URL(`/runs/${n.id}`, r.serverUrl).toString()
8771
+ };
8772
+ await _.triggerWorkflowStart(v);
8773
+ let { cancelled: y, failed: ee } = await ms({
8575
8774
  payload: s,
8576
8775
  steps: c,
8577
8776
  executionContext: m,
8578
8777
  ...e
8579
- }, g), y = _ ? "cancelled" : v ? "failed" : "success";
8580
- g.status = y, g.durationMs = Date.now() - u, g.finishedAt = (/* @__PURE__ */ new Date()).toISOString(), await o.completeJob(n.id, y, g), console.log(`[${t}] ✅ Job #${n.id} completed as: ${y}`), await ss(t, r.reporters, g);
8778
+ }, g), b = y ? "cancelled" : ee ? "failed" : "success";
8779
+ g.status = b, g.durationMs = Date.now() - u, g.finishedAt = (/* @__PURE__ */ new Date()).toISOString(), await o.completeJob(n.id, b, g), fs(r, n.id), console.log(`[${t}] ✅ Job #${n.id} completed as: ${b}`), await _.triggerWorkflowFinish(v, b);
8581
8780
  }
8582
- async function es(e, t) {
8781
+ async function ms(e, t) {
8583
8782
  let { driver: n, workerId: r, queue: i, config: a, job: o, payload: s, steps: c, executionContext: l } = e, u = t.steps, d = !1, f = !1, p = 0;
8584
8783
  try {
8585
- s.env && Object.assign(l.env, await rs(s.env, l));
8784
+ s.env && Object.assign(l.env, await _s(s.env, l));
8586
8785
  for (let e = 0; e < c.length; e++) {
8587
8786
  let s = c[e], m = (/* @__PURE__ */ new Date()).toISOString();
8588
8787
  u[e] = {
@@ -8595,8 +8794,8 @@ async function es(e, t) {
8595
8794
  logContent: "",
8596
8795
  startedAt: m,
8597
8796
  finishedAt: void 0
8598
- }, t.durationMs = Date.now() - Date.parse(t.startedAt), await i.saveReport(o.id, t);
8599
- let h = await ts({
8797
+ }, t.durationMs = Date.now() - Date.parse(t.startedAt), await i.saveReport(o.id, t), fs(a, o.id);
8798
+ let h = await hs({
8600
8799
  workerId: r,
8601
8800
  jobId: o.id,
8602
8801
  step: s,
@@ -8623,7 +8822,7 @@ async function es(e, t) {
8623
8822
  exitCode: 1,
8624
8823
  error: "Step failed before execution started",
8625
8824
  finishedAt: (/* @__PURE__ */ new Date()).toISOString()
8626
- }, p = e + 1, t.durationMs = Date.now() - Date.parse(t.startedAt), await i.saveReport(o.id, t), h.skipped) break;
8825
+ }, p = e + 1, t.durationMs = Date.now() - Date.parse(t.startedAt), await i.saveReport(o.id, t), fs(a, o.id), h.skipped) break;
8627
8826
  if (h.cancelled) {
8628
8827
  f = !0, d = !0;
8629
8828
  break;
@@ -8636,22 +8835,22 @@ async function es(e, t) {
8636
8835
  } catch (e) {
8637
8836
  console.log("🛑 Step failed", e), d = !0;
8638
8837
  }
8639
- return p < c.length && (as(c, p, u), t.durationMs = Date.now() - Date.parse(t.startedAt), await i.saveReport(o.id, t)), {
8838
+ return p < c.length && (ys(c, p, u), t.durationMs = Date.now() - Date.parse(t.startedAt), await i.saveReport(o.id, t), fs(a, o.id)), {
8640
8839
  failed: d,
8641
8840
  cancelled: f,
8642
8841
  stepReports: u
8643
8842
  };
8644
8843
  }
8645
- async function ts(e) {
8844
+ async function hs(e) {
8646
8845
  let { step: t, stepIndex: n, executionContext: r } = e;
8647
- if (t.id ||= `step-${n}`, t.name ||= t.id, t.if && !await ko.evaluateConditions(t.if, r)) return Yo && console.log(`⏩ Skipped step ${t.id} based on condition: ${t.if}`, r), {
8846
+ if (t.id ||= `step-${n}`, t.name ||= t.id, t.if && !await ko.evaluateConditions(t.if, r)) return ts && console.log(`⏩ Skipped step ${t.id} based on condition: ${t.if}`, r), {
8648
8847
  failed: !1,
8649
8848
  cancelled: !1,
8650
8849
  skipped: !0,
8651
8850
  report: null
8652
8851
  };
8653
8852
  try {
8654
- let n = await rs(t.env, r), i = {
8853
+ let n = await _s(t.env, r), i = {
8655
8854
  jobId: String(e.jobId),
8656
8855
  step: t,
8657
8856
  command: t.run,
@@ -8665,15 +8864,15 @@ async function ts(e) {
8665
8864
  WORKING_DIR: r.workingDir
8666
8865
  }
8667
8866
  };
8668
- return t.eval ? ns({
8867
+ return t.eval ? gs({
8669
8868
  ...e,
8670
8869
  stepContext: i
8671
- }) : is({
8870
+ }) : vs({
8672
8871
  ...e,
8673
8872
  stepContext: i
8674
8873
  });
8675
8874
  } catch (e) {
8676
- return Yo && console.error(`⏩ Failed to run step ${t.id}`, r, e), {
8875
+ return ts && console.error(`⏩ Failed to run step ${t.id}`, r, e), {
8677
8876
  failed: !0,
8678
8877
  cancelled: !1,
8679
8878
  skipped: !1,
@@ -8681,7 +8880,7 @@ async function ts(e) {
8681
8880
  };
8682
8881
  }
8683
8882
  }
8684
- async function ns(e) {
8883
+ async function gs(e) {
8685
8884
  let { queue: t, stepContext: n, executionContext: r } = e, { jobId: i, step: a } = n, o = Date.now(), s = a.id, c = a.name;
8686
8885
  try {
8687
8886
  let e = await ko.evaluateExpression(a.eval, r);
@@ -8727,12 +8926,12 @@ async function ns(e) {
8727
8926
  };
8728
8927
  }
8729
8928
  }
8730
- async function rs(e, t) {
8929
+ async function _s(e, t) {
8731
8930
  let n = {};
8732
8931
  if (e) for (let [r, i] of Object.entries(e)) n[r] = String(await ko.evaluateValue(i, t));
8733
8932
  return n;
8734
8933
  }
8735
- async function is(t) {
8934
+ async function vs(t) {
8736
8935
  let { workerId: n, jobId: r, stepContext: i, executionContext: a, driver: o, queue: s } = t, { step: c } = i, l = c.id, u = c.name, d;
8737
8936
  try {
8738
8937
  d = o.execute(i);
@@ -8747,10 +8946,11 @@ async function is(t) {
8747
8946
  logFilePath: ""
8748
8947
  };
8749
8948
  }
8949
+ rs.add(d);
8750
8950
  let f = !1, p = setInterval(async () => {
8751
8951
  await s.isCancelled(r) && (console.log(`[${n}] 🛑 Job #${r} was cancelled! Halting execution.`), f = !0, clearInterval(p), await d.cancel());
8752
8952
  }, 3e3), m = await d.done;
8753
- if (clearInterval(p), d.logFilePath && e.existsSync(d.logFilePath)) try {
8953
+ if (clearInterval(p), rs.delete(d), d.logFilePath && e.existsSync(d.logFilePath)) try {
8754
8954
  let e = await o.readLog(d.logFilePath);
8755
8955
  await s.saveStepLog(r, l, e);
8756
8956
  } catch (e) {
@@ -8777,7 +8977,7 @@ async function is(t) {
8777
8977
  }
8778
8978
  };
8779
8979
  }
8780
- function as(e, t, n) {
8980
+ function ys(e, t, n) {
8781
8981
  for (let r = t; r < e.length; r++) {
8782
8982
  let t = e[r], i = t.id || `step-${r}`;
8783
8983
  n[r] = {
@@ -8791,7 +8991,7 @@ function as(e, t, n) {
8791
8991
  };
8792
8992
  }
8793
8993
  }
8794
- function os(e, t, n, r, i, a, o) {
8994
+ function bs(e, t, n, r, i, a, o) {
8795
8995
  return {
8796
8996
  jobId: String(e.id),
8797
8997
  parentId: String(e.parentId || ""),
@@ -8810,56 +9010,51 @@ function os(e, t, n, r, i, a, o) {
8810
9010
  })
8811
9011
  };
8812
9012
  }
8813
- async function ss(e, t = [], n) {
8814
- !Array.isArray(t) || t.length === 0 || (console.log(`[${e}] 📢 Dispatching execution report to ${t.length} reporter(s)...`), await Promise.allSettled(t.map(async (t) => {
8815
- try {
8816
- await t.report(n);
8817
- } catch (n) {
8818
- console.error(`[${e}] ⚠️ Reporter '${t.name || "unknown"}' failed:`, n.message);
8819
- }
8820
- })));
8821
- }
8822
9013
  //#endregion
8823
- //#region src/reporters/json-file.reporter.ts
8824
- var cs = class {
8825
- name = "json-file";
8826
- outputDir;
8827
- constructor(e) {
8828
- this.outputDir = e.outputDir;
8829
- }
8830
- async report(t) {
8831
- e.mkdirSync(this.outputDir, { recursive: !0 });
8832
- let n = a.join(this.outputDir, `run-${t.jobId}.json`);
8833
- e.writeFileSync(n, JSON.stringify(t, null, 2), "utf-8"), console.log(`📊 Execution report saved to: ${n}`);
8834
- }
8835
- }, ls = class {
8836
- name = "slack";
8837
- webhookUrl = "";
8838
- token = "";
8839
- channel = "";
8840
- notifyOn = ["failed"];
9014
+ //#region src/plugins/github-status.plugin.ts
9015
+ var xs = class {
9016
+ name = "github-commit-status";
9017
+ token;
9018
+ apiUrl;
9019
+ context;
8841
9020
  constructor(e) {
8842
- Object.assign(this, e);
9021
+ this.token = e.token, this.apiUrl = e.apiUrl || "https://api.github.com/", this.context = e.context || "on";
9022
+ }
9023
+ async onWorkflowStart(e) {
9024
+ this.hasCoordinates(e) && await this.updateStatus(e, "pending", "Workflow build has started.");
8843
9025
  }
8844
- async report(e) {
8845
- if (!this.notifyOn.includes(e.status)) return;
8846
- let t = `${e.status === "success" ? "" : ""} *Workflow ${e.workflowName} (#${e.jobId})* finished with status: *${e.status.toUpperCase()}* (${e.durationMs}ms)`;
8847
- await fetch(this.webhookUrl, {
9026
+ async onWorkflowFinish(e, t) {
9027
+ if (!this.hasCoordinates(e)) return;
9028
+ let n = t === "success" ? "success" : t === "failed" ? "failure" : "error", r = t === "success" ? "Workflow completed successfully." : `Workflow ${t}.`;
9029
+ await this.updateStatus(e, n, r);
9030
+ }
9031
+ async updateStatus(e, t, n) {
9032
+ let r = encodeURIComponent(String(e.inputs.owner)), i = encodeURIComponent(String(e.inputs.repo)), a = encodeURIComponent(String(e.inputs.commit_sha)), o = this.apiUrl.endsWith("/") ? this.apiUrl : `${this.apiUrl}/`, s = await fetch(new URL(`repos/${r}/${i}/statuses/${a}`, o), {
8848
9033
  method: "POST",
8849
9034
  headers: {
9035
+ Accept: "application/vnd.github+json",
8850
9036
  Authorization: `Bearer ${this.token}`,
8851
- "Content-Type": "application/json"
9037
+ "Content-Type": "application/json",
9038
+ "User-Agent": "@cloud-cli/on",
9039
+ "X-GitHub-Api-Version": "2022-11-28"
8852
9040
  },
9041
+ signal: AbortSignal.timeout(1e4),
8853
9042
  body: JSON.stringify({
8854
- channel: this.channel,
8855
- text: t
9043
+ state: t,
9044
+ target_url: e.runUrl,
9045
+ description: n.slice(0, 140),
9046
+ context: `${this.context}/${e.workflowName}`.slice(0, 100)
8856
9047
  })
8857
9048
  });
9049
+ if (!s.ok) throw Error(`GitHub status update failed with HTTP ${s.status}: ${await s.text()}`);
9050
+ }
9051
+ hasCoordinates(e) {
9052
+ return !!(this.token && e.inputs.owner && e.inputs.repo && e.inputs.commit_sha);
8858
9053
  }
8859
9054
  };
8860
9055
  //#endregion
8861
9056
  //#region src/index.ts
8862
- function us(e) {
9057
+ function Ss(e) {
8863
9058
  console.log("🔍 Validating Workflows in:", e.workflows);
8864
9059
  let t = new di(e.workflows), n = r(e.workflows).filter((e) => e.endsWith(".yml") || e.endsWith(".yaml"));
8865
9060
  for (let e of n) {
@@ -8867,13 +9062,13 @@ function us(e) {
8867
9062
  console.log(` ✅ ${e} -> Valid! (${n.length} job matrix variant(s) generated)`);
8868
9063
  }
8869
9064
  }
8870
- async function ds() {
9065
+ async function Cs() {
8871
9066
  let { config: e, command: t } = await fe();
8872
- if (e || process.exit(1), t === "validate") return us(e);
9067
+ if (e || process.exit(1), t === "validate") return Ss(e);
8873
9068
  let n = new mi("./.env"), r = new pi(process.env.WORKER_NAME || "cli");
8874
9069
  switch (t) {
8875
9070
  case "start-server":
8876
- console.log("🌐 Starting Ingress Gateway..."), await r.init(), await Uo.withPort({
9071
+ console.log("🌐 Starting Ingress Gateway..."), await r.init(), await Jo.withPort({
8877
9072
  config: e,
8878
9073
  queue: r,
8879
9074
  secrets: n,
@@ -8882,11 +9077,11 @@ async function ds() {
8882
9077
  });
8883
9078
  break;
8884
9079
  case "start-workers":
8885
- console.log(`⚙️ Starting ${e.workers} Worker Loop(s)...`), await r.init(), Zo(e.workers, r, n, e);
9080
+ console.log(`⚙️ Starting worker scheduler with ${e.workers} concurrent slot(s)...`), await r.init(), ss(e.workers, r, n, e);
8886
9081
  break;
8887
9082
  default: console.error(`❌ Unknown command: '${t}'`), de(), process.exit(1);
8888
9083
  }
8889
9084
  }
8890
- ds().catch(console.error);
9085
+ Cs().catch(console.error);
8891
9086
  //#endregion
8892
- export { Io as HtmlReporter, cs as JsonFileReporter, ls as SlackReporter };
9087
+ export { xs as GitHubStatusPlugin };