@lumerahq/cli 0.17.1 → 0.18.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/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-HLQTZV37.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-HLQTZV37.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-HLQTZV37.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-HLQTZV37.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-HLQTZV37.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-HLQTZV37.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-HLQTZV37.js").then((m) => m.diff(args.slice(1)));
237
237
  break;
238
238
  // Development
239
239
  case "dev":
@@ -252,8 +252,10 @@ function parseResource(resourcePath) {
252
252
  const parts = resourcePath.split("/");
253
253
  const type = parts[0];
254
254
  const name = parts.slice(1).join("/") || null;
255
- if (!["collections", "automations", "hooks", "agents", "app"].includes(type)) {
256
- return { type: null, name: null };
255
+ const validTypes = ["collections", "automations", "hooks", "agents", "app"];
256
+ if (!validTypes.includes(type)) {
257
+ console.log(pc.red(` Unknown resource type "${type}". Valid types: ${validTypes.join(", ")}`));
258
+ process.exit(1);
257
259
  }
258
260
  return { type, name };
259
261
  }
@@ -301,6 +303,16 @@ function loadLocalCollections(platformDir, filterName) {
301
303
  errors.push(`${file}: collection name "${collection.name}" contains invalid characters`);
302
304
  continue;
303
305
  }
306
+ const existingById = collections.find((c) => c.id === collection.id);
307
+ if (existingById) {
308
+ errors.push(`${file}: duplicate collection id "${collection.id}" (also defined in another file)`);
309
+ continue;
310
+ }
311
+ const existingByName = collections.find((c) => c.name === collection.name);
312
+ if (existingByName) {
313
+ errors.push(`${file}: duplicate collection name "${collection.name}" (also defined in another file)`);
314
+ continue;
315
+ }
304
316
  collections.push(collection);
305
317
  } catch (e) {
306
318
  errors.push(`${file}: failed to parse - ${e}`);
@@ -357,6 +369,11 @@ function loadLocalAutomations(platformDir, filterName, appName) {
357
369
  errors.push(`${entry.name}: missing inputs.schema in config.json`);
358
370
  continue;
359
371
  }
372
+ const existingByExtId = automations.find((a) => a.automation.external_id === config.external_id);
373
+ if (existingByExtId) {
374
+ errors.push(`${entry.name}: duplicate external_id "${config.external_id}" (also defined in ${existingByExtId.automation.name})`);
375
+ continue;
376
+ }
360
377
  const code = readFileSync(mainPath, "utf-8");
361
378
  automations.push({ automation: config, code });
362
379
  } catch (e) {
@@ -409,22 +426,28 @@ function loadLocalHooks(platformDir, filterName, appName) {
409
426
  function parseHookConfig(content) {
410
427
  const configMatch = content.match(/export\s+const\s+config\s*[=:]\s*(\{[\s\S]*?\});?/);
411
428
  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
- };
429
+ const externalId = configMatch[1].match(/external_id\s*:\s*['"]([^'"]+)['"]/)?.[1];
430
+ const collection = configMatch[1].match(/collection\s*:\s*['"]([^'"]+)['"]/)?.[1];
431
+ const trigger = configMatch[1].match(/trigger\s*:\s*['"]([^'"]+)['"]/)?.[1];
432
+ const enabled = configMatch[1].match(/enabled\s*:\s*(true|false)/)?.[1];
433
+ const name = configMatch[1].match(/name\s*:\s*['"]([^'"]+)['"]/)?.[1];
434
+ if (!collection || !trigger) return null;
435
+ const hook = {
436
+ external_id: externalId || "",
437
+ collection,
438
+ trigger,
439
+ enabled: enabled !== "false"
440
+ };
441
+ if (name) hook.name = name;
442
+ const metadataMatch = configMatch[1].match(/metadata\s*:\s*(\{[^}]*\})/);
443
+ if (metadataMatch) {
444
+ try {
445
+ const metaStr = metadataMatch[1].replace(/'/g, '"').replace(/(\w+)\s*:/g, '"$1":').replace(/,\s*}/g, "}");
446
+ hook.metadata = JSON.parse(metaStr);
447
+ } catch {
448
+ }
427
449
  }
450
+ return hook;
428
451
  }
429
452
  function extractHookScript(content) {
430
453
  const handlerMatch = content.match(
@@ -508,8 +531,37 @@ function fieldsDiffer(local, remote) {
508
531
  if (localMultiple && remoteMaxSelect <= 1) return true;
509
532
  if (!localMultiple && remoteMaxSelect > 1) return true;
510
533
  }
511
- if (local.min !== void 0 && local.min !== opts.min) return true;
512
- if (local.max !== void 0 && local.max !== opts.max) return true;
534
+ const localMin = local.min !== void 0 ? local.min : void 0;
535
+ const remoteMin = opts.min !== void 0 ? opts.min : void 0;
536
+ if (localMin !== remoteMin) return true;
537
+ const localMax = local.max !== void 0 ? local.max : void 0;
538
+ const remoteMax = opts.max !== void 0 ? opts.max : void 0;
539
+ if (localMax !== remoteMax) return true;
540
+ return false;
541
+ }
542
+ function localIndexesToSql(local) {
543
+ if (!local.indexes || local.indexes.length === 0) return [];
544
+ return local.indexes.map((idx) => {
545
+ const fields = idx.fields.join(", ");
546
+ const unique = idx.unique ? "UNIQUE " : "";
547
+ const indexName = `idx_${local.id}_${idx.fields.join("_")}`;
548
+ return `CREATE ${unique}INDEX ${indexName} ON ${local.name} (${fields})`;
549
+ }).sort();
550
+ }
551
+ function normalizeRemoteIndexes(remoteIndexes) {
552
+ return remoteIndexes.filter((idx) => {
553
+ const lower = idx.toLowerCase();
554
+ if (lower.includes("external_id") || lower.includes("updated")) return false;
555
+ return true;
556
+ }).sort();
557
+ }
558
+ function indexesDiffer(local, remoteIndexes) {
559
+ const localSql = localIndexesToSql(local);
560
+ const remoteSql = normalizeRemoteIndexes(remoteIndexes);
561
+ if (localSql.length !== remoteSql.length) return true;
562
+ for (let i = 0; i < localSql.length; i++) {
563
+ if (localSql[i] !== remoteSql[i]) return true;
564
+ }
513
565
  return false;
514
566
  }
515
567
  async function planCollections(api, localCollections) {
@@ -548,11 +600,13 @@ async function planCollections(api, localCollections) {
548
600
  modified.push(name);
549
601
  }
550
602
  }
551
- if (added.length > 0 || removed.length > 0 || modified.length > 0) {
603
+ const indexesChanged = indexesDiffer(local, remote.indexes || []);
604
+ if (added.length > 0 || removed.length > 0 || modified.length > 0 || indexesChanged) {
552
605
  const details = [];
553
606
  if (added.length > 0) details.push(`+${added.length} field${added.length > 1 ? "s" : ""}`);
554
607
  if (removed.length > 0) details.push(`-${removed.length} field${removed.length > 1 ? "s" : ""}`);
555
608
  if (modified.length > 0) details.push(`~${modified.length} field${modified.length > 1 ? "s" : ""} (${modified.join(", ")})`);
609
+ if (indexesChanged) details.push("indexes changed");
556
610
  const fieldDetails = [];
557
611
  for (const name of added) {
558
612
  const f = localFieldMap.get(name);
@@ -592,11 +646,19 @@ async function planAutomations(api, localAutomations) {
592
646
  const codeChanged = remote.code !== code;
593
647
  const nameChanged = remote.name !== automation.name;
594
648
  const descChanged = (remote.description || "") !== (automation.description || "");
595
- if (codeChanged || nameChanged || descChanged) {
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) : "";
651
+ const schemaChanged = localSchema !== remoteSchema;
652
+ const localCron = automation.schedule?.cron || "";
653
+ const remoteCron = remote.schedule || "";
654
+ const scheduleChanged = localCron !== remoteCron;
655
+ if (codeChanged || nameChanged || descChanged || schemaChanged || scheduleChanged) {
596
656
  const details = [];
597
657
  if (codeChanged) details.push("code");
598
658
  if (nameChanged) details.push("name");
599
659
  if (descChanged) details.push("description");
660
+ if (schemaChanged) details.push("input_schema");
661
+ if (scheduleChanged) details.push("schedule");
600
662
  const textDiffs = [];
601
663
  if (codeChanged) {
602
664
  textDiffs.push({ field: "main.py", oldText: remote.code || "", newText: code });
@@ -635,10 +697,16 @@ async function planHooks(api, localHooks, collections) {
635
697
  } else {
636
698
  const scriptChanged = remote.script.trim() !== script.trim();
637
699
  const eventChanged = remote.event !== hook.trigger;
638
- if (scriptChanged || eventChanged) {
700
+ const enabledChanged = remote.enabled !== void 0 && remote.enabled !== (hook.enabled !== false);
701
+ const nameChanged = hook.name && remote.name !== hook.name || false;
702
+ const collectionChanged = remote.collection_id !== collectionId;
703
+ if (scriptChanged || eventChanged || enabledChanged || nameChanged || collectionChanged) {
639
704
  const details = [];
640
705
  if (scriptChanged) details.push("script");
641
706
  if (eventChanged) details.push("trigger");
707
+ if (enabledChanged) details.push("enabled");
708
+ if (nameChanged) details.push("name");
709
+ if (collectionChanged) details.push("collection");
642
710
  const textDiffs = [];
643
711
  if (scriptChanged) {
644
712
  textDiffs.push({ field: fileName, oldText: remote.script || "", newText: script });
@@ -660,30 +728,25 @@ async function applyCollections(api, localCollections) {
660
728
  let errors = 0;
661
729
  const hasRelations = localCollections.some((c) => c.fields.some((f) => f.type === "relation"));
662
730
  if (hasRelations) {
731
+ const pass1Failed = /* @__PURE__ */ new Set();
663
732
  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
733
  try {
672
- await api.ensureCollection(local.name, apiFormat);
734
+ await api.ensureCollection(local.name, { id: local.id });
673
735
  console.log(pc.green(" \u2713"), `${local.name}`);
674
736
  } catch (e) {
675
737
  console.log(pc.red(" \u2717"), `${local.name}: ${e}`);
738
+ pass1Failed.add(local.name);
676
739
  errors++;
677
740
  }
678
741
  }
679
- const collectionsWithRelations = localCollections.filter((c) => c.fields.some((f) => f.type === "relation"));
680
- for (const local of collectionsWithRelations) {
742
+ for (const local of localCollections) {
743
+ if (pass1Failed.has(local.name)) continue;
681
744
  const apiFormat = convertCollectionToApiFormat(local);
682
745
  try {
683
746
  await api.ensureCollection(local.name, apiFormat);
684
- console.log(pc.green(" \u2713"), `${local.name} (relations)`);
747
+ console.log(pc.green(" \u2713"), `${local.name} (schema)`);
685
748
  } catch (e) {
686
- console.log(pc.red(" \u2717"), `${local.name} (relations): ${e}`);
749
+ console.log(pc.red(" \u2717"), `${local.name} (schema): ${e}`);
687
750
  errors++;
688
751
  }
689
752
  }
@@ -746,6 +809,9 @@ async function applyAutomations(api, localAutomations) {
746
809
  async function syncPresets(api, automationId, localPresets) {
747
810
  const remotePresets = await api.listPresets(automationId);
748
811
  const remoteByName = new Map(remotePresets.map((p) => [p.name, p]));
812
+ const localPresetNames = new Set(
813
+ Object.entries(localPresets).map(([key, preset]) => preset.label || key)
814
+ );
749
815
  for (const [presetKey, preset] of Object.entries(localPresets)) {
750
816
  const presetName = preset.label || presetKey;
751
817
  const existing = remoteByName.get(presetName);
@@ -761,6 +827,16 @@ async function syncPresets(api, automationId, localPresets) {
761
827
  console.log(pc.yellow(` \u26A0 Failed to sync preset ${presetName}: ${e}`));
762
828
  }
763
829
  }
830
+ for (const remote of remotePresets) {
831
+ if (!localPresetNames.has(remote.name)) {
832
+ try {
833
+ await api.deletePreset(remote.id);
834
+ console.log(pc.dim(` Deleted preset: ${remote.name}`));
835
+ } catch (e) {
836
+ console.log(pc.yellow(` \u26A0 Failed to delete preset ${remote.name}: ${e}`));
837
+ }
838
+ }
839
+ }
764
840
  }
765
841
  async function setSchedule(api, automationId, schedule, localPresets) {
766
842
  const presetName = localPresets[schedule.preset]?.label || schedule.preset;
@@ -1706,7 +1782,8 @@ async function plan(args) {
1706
1782
  const api = createApiClient();
1707
1783
  const appName = getAppName(projectRoot);
1708
1784
  const projectId = getProjectId(projectRoot);
1709
- const { type, name } = parseResource(args[0]);
1785
+ const positionalArgs = args.filter((a) => !a.startsWith("-"));
1786
+ const { type, name } = parseResource(positionalArgs[0]);
1710
1787
  console.log();
1711
1788
  console.log(pc.cyan(pc.bold(" Plan")));
1712
1789
  console.log(pc.dim(" Comparing local files to remote state..."));
@@ -1803,7 +1880,8 @@ async function apply(args) {
1803
1880
  const api = createApiClient();
1804
1881
  const appName = getAppName(projectRoot);
1805
1882
  const projectId = getProjectId(projectRoot);
1806
- const { type, name } = parseResource(args.filter((a) => a !== "--yes" && a !== "-y")[0]);
1883
+ const positionalArgs = args.filter((a) => !a.startsWith("-"));
1884
+ const { type, name } = parseResource(positionalArgs[0]);
1807
1885
  const autoConfirm = args.includes("--yes") || args.includes("-y") || !!process.env.CI;
1808
1886
  if (type === "app") {
1809
1887
  console.log();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lumerahq/cli",
3
- "version": "0.17.1",
3
+ "version": "0.18.0",
4
4
  "description": "CLI for building and deploying Lumera apps",
5
5
  "type": "module",
6
6
  "engines": {