crontick 0.1.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/LICENSE +21 -0
- package/README.md +56 -0
- package/dist/chunk-35FFLWP3.js +79 -0
- package/dist/chunk-35FFLWP3.js.map +1 -0
- package/dist/chunk-FMGZ3SSS.js +57 -0
- package/dist/chunk-FMGZ3SSS.js.map +1 -0
- package/dist/chunk-LPAFZNXK.js +214 -0
- package/dist/chunk-LPAFZNXK.js.map +1 -0
- package/dist/chunk-YMAT5MON.js +22 -0
- package/dist/chunk-YMAT5MON.js.map +1 -0
- package/dist/cli/index.cjs +879 -0
- package/dist/cli/index.cjs.map +1 -0
- package/dist/cli/index.d.cts +2 -0
- package/dist/cli/index.d.ts +2 -0
- package/dist/cli/index.js +554 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/daemon/index.cjs +1655 -0
- package/dist/daemon/index.cjs.map +1 -0
- package/dist/daemon/index.d.cts +2 -0
- package/dist/daemon/index.d.ts +2 -0
- package/dist/daemon/index.js +1306 -0
- package/dist/daemon/index.js.map +1 -0
- package/dist/dashboard/.gitkeep +0 -0
- package/dist/dashboard/dashboard.css +192 -0
- package/dist/dashboard/dashboard.js +316 -0
- package/dist/dashboard/index.html +108 -0
- package/dist/index.cjs +180 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +121 -0
- package/dist/index.d.ts +121 -0
- package/dist/index.js +32 -0
- package/dist/index.js.map +1 -0
- package/dist/job-JQGLDEJV.js +26 -0
- package/dist/job-JQGLDEJV.js.map +1 -0
- package/dist/mcp/index.cjs +1000 -0
- package/dist/mcp/index.cjs.map +1 -0
- package/dist/mcp/index.d.cts +13 -0
- package/dist/mcp/index.d.ts +13 -0
- package/dist/mcp/index.js +876 -0
- package/dist/mcp/index.js.map +1 -0
- package/package.json +81 -0
- package/plugin/.gitkeep +0 -0
- package/plugin/README.md +50 -0
- package/plugin/install.mjs +155 -0
- package/plugin/plugin.json +11 -0
- package/plugin/uninstall.mjs +68 -0
- package/src/skill/SKILL.md +227 -0
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const BASE = '';
|
|
4
|
+
|
|
5
|
+
let jobs = [];
|
|
6
|
+
let runs = [];
|
|
7
|
+
let currentPage = 'jobs';
|
|
8
|
+
|
|
9
|
+
async function apiFetch(method, path, body) {
|
|
10
|
+
const opts = { method, headers: { 'Content-Type': 'application/json' } };
|
|
11
|
+
if (body !== undefined) opts.body = JSON.stringify(body);
|
|
12
|
+
const res = await fetch(BASE + path, opts);
|
|
13
|
+
const text = await res.text();
|
|
14
|
+
try {
|
|
15
|
+
return JSON.parse(text);
|
|
16
|
+
} catch {
|
|
17
|
+
return text;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
async function pollHealth() {
|
|
22
|
+
const badge = document.getElementById('health-badge');
|
|
23
|
+
const versionEl = document.getElementById('version-info');
|
|
24
|
+
try {
|
|
25
|
+
const h = await apiFetch('GET', '/health');
|
|
26
|
+
if (h && h.ok) {
|
|
27
|
+
badge.textContent = `✓ up ${formatDuration(h.uptimeSec)}`;
|
|
28
|
+
badge.className = 'badge badge-ok';
|
|
29
|
+
if (versionEl) {
|
|
30
|
+
versionEl.textContent = `v${h.version} · pid ${h.pid} · node ${h.node} · ${h.jobs.total} jobs`;
|
|
31
|
+
}
|
|
32
|
+
} else {
|
|
33
|
+
badge.textContent = '✗ unhealthy';
|
|
34
|
+
badge.className = 'badge badge-error';
|
|
35
|
+
}
|
|
36
|
+
} catch {
|
|
37
|
+
badge.textContent = '✗ unreachable';
|
|
38
|
+
badge.className = 'badge badge-error';
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function formatDuration(sec) {
|
|
43
|
+
if (sec < 60) return `${sec}s`;
|
|
44
|
+
if (sec < 3600) return `${Math.floor(sec / 60)}m`;
|
|
45
|
+
return `${Math.floor(sec / 3600)}h ${Math.floor((sec % 3600) / 60)}m`;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async function loadJobs() {
|
|
49
|
+
jobs = (await apiFetch('GET', '/api/jobs')) || [];
|
|
50
|
+
renderJobs();
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function renderJobs() {
|
|
54
|
+
const tbody = document.getElementById('jobs-tbody');
|
|
55
|
+
if (!tbody) return;
|
|
56
|
+
tbody.innerHTML = jobs.map((j) => `
|
|
57
|
+
<tr>
|
|
58
|
+
<td><a href="#" class="link" data-job="${escHtml(j.id)}">${escHtml(j.id)}</a></td>
|
|
59
|
+
<td>${escHtml(j.description || '—')}</td>
|
|
60
|
+
<td><code>${escHtml(scheduleLabel(j.schedule))}</code></td>
|
|
61
|
+
<td>
|
|
62
|
+
<label class="toggle">
|
|
63
|
+
<input type="checkbox" ${j.enabled ? 'checked' : ''} data-toggle="${escHtml(j.id)}">
|
|
64
|
+
</label>
|
|
65
|
+
</td>
|
|
66
|
+
<td id="last-status-${escHtml(j.id)}">—</td>
|
|
67
|
+
<td>
|
|
68
|
+
<button class="btn" data-run-now="${escHtml(j.id)}">▶ Run</button>
|
|
69
|
+
<button class="btn btn-danger" data-delete="${escHtml(j.id)}">✕</button>
|
|
70
|
+
</td>
|
|
71
|
+
</tr>
|
|
72
|
+
`).join('');
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function scheduleLabel(schedule) {
|
|
76
|
+
if (!schedule) return '?';
|
|
77
|
+
if (schedule.kind === 'cron') return schedule.cron + (schedule.tz ? ` (${schedule.tz})` : '');
|
|
78
|
+
if (schedule.kind === 'interval') return `every ${schedule.everySec}s`;
|
|
79
|
+
if (schedule.kind === 'one-shot') return `once at ${schedule.runAt}`;
|
|
80
|
+
return schedule.kind;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
async function loadRuns() {
|
|
84
|
+
const filterJobId = document.getElementById('runs-filter')?.value || '';
|
|
85
|
+
const filterStatus = document.getElementById('runs-status-filter')?.value || '';
|
|
86
|
+
const qs = new URLSearchParams({ limit: '100' });
|
|
87
|
+
if (filterJobId) qs.set('jobId', filterJobId);
|
|
88
|
+
runs = (await apiFetch('GET', `/api/runs?${qs}`)) || [];
|
|
89
|
+
if (filterStatus) runs = runs.filter((r) => r.status === filterStatus);
|
|
90
|
+
renderRuns();
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function renderRuns() {
|
|
94
|
+
const tbody = document.getElementById('runs-tbody');
|
|
95
|
+
if (!tbody) return;
|
|
96
|
+
tbody.innerHTML = runs.map((r) => `
|
|
97
|
+
<tr>
|
|
98
|
+
<td><code>${escHtml(r.id.slice(0, 12))}…</code></td>
|
|
99
|
+
<td>${escHtml(r.jobId)}</td>
|
|
100
|
+
<td class="status-${r.status}">${escHtml(r.status)}</td>
|
|
101
|
+
<td>${new Date(r.startedAt).toLocaleString()}</td>
|
|
102
|
+
<td>${r.durationMs != null ? `${r.durationMs}ms` : '—'}</td>
|
|
103
|
+
<td><button class="btn" data-view-logs="${escHtml(r.id)}">Logs</button></td>
|
|
104
|
+
</tr>
|
|
105
|
+
`).join('');
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function openDrawer(title, content) {
|
|
109
|
+
document.getElementById('drawer-title').textContent = title;
|
|
110
|
+
document.getElementById('drawer-content').innerHTML = content;
|
|
111
|
+
document.getElementById('drawer').classList.add('open');
|
|
112
|
+
document.getElementById('overlay').classList.add('active');
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function closeDrawer() {
|
|
116
|
+
document.getElementById('drawer').classList.remove('open');
|
|
117
|
+
document.getElementById('overlay').classList.remove('active');
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
async function openJobDrawer(jobId) {
|
|
121
|
+
const job = await apiFetch('GET', `/api/jobs/${encodeURIComponent(jobId)}`);
|
|
122
|
+
const jobRuns = (await apiFetch('GET', `/api/runs?jobId=${encodeURIComponent(jobId)}&limit=20`)) || [];
|
|
123
|
+
const runsHtml = jobRuns.map((r) => `
|
|
124
|
+
<tr>
|
|
125
|
+
<td class="status-${r.status}">${r.status}</td>
|
|
126
|
+
<td>${new Date(r.startedAt).toLocaleString()}</td>
|
|
127
|
+
<td>${r.durationMs != null ? `${r.durationMs}ms` : '—'}</td>
|
|
128
|
+
<td><button class="btn" data-view-logs="${escHtml(r.id)}">Logs</button></td>
|
|
129
|
+
</tr>
|
|
130
|
+
`).join('');
|
|
131
|
+
openDrawer(`Job: ${jobId}`, `
|
|
132
|
+
<h3>Definition</h3>
|
|
133
|
+
<pre>${escHtml(JSON.stringify(job, null, 2))}</pre>
|
|
134
|
+
<h3 style="margin-top:16px">Recent Runs (last 20)</h3>
|
|
135
|
+
<table>
|
|
136
|
+
<thead><tr><th>Status</th><th>Started</th><th>Duration</th><th></th></tr></thead>
|
|
137
|
+
<tbody>${runsHtml}</tbody>
|
|
138
|
+
</table>
|
|
139
|
+
`);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
async function openLogsDrawer(runId) {
|
|
143
|
+
const logs = await apiFetch('GET', `/api/runs/${encodeURIComponent(runId)}/logs`);
|
|
144
|
+
const text = Array.isArray(logs) ? logs.map((l) => l.data).join('') : String(logs);
|
|
145
|
+
openDrawer(`Logs: ${runId.slice(0, 12)}…`, `
|
|
146
|
+
<pre id="log-pre">${escHtml(text)}</pre>
|
|
147
|
+
<button class="btn" id="btn-tail-logs" data-run-id="${escHtml(runId)}">↓ Stream (SSE)</button>
|
|
148
|
+
`);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function openCreateModal() {
|
|
152
|
+
document.getElementById('modal-create').classList.add('open');
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function closeCreateModal() {
|
|
156
|
+
document.getElementById('modal-create').classList.remove('open');
|
|
157
|
+
document.getElementById('form-error').textContent = '';
|
|
158
|
+
document.getElementById('form-create').reset();
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
async function handleCreateJob(e) {
|
|
162
|
+
e.preventDefault();
|
|
163
|
+
const fd = new FormData(e.target);
|
|
164
|
+
const id = fd.get('id');
|
|
165
|
+
const cron = fd.get('cron');
|
|
166
|
+
const script = fd.get('script');
|
|
167
|
+
const shell = fd.get('shell');
|
|
168
|
+
const timeout = fd.get('timeout');
|
|
169
|
+
const overlap = fd.get('overlap');
|
|
170
|
+
const retry = fd.get('retry');
|
|
171
|
+
const description = fd.get('description');
|
|
172
|
+
|
|
173
|
+
if (!id || !cron || !script) {
|
|
174
|
+
document.getElementById('form-error').textContent = 'ID, cron, and script are required.';
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const body = {
|
|
179
|
+
id,
|
|
180
|
+
description: description || undefined,
|
|
181
|
+
schedule: { kind: 'cron', cron },
|
|
182
|
+
action: {
|
|
183
|
+
kind: 'script',
|
|
184
|
+
script,
|
|
185
|
+
shell,
|
|
186
|
+
timeoutSec: timeout ? parseInt(timeout, 10) : undefined,
|
|
187
|
+
},
|
|
188
|
+
overlap,
|
|
189
|
+
retry: retry ? { max: parseInt(retry, 10), backoffSec: 30 } : undefined,
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
const result = await apiFetch('POST', '/api/jobs', body);
|
|
193
|
+
if (result && result.error) {
|
|
194
|
+
document.getElementById('form-error').textContent = JSON.stringify(result.error);
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
closeCreateModal();
|
|
198
|
+
await loadJobs();
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function showPage(page) {
|
|
202
|
+
currentPage = page;
|
|
203
|
+
document.querySelectorAll('.page').forEach((el) => el.classList.remove('active'));
|
|
204
|
+
document.querySelectorAll('.nav-btn').forEach((el) => el.classList.remove('active'));
|
|
205
|
+
document.getElementById(`page-${page}`)?.classList.add('active');
|
|
206
|
+
document.querySelector(`[data-page="${page}"]`)?.classList.add('active');
|
|
207
|
+
if (page === 'runs') void loadRuns();
|
|
208
|
+
if (page === 'jobs') void loadJobs();
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
document.addEventListener('click', async (e) => {
|
|
212
|
+
const target = e.target;
|
|
213
|
+
if (!(target instanceof HTMLElement)) return;
|
|
214
|
+
|
|
215
|
+
const navPage = target.dataset?.page;
|
|
216
|
+
if (navPage) {
|
|
217
|
+
showPage(navPage);
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
const jobId = target.dataset?.job;
|
|
222
|
+
if (jobId) {
|
|
223
|
+
e.preventDefault();
|
|
224
|
+
await openJobDrawer(jobId);
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const runNow = target.dataset?.runNow;
|
|
229
|
+
if (runNow) {
|
|
230
|
+
await apiFetch('POST', `/api/jobs/${encodeURIComponent(runNow)}/run`);
|
|
231
|
+
setTimeout(() => void loadRuns(), 500);
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
const del = target.dataset?.delete;
|
|
236
|
+
if (del) {
|
|
237
|
+
if (confirm(`Delete job "${del}"? This cannot be undone.`)) {
|
|
238
|
+
await apiFetch('DELETE', `/api/jobs/${encodeURIComponent(del)}`);
|
|
239
|
+
await loadJobs();
|
|
240
|
+
}
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
const viewLogs = target.dataset?.viewLogs;
|
|
245
|
+
if (viewLogs) {
|
|
246
|
+
await openLogsDrawer(viewLogs);
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
const tailRun = target.dataset?.runId;
|
|
251
|
+
if (tailRun && target.id === 'btn-tail-logs') {
|
|
252
|
+
const pre = document.getElementById('log-pre');
|
|
253
|
+
if (pre) {
|
|
254
|
+
const es = new EventSource(`/api/runs/${encodeURIComponent(tailRun)}/logs/stream`);
|
|
255
|
+
es.onmessage = (ev) => {
|
|
256
|
+
const d = JSON.parse(ev.data);
|
|
257
|
+
if (d.done) {
|
|
258
|
+
es.close();
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
pre.textContent += d.data || '';
|
|
262
|
+
pre.scrollTop = pre.scrollHeight;
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
if (target.id === 'drawer-close' || target.id === 'overlay') {
|
|
269
|
+
closeDrawer();
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
if (target.id === 'btn-create-job') {
|
|
274
|
+
openCreateModal();
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
if (target.id === 'modal-cancel') {
|
|
278
|
+
closeCreateModal();
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
if (target.id === 'btn-refresh') {
|
|
283
|
+
if (currentPage === 'jobs') await loadJobs();
|
|
284
|
+
else await loadRuns();
|
|
285
|
+
}
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
document.addEventListener('change', async (e) => {
|
|
289
|
+
const target = e.target;
|
|
290
|
+
if (!(target instanceof HTMLInputElement)) return;
|
|
291
|
+
const toggleId = target.dataset?.toggle;
|
|
292
|
+
if (toggleId) {
|
|
293
|
+
const endpoint = target.checked ? 'enable' : 'disable';
|
|
294
|
+
await apiFetch('POST', `/api/jobs/${encodeURIComponent(toggleId)}/${endpoint}`);
|
|
295
|
+
await loadJobs();
|
|
296
|
+
}
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
document.addEventListener('submit', (e) => {
|
|
300
|
+
if (e.target instanceof HTMLFormElement && e.target.id === 'form-create') {
|
|
301
|
+
void handleCreateJob(e);
|
|
302
|
+
}
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
void pollHealth();
|
|
306
|
+
setInterval(() => void pollHealth(), 5000);
|
|
307
|
+
void loadJobs();
|
|
308
|
+
|
|
309
|
+
function escHtml(str) {
|
|
310
|
+
return String(str ?? '')
|
|
311
|
+
.replace(/&/g, '&')
|
|
312
|
+
.replace(/</g, '<')
|
|
313
|
+
.replace(/>/g, '>')
|
|
314
|
+
.replace(/"/g, '"')
|
|
315
|
+
.replace(/'/g, ''');
|
|
316
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
+
<title>crontick</title>
|
|
7
|
+
<link rel="stylesheet" href="/dashboard/dashboard.css">
|
|
8
|
+
</head>
|
|
9
|
+
<body>
|
|
10
|
+
<header>
|
|
11
|
+
<h1>⏱ crontick</h1>
|
|
12
|
+
<div id="version-info"></div>
|
|
13
|
+
<div id="health-badge" class="badge badge-unknown">checking…</div>
|
|
14
|
+
</header>
|
|
15
|
+
<nav>
|
|
16
|
+
<button class="nav-btn active" data-page="jobs">Jobs</button>
|
|
17
|
+
<button class="nav-btn" data-page="runs">Runs</button>
|
|
18
|
+
</nav>
|
|
19
|
+
<main>
|
|
20
|
+
<section id="page-jobs" class="page active">
|
|
21
|
+
<div class="toolbar">
|
|
22
|
+
<button id="btn-create-job" class="btn btn-primary">+ New Job</button>
|
|
23
|
+
<button id="btn-refresh" class="btn">↻ Refresh</button>
|
|
24
|
+
</div>
|
|
25
|
+
<table id="jobs-table">
|
|
26
|
+
<thead>
|
|
27
|
+
<tr>
|
|
28
|
+
<th>ID</th>
|
|
29
|
+
<th>Description</th>
|
|
30
|
+
<th>Schedule</th>
|
|
31
|
+
<th>Enabled</th>
|
|
32
|
+
<th>Last Status</th>
|
|
33
|
+
<th>Actions</th>
|
|
34
|
+
</tr>
|
|
35
|
+
</thead>
|
|
36
|
+
<tbody id="jobs-tbody"></tbody>
|
|
37
|
+
</table>
|
|
38
|
+
</section>
|
|
39
|
+
|
|
40
|
+
<section id="page-runs" class="page">
|
|
41
|
+
<div class="toolbar">
|
|
42
|
+
<label>Filter by job: <input id="runs-filter" type="text" placeholder="job ID…"></label>
|
|
43
|
+
<select id="runs-status-filter">
|
|
44
|
+
<option value="">All statuses</option>
|
|
45
|
+
<option value="success">success</option>
|
|
46
|
+
<option value="failed">failed</option>
|
|
47
|
+
<option value="running">running</option>
|
|
48
|
+
<option value="canceled">canceled</option>
|
|
49
|
+
</select>
|
|
50
|
+
</div>
|
|
51
|
+
<table id="runs-table">
|
|
52
|
+
<thead>
|
|
53
|
+
<tr>
|
|
54
|
+
<th>Run ID</th>
|
|
55
|
+
<th>Job</th>
|
|
56
|
+
<th>Status</th>
|
|
57
|
+
<th>Started</th>
|
|
58
|
+
<th>Duration</th>
|
|
59
|
+
<th>Actions</th>
|
|
60
|
+
</tr>
|
|
61
|
+
</thead>
|
|
62
|
+
<tbody id="runs-tbody"></tbody>
|
|
63
|
+
</table>
|
|
64
|
+
</section>
|
|
65
|
+
</main>
|
|
66
|
+
|
|
67
|
+
<div id="drawer" class="drawer">
|
|
68
|
+
<div class="drawer-header">
|
|
69
|
+
<h2 id="drawer-title">Job Detail</h2>
|
|
70
|
+
<button id="drawer-close" class="btn">✕</button>
|
|
71
|
+
</div>
|
|
72
|
+
<div id="drawer-content" class="drawer-content"></div>
|
|
73
|
+
</div>
|
|
74
|
+
<div id="overlay" class="overlay"></div>
|
|
75
|
+
|
|
76
|
+
<div id="modal-create" class="modal">
|
|
77
|
+
<div class="modal-box">
|
|
78
|
+
<h2>Create New Job</h2>
|
|
79
|
+
<form id="form-create">
|
|
80
|
+
<label>ID (kebab-case): <input name="id" required pattern="[a-z0-9]+(-[a-z0-9]+)*"></label>
|
|
81
|
+
<label>Description: <input name="description"></label>
|
|
82
|
+
<label>Cron expression: <input name="cron" placeholder="0 9 * * * or @daily"></label>
|
|
83
|
+
<label>Script: <textarea name="script" rows="6" placeholder="#!/usr/bin/env bash echo hello"></textarea></label>
|
|
84
|
+
<label>Shell: <select name="shell">
|
|
85
|
+
<option value="auto">auto</option>
|
|
86
|
+
<option value="bash">bash</option>
|
|
87
|
+
<option value="pwsh">pwsh</option>
|
|
88
|
+
<option value="cmd">cmd</option>
|
|
89
|
+
</select></label>
|
|
90
|
+
<label>Timeout (sec): <input name="timeout" type="number" min="1"></label>
|
|
91
|
+
<label>Overlap: <select name="overlap">
|
|
92
|
+
<option value="skip">skip</option>
|
|
93
|
+
<option value="queue">queue</option>
|
|
94
|
+
<option value="cancel-previous">cancel-previous</option>
|
|
95
|
+
</select></label>
|
|
96
|
+
<label>Retry count: <input name="retry" type="number" min="0" value="0"></label>
|
|
97
|
+
<div class="form-actions">
|
|
98
|
+
<button type="submit" class="btn btn-primary">Create</button>
|
|
99
|
+
<button type="button" id="modal-cancel" class="btn">Cancel</button>
|
|
100
|
+
</div>
|
|
101
|
+
<p id="form-error" class="error"></p>
|
|
102
|
+
</form>
|
|
103
|
+
</div>
|
|
104
|
+
</div>
|
|
105
|
+
|
|
106
|
+
<script src="/dashboard/dashboard.js"></script>
|
|
107
|
+
</body>
|
|
108
|
+
</html>
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
var __create = Object.create;
|
|
4
|
+
var __defProp = Object.defineProperty;
|
|
5
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
6
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
7
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
8
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
9
|
+
var __export = (target, all) => {
|
|
10
|
+
for (var name in all)
|
|
11
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
12
|
+
};
|
|
13
|
+
var __copyProps = (to, from, except, desc) => {
|
|
14
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
15
|
+
for (let key of __getOwnPropNames(from))
|
|
16
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
17
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
18
|
+
}
|
|
19
|
+
return to;
|
|
20
|
+
};
|
|
21
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
22
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
23
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
24
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
25
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
26
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
27
|
+
mod
|
|
28
|
+
));
|
|
29
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
30
|
+
|
|
31
|
+
// src/index.ts
|
|
32
|
+
var src_exports = {};
|
|
33
|
+
__export(src_exports, {
|
|
34
|
+
CrontickError: () => CrontickError,
|
|
35
|
+
JobSchema: () => JobSchema,
|
|
36
|
+
VERSION: () => VERSION,
|
|
37
|
+
configPath: () => configPath,
|
|
38
|
+
dataDir: () => dataDir,
|
|
39
|
+
ensureDirs: () => ensureDirs,
|
|
40
|
+
jobsDir: () => jobsDir,
|
|
41
|
+
logsDir: () => logsDir,
|
|
42
|
+
pidFilePath: () => pidFilePath,
|
|
43
|
+
portFilePath: () => portFilePath,
|
|
44
|
+
runsDbPath: () => runsDbPath
|
|
45
|
+
});
|
|
46
|
+
module.exports = __toCommonJS(src_exports);
|
|
47
|
+
|
|
48
|
+
// src/version.ts
|
|
49
|
+
var VERSION = "0.1.0";
|
|
50
|
+
|
|
51
|
+
// src/errors.ts
|
|
52
|
+
var CrontickError = class _CrontickError extends Error {
|
|
53
|
+
code;
|
|
54
|
+
details;
|
|
55
|
+
constructor(code, message, details) {
|
|
56
|
+
super(message);
|
|
57
|
+
this.name = "CrontickError";
|
|
58
|
+
this.code = code;
|
|
59
|
+
this.details = details;
|
|
60
|
+
Object.setPrototypeOf(this, _CrontickError.prototype);
|
|
61
|
+
}
|
|
62
|
+
toJSON() {
|
|
63
|
+
return { code: this.code, message: this.message, details: this.details };
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
// src/paths.ts
|
|
68
|
+
var import_env_paths = __toESM(require("env-paths"), 1);
|
|
69
|
+
var import_node_fs = require("fs");
|
|
70
|
+
var import_node_path = require("path");
|
|
71
|
+
function root() {
|
|
72
|
+
const override = process.env["CRONTICK_HOME"];
|
|
73
|
+
if (override) return override;
|
|
74
|
+
return (0, import_env_paths.default)("crontick", { suffix: "" }).data;
|
|
75
|
+
}
|
|
76
|
+
function dataDir() {
|
|
77
|
+
return root();
|
|
78
|
+
}
|
|
79
|
+
function jobsDir() {
|
|
80
|
+
return (0, import_node_path.join)(root(), "jobs");
|
|
81
|
+
}
|
|
82
|
+
function runsDbPath() {
|
|
83
|
+
return (0, import_node_path.join)(root(), "runs.db");
|
|
84
|
+
}
|
|
85
|
+
function logsDir() {
|
|
86
|
+
return (0, import_node_path.join)(root(), "logs");
|
|
87
|
+
}
|
|
88
|
+
function configPath() {
|
|
89
|
+
return (0, import_node_path.join)(root(), "config.json");
|
|
90
|
+
}
|
|
91
|
+
function pidFilePath() {
|
|
92
|
+
return (0, import_node_path.join)(root(), "daemon.pid");
|
|
93
|
+
}
|
|
94
|
+
function portFilePath() {
|
|
95
|
+
return (0, import_node_path.join)(root(), "daemon.port");
|
|
96
|
+
}
|
|
97
|
+
function ensureDirs() {
|
|
98
|
+
for (const dir of [dataDir(), jobsDir(), logsDir()]) {
|
|
99
|
+
(0, import_node_fs.mkdirSync)(dir, { recursive: true });
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// src/schemas/job.ts
|
|
104
|
+
var import_zod = require("zod");
|
|
105
|
+
var CronScheduleSchema = import_zod.z.object({
|
|
106
|
+
kind: import_zod.z.literal("cron"),
|
|
107
|
+
cron: import_zod.z.string().min(1),
|
|
108
|
+
tz: import_zod.z.string().optional()
|
|
109
|
+
});
|
|
110
|
+
var IntervalScheduleSchema = import_zod.z.object({
|
|
111
|
+
kind: import_zod.z.literal("interval"),
|
|
112
|
+
everySec: import_zod.z.number().positive(),
|
|
113
|
+
startAt: import_zod.z.string().optional()
|
|
114
|
+
// ISO-8601
|
|
115
|
+
});
|
|
116
|
+
var OneShotScheduleSchema = import_zod.z.object({
|
|
117
|
+
kind: import_zod.z.literal("one-shot"),
|
|
118
|
+
runAt: import_zod.z.string().min(1)
|
|
119
|
+
// ISO-8601
|
|
120
|
+
});
|
|
121
|
+
var ScheduleSchema = import_zod.z.discriminatedUnion("kind", [
|
|
122
|
+
CronScheduleSchema,
|
|
123
|
+
IntervalScheduleSchema,
|
|
124
|
+
OneShotScheduleSchema
|
|
125
|
+
]);
|
|
126
|
+
var ScriptActionSchema = import_zod.z.object({
|
|
127
|
+
kind: import_zod.z.literal("script"),
|
|
128
|
+
script: import_zod.z.string().min(1),
|
|
129
|
+
shell: import_zod.z.enum(["auto", "bash", "pwsh", "cmd"]).default("auto"),
|
|
130
|
+
cwd: import_zod.z.string().optional(),
|
|
131
|
+
env: import_zod.z.record(import_zod.z.string(), import_zod.z.string()).optional(),
|
|
132
|
+
envFile: import_zod.z.string().optional(),
|
|
133
|
+
timeoutSec: import_zod.z.number().positive().optional()
|
|
134
|
+
});
|
|
135
|
+
var ExecActionSchema = import_zod.z.object({
|
|
136
|
+
kind: import_zod.z.literal("exec"),
|
|
137
|
+
command: import_zod.z.string().min(1),
|
|
138
|
+
args: import_zod.z.array(import_zod.z.string()).default([]),
|
|
139
|
+
cwd: import_zod.z.string().optional(),
|
|
140
|
+
env: import_zod.z.record(import_zod.z.string(), import_zod.z.string()).optional(),
|
|
141
|
+
envFile: import_zod.z.string().optional(),
|
|
142
|
+
timeoutSec: import_zod.z.number().positive().optional()
|
|
143
|
+
// shell is intentionally absent: exec always uses shell=false to prevent injection
|
|
144
|
+
});
|
|
145
|
+
var ActionSchema = import_zod.z.discriminatedUnion("kind", [ScriptActionSchema, ExecActionSchema]);
|
|
146
|
+
var RetrySchema = import_zod.z.object({
|
|
147
|
+
max: import_zod.z.number().int().min(0).default(0),
|
|
148
|
+
backoffSec: import_zod.z.number().positive().default(30)
|
|
149
|
+
});
|
|
150
|
+
var BudgetsSchema = import_zod.z.object({
|
|
151
|
+
maxRunsPerDay: import_zod.z.number().int().positive().nullable().default(null),
|
|
152
|
+
maxTokensPerRun: import_zod.z.number().int().positive().nullable().default(null)
|
|
153
|
+
});
|
|
154
|
+
var kebabCase = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
155
|
+
var JobSchema = import_zod.z.object({
|
|
156
|
+
id: import_zod.z.string().regex(kebabCase, 'Job ID must be kebab-case (e.g. "my-job")'),
|
|
157
|
+
description: import_zod.z.string().optional(),
|
|
158
|
+
enabled: import_zod.z.boolean().default(true),
|
|
159
|
+
schedule: ScheduleSchema,
|
|
160
|
+
action: ActionSchema,
|
|
161
|
+
catchup: import_zod.z.enum(["run-once", "run-all", "skip"]).default("skip"),
|
|
162
|
+
overlap: import_zod.z.enum(["skip", "queue", "cancel-previous"]).default("skip"),
|
|
163
|
+
retry: RetrySchema.default({ max: 0, backoffSec: 30 }),
|
|
164
|
+
budgets: BudgetsSchema.default({ maxRunsPerDay: null, maxTokensPerRun: null })
|
|
165
|
+
});
|
|
166
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
167
|
+
0 && (module.exports = {
|
|
168
|
+
CrontickError,
|
|
169
|
+
JobSchema,
|
|
170
|
+
VERSION,
|
|
171
|
+
configPath,
|
|
172
|
+
dataDir,
|
|
173
|
+
ensureDirs,
|
|
174
|
+
jobsDir,
|
|
175
|
+
logsDir,
|
|
176
|
+
pidFilePath,
|
|
177
|
+
portFilePath,
|
|
178
|
+
runsDbPath
|
|
179
|
+
});
|
|
180
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/version.ts","../src/errors.ts","../src/paths.ts","../src/schemas/job.ts"],"sourcesContent":["export { VERSION } from './version.js';\nexport { CrontickError } from './errors.js';\nexport {\n dataDir,\n jobsDir,\n runsDbPath,\n logsDir,\n configPath,\n pidFilePath,\n portFilePath,\n ensureDirs,\n} from './paths.js';\nexport { JobSchema } from './schemas/job.js';\nexport type { Job, JobInput, Schedule, Action } from './schemas/job.js';\n","// Version constant injected at build time by tsup/esbuild (define: __CRONTICK_VERSION__)\n// and at test time by vitest (define in vitest.config.ts).\ndeclare const __CRONTICK_VERSION__: string;\nexport const VERSION: string = __CRONTICK_VERSION__;\n","export class CrontickError extends Error {\n code: string;\n details?: unknown;\n\n constructor(code: string, message: string, details?: unknown) {\n super(message);\n this.name = 'CrontickError';\n this.code = code;\n this.details = details;\n // Maintain proper prototype chain for instanceof checks\n Object.setPrototypeOf(this, CrontickError.prototype);\n }\n\n toJSON(): { code: string; message: string; details?: unknown } {\n return { code: this.code, message: this.message, details: this.details };\n }\n}\n","import envPaths from 'env-paths';\nimport { mkdirSync } from 'node:fs';\nimport { join } from 'node:path';\n\nfunction root(): string {\n const override = process.env['CRONTICK_HOME'];\n if (override) return override;\n // env-paths v3: data dir on windows = %LOCALAPPDATA%\\crontick\n return envPaths('crontick', { suffix: '' }).data;\n}\n\nexport function dataDir(): string {\n return root();\n}\n\nexport function jobsDir(): string {\n return join(root(), 'jobs');\n}\n\nexport function runsDbPath(): string {\n return join(root(), 'runs.db');\n}\n\nexport function logsDir(): string {\n return join(root(), 'logs');\n}\n\nexport function configPath(): string {\n return join(root(), 'config.json');\n}\n\nexport function pidFilePath(): string {\n return join(root(), 'daemon.pid');\n}\n\nexport function portFilePath(): string {\n return join(root(), 'daemon.port');\n}\n\nexport function autostartDir(): string {\n return join(root(), 'autostart');\n}\n\nexport function ensureDirs(): void {\n for (const dir of [dataDir(), jobsDir(), logsDir()]) {\n mkdirSync(dir, { recursive: true });\n }\n}\n","import { z } from 'zod';\n\n// ── Schedule ──────────────────────────────────────────────────────────────────\n\nexport const CronScheduleSchema = z.object({\n kind: z.literal('cron'),\n cron: z.string().min(1),\n tz: z.string().optional(),\n});\n\nexport const IntervalScheduleSchema = z.object({\n kind: z.literal('interval'),\n everySec: z.number().positive(),\n startAt: z.string().optional(), // ISO-8601\n});\n\nexport const OneShotScheduleSchema = z.object({\n kind: z.literal('one-shot'),\n runAt: z.string().min(1), // ISO-8601\n});\n\nexport const ScheduleSchema = z.discriminatedUnion('kind', [\n CronScheduleSchema,\n IntervalScheduleSchema,\n OneShotScheduleSchema,\n]);\n\n// ── Action ────────────────────────────────────────────────────────────────────\n\nexport const ScriptActionSchema = z.object({\n kind: z.literal('script'),\n script: z.string().min(1),\n shell: z.enum(['auto', 'bash', 'pwsh', 'cmd']).default('auto'),\n cwd: z.string().optional(),\n env: z.record(z.string(), z.string()).optional(),\n envFile: z.string().optional(),\n timeoutSec: z.number().positive().optional(),\n});\n\nexport const ExecActionSchema = z.object({\n kind: z.literal('exec'),\n command: z.string().min(1),\n args: z.array(z.string()).default([]),\n cwd: z.string().optional(),\n env: z.record(z.string(), z.string()).optional(),\n envFile: z.string().optional(),\n timeoutSec: z.number().positive().optional(),\n // shell is intentionally absent: exec always uses shell=false to prevent injection\n});\n\nexport const ActionSchema = z.discriminatedUnion('kind', [ScriptActionSchema, ExecActionSchema]);\n\n// ── Supporting types ──────────────────────────────────────────────────────────\n\nexport const RetrySchema = z.object({\n max: z.number().int().min(0).default(0),\n backoffSec: z.number().positive().default(30),\n});\n\nexport const BudgetsSchema = z.object({\n maxRunsPerDay: z.number().int().positive().nullable().default(null),\n maxTokensPerRun: z.number().int().positive().nullable().default(null),\n});\n\n// ── Job ───────────────────────────────────────────────────────────────────────\n\nconst kebabCase = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;\n\nexport const JobSchema = z.object({\n id: z.string().regex(kebabCase, 'Job ID must be kebab-case (e.g. \"my-job\")'),\n description: z.string().optional(),\n enabled: z.boolean().default(true),\n schedule: ScheduleSchema,\n action: ActionSchema,\n catchup: z.enum(['run-once', 'run-all', 'skip']).default('skip'),\n overlap: z.enum(['skip', 'queue', 'cancel-previous']).default('skip'),\n retry: RetrySchema.default({ max: 0, backoffSec: 30 }),\n budgets: BudgetsSchema.default({ maxRunsPerDay: null, maxTokensPerRun: null }),\n});\n\nexport type Job = z.infer<typeof JobSchema>;\nexport type JobInput = z.input<typeof JobSchema>;\nexport type Schedule = z.infer<typeof ScheduleSchema>;\nexport type Action = z.infer<typeof ActionSchema>;\n"],"mappings":"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","names":["envPaths"]}
|