@codyswann/lisa 2.88.0 → 2.90.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/package.json +1 -1
- package/plugins/lisa/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa/scripts/automation-status-codex-adapter.mjs +510 -0
- package/plugins/lisa/scripts/automation-status-contract-drift.mjs +328 -0
- package/plugins/lisa-cdk/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-cdk/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-expo/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-expo/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-harper-fabric/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-harper-fabric/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-openclaw/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-openclaw/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-rails/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-rails/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki/.codex-plugin/plugin.json +1 -1
- package/plugins/src/base/scripts/automation-status-codex-adapter.mjs +510 -0
- package/plugins/src/base/scripts/automation-status-contract-drift.mjs +328 -0
package/package.json
CHANGED
|
@@ -82,7 +82,7 @@
|
|
|
82
82
|
"lodash": ">=4.18.1"
|
|
83
83
|
},
|
|
84
84
|
"name": "@codyswann/lisa",
|
|
85
|
-
"version": "2.
|
|
85
|
+
"version": "2.90.0",
|
|
86
86
|
"description": "Claude Code governance framework that applies guardrails, guidance, and automated enforcement to projects",
|
|
87
87
|
"main": "dist/index.js",
|
|
88
88
|
"exports": {
|
|
@@ -0,0 +1,510 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Shared Codex runtime adapter for `/lisa:automation-status`.
|
|
4
|
+
*
|
|
5
|
+
* This adapter inspects the local Codex automation backing store read-only:
|
|
6
|
+
* the per-automation `automation.toml` contract plus the automation memory file
|
|
7
|
+
* used by recurring runs. It scopes the scan to the current repo's expected
|
|
8
|
+
* Lisa automation prefix, derives normalized command/cadence metadata, and
|
|
9
|
+
* overlays available recency/failure signals onto the shared fleet report
|
|
10
|
+
* contract.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import fs from "node:fs/promises";
|
|
14
|
+
import os from "node:os";
|
|
15
|
+
import path from "node:path";
|
|
16
|
+
|
|
17
|
+
import { compareAutomationContract } from "./automation-status-contract-drift.mjs";
|
|
18
|
+
|
|
19
|
+
const CODEx_RUNTIME_LABEL = "Codex automations";
|
|
20
|
+
const RUN_FAILURE_PATTERN =
|
|
21
|
+
/\b(failed|failure|errored|error|exception|crash(?:ed)?)\b/i;
|
|
22
|
+
const NEGATED_FAILURE_PATTERN =
|
|
23
|
+
/\b(no|without)\s+(?:recent\s+)?fail(?:ure|ed)\b/i;
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* @typedef {import("./automation-status-expected-fleet.mjs").resolveExpectedAutomationFleet extends (...args: any[]) => infer T ? T : never} ExpectedFleet
|
|
27
|
+
*
|
|
28
|
+
* @typedef {{
|
|
29
|
+
* readonly automationId: string
|
|
30
|
+
* readonly status?: string
|
|
31
|
+
* readonly prompt?: string
|
|
32
|
+
* readonly observedCadence?: string
|
|
33
|
+
* readonly observedRRule?: string
|
|
34
|
+
* readonly observedCommand?: string
|
|
35
|
+
* readonly cwd?: string | null
|
|
36
|
+
* readonly createdAt?: number | null
|
|
37
|
+
* readonly updatedAt?: number | null
|
|
38
|
+
* readonly lastRunAt?: string | null
|
|
39
|
+
* readonly lastRunSummary?: string | null
|
|
40
|
+
* readonly lastRunFailed?: boolean
|
|
41
|
+
* }} ObservedCodexAutomation
|
|
42
|
+
*/
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Inspect the current repo's Codex automation fleet and map it to the shared
|
|
46
|
+
* automation-status report contract.
|
|
47
|
+
*
|
|
48
|
+
* @param {{
|
|
49
|
+
* readonly expectedFleet: ExpectedFleet
|
|
50
|
+
* readonly automationsDir?: string
|
|
51
|
+
* readonly now?: string | Date
|
|
52
|
+
* }} input
|
|
53
|
+
* @returns {Promise<{
|
|
54
|
+
* readonly runtime: string
|
|
55
|
+
* readonly generatedAt: string
|
|
56
|
+
* readonly groups: readonly {
|
|
57
|
+
* readonly id: string
|
|
58
|
+
* readonly title: string
|
|
59
|
+
* readonly items: readonly {
|
|
60
|
+
* readonly id: string
|
|
61
|
+
* readonly status: "HEALTHY" | "MISSING" | "UNSUPPORTED" | "DRIFTED" | "STALE" | "FAILING"
|
|
62
|
+
* readonly summary: string
|
|
63
|
+
* readonly expectedCadence?: string
|
|
64
|
+
* readonly expectedCommand?: string
|
|
65
|
+
* readonly observed?: string
|
|
66
|
+
* readonly remediation?: string
|
|
67
|
+
* }[]
|
|
68
|
+
* }[]
|
|
69
|
+
* readonly observedAutomations: readonly ObservedCodexAutomation[]
|
|
70
|
+
* }>}
|
|
71
|
+
*/
|
|
72
|
+
export async function inspectCodexAutomationFleet(input) {
|
|
73
|
+
const expectedFleet = input.expectedFleet;
|
|
74
|
+
const now = normalizeDate(input.now);
|
|
75
|
+
const observedAutomations = await listCodexAutomations({
|
|
76
|
+
automationsDir: input.automationsDir,
|
|
77
|
+
automationPrefix: expectedFleet.automationPrefix,
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
const expectedGroups = new Map([
|
|
81
|
+
["core", []],
|
|
82
|
+
["exploratory", []],
|
|
83
|
+
]);
|
|
84
|
+
|
|
85
|
+
for (const expected of expectedFleet.expected) {
|
|
86
|
+
const comparison = compareAutomationContract({
|
|
87
|
+
expected,
|
|
88
|
+
observedAutomations,
|
|
89
|
+
});
|
|
90
|
+
expectedGroups.get(expected.group)?.push(
|
|
91
|
+
createObservedStatusItem({
|
|
92
|
+
expected,
|
|
93
|
+
comparison,
|
|
94
|
+
now,
|
|
95
|
+
})
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
for (const unsupported of expectedFleet.unsupported) {
|
|
100
|
+
expectedGroups.get(unsupported.group)?.push({
|
|
101
|
+
id: unsupported.automationId,
|
|
102
|
+
status: "UNSUPPORTED",
|
|
103
|
+
summary: unsupported.reason,
|
|
104
|
+
expectedCadence: unsupported.expectedCadence,
|
|
105
|
+
observed: "No automation is expected for this repo/runtime combination.",
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return {
|
|
110
|
+
runtime: `${CODEx_RUNTIME_LABEL} (backing-store metadata)`,
|
|
111
|
+
generatedAt: now.toISOString(),
|
|
112
|
+
groups: [
|
|
113
|
+
{
|
|
114
|
+
id: "1",
|
|
115
|
+
title: "Core automations",
|
|
116
|
+
items: expectedGroups.get("core") ?? [],
|
|
117
|
+
},
|
|
118
|
+
{
|
|
119
|
+
id: "2",
|
|
120
|
+
title: "Exploratory automations",
|
|
121
|
+
items: expectedGroups.get("exploratory") ?? [],
|
|
122
|
+
},
|
|
123
|
+
],
|
|
124
|
+
observedAutomations,
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Read every Codex automation whose id matches the current repo's Lisa prefix.
|
|
130
|
+
*
|
|
131
|
+
* @param {{
|
|
132
|
+
* readonly automationsDir?: string
|
|
133
|
+
* readonly automationPrefix: string
|
|
134
|
+
* }} input
|
|
135
|
+
* @returns {Promise<readonly ObservedCodexAutomation[]>}
|
|
136
|
+
*/
|
|
137
|
+
export async function listCodexAutomations(input) {
|
|
138
|
+
const automationsDir =
|
|
139
|
+
input.automationsDir ?? resolveDefaultCodexAutomationsDir();
|
|
140
|
+
const dirEntries = await fs.readdir(automationsDir, {
|
|
141
|
+
withFileTypes: true,
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
const automationDirs = dirEntries
|
|
145
|
+
.filter(
|
|
146
|
+
entry =>
|
|
147
|
+
entry.isDirectory() && entry.name.startsWith(input.automationPrefix)
|
|
148
|
+
)
|
|
149
|
+
.map(entry => path.join(automationsDir, entry.name))
|
|
150
|
+
.sort((left, right) => left.localeCompare(right));
|
|
151
|
+
|
|
152
|
+
const automations = await Promise.all(
|
|
153
|
+
automationDirs.map(dir => readCodexAutomation(dir))
|
|
154
|
+
);
|
|
155
|
+
|
|
156
|
+
return automations.filter(Boolean);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Normalize a Lisa Codex automation prompt back into the slash-command surface
|
|
161
|
+
* expected by the shared drift classifier.
|
|
162
|
+
*
|
|
163
|
+
* @param {string | undefined} prompt
|
|
164
|
+
* @returns {string | undefined}
|
|
165
|
+
*/
|
|
166
|
+
export function deriveCodexObservedCommand(prompt) {
|
|
167
|
+
if (!prompt) {
|
|
168
|
+
return undefined;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const lisaSkillMatch = prompt.match(
|
|
172
|
+
/Use the Lisa ([a-z0-9:-]+) skill with arguments `([^`]+)`/i
|
|
173
|
+
);
|
|
174
|
+
if (lisaSkillMatch?.[1] && lisaSkillMatch[2]) {
|
|
175
|
+
return `/lisa:${lisaSkillMatch[1]} ${lisaSkillMatch[2]}`.trim();
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const aliasSkillMatch = prompt.match(
|
|
179
|
+
/Use the `\$([a-z0-9:-]+)` skill with arguments `([^`]+)`/i
|
|
180
|
+
);
|
|
181
|
+
if (aliasSkillMatch?.[1] && aliasSkillMatch[2]) {
|
|
182
|
+
return `/${aliasSkillMatch[1]} ${aliasSkillMatch[2]}`.trim();
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
return undefined;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Parse the latest run metadata from an automation memory file.
|
|
190
|
+
*
|
|
191
|
+
* @param {string | undefined} memoryContent
|
|
192
|
+
* @returns {{
|
|
193
|
+
* readonly lastRunAt: string | null
|
|
194
|
+
* readonly lastRunSummary: string | null
|
|
195
|
+
* readonly lastRunFailed: boolean
|
|
196
|
+
* }}
|
|
197
|
+
*/
|
|
198
|
+
export function parseCodexAutomationMemory(memoryContent) {
|
|
199
|
+
if (!memoryContent) {
|
|
200
|
+
return {
|
|
201
|
+
lastRunAt: null,
|
|
202
|
+
lastRunSummary: null,
|
|
203
|
+
lastRunFailed: false,
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const timestampMatch = memoryContent.match(
|
|
208
|
+
/20\d{2}-\d\d-\d\dT\d\d:\d\d:\d\d(?:\.\d+)?Z/
|
|
209
|
+
);
|
|
210
|
+
const lines = memoryContent.split(/\r?\n/);
|
|
211
|
+
const summaryLine =
|
|
212
|
+
lines
|
|
213
|
+
.find(line => line.startsWith("- "))
|
|
214
|
+
?.replace(/^- /, "")
|
|
215
|
+
.trim() ?? null;
|
|
216
|
+
|
|
217
|
+
const latestBlock = lines.slice(0, 20).join("\n");
|
|
218
|
+
const lastRunFailed =
|
|
219
|
+
RUN_FAILURE_PATTERN.test(latestBlock) &&
|
|
220
|
+
!NEGATED_FAILURE_PATTERN.test(latestBlock);
|
|
221
|
+
|
|
222
|
+
return {
|
|
223
|
+
lastRunAt: timestampMatch?.[0] ?? null,
|
|
224
|
+
lastRunSummary: summaryLine,
|
|
225
|
+
lastRunFailed,
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
async function readCodexAutomation(automationDir) {
|
|
230
|
+
const tomlPath = path.join(automationDir, "automation.toml");
|
|
231
|
+
const memoryPath = path.join(automationDir, "memory.md");
|
|
232
|
+
const tomlContent = await fs.readFile(tomlPath, "utf8");
|
|
233
|
+
const automation = parseAutomationToml(tomlContent);
|
|
234
|
+
const memoryContent = await fs.readFile(memoryPath, "utf8").catch(() => "");
|
|
235
|
+
const memory = parseCodexAutomationMemory(memoryContent);
|
|
236
|
+
const cwd = Array.isArray(automation.cwds) ? automation.cwds[0] : null;
|
|
237
|
+
|
|
238
|
+
return {
|
|
239
|
+
automationId:
|
|
240
|
+
stringOrUndefined(automation.id) ?? path.basename(automationDir),
|
|
241
|
+
status: stringOrUndefined(automation.status),
|
|
242
|
+
prompt: stringOrUndefined(automation.prompt),
|
|
243
|
+
observedCadence: humanizeAutomationCadence(
|
|
244
|
+
stringOrUndefined(automation.rrule)
|
|
245
|
+
),
|
|
246
|
+
observedRRule: stringOrUndefined(automation.rrule),
|
|
247
|
+
observedCommand: deriveCodexObservedCommand(
|
|
248
|
+
stringOrUndefined(automation.prompt)
|
|
249
|
+
),
|
|
250
|
+
cwd: typeof cwd === "string" ? cwd : null,
|
|
251
|
+
createdAt: numberOrNull(automation.created_at),
|
|
252
|
+
updatedAt: numberOrNull(automation.updated_at),
|
|
253
|
+
...memory,
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function createObservedStatusItem(input) {
|
|
258
|
+
const expected = input.expected;
|
|
259
|
+
const comparison = input.comparison;
|
|
260
|
+
const observed = comparison.observedAutomation;
|
|
261
|
+
const runSignal = classifyAutomationRunSignal({
|
|
262
|
+
expected,
|
|
263
|
+
observedAutomation: observed,
|
|
264
|
+
now: input.now,
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
const observedDetails = [comparison.observed];
|
|
268
|
+
if (observed?.status) {
|
|
269
|
+
observedDetails.push(`Scheduler status: ${observed.status}`);
|
|
270
|
+
}
|
|
271
|
+
if (observed?.lastRunAt) {
|
|
272
|
+
observedDetails.push(`Last run: ${observed.lastRunAt}`);
|
|
273
|
+
} else if (observed) {
|
|
274
|
+
observedDetails.push("Last run metadata unavailable.");
|
|
275
|
+
}
|
|
276
|
+
if (observed?.lastRunSummary) {
|
|
277
|
+
observedDetails.push(`Latest summary: ${observed.lastRunSummary}`);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
const status =
|
|
281
|
+
runSignal?.status ??
|
|
282
|
+
/** @type {"HEALTHY" | "MISSING" | "DRIFTED"} */ (comparison.status);
|
|
283
|
+
|
|
284
|
+
return {
|
|
285
|
+
id: expected.automationId,
|
|
286
|
+
status,
|
|
287
|
+
summary: composeAutomationSummary({
|
|
288
|
+
comparison,
|
|
289
|
+
runSignal,
|
|
290
|
+
}),
|
|
291
|
+
expectedCadence: expected.expectedCadence,
|
|
292
|
+
expectedCommand: expected.expectedCommand,
|
|
293
|
+
observed: observedDetails.join(" "),
|
|
294
|
+
remediation: runSignal?.remediation ?? comparison.remediation,
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
function composeAutomationSummary(input) {
|
|
299
|
+
const comparisonSummary = input.comparison.summary;
|
|
300
|
+
if (!input.runSignal) {
|
|
301
|
+
return comparisonSummary;
|
|
302
|
+
}
|
|
303
|
+
if (input.comparison.status === "HEALTHY") {
|
|
304
|
+
return input.runSignal.summary;
|
|
305
|
+
}
|
|
306
|
+
return `${input.runSignal.summary}; ${comparisonSummary}`;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
function classifyAutomationRunSignal(input) {
|
|
310
|
+
const observed = input.observedAutomation;
|
|
311
|
+
if (!observed) {
|
|
312
|
+
return null;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
if (observed.status && observed.status !== "ACTIVE") {
|
|
316
|
+
return {
|
|
317
|
+
status: "FAILING",
|
|
318
|
+
summary: `scheduler entry is ${observed.status.toLowerCase()}`,
|
|
319
|
+
remediation: "Resume or re-enable the automation in Codex.",
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
if (observed.lastRunFailed) {
|
|
324
|
+
return {
|
|
325
|
+
status: "FAILING",
|
|
326
|
+
summary: "latest recorded run failed",
|
|
327
|
+
remediation:
|
|
328
|
+
"Inspect the latest automation run output and fix the failing job before re-running setup.",
|
|
329
|
+
};
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
if (!observed.lastRunAt) {
|
|
333
|
+
return null;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
const cadenceMs =
|
|
337
|
+
rruleToIntervalMs(observed.observedRRule) ??
|
|
338
|
+
cadenceLabelToIntervalMs(input.expected.expectedCadence);
|
|
339
|
+
if (!cadenceMs) {
|
|
340
|
+
return null;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
const lastRunAt = Date.parse(observed.lastRunAt);
|
|
344
|
+
if (Number.isNaN(lastRunAt)) {
|
|
345
|
+
return null;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
const staleAfterMs = cadenceMs * 3;
|
|
349
|
+
if (input.now.getTime() - lastRunAt > staleAfterMs) {
|
|
350
|
+
return {
|
|
351
|
+
status: "STALE",
|
|
352
|
+
summary: `last recorded run is stale for the expected cadence`,
|
|
353
|
+
remediation:
|
|
354
|
+
"Inspect why the automation has not run recently, then resume normal scheduling or recreate it with `/lisa:setup-automations`.",
|
|
355
|
+
};
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
return null;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
function resolveDefaultCodexAutomationsDir() {
|
|
362
|
+
return path.join(
|
|
363
|
+
process.env.CODEX_HOME ?? path.join(os.homedir(), ".codex"),
|
|
364
|
+
"automations"
|
|
365
|
+
);
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
function humanizeAutomationCadence(rrule) {
|
|
369
|
+
if (!rrule) {
|
|
370
|
+
return undefined;
|
|
371
|
+
}
|
|
372
|
+
if (rrule === "FREQ=HOURLY;INTERVAL=1") {
|
|
373
|
+
return "every 60 minutes";
|
|
374
|
+
}
|
|
375
|
+
if (rrule === "FREQ=MINUTELY;INTERVAL=10") {
|
|
376
|
+
return "every 10 minutes";
|
|
377
|
+
}
|
|
378
|
+
if (rrule === "FREQ=DAILY;INTERVAL=1") {
|
|
379
|
+
return "once a day";
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
const everyMinutes = rrule.match(/^FREQ=MINUTELY;INTERVAL=(\d+)$/);
|
|
383
|
+
if (everyMinutes?.[1]) {
|
|
384
|
+
return `every ${everyMinutes[1]} minutes`;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
const everyHours = rrule.match(/^FREQ=HOURLY;INTERVAL=(\d+)$/);
|
|
388
|
+
if (everyHours?.[1]) {
|
|
389
|
+
return `every ${Number(everyHours[1]) * 60} minutes`;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
const everyDays = rrule.match(/^FREQ=DAILY;INTERVAL=(\d+)$/);
|
|
393
|
+
if (everyDays?.[1]) {
|
|
394
|
+
return Number(everyDays[1]) === 1
|
|
395
|
+
? "once a day"
|
|
396
|
+
: `every ${everyDays[1]} days`;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
return rrule;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
function rruleToIntervalMs(rrule) {
|
|
403
|
+
if (!rrule) {
|
|
404
|
+
return null;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
const minutely = rrule.match(/^FREQ=MINUTELY;INTERVAL=(\d+)$/);
|
|
408
|
+
if (minutely?.[1]) {
|
|
409
|
+
return Number(minutely[1]) * 60_000;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
const hourly = rrule.match(/^FREQ=HOURLY;INTERVAL=(\d+)$/);
|
|
413
|
+
if (hourly?.[1]) {
|
|
414
|
+
return Number(hourly[1]) * 60 * 60_000;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
const daily = rrule.match(/^FREQ=DAILY;INTERVAL=(\d+)$/);
|
|
418
|
+
if (daily?.[1]) {
|
|
419
|
+
return Number(daily[1]) * 24 * 60 * 60_000;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
return null;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
function cadenceLabelToIntervalMs(label) {
|
|
426
|
+
if (!label) {
|
|
427
|
+
return null;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
const everyMinutes = label.match(/^every (\d+) minutes$/);
|
|
431
|
+
if (everyMinutes?.[1]) {
|
|
432
|
+
return Number(everyMinutes[1]) * 60_000;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
if (label === "once a day") {
|
|
436
|
+
return 24 * 60 * 60_000;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
return null;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
function normalizeDate(value) {
|
|
443
|
+
if (value instanceof Date) {
|
|
444
|
+
return value;
|
|
445
|
+
}
|
|
446
|
+
if (typeof value === "string") {
|
|
447
|
+
return new Date(value);
|
|
448
|
+
}
|
|
449
|
+
return new Date();
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
function stringOrUndefined(value) {
|
|
453
|
+
return typeof value === "string" ? value : undefined;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
function numberOrNull(value) {
|
|
457
|
+
return typeof value === "number" ? value : null;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
function parseAutomationToml(tomlContent) {
|
|
461
|
+
return tomlContent
|
|
462
|
+
.split(/\r?\n/)
|
|
463
|
+
.map(line => line.trim())
|
|
464
|
+
.filter(line => line && !line.startsWith("#"))
|
|
465
|
+
.reduce((parsed, line) => {
|
|
466
|
+
const separatorIndex = line.indexOf("=");
|
|
467
|
+
if (separatorIndex === -1) {
|
|
468
|
+
return parsed;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
const key = line.slice(0, separatorIndex).trim();
|
|
472
|
+
const rawValue = line.slice(separatorIndex + 1).trim();
|
|
473
|
+
|
|
474
|
+
return {
|
|
475
|
+
...parsed,
|
|
476
|
+
[key]: parseTomlValue(rawValue),
|
|
477
|
+
};
|
|
478
|
+
}, {});
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
function parseTomlValue(rawValue) {
|
|
482
|
+
if (rawValue.startsWith('"') && rawValue.endsWith('"')) {
|
|
483
|
+
return rawValue.slice(1, -1).replace(/\\"/g, '"').replace(/\\\\/g, "\\");
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
if (rawValue.startsWith("[") && rawValue.endsWith("]")) {
|
|
487
|
+
const inner = rawValue.slice(1, -1).trim();
|
|
488
|
+
if (!inner) {
|
|
489
|
+
return [];
|
|
490
|
+
}
|
|
491
|
+
return inner
|
|
492
|
+
.split(",")
|
|
493
|
+
.map(value => parseTomlValue(value.trim()))
|
|
494
|
+
.filter(value => value !== undefined);
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
if (/^-?\d+$/.test(rawValue)) {
|
|
498
|
+
return Number(rawValue);
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
if (rawValue === "true") {
|
|
502
|
+
return true;
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
if (rawValue === "false") {
|
|
506
|
+
return false;
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
return rawValue;
|
|
510
|
+
}
|