@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 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-HLQTZV37.js").then((m) => m.plan(args.slice(1)));
218
+ await import("./resources-E4XMWX7N.js").then((m) => m.plan(args.slice(1)));
219
219
  break;
220
220
  case "apply":
221
- await import("./resources-HLQTZV37.js").then((m) => m.apply(args.slice(1)));
221
+ await import("./resources-E4XMWX7N.js").then((m) => m.apply(args.slice(1)));
222
222
  break;
223
223
  case "pull":
224
- await import("./resources-HLQTZV37.js").then((m) => m.pull(args.slice(1)));
224
+ await import("./resources-E4XMWX7N.js").then((m) => m.pull(args.slice(1)));
225
225
  break;
226
226
  case "destroy":
227
- await import("./resources-HLQTZV37.js").then((m) => m.destroy(args.slice(1)));
227
+ await import("./resources-E4XMWX7N.js").then((m) => m.destroy(args.slice(1)));
228
228
  break;
229
229
  case "list":
230
- await import("./resources-HLQTZV37.js").then((m) => m.list(args.slice(1)));
230
+ await import("./resources-E4XMWX7N.js").then((m) => m.list(args.slice(1)));
231
231
  break;
232
232
  case "show":
233
- await import("./resources-HLQTZV37.js").then((m) => m.show(args.slice(1)));
233
+ await import("./resources-E4XMWX7N.js").then((m) => m.show(args.slice(1)));
234
234
  break;
235
235
  case "diff":
236
- await import("./resources-HLQTZV37.js").then((m) => m.diff(args.slice(1)));
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 config = JSON.parse(configContent);
353
- if (filterName && config.external_id !== filterName && config.name !== filterName && entry.name !== 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 (!config.external_id) {
519
+ if (!rawConfig.external_id) {
357
520
  if (appName) {
358
- config.external_id = `${appName}:${entry.name}`;
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 (!config.name) {
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 ? JSON.stringify(automation.inputs.schema) : "";
650
- const remoteSchema = remote.input_schema ? typeof remote.input_schema === "string" ? remote.input_schema : JSON.stringify(remote.input_schema) : "";
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 scheduleChanged = localCron !== remoteCron;
655
- if (codeChanged || nameChanged || descChanged || schemaChanged || scheduleChanged) {
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 = localPresets[schedule.preset]?.label || schedule.preset;
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) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lumerahq/cli",
3
- "version": "0.18.0",
3
+ "version": "0.18.1",
4
4
  "description": "CLI for building and deploying Lumera apps",
5
5
  "type": "module",
6
6
  "engines": {