@lumerahq/cli 0.17.2 → 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-64MEBAL5.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-64MEBAL5.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-64MEBAL5.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-64MEBAL5.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-64MEBAL5.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-64MEBAL5.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-64MEBAL5.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:")}
@@ -252,8 +415,10 @@ function parseResource(resourcePath) {
252
415
  const parts = resourcePath.split("/");
253
416
  const type = parts[0];
254
417
  const name = parts.slice(1).join("/") || null;
255
- if (!["collections", "automations", "hooks", "agents", "app"].includes(type)) {
256
- return { type: null, name: null };
418
+ const validTypes = ["collections", "automations", "hooks", "agents", "app"];
419
+ if (!validTypes.includes(type)) {
420
+ console.log(pc.red(` Unknown resource type "${type}". Valid types: ${validTypes.join(", ")}`));
421
+ process.exit(1);
257
422
  }
258
423
  return { type, name };
259
424
  }
@@ -301,6 +466,16 @@ function loadLocalCollections(platformDir, filterName) {
301
466
  errors.push(`${file}: collection name "${collection.name}" contains invalid characters`);
302
467
  continue;
303
468
  }
469
+ const existingById = collections.find((c) => c.id === collection.id);
470
+ if (existingById) {
471
+ errors.push(`${file}: duplicate collection id "${collection.id}" (also defined in another file)`);
472
+ continue;
473
+ }
474
+ const existingByName = collections.find((c) => c.name === collection.name);
475
+ if (existingByName) {
476
+ errors.push(`${file}: duplicate collection name "${collection.name}" (also defined in another file)`);
477
+ continue;
478
+ }
304
479
  collections.push(collection);
305
480
  } catch (e) {
306
481
  errors.push(`${file}: failed to parse - ${e}`);
@@ -337,26 +512,32 @@ function loadLocalAutomations(platformDir, filterName, appName) {
337
512
  }
338
513
  try {
339
514
  const configContent = readFileSync(configPath, "utf-8");
340
- const config = JSON.parse(configContent);
341
- 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) {
342
517
  continue;
343
518
  }
344
- if (!config.external_id) {
519
+ if (!rawConfig.external_id) {
345
520
  if (appName) {
346
- config.external_id = `${appName}:${entry.name}`;
521
+ rawConfig.external_id = `${appName}:${entry.name}`;
347
522
  } else {
348
523
  errors.push(`${entry.name}: missing external_id in config.json`);
349
524
  continue;
350
525
  }
351
526
  }
352
- if (!config.name) {
527
+ if (!rawConfig.name) {
353
528
  errors.push(`${entry.name}: missing name in config.json`);
354
529
  continue;
355
530
  }
531
+ const config = normalizeLocalAutomation(rawConfig);
356
532
  if (!config.inputs?.schema) {
357
533
  errors.push(`${entry.name}: missing inputs.schema in config.json`);
358
534
  continue;
359
535
  }
536
+ const existingByExtId = automations.find((a) => a.automation.external_id === config.external_id);
537
+ if (existingByExtId) {
538
+ errors.push(`${entry.name}: duplicate external_id "${config.external_id}" (also defined in ${existingByExtId.automation.name})`);
539
+ continue;
540
+ }
360
541
  const code = readFileSync(mainPath, "utf-8");
361
542
  automations.push({ automation: config, code });
362
543
  } catch (e) {
@@ -409,22 +590,28 @@ function loadLocalHooks(platformDir, filterName, appName) {
409
590
  function parseHookConfig(content) {
410
591
  const configMatch = content.match(/export\s+const\s+config\s*[=:]\s*(\{[\s\S]*?\});?/);
411
592
  if (!configMatch) return null;
412
- try {
413
- let configStr = configMatch[1].replace(/'/g, '"').replace(/(\w+):/g, '"$1":').replace(/,\s*}/g, "}");
414
- return JSON.parse(configStr);
415
- } catch {
416
- const externalId = content.match(/external_id:\s*['"]([^'"]+)['"]/)?.[1];
417
- const collection = content.match(/collection:\s*['"]([^'"]+)['"]/)?.[1];
418
- const trigger = content.match(/trigger:\s*['"]([^'"]+)['"]/)?.[1];
419
- const enabled = content.match(/enabled:\s*(true|false)/)?.[1];
420
- if (!collection || !trigger) return null;
421
- return {
422
- external_id: externalId || "",
423
- collection,
424
- trigger,
425
- enabled: enabled !== "false"
426
- };
593
+ const externalId = configMatch[1].match(/external_id\s*:\s*['"]([^'"]+)['"]/)?.[1];
594
+ const collection = configMatch[1].match(/collection\s*:\s*['"]([^'"]+)['"]/)?.[1];
595
+ const trigger = configMatch[1].match(/trigger\s*:\s*['"]([^'"]+)['"]/)?.[1];
596
+ const enabled = configMatch[1].match(/enabled\s*:\s*(true|false)/)?.[1];
597
+ const name = configMatch[1].match(/name\s*:\s*['"]([^'"]+)['"]/)?.[1];
598
+ if (!collection || !trigger) return null;
599
+ const hook = {
600
+ external_id: externalId || "",
601
+ collection,
602
+ trigger,
603
+ enabled: enabled !== "false"
604
+ };
605
+ if (name) hook.name = name;
606
+ const metadataMatch = configMatch[1].match(/metadata\s*:\s*(\{[^}]*\})/);
607
+ if (metadataMatch) {
608
+ try {
609
+ const metaStr = metadataMatch[1].replace(/'/g, '"').replace(/(\w+)\s*:/g, '"$1":').replace(/,\s*}/g, "}");
610
+ hook.metadata = JSON.parse(metaStr);
611
+ } catch {
612
+ }
427
613
  }
614
+ return hook;
428
615
  }
429
616
  function extractHookScript(content) {
430
617
  const handlerMatch = content.match(
@@ -508,8 +695,37 @@ function fieldsDiffer(local, remote) {
508
695
  if (localMultiple && remoteMaxSelect <= 1) return true;
509
696
  if (!localMultiple && remoteMaxSelect > 1) return true;
510
697
  }
511
- if (local.min !== void 0 && local.min !== opts.min) return true;
512
- if (local.max !== void 0 && local.max !== opts.max) return true;
698
+ const localMin = local.min !== void 0 ? local.min : void 0;
699
+ const remoteMin = opts.min !== void 0 ? opts.min : void 0;
700
+ if (localMin !== remoteMin) return true;
701
+ const localMax = local.max !== void 0 ? local.max : void 0;
702
+ const remoteMax = opts.max !== void 0 ? opts.max : void 0;
703
+ if (localMax !== remoteMax) return true;
704
+ return false;
705
+ }
706
+ function localIndexesToSql(local) {
707
+ if (!local.indexes || local.indexes.length === 0) return [];
708
+ return local.indexes.map((idx) => {
709
+ const fields = idx.fields.join(", ");
710
+ const unique = idx.unique ? "UNIQUE " : "";
711
+ const indexName = `idx_${local.id}_${idx.fields.join("_")}`;
712
+ return `CREATE ${unique}INDEX ${indexName} ON ${local.name} (${fields})`;
713
+ }).sort();
714
+ }
715
+ function normalizeRemoteIndexes(remoteIndexes) {
716
+ return remoteIndexes.filter((idx) => {
717
+ const lower = idx.toLowerCase();
718
+ if (lower.includes("external_id") || lower.includes("updated")) return false;
719
+ return true;
720
+ }).sort();
721
+ }
722
+ function indexesDiffer(local, remoteIndexes) {
723
+ const localSql = localIndexesToSql(local);
724
+ const remoteSql = normalizeRemoteIndexes(remoteIndexes);
725
+ if (localSql.length !== remoteSql.length) return true;
726
+ for (let i = 0; i < localSql.length; i++) {
727
+ if (localSql[i] !== remoteSql[i]) return true;
728
+ }
513
729
  return false;
514
730
  }
515
731
  async function planCollections(api, localCollections) {
@@ -548,11 +764,13 @@ async function planCollections(api, localCollections) {
548
764
  modified.push(name);
549
765
  }
550
766
  }
551
- if (added.length > 0 || removed.length > 0 || modified.length > 0) {
767
+ const indexesChanged = indexesDiffer(local, remote.indexes || []);
768
+ if (added.length > 0 || removed.length > 0 || modified.length > 0 || indexesChanged) {
552
769
  const details = [];
553
770
  if (added.length > 0) details.push(`+${added.length} field${added.length > 1 ? "s" : ""}`);
554
771
  if (removed.length > 0) details.push(`-${removed.length} field${removed.length > 1 ? "s" : ""}`);
555
772
  if (modified.length > 0) details.push(`~${modified.length} field${modified.length > 1 ? "s" : ""} (${modified.join(", ")})`);
773
+ if (indexesChanged) details.push("indexes changed");
556
774
  const fieldDetails = [];
557
775
  for (const name of added) {
558
776
  const f = localFieldMap.get(name);
@@ -579,6 +797,15 @@ async function planAutomations(api, localAutomations) {
579
797
  const changes = [];
580
798
  const remoteAutomations = await api.listAutomations({ include_code: true });
581
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
+ };
582
809
  for (const { automation, code } of localAutomations) {
583
810
  const remote = remoteByExternalId.get(automation.external_id);
584
811
  if (!remote) {
@@ -592,11 +819,32 @@ async function planAutomations(api, localAutomations) {
592
819
  const codeChanged = remote.code !== code;
593
820
  const nameChanged = remote.name !== automation.name;
594
821
  const descChanged = (remote.description || "") !== (automation.description || "");
595
- if (codeChanged || nameChanged || descChanged) {
822
+ const localSchema = normalizeJsonComparable(automation.inputs?.schema);
823
+ const remoteSchema = normalizeJsonComparable(remote.input_schema);
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;
833
+ const localCron = automation.schedule?.cron || "";
834
+ const remoteCron = remote.schedule || "";
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) {
596
841
  const details = [];
597
842
  if (codeChanged) details.push("code");
598
843
  if (nameChanged) details.push("name");
599
844
  if (descChanged) details.push("description");
845
+ if (schemaChanged) details.push("input_schema");
846
+ if (presetsChanged) details.push("presets");
847
+ if (scheduleChanged) details.push("schedule");
600
848
  const textDiffs = [];
601
849
  if (codeChanged) {
602
850
  textDiffs.push({ field: "main.py", oldText: remote.code || "", newText: code });
@@ -635,10 +883,16 @@ async function planHooks(api, localHooks, collections) {
635
883
  } else {
636
884
  const scriptChanged = remote.script.trim() !== script.trim();
637
885
  const eventChanged = remote.event !== hook.trigger;
638
- if (scriptChanged || eventChanged) {
886
+ const enabledChanged = remote.enabled !== void 0 && remote.enabled !== (hook.enabled !== false);
887
+ const nameChanged = hook.name && remote.name !== hook.name || false;
888
+ const collectionChanged = remote.collection_id !== collectionId;
889
+ if (scriptChanged || eventChanged || enabledChanged || nameChanged || collectionChanged) {
639
890
  const details = [];
640
891
  if (scriptChanged) details.push("script");
641
892
  if (eventChanged) details.push("trigger");
893
+ if (enabledChanged) details.push("enabled");
894
+ if (nameChanged) details.push("name");
895
+ if (collectionChanged) details.push("collection");
642
896
  const textDiffs = [];
643
897
  if (scriptChanged) {
644
898
  textDiffs.push({ field: fileName, oldText: remote.script || "", newText: script });
@@ -660,30 +914,25 @@ async function applyCollections(api, localCollections) {
660
914
  let errors = 0;
661
915
  const hasRelations = localCollections.some((c) => c.fields.some((f) => f.type === "relation"));
662
916
  if (hasRelations) {
917
+ const pass1Failed = /* @__PURE__ */ new Set();
663
918
  for (const local of localCollections) {
664
- const relationFieldNames = new Set(local.fields.filter((f) => f.type === "relation").map((f) => f.name));
665
- const withoutRelations = {
666
- ...local,
667
- fields: local.fields.filter((f) => f.type !== "relation"),
668
- indexes: local.indexes?.filter((idx) => !idx.fields.some((f) => relationFieldNames.has(f)))
669
- };
670
- const apiFormat = convertCollectionToApiFormat(withoutRelations);
671
919
  try {
672
- await api.ensureCollection(local.name, apiFormat);
920
+ await api.ensureCollection(local.name, { id: local.id });
673
921
  console.log(pc.green(" \u2713"), `${local.name}`);
674
922
  } catch (e) {
675
923
  console.log(pc.red(" \u2717"), `${local.name}: ${e}`);
924
+ pass1Failed.add(local.name);
676
925
  errors++;
677
926
  }
678
927
  }
679
- const collectionsWithRelations = localCollections.filter((c) => c.fields.some((f) => f.type === "relation"));
680
- for (const local of collectionsWithRelations) {
928
+ for (const local of localCollections) {
929
+ if (pass1Failed.has(local.name)) continue;
681
930
  const apiFormat = convertCollectionToApiFormat(local);
682
931
  try {
683
932
  await api.ensureCollection(local.name, apiFormat);
684
- console.log(pc.green(" \u2713"), `${local.name} (relations)`);
933
+ console.log(pc.green(" \u2713"), `${local.name} (schema)`);
685
934
  } catch (e) {
686
- console.log(pc.red(" \u2717"), `${local.name} (relations): ${e}`);
935
+ console.log(pc.red(" \u2717"), `${local.name} (schema): ${e}`);
687
936
  errors++;
688
937
  }
689
938
  }
@@ -746,6 +995,9 @@ async function applyAutomations(api, localAutomations) {
746
995
  async function syncPresets(api, automationId, localPresets) {
747
996
  const remotePresets = await api.listPresets(automationId);
748
997
  const remoteByName = new Map(remotePresets.map((p) => [p.name, p]));
998
+ const localPresetNames = new Set(
999
+ Object.entries(localPresets).map(([key, preset]) => preset.label || key)
1000
+ );
749
1001
  for (const [presetKey, preset] of Object.entries(localPresets)) {
750
1002
  const presetName = preset.label || presetKey;
751
1003
  const existing = remoteByName.get(presetName);
@@ -761,9 +1013,19 @@ async function syncPresets(api, automationId, localPresets) {
761
1013
  console.log(pc.yellow(` \u26A0 Failed to sync preset ${presetName}: ${e}`));
762
1014
  }
763
1015
  }
1016
+ for (const remote of remotePresets) {
1017
+ if (!localPresetNames.has(remote.name)) {
1018
+ try {
1019
+ await api.deletePreset(remote.id);
1020
+ console.log(pc.dim(` Deleted preset: ${remote.name}`));
1021
+ } catch (e) {
1022
+ console.log(pc.yellow(` \u26A0 Failed to delete preset ${remote.name}: ${e}`));
1023
+ }
1024
+ }
1025
+ }
764
1026
  }
765
1027
  async function setSchedule(api, automationId, schedule, localPresets) {
766
- const presetName = localPresets[schedule.preset]?.label || schedule.preset;
1028
+ const presetName = resolveSchedulePresetName(schedule, localPresets);
767
1029
  const remotePresets = await api.listPresets(automationId);
768
1030
  const preset = remotePresets.find((p) => p.name === presetName);
769
1031
  if (!preset) {
@@ -1706,7 +1968,8 @@ async function plan(args) {
1706
1968
  const api = createApiClient();
1707
1969
  const appName = getAppName(projectRoot);
1708
1970
  const projectId = getProjectId(projectRoot);
1709
- const { type, name } = parseResource(args[0]);
1971
+ const positionalArgs = args.filter((a) => !a.startsWith("-"));
1972
+ const { type, name } = parseResource(positionalArgs[0]);
1710
1973
  console.log();
1711
1974
  console.log(pc.cyan(pc.bold(" Plan")));
1712
1975
  console.log(pc.dim(" Comparing local files to remote state..."));
@@ -1803,7 +2066,8 @@ async function apply(args) {
1803
2066
  const api = createApiClient();
1804
2067
  const appName = getAppName(projectRoot);
1805
2068
  const projectId = getProjectId(projectRoot);
1806
- const { type, name } = parseResource(args.filter((a) => a !== "--yes" && a !== "-y")[0]);
2069
+ const positionalArgs = args.filter((a) => !a.startsWith("-"));
2070
+ const { type, name } = parseResource(positionalArgs[0]);
1807
2071
  const autoConfirm = args.includes("--yes") || args.includes("-y") || !!process.env.CI;
1808
2072
  if (type === "app") {
1809
2073
  console.log();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lumerahq/cli",
3
- "version": "0.17.2",
3
+ "version": "0.18.1",
4
4
  "description": "CLI for building and deploying Lumera apps",
5
5
  "type": "module",
6
6
  "engines": {