@lumerahq/cli 0.18.0 → 0.18.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/README.md +57 -0
- package/dist/index.js +7 -7
- package/dist/{resources-HLQTZV37.js → resources-E4XMWX7N.js} +196 -10
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -95,6 +95,63 @@ The CLI checks for credentials in this order:
|
|
|
95
95
|
2. Project-local credentials (`.lumera/credentials.json`)
|
|
96
96
|
3. Global credentials (`~/.config/lumera/credentials.json`)
|
|
97
97
|
|
|
98
|
+
## Automation schedules in `config.json`
|
|
99
|
+
|
|
100
|
+
Automations can define schedules in two supported shapes.
|
|
101
|
+
|
|
102
|
+
### Canonical format
|
|
103
|
+
|
|
104
|
+
```json
|
|
105
|
+
{
|
|
106
|
+
"external_id": "my-app:daily-report",
|
|
107
|
+
"name": "Daily Report",
|
|
108
|
+
"inputs": {
|
|
109
|
+
"schema": {
|
|
110
|
+
"function": {
|
|
111
|
+
"name": "main",
|
|
112
|
+
"parameters": { "type": "object" }
|
|
113
|
+
}
|
|
114
|
+
},
|
|
115
|
+
"presets": {
|
|
116
|
+
"daily": {
|
|
117
|
+
"label": "Daily Run",
|
|
118
|
+
"inputs": { "region": "west" }
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
},
|
|
122
|
+
"schedule": {
|
|
123
|
+
"cron": "0 7 * * *",
|
|
124
|
+
"timezone": "America/Los_Angeles",
|
|
125
|
+
"preset": "daily"
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
### Shorthand format
|
|
131
|
+
|
|
132
|
+
```json
|
|
133
|
+
{
|
|
134
|
+
"external_id": "my-app:daily-report",
|
|
135
|
+
"name": "Daily Report",
|
|
136
|
+
"inputs": {
|
|
137
|
+
"schema": {
|
|
138
|
+
"function": {
|
|
139
|
+
"name": "main",
|
|
140
|
+
"parameters": { "type": "object" }
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
},
|
|
144
|
+
"schedule": "0 7 * * *",
|
|
145
|
+
"schedule_tz": "America/Los_Angeles",
|
|
146
|
+
"schedule_inputs": { "region": "west" }
|
|
147
|
+
}
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
Notes:
|
|
151
|
+
- `schedule_inputs` creates/updates a schedule preset automatically.
|
|
152
|
+
- If you already define `inputs.presets`, you can point the schedule at one with `schedule_preset` (or `schedule.preset`).
|
|
153
|
+
- If exactly one preset exists, the CLI uses it automatically.
|
|
154
|
+
|
|
98
155
|
## Security
|
|
99
156
|
|
|
100
157
|
### Credential Storage
|
package/dist/index.js
CHANGED
|
@@ -215,25 +215,25 @@ async function main() {
|
|
|
215
215
|
switch (command) {
|
|
216
216
|
// Resource commands
|
|
217
217
|
case "plan":
|
|
218
|
-
await import("./resources-
|
|
218
|
+
await import("./resources-E4XMWX7N.js").then((m) => m.plan(args.slice(1)));
|
|
219
219
|
break;
|
|
220
220
|
case "apply":
|
|
221
|
-
await import("./resources-
|
|
221
|
+
await import("./resources-E4XMWX7N.js").then((m) => m.apply(args.slice(1)));
|
|
222
222
|
break;
|
|
223
223
|
case "pull":
|
|
224
|
-
await import("./resources-
|
|
224
|
+
await import("./resources-E4XMWX7N.js").then((m) => m.pull(args.slice(1)));
|
|
225
225
|
break;
|
|
226
226
|
case "destroy":
|
|
227
|
-
await import("./resources-
|
|
227
|
+
await import("./resources-E4XMWX7N.js").then((m) => m.destroy(args.slice(1)));
|
|
228
228
|
break;
|
|
229
229
|
case "list":
|
|
230
|
-
await import("./resources-
|
|
230
|
+
await import("./resources-E4XMWX7N.js").then((m) => m.list(args.slice(1)));
|
|
231
231
|
break;
|
|
232
232
|
case "show":
|
|
233
|
-
await import("./resources-
|
|
233
|
+
await import("./resources-E4XMWX7N.js").then((m) => m.show(args.slice(1)));
|
|
234
234
|
break;
|
|
235
235
|
case "diff":
|
|
236
|
-
await import("./resources-
|
|
236
|
+
await import("./resources-E4XMWX7N.js").then((m) => m.diff(args.slice(1)));
|
|
237
237
|
break;
|
|
238
238
|
// Development
|
|
239
239
|
case "dev":
|
|
@@ -84,6 +84,169 @@ function computeLineDiff(oldText, newText) {
|
|
|
84
84
|
}
|
|
85
85
|
return result;
|
|
86
86
|
}
|
|
87
|
+
function isPlainObject(value) {
|
|
88
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
89
|
+
}
|
|
90
|
+
function sortJsonValue(value) {
|
|
91
|
+
if (Array.isArray(value)) {
|
|
92
|
+
return value.map((item) => sortJsonValue(item));
|
|
93
|
+
}
|
|
94
|
+
if (!isPlainObject(value)) {
|
|
95
|
+
return value;
|
|
96
|
+
}
|
|
97
|
+
return Object.fromEntries(
|
|
98
|
+
Object.entries(value).sort(([a], [b]) => a.localeCompare(b)).map(([key, child]) => [key, sortJsonValue(child)])
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
function stableJsonStringify(value) {
|
|
102
|
+
return JSON.stringify(sortJsonValue(value));
|
|
103
|
+
}
|
|
104
|
+
function normalizeJsonComparable(value) {
|
|
105
|
+
if (value === void 0 || value === null || value === "") return "";
|
|
106
|
+
if (typeof value === "string") {
|
|
107
|
+
const trimmed = value.trim();
|
|
108
|
+
if (!trimmed) return "";
|
|
109
|
+
try {
|
|
110
|
+
return stableJsonStringify(JSON.parse(trimmed));
|
|
111
|
+
} catch {
|
|
112
|
+
return trimmed;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
return stableJsonStringify(value);
|
|
116
|
+
}
|
|
117
|
+
function resolveLocalPresetKey(localPresets, presetRef) {
|
|
118
|
+
if (!presetRef) return void 0;
|
|
119
|
+
if (presetRef in localPresets) return presetRef;
|
|
120
|
+
for (const [key, preset] of Object.entries(localPresets)) {
|
|
121
|
+
if ((preset.label || key) === presetRef) {
|
|
122
|
+
return key;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
return void 0;
|
|
126
|
+
}
|
|
127
|
+
function resolveSchedulePresetName(schedule, localPresets) {
|
|
128
|
+
return localPresets[schedule.preset]?.label || schedule.preset;
|
|
129
|
+
}
|
|
130
|
+
function buildLocalPresetComparisonMap(localPresets) {
|
|
131
|
+
const out = /* @__PURE__ */ new Map();
|
|
132
|
+
for (const [key, preset] of Object.entries(localPresets)) {
|
|
133
|
+
out.set(preset.label || key, normalizeJsonComparable(preset.inputs));
|
|
134
|
+
}
|
|
135
|
+
return out;
|
|
136
|
+
}
|
|
137
|
+
function buildRemotePresetComparisonMap(remotePresets) {
|
|
138
|
+
const out = /* @__PURE__ */ new Map();
|
|
139
|
+
for (const preset of remotePresets) {
|
|
140
|
+
out.set(preset.name, normalizeJsonComparable(preset.inputs));
|
|
141
|
+
}
|
|
142
|
+
return out;
|
|
143
|
+
}
|
|
144
|
+
function comparisonMapsDiffer(left, right) {
|
|
145
|
+
if (left.size !== right.size) return true;
|
|
146
|
+
for (const [key, value] of left) {
|
|
147
|
+
if (right.get(key) !== value) return true;
|
|
148
|
+
}
|
|
149
|
+
return false;
|
|
150
|
+
}
|
|
151
|
+
function normalizeLocalAutomation(config) {
|
|
152
|
+
let inputs;
|
|
153
|
+
if (config.inputs) {
|
|
154
|
+
inputs = {
|
|
155
|
+
schema: config.inputs.schema,
|
|
156
|
+
presets: config.inputs.presets ? { ...config.inputs.presets } : void 0
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
if (config.schedule_tz !== void 0 && typeof config.schedule_tz !== "string") {
|
|
160
|
+
throw new Error("schedule_tz must be a string");
|
|
161
|
+
}
|
|
162
|
+
if (config.schedule_preset !== void 0 && typeof config.schedule_preset !== "string") {
|
|
163
|
+
throw new Error("schedule_preset must be a string");
|
|
164
|
+
}
|
|
165
|
+
if (config.schedule_inputs !== void 0 && !isPlainObject(config.schedule_inputs)) {
|
|
166
|
+
throw new Error("schedule_inputs must be an object");
|
|
167
|
+
}
|
|
168
|
+
let scheduleCron = "";
|
|
169
|
+
let scheduleTimezone = config.schedule_tz?.trim();
|
|
170
|
+
let schedulePresetRef = config.schedule_preset?.trim();
|
|
171
|
+
const hasScheduleInputs = config.schedule_inputs !== void 0;
|
|
172
|
+
if (typeof config.schedule === "string") {
|
|
173
|
+
scheduleCron = config.schedule.trim();
|
|
174
|
+
} else if (config.schedule === void 0 || config.schedule === null) {
|
|
175
|
+
scheduleCron = "";
|
|
176
|
+
} else if (isPlainObject(config.schedule)) {
|
|
177
|
+
if (typeof config.schedule.cron !== "string" || config.schedule.cron.trim() === "") {
|
|
178
|
+
throw new Error("schedule.cron must be a non-empty string");
|
|
179
|
+
}
|
|
180
|
+
scheduleCron = config.schedule.cron.trim();
|
|
181
|
+
if (config.schedule.timezone !== void 0) {
|
|
182
|
+
if (typeof config.schedule.timezone !== "string") {
|
|
183
|
+
throw new Error("schedule.timezone must be a string");
|
|
184
|
+
}
|
|
185
|
+
scheduleTimezone = config.schedule.timezone.trim();
|
|
186
|
+
}
|
|
187
|
+
if (config.schedule.preset !== void 0) {
|
|
188
|
+
if (typeof config.schedule.preset !== "string") {
|
|
189
|
+
throw new Error("schedule.preset must be a string");
|
|
190
|
+
}
|
|
191
|
+
schedulePresetRef = config.schedule.preset.trim();
|
|
192
|
+
}
|
|
193
|
+
} else {
|
|
194
|
+
throw new Error("schedule must be a string or object");
|
|
195
|
+
}
|
|
196
|
+
const noSchedule = scheduleCron === "" || /^none$/i.test(scheduleCron);
|
|
197
|
+
if (noSchedule) {
|
|
198
|
+
if (schedulePresetRef || hasScheduleInputs) {
|
|
199
|
+
throw new Error("schedule_preset and schedule_inputs require schedule");
|
|
200
|
+
}
|
|
201
|
+
return {
|
|
202
|
+
external_id: config.external_id,
|
|
203
|
+
name: config.name,
|
|
204
|
+
description: config.description,
|
|
205
|
+
inputs
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
const localPresets = inputs?.presets ? { ...inputs.presets } : {};
|
|
209
|
+
if (hasScheduleInputs) {
|
|
210
|
+
const existingKey = resolveLocalPresetKey(localPresets, schedulePresetRef);
|
|
211
|
+
const fallbackKey = Object.keys(localPresets).length === 1 ? Object.keys(localPresets)[0] : "schedule";
|
|
212
|
+
const presetKey = existingKey || schedulePresetRef || fallbackKey;
|
|
213
|
+
const existingPreset = localPresets[presetKey];
|
|
214
|
+
localPresets[presetKey] = {
|
|
215
|
+
label: existingPreset?.label || schedulePresetRef || "Scheduled Run",
|
|
216
|
+
inputs: config.schedule_inputs
|
|
217
|
+
};
|
|
218
|
+
schedulePresetRef = presetKey;
|
|
219
|
+
} else {
|
|
220
|
+
const resolvedKey = resolveLocalPresetKey(localPresets, schedulePresetRef);
|
|
221
|
+
if (resolvedKey) {
|
|
222
|
+
schedulePresetRef = resolvedKey;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
if (!schedulePresetRef) {
|
|
226
|
+
const presetKeys = Object.keys(localPresets);
|
|
227
|
+
if (presetKeys.length === 1) {
|
|
228
|
+
schedulePresetRef = presetKeys[0];
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
if (!schedulePresetRef) {
|
|
232
|
+
throw new Error("schedule requires a preset. Add schedule.preset, schedule_preset, or schedule_inputs");
|
|
233
|
+
}
|
|
234
|
+
const normalizedInputs = inputs ? {
|
|
235
|
+
...inputs,
|
|
236
|
+
presets: Object.keys(localPresets).length > 0 ? localPresets : inputs.presets
|
|
237
|
+
} : Object.keys(localPresets).length > 0 ? { presets: localPresets } : void 0;
|
|
238
|
+
return {
|
|
239
|
+
external_id: config.external_id,
|
|
240
|
+
name: config.name,
|
|
241
|
+
description: config.description,
|
|
242
|
+
inputs: normalizedInputs,
|
|
243
|
+
schedule: {
|
|
244
|
+
cron: scheduleCron,
|
|
245
|
+
timezone: scheduleTimezone || "UTC",
|
|
246
|
+
preset: schedulePresetRef
|
|
247
|
+
}
|
|
248
|
+
};
|
|
249
|
+
}
|
|
87
250
|
function showPlanHelp() {
|
|
88
251
|
console.log(`
|
|
89
252
|
${pc.dim("Usage:")}
|
|
@@ -349,22 +512,23 @@ function loadLocalAutomations(platformDir, filterName, appName) {
|
|
|
349
512
|
}
|
|
350
513
|
try {
|
|
351
514
|
const configContent = readFileSync(configPath, "utf-8");
|
|
352
|
-
const
|
|
353
|
-
if (filterName &&
|
|
515
|
+
const rawConfig = JSON.parse(configContent);
|
|
516
|
+
if (filterName && rawConfig.external_id !== filterName && rawConfig.name !== filterName && entry.name !== filterName) {
|
|
354
517
|
continue;
|
|
355
518
|
}
|
|
356
|
-
if (!
|
|
519
|
+
if (!rawConfig.external_id) {
|
|
357
520
|
if (appName) {
|
|
358
|
-
|
|
521
|
+
rawConfig.external_id = `${appName}:${entry.name}`;
|
|
359
522
|
} else {
|
|
360
523
|
errors.push(`${entry.name}: missing external_id in config.json`);
|
|
361
524
|
continue;
|
|
362
525
|
}
|
|
363
526
|
}
|
|
364
|
-
if (!
|
|
527
|
+
if (!rawConfig.name) {
|
|
365
528
|
errors.push(`${entry.name}: missing name in config.json`);
|
|
366
529
|
continue;
|
|
367
530
|
}
|
|
531
|
+
const config = normalizeLocalAutomation(rawConfig);
|
|
368
532
|
if (!config.inputs?.schema) {
|
|
369
533
|
errors.push(`${entry.name}: missing inputs.schema in config.json`);
|
|
370
534
|
continue;
|
|
@@ -633,6 +797,15 @@ async function planAutomations(api, localAutomations) {
|
|
|
633
797
|
const changes = [];
|
|
634
798
|
const remoteAutomations = await api.listAutomations({ include_code: true });
|
|
635
799
|
const remoteByExternalId = new Map(remoteAutomations.filter((a) => a.external_id).map((a) => [a.external_id, a]));
|
|
800
|
+
const remotePresetCache = /* @__PURE__ */ new Map();
|
|
801
|
+
const getRemotePresets = (automationId) => {
|
|
802
|
+
let cached = remotePresetCache.get(automationId);
|
|
803
|
+
if (!cached) {
|
|
804
|
+
cached = api.listPresets(automationId).catch(() => []);
|
|
805
|
+
remotePresetCache.set(automationId, cached);
|
|
806
|
+
}
|
|
807
|
+
return cached;
|
|
808
|
+
};
|
|
636
809
|
for (const { automation, code } of localAutomations) {
|
|
637
810
|
const remote = remoteByExternalId.get(automation.external_id);
|
|
638
811
|
if (!remote) {
|
|
@@ -646,18 +819,31 @@ async function planAutomations(api, localAutomations) {
|
|
|
646
819
|
const codeChanged = remote.code !== code;
|
|
647
820
|
const nameChanged = remote.name !== automation.name;
|
|
648
821
|
const descChanged = (remote.description || "") !== (automation.description || "");
|
|
649
|
-
const localSchema = automation.inputs?.schema
|
|
650
|
-
const remoteSchema =
|
|
822
|
+
const localSchema = normalizeJsonComparable(automation.inputs?.schema);
|
|
823
|
+
const remoteSchema = normalizeJsonComparable(remote.input_schema);
|
|
651
824
|
const schemaChanged = localSchema !== remoteSchema;
|
|
825
|
+
let remotePresets = [];
|
|
826
|
+
if (automation.inputs?.presets || automation.schedule || remote.schedule_preset_id) {
|
|
827
|
+
remotePresets = await getRemotePresets(remote.id);
|
|
828
|
+
}
|
|
829
|
+
const presetsChanged = automation.inputs?.presets ? comparisonMapsDiffer(
|
|
830
|
+
buildLocalPresetComparisonMap(automation.inputs.presets),
|
|
831
|
+
buildRemotePresetComparisonMap(remotePresets)
|
|
832
|
+
) : false;
|
|
652
833
|
const localCron = automation.schedule?.cron || "";
|
|
653
834
|
const remoteCron = remote.schedule || "";
|
|
654
|
-
const
|
|
655
|
-
|
|
835
|
+
const localTz = automation.schedule ? automation.schedule.timezone || "UTC" : "";
|
|
836
|
+
const remoteTz = remote.schedule ? remote.schedule_tz || "UTC" : "";
|
|
837
|
+
const localPresetName = automation.schedule ? resolveSchedulePresetName(automation.schedule, automation.inputs?.presets || {}) : "";
|
|
838
|
+
const remotePresetName = remote.schedule_preset_id ? remotePresets.find((preset) => preset.id === remote.schedule_preset_id)?.name || remote.schedule_preset_id : "";
|
|
839
|
+
const scheduleChanged = localCron !== remoteCron || localTz !== remoteTz || localPresetName !== remotePresetName;
|
|
840
|
+
if (codeChanged || nameChanged || descChanged || schemaChanged || presetsChanged || scheduleChanged) {
|
|
656
841
|
const details = [];
|
|
657
842
|
if (codeChanged) details.push("code");
|
|
658
843
|
if (nameChanged) details.push("name");
|
|
659
844
|
if (descChanged) details.push("description");
|
|
660
845
|
if (schemaChanged) details.push("input_schema");
|
|
846
|
+
if (presetsChanged) details.push("presets");
|
|
661
847
|
if (scheduleChanged) details.push("schedule");
|
|
662
848
|
const textDiffs = [];
|
|
663
849
|
if (codeChanged) {
|
|
@@ -839,7 +1025,7 @@ async function syncPresets(api, automationId, localPresets) {
|
|
|
839
1025
|
}
|
|
840
1026
|
}
|
|
841
1027
|
async function setSchedule(api, automationId, schedule, localPresets) {
|
|
842
|
-
const presetName =
|
|
1028
|
+
const presetName = resolveSchedulePresetName(schedule, localPresets);
|
|
843
1029
|
const remotePresets = await api.listPresets(automationId);
|
|
844
1030
|
const preset = remotePresets.find((p) => p.name === presetName);
|
|
845
1031
|
if (!preset) {
|