paneful 0.9.17 → 0.9.19
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/README.md +29 -0
- package/dist/server/git-source-control.js +398 -0
- package/dist/server/index.js +31 -0
- package/dist/server/pty-manager.js +54 -2
- package/dist/server/schedule-store.js +99 -0
- package/dist/server/scheduler.js +189 -0
- package/dist/server/settings-store.js +4 -0
- package/dist/server/ws-handler.js +418 -6
- package/dist/web/assets/index-BCSI40O7.css +32 -0
- package/dist/web/assets/{index-BO_xZPlW.js → index-gI76jzKp.js} +10 -10
- package/dist/web/index.html +2 -2
- package/package.json +1 -1
- package/dist/web/assets/index-BptWthNY.css +0 -32
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Minimal cron matcher. Supports 5-field standard cron:
|
|
3
|
+
* minute hour day-of-month month day-of-week
|
|
4
|
+
*
|
|
5
|
+
* Field syntax:
|
|
6
|
+
* * (any)
|
|
7
|
+
* N (literal)
|
|
8
|
+
* N,M,O (list)
|
|
9
|
+
* N-M (range)
|
|
10
|
+
* *\/N (step)
|
|
11
|
+
* N-M/K (stepped range)
|
|
12
|
+
*
|
|
13
|
+
* Day-of-week: 0-6 (0=Sunday).
|
|
14
|
+
*/
|
|
15
|
+
const RANGES = [
|
|
16
|
+
[0, 59], // minute
|
|
17
|
+
[0, 23], // hour
|
|
18
|
+
[1, 31], // day of month
|
|
19
|
+
[1, 12], // month
|
|
20
|
+
[0, 6], // day of week
|
|
21
|
+
];
|
|
22
|
+
function parseField(field, min, max) {
|
|
23
|
+
if (field === '*')
|
|
24
|
+
return 'any';
|
|
25
|
+
const values = new Set();
|
|
26
|
+
for (const part of field.split(',')) {
|
|
27
|
+
let stepStr;
|
|
28
|
+
let rangeStr = part;
|
|
29
|
+
const slashIdx = part.indexOf('/');
|
|
30
|
+
if (slashIdx >= 0) {
|
|
31
|
+
rangeStr = part.slice(0, slashIdx);
|
|
32
|
+
stepStr = part.slice(slashIdx + 1);
|
|
33
|
+
}
|
|
34
|
+
const step = stepStr ? parseInt(stepStr, 10) : 1;
|
|
35
|
+
if (!Number.isFinite(step) || step <= 0)
|
|
36
|
+
continue;
|
|
37
|
+
let lo;
|
|
38
|
+
let hi;
|
|
39
|
+
if (rangeStr === '*') {
|
|
40
|
+
lo = min;
|
|
41
|
+
hi = max;
|
|
42
|
+
}
|
|
43
|
+
else if (rangeStr.includes('-')) {
|
|
44
|
+
const [a, b] = rangeStr.split('-');
|
|
45
|
+
lo = parseInt(a, 10);
|
|
46
|
+
hi = parseInt(b, 10);
|
|
47
|
+
}
|
|
48
|
+
else {
|
|
49
|
+
lo = parseInt(rangeStr, 10);
|
|
50
|
+
hi = lo;
|
|
51
|
+
}
|
|
52
|
+
if (!Number.isFinite(lo) || !Number.isFinite(hi))
|
|
53
|
+
continue;
|
|
54
|
+
for (let v = lo; v <= hi; v += step) {
|
|
55
|
+
if (v >= min && v <= max)
|
|
56
|
+
values.add(v);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return values;
|
|
60
|
+
}
|
|
61
|
+
export function parseCron(expr) {
|
|
62
|
+
// Aliases
|
|
63
|
+
let trimmed = expr.trim();
|
|
64
|
+
switch (trimmed) {
|
|
65
|
+
case '@yearly':
|
|
66
|
+
case '@annually':
|
|
67
|
+
trimmed = '0 0 1 1 *';
|
|
68
|
+
break;
|
|
69
|
+
case '@monthly':
|
|
70
|
+
trimmed = '0 0 1 * *';
|
|
71
|
+
break;
|
|
72
|
+
case '@weekly':
|
|
73
|
+
trimmed = '0 0 * * 0';
|
|
74
|
+
break;
|
|
75
|
+
case '@daily':
|
|
76
|
+
case '@midnight':
|
|
77
|
+
trimmed = '0 0 * * *';
|
|
78
|
+
break;
|
|
79
|
+
case '@hourly':
|
|
80
|
+
trimmed = '0 * * * *';
|
|
81
|
+
break;
|
|
82
|
+
}
|
|
83
|
+
const fields = trimmed.split(/\s+/);
|
|
84
|
+
if (fields.length !== 5)
|
|
85
|
+
return null;
|
|
86
|
+
const parsed = {
|
|
87
|
+
minute: parseField(fields[0], RANGES[0][0], RANGES[0][1]),
|
|
88
|
+
hour: parseField(fields[1], RANGES[1][0], RANGES[1][1]),
|
|
89
|
+
dom: parseField(fields[2], RANGES[2][0], RANGES[2][1]),
|
|
90
|
+
month: parseField(fields[3], RANGES[3][0], RANGES[3][1]),
|
|
91
|
+
dow: parseField(fields[4], RANGES[4][0], RANGES[4][1]),
|
|
92
|
+
};
|
|
93
|
+
return parsed;
|
|
94
|
+
}
|
|
95
|
+
function matches(set, val) {
|
|
96
|
+
return set === 'any' || set.has(val);
|
|
97
|
+
}
|
|
98
|
+
export function cronMatches(expr, date) {
|
|
99
|
+
const parsed = parseCron(expr);
|
|
100
|
+
if (!parsed)
|
|
101
|
+
return false;
|
|
102
|
+
return (matches(parsed.minute, date.getMinutes()) &&
|
|
103
|
+
matches(parsed.hour, date.getHours()) &&
|
|
104
|
+
matches(parsed.dom, date.getDate()) &&
|
|
105
|
+
matches(parsed.month, date.getMonth() + 1) &&
|
|
106
|
+
matches(parsed.dow, date.getDay()));
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Compute the next firing time at-or-after `from`, scanning up to
|
|
110
|
+
* `maxMinutes` ahead. Used for "next run in X" UI hints.
|
|
111
|
+
*/
|
|
112
|
+
export function nextRun(expr, from, maxMinutes = 60 * 24 * 7) {
|
|
113
|
+
const parsed = parseCron(expr);
|
|
114
|
+
if (!parsed)
|
|
115
|
+
return null;
|
|
116
|
+
const cursor = new Date(from);
|
|
117
|
+
cursor.setSeconds(0, 0);
|
|
118
|
+
cursor.setMinutes(cursor.getMinutes() + 1);
|
|
119
|
+
for (let i = 0; i < maxMinutes; i++) {
|
|
120
|
+
if (matches(parsed.minute, cursor.getMinutes()) &&
|
|
121
|
+
matches(parsed.hour, cursor.getHours()) &&
|
|
122
|
+
matches(parsed.dom, cursor.getDate()) &&
|
|
123
|
+
matches(parsed.month, cursor.getMonth() + 1) &&
|
|
124
|
+
matches(parsed.dow, cursor.getDay())) {
|
|
125
|
+
return cursor;
|
|
126
|
+
}
|
|
127
|
+
cursor.setMinutes(cursor.getMinutes() + 1);
|
|
128
|
+
}
|
|
129
|
+
return null;
|
|
130
|
+
}
|
|
131
|
+
export class Scheduler {
|
|
132
|
+
store;
|
|
133
|
+
onFire;
|
|
134
|
+
timer = null;
|
|
135
|
+
lastTickMinute = null;
|
|
136
|
+
destroyed = false;
|
|
137
|
+
constructor(store, onFire) {
|
|
138
|
+
this.store = store;
|
|
139
|
+
this.onFire = onFire;
|
|
140
|
+
}
|
|
141
|
+
start() {
|
|
142
|
+
if (this.destroyed || this.timer)
|
|
143
|
+
return;
|
|
144
|
+
this.scheduleNextTick();
|
|
145
|
+
}
|
|
146
|
+
stop() {
|
|
147
|
+
if (this.timer) {
|
|
148
|
+
clearTimeout(this.timer);
|
|
149
|
+
this.timer = null;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
destroy() {
|
|
153
|
+
this.destroyed = true;
|
|
154
|
+
this.stop();
|
|
155
|
+
}
|
|
156
|
+
scheduleNextTick() {
|
|
157
|
+
if (this.destroyed)
|
|
158
|
+
return;
|
|
159
|
+
const now = new Date();
|
|
160
|
+
// Sleep until the start of the next minute (+50ms slack to avoid edge cases)
|
|
161
|
+
const msToNextMinute = (60 - now.getSeconds()) * 1000 - now.getMilliseconds() + 50;
|
|
162
|
+
this.timer = setTimeout(() => {
|
|
163
|
+
this.tick();
|
|
164
|
+
this.scheduleNextTick();
|
|
165
|
+
}, msToNextMinute);
|
|
166
|
+
}
|
|
167
|
+
tick() {
|
|
168
|
+
if (this.destroyed)
|
|
169
|
+
return;
|
|
170
|
+
const now = new Date();
|
|
171
|
+
now.setSeconds(0, 0);
|
|
172
|
+
const minuteKey = now.getTime();
|
|
173
|
+
if (minuteKey === this.lastTickMinute)
|
|
174
|
+
return;
|
|
175
|
+
this.lastTickMinute = minuteKey;
|
|
176
|
+
for (const job of this.store.listJobs()) {
|
|
177
|
+
if (!job.enabled)
|
|
178
|
+
continue;
|
|
179
|
+
try {
|
|
180
|
+
if (cronMatches(job.cron, now)) {
|
|
181
|
+
this.onFire(job);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
catch (e) {
|
|
185
|
+
console.error('schedule check failed for', job.id, e);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
@@ -6,6 +6,8 @@ const DEFAULTS = {
|
|
|
6
6
|
theme: 'system',
|
|
7
7
|
sidebarWidth: 224,
|
|
8
8
|
editorSyncEnabled: true,
|
|
9
|
+
sourceControlOpen: false,
|
|
10
|
+
sourceControlWidth: 360,
|
|
9
11
|
},
|
|
10
12
|
activeProjectId: null,
|
|
11
13
|
};
|
|
@@ -24,6 +26,8 @@ export class SettingsStore {
|
|
|
24
26
|
theme: raw.ui?.theme ?? DEFAULTS.ui.theme,
|
|
25
27
|
sidebarWidth: raw.ui?.sidebarWidth ?? DEFAULTS.ui.sidebarWidth,
|
|
26
28
|
editorSyncEnabled: raw.ui?.editorSyncEnabled ?? DEFAULTS.ui.editorSyncEnabled,
|
|
29
|
+
sourceControlOpen: raw.ui?.sourceControlOpen ?? DEFAULTS.ui.sourceControlOpen,
|
|
30
|
+
sourceControlWidth: raw.ui?.sourceControlWidth ?? DEFAULTS.ui.sourceControlWidth,
|
|
27
31
|
},
|
|
28
32
|
activeProjectId: raw.activeProjectId ?? DEFAULTS.activeProjectId,
|
|
29
33
|
};
|