@lumerahq/cli 0.7.0 → 0.8.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
|
@@ -92,22 +92,22 @@ async function main() {
|
|
|
92
92
|
switch (command) {
|
|
93
93
|
// Resource commands
|
|
94
94
|
case "plan":
|
|
95
|
-
await import("./resources-
|
|
95
|
+
await import("./resources-J3B5HNQZ.js").then((m) => m.plan(args.slice(1)));
|
|
96
96
|
break;
|
|
97
97
|
case "apply":
|
|
98
|
-
await import("./resources-
|
|
98
|
+
await import("./resources-J3B5HNQZ.js").then((m) => m.apply(args.slice(1)));
|
|
99
99
|
break;
|
|
100
100
|
case "pull":
|
|
101
|
-
await import("./resources-
|
|
101
|
+
await import("./resources-J3B5HNQZ.js").then((m) => m.pull(args.slice(1)));
|
|
102
102
|
break;
|
|
103
103
|
case "destroy":
|
|
104
|
-
await import("./resources-
|
|
104
|
+
await import("./resources-J3B5HNQZ.js").then((m) => m.destroy(args.slice(1)));
|
|
105
105
|
break;
|
|
106
106
|
case "list":
|
|
107
|
-
await import("./resources-
|
|
107
|
+
await import("./resources-J3B5HNQZ.js").then((m) => m.list(args.slice(1)));
|
|
108
108
|
break;
|
|
109
109
|
case "show":
|
|
110
|
-
await import("./resources-
|
|
110
|
+
await import("./resources-J3B5HNQZ.js").then((m) => m.show(args.slice(1)));
|
|
111
111
|
break;
|
|
112
112
|
// Development
|
|
113
113
|
case "dev":
|
|
@@ -121,7 +121,7 @@ async function main() {
|
|
|
121
121
|
await import("./init-OQCIET53.js").then((m) => m.init(args.slice(1)));
|
|
122
122
|
break;
|
|
123
123
|
case "status":
|
|
124
|
-
await import("./status-
|
|
124
|
+
await import("./status-E4IHEUKO.js").then((m) => m.status(args.slice(1)));
|
|
125
125
|
break;
|
|
126
126
|
case "migrate":
|
|
127
127
|
await import("./migrate-2DZ6RQ5K.js").then((m) => m.migrate(args.slice(1)));
|
|
@@ -114,6 +114,7 @@ ${pc.dim("Resources:")}
|
|
|
114
114
|
|
|
115
115
|
${pc.dim("Options:")}
|
|
116
116
|
--confirm Skip confirmation prompt
|
|
117
|
+
--force-cycles Remove relation fields to break circular references before deleting
|
|
117
118
|
|
|
118
119
|
${pc.dim("Examples:")}
|
|
119
120
|
lumera destroy # Destroy everything
|
|
@@ -124,10 +125,10 @@ ${pc.dim("Examples:")}
|
|
|
124
125
|
function showListHelp() {
|
|
125
126
|
console.log(`
|
|
126
127
|
${pc.dim("Usage:")}
|
|
127
|
-
lumera list [type]
|
|
128
|
+
lumera list [type] [--all]
|
|
128
129
|
|
|
129
130
|
${pc.dim("Description:")}
|
|
130
|
-
List resources with status
|
|
131
|
+
List resources with status. By default, remote-only resources are hidden.
|
|
131
132
|
|
|
132
133
|
${pc.dim("Types:")}
|
|
133
134
|
(none) List all resources
|
|
@@ -135,8 +136,12 @@ ${pc.dim("Types:")}
|
|
|
135
136
|
automations List only automations
|
|
136
137
|
hooks List only hooks
|
|
137
138
|
|
|
139
|
+
${pc.dim("Options:")}
|
|
140
|
+
--all Include remote-only resources
|
|
141
|
+
|
|
138
142
|
${pc.dim("Examples:")}
|
|
139
|
-
lumera list # List
|
|
143
|
+
lumera list # List local resources
|
|
144
|
+
lumera list --all # Include remote-only resources
|
|
140
145
|
lumera list collections # List only collections
|
|
141
146
|
`);
|
|
142
147
|
}
|
|
@@ -404,23 +409,41 @@ async function planCollections(api, localCollections) {
|
|
|
404
409
|
resource: "collection",
|
|
405
410
|
id: local.id,
|
|
406
411
|
name: local.name,
|
|
407
|
-
details: `${local.fields.length} fields
|
|
412
|
+
details: `${local.fields.length} fields`,
|
|
413
|
+
fieldDetails: local.fields.map((f) => ({
|
|
414
|
+
action: "+",
|
|
415
|
+
name: f.name,
|
|
416
|
+
type: f.type,
|
|
417
|
+
required: f.required
|
|
418
|
+
}))
|
|
408
419
|
});
|
|
409
420
|
} else {
|
|
410
421
|
const localFieldNames = new Set(local.fields.map((f) => f.name));
|
|
411
422
|
const remoteFieldNames = new Set(remote.schema.map((f) => f.name));
|
|
423
|
+
const localFieldMap = new Map(local.fields.map((f) => [f.name, f]));
|
|
424
|
+
const remoteFieldMap = new Map(remote.schema.map((f) => [f.name, f]));
|
|
412
425
|
const added = [...localFieldNames].filter((n) => !remoteFieldNames.has(n));
|
|
413
426
|
const removed = [...remoteFieldNames].filter((n) => !localFieldNames.has(n));
|
|
414
427
|
if (added.length > 0 || removed.length > 0) {
|
|
415
428
|
const details = [];
|
|
416
|
-
if (added.length > 0) details.push(`+${added.length}
|
|
417
|
-
if (removed.length > 0) details.push(`-${removed.length}
|
|
429
|
+
if (added.length > 0) details.push(`+${added.length} field${added.length > 1 ? "s" : ""}`);
|
|
430
|
+
if (removed.length > 0) details.push(`-${removed.length} field${removed.length > 1 ? "s" : ""}`);
|
|
431
|
+
const fieldDetails = [];
|
|
432
|
+
for (const name of added) {
|
|
433
|
+
const f = localFieldMap.get(name);
|
|
434
|
+
fieldDetails.push({ action: "+", name: f.name, type: f.type, required: f.required });
|
|
435
|
+
}
|
|
436
|
+
for (const name of removed) {
|
|
437
|
+
const f = remoteFieldMap.get(name);
|
|
438
|
+
fieldDetails.push({ action: "-", name: f.name, type: f.type, required: f.required });
|
|
439
|
+
}
|
|
418
440
|
changes.push({
|
|
419
441
|
type: "update",
|
|
420
442
|
resource: "collection",
|
|
421
443
|
id: local.id,
|
|
422
444
|
name: local.name,
|
|
423
|
-
details: details.join(", ")
|
|
445
|
+
details: details.join(", "),
|
|
446
|
+
fieldDetails
|
|
424
447
|
});
|
|
425
448
|
}
|
|
426
449
|
}
|
|
@@ -499,17 +522,50 @@ async function planHooks(api, localHooks, collections) {
|
|
|
499
522
|
return changes;
|
|
500
523
|
}
|
|
501
524
|
async function applyCollections(api, localCollections) {
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
525
|
+
let errors = 0;
|
|
526
|
+
const hasRelations = localCollections.some((c) => c.fields.some((f) => f.type === "relation"));
|
|
527
|
+
if (hasRelations) {
|
|
528
|
+
for (const local of localCollections) {
|
|
529
|
+
const withoutRelations = {
|
|
530
|
+
...local,
|
|
531
|
+
fields: local.fields.filter((f) => f.type !== "relation")
|
|
532
|
+
};
|
|
533
|
+
const apiFormat = convertCollectionToApiFormat(withoutRelations);
|
|
534
|
+
try {
|
|
535
|
+
await api.ensureCollection(local.name, apiFormat);
|
|
536
|
+
console.log(pc.green(" \u2713"), `${local.name}`);
|
|
537
|
+
} catch (e) {
|
|
538
|
+
console.log(pc.red(" \u2717"), `${local.name}: ${e}`);
|
|
539
|
+
errors++;
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
const collectionsWithRelations = localCollections.filter((c) => c.fields.some((f) => f.type === "relation"));
|
|
543
|
+
for (const local of collectionsWithRelations) {
|
|
544
|
+
const apiFormat = convertCollectionToApiFormat(local);
|
|
545
|
+
try {
|
|
546
|
+
await api.ensureCollection(local.name, apiFormat);
|
|
547
|
+
console.log(pc.green(" \u2713"), `${local.name} (relations)`);
|
|
548
|
+
} catch (e) {
|
|
549
|
+
console.log(pc.red(" \u2717"), `${local.name} (relations): ${e}`);
|
|
550
|
+
errors++;
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
} else {
|
|
554
|
+
for (const local of localCollections) {
|
|
555
|
+
const apiFormat = convertCollectionToApiFormat(local);
|
|
556
|
+
try {
|
|
557
|
+
await api.ensureCollection(local.name, apiFormat);
|
|
558
|
+
console.log(pc.green(" \u2713"), `${local.name}`);
|
|
559
|
+
} catch (e) {
|
|
560
|
+
console.log(pc.red(" \u2717"), `${local.name}: ${e}`);
|
|
561
|
+
errors++;
|
|
562
|
+
}
|
|
509
563
|
}
|
|
510
564
|
}
|
|
565
|
+
return errors;
|
|
511
566
|
}
|
|
512
567
|
async function applyAutomations(api, localAutomations) {
|
|
568
|
+
let errors = 0;
|
|
513
569
|
const remoteAutomations = await api.listAutomations();
|
|
514
570
|
const remoteByExternalId = new Map(remoteAutomations.filter((a) => a.external_id).map((a) => [a.external_id, a]));
|
|
515
571
|
for (const { automation, code } of localAutomations) {
|
|
@@ -545,8 +601,10 @@ async function applyAutomations(api, localAutomations) {
|
|
|
545
601
|
}
|
|
546
602
|
} catch (e) {
|
|
547
603
|
console.log(pc.red(" \u2717"), `${automation.name}: ${e}`);
|
|
604
|
+
errors++;
|
|
548
605
|
}
|
|
549
606
|
}
|
|
607
|
+
return errors;
|
|
550
608
|
}
|
|
551
609
|
async function syncPresets(api, automationId, localPresets) {
|
|
552
610
|
const remotePresets = await api.listPresets(automationId);
|
|
@@ -587,13 +645,15 @@ async function setSchedule(api, automationId, schedule, localPresets) {
|
|
|
587
645
|
}
|
|
588
646
|
}
|
|
589
647
|
async function applyHooks(api, localHooks, collections) {
|
|
648
|
+
let errors = 0;
|
|
590
649
|
const remoteHooks = await api.listHooks();
|
|
591
650
|
const remoteByExternalId = new Map(remoteHooks.filter((h) => h.external_id).map((h) => [h.external_id, h]));
|
|
592
651
|
for (const { hook, script, fileName } of localHooks) {
|
|
593
652
|
const remote = remoteByExternalId.get(hook.external_id);
|
|
594
653
|
const collectionId = collections.get(hook.collection);
|
|
595
654
|
if (!collectionId) {
|
|
596
|
-
console.log(pc.
|
|
655
|
+
console.log(pc.red(` \u2717 ${fileName}: collection '${hook.collection}' not found. Apply the collection first or use 'lumera apply' to apply all resources.`));
|
|
656
|
+
errors++;
|
|
597
657
|
continue;
|
|
598
658
|
}
|
|
599
659
|
const payload = {
|
|
@@ -615,8 +675,10 @@ async function applyHooks(api, localHooks, collections) {
|
|
|
615
675
|
}
|
|
616
676
|
} catch (e) {
|
|
617
677
|
console.log(pc.red(" \u2717"), `${payload.name}: ${e}`);
|
|
678
|
+
errors++;
|
|
618
679
|
}
|
|
619
680
|
}
|
|
681
|
+
return errors;
|
|
620
682
|
}
|
|
621
683
|
async function applyApp(args) {
|
|
622
684
|
const skipBuild = args.includes("--skip-build");
|
|
@@ -836,7 +898,76 @@ async function listResources(api, platformDir, filterType) {
|
|
|
836
898
|
}
|
|
837
899
|
return results;
|
|
838
900
|
}
|
|
839
|
-
|
|
901
|
+
function planCollectionDelete(collections, platformDir) {
|
|
902
|
+
if (collections.length <= 1) {
|
|
903
|
+
return { sorted: collections, cycleNames: [], cycleEdges: [] };
|
|
904
|
+
}
|
|
905
|
+
const localCollections = loadLocalCollections(platformDir);
|
|
906
|
+
const localByName = new Map(localCollections.map((c) => [c.name, c]));
|
|
907
|
+
const deletingNames = new Set(collections.map((c) => c.name));
|
|
908
|
+
const dependsOn = /* @__PURE__ */ new Map();
|
|
909
|
+
for (const col of collections) {
|
|
910
|
+
const local = localByName.get(col.name);
|
|
911
|
+
if (!local) continue;
|
|
912
|
+
const deps = /* @__PURE__ */ new Set();
|
|
913
|
+
for (const field of local.fields) {
|
|
914
|
+
if (field.type === "relation" && field.collection && deletingNames.has(field.collection)) {
|
|
915
|
+
deps.add(field.collection);
|
|
916
|
+
}
|
|
917
|
+
}
|
|
918
|
+
if (deps.size > 0) dependsOn.set(col.name, deps);
|
|
919
|
+
}
|
|
920
|
+
const inDegree = /* @__PURE__ */ new Map();
|
|
921
|
+
const reverseAdj = /* @__PURE__ */ new Map();
|
|
922
|
+
for (const col of collections) {
|
|
923
|
+
inDegree.set(col.name, 0);
|
|
924
|
+
reverseAdj.set(col.name, []);
|
|
925
|
+
}
|
|
926
|
+
for (const [name, deps] of dependsOn) {
|
|
927
|
+
for (const dep of deps) {
|
|
928
|
+
inDegree.set(dep, (inDegree.get(dep) || 0) + 1);
|
|
929
|
+
reverseAdj.get(name)?.push(dep);
|
|
930
|
+
}
|
|
931
|
+
}
|
|
932
|
+
const queue = [];
|
|
933
|
+
for (const [name, degree] of inDegree) {
|
|
934
|
+
if (degree === 0) queue.push(name);
|
|
935
|
+
}
|
|
936
|
+
const sorted = [];
|
|
937
|
+
while (queue.length > 0) {
|
|
938
|
+
const name = queue.shift();
|
|
939
|
+
sorted.push(name);
|
|
940
|
+
for (const neighbor of reverseAdj.get(name) || []) {
|
|
941
|
+
const newDegree = (inDegree.get(neighbor) || 1) - 1;
|
|
942
|
+
inDegree.set(neighbor, newDegree);
|
|
943
|
+
if (newDegree === 0) queue.push(neighbor);
|
|
944
|
+
}
|
|
945
|
+
}
|
|
946
|
+
const sortedSet = new Set(sorted);
|
|
947
|
+
const cycleNames = [];
|
|
948
|
+
const cycleEdges = [];
|
|
949
|
+
for (const col of collections) {
|
|
950
|
+
if (!sortedSet.has(col.name)) {
|
|
951
|
+
cycleNames.push(col.name);
|
|
952
|
+
sorted.push(col.name);
|
|
953
|
+
const local = localByName.get(col.name);
|
|
954
|
+
if (local) {
|
|
955
|
+
for (const field of local.fields) {
|
|
956
|
+
if (field.type === "relation" && field.collection && deletingNames.has(field.collection)) {
|
|
957
|
+
cycleEdges.push({ from: col.name, field: field.name, to: field.collection });
|
|
958
|
+
}
|
|
959
|
+
}
|
|
960
|
+
}
|
|
961
|
+
}
|
|
962
|
+
}
|
|
963
|
+
const byName = new Map(collections.map((c) => [c.name, c]));
|
|
964
|
+
return {
|
|
965
|
+
sorted: sorted.map((name) => byName.get(name)),
|
|
966
|
+
cycleNames,
|
|
967
|
+
cycleEdges
|
|
968
|
+
};
|
|
969
|
+
}
|
|
970
|
+
async function destroyResources(api, platformDir, resourceType, resourceName, skipConfirm, forceCycles) {
|
|
840
971
|
const toDelete = [];
|
|
841
972
|
if (!resourceType || resourceType === "collections") {
|
|
842
973
|
const localCollections = loadLocalCollections(platformDir, resourceName || void 0);
|
|
@@ -899,12 +1030,14 @@ async function destroyResources(api, platformDir, resourceType, resourceName, sk
|
|
|
899
1030
|
const hooks = toDelete.filter((r) => r.type === "hook");
|
|
900
1031
|
const automations = toDelete.filter((r) => r.type === "automation");
|
|
901
1032
|
const collections = toDelete.filter((r) => r.type === "collection");
|
|
1033
|
+
let errors = 0;
|
|
902
1034
|
for (const resource of hooks) {
|
|
903
1035
|
try {
|
|
904
1036
|
await api.deleteHook(resource.remoteId);
|
|
905
1037
|
console.log(pc.green(" \u2713"), `Deleted hook: ${resource.name}`);
|
|
906
1038
|
} catch (e) {
|
|
907
1039
|
console.log(pc.red(" \u2717"), `Failed to delete hook ${resource.name}: ${e}`);
|
|
1040
|
+
errors++;
|
|
908
1041
|
}
|
|
909
1042
|
}
|
|
910
1043
|
for (const resource of automations) {
|
|
@@ -913,16 +1046,53 @@ async function destroyResources(api, platformDir, resourceType, resourceName, sk
|
|
|
913
1046
|
console.log(pc.green(" \u2713"), `Deleted automation: ${resource.name}`);
|
|
914
1047
|
} catch (e) {
|
|
915
1048
|
console.log(pc.red(" \u2717"), `Failed to delete automation ${resource.name}: ${e}`);
|
|
1049
|
+
errors++;
|
|
1050
|
+
}
|
|
1051
|
+
}
|
|
1052
|
+
const deletePlan = planCollectionDelete(collections, platformDir);
|
|
1053
|
+
if (deletePlan.cycleNames.length > 0 && !forceCycles) {
|
|
1054
|
+
console.log();
|
|
1055
|
+
console.log(pc.yellow(" Circular references detected:"));
|
|
1056
|
+
for (const edge of deletePlan.cycleEdges) {
|
|
1057
|
+
console.log(pc.yellow(` ${edge.from}.${edge.field} \u2192 ${edge.to}`));
|
|
1058
|
+
}
|
|
1059
|
+
console.log();
|
|
1060
|
+
console.log(pc.dim(" To destroy these, relation fields forming the cycle must be removed first."));
|
|
1061
|
+
console.log(pc.dim(" Use --force-cycles to proceed."));
|
|
1062
|
+
console.log();
|
|
1063
|
+
process.exit(1);
|
|
1064
|
+
}
|
|
1065
|
+
if (deletePlan.cycleNames.length > 0 && forceCycles) {
|
|
1066
|
+
console.log(pc.dim(" Breaking circular references..."));
|
|
1067
|
+
for (const edge of deletePlan.cycleEdges) {
|
|
1068
|
+
const resource = collections.find((c) => c.name === edge.from);
|
|
1069
|
+
if (!resource?.remoteId) continue;
|
|
1070
|
+
try {
|
|
1071
|
+
const remoteCollections = await api.listCollections();
|
|
1072
|
+
const remote = remoteCollections.find((c) => c.id === resource.remoteId);
|
|
1073
|
+
if (remote) {
|
|
1074
|
+
const updatedSchema = remote.schema.filter((f) => f.name !== edge.field);
|
|
1075
|
+
await api.ensureCollection(remote.name, { name: remote.name, schema: updatedSchema });
|
|
1076
|
+
console.log(pc.green(" \u2713"), `Removed ${edge.from}.${edge.field}`);
|
|
1077
|
+
}
|
|
1078
|
+
} catch (e) {
|
|
1079
|
+
console.log(pc.red(" \u2717"), `Failed to remove ${edge.from}.${edge.field}: ${e}`);
|
|
1080
|
+
errors++;
|
|
1081
|
+
}
|
|
916
1082
|
}
|
|
917
1083
|
}
|
|
918
|
-
for (const resource of
|
|
1084
|
+
for (const resource of deletePlan.sorted) {
|
|
919
1085
|
try {
|
|
920
1086
|
await api.deleteCollection(resource.remoteId);
|
|
921
1087
|
console.log(pc.green(" \u2713"), `Deleted collection: ${resource.name}`);
|
|
922
1088
|
} catch (e) {
|
|
923
1089
|
console.log(pc.red(" \u2717"), `Failed to delete collection ${resource.name}: ${e}`);
|
|
1090
|
+
errors++;
|
|
924
1091
|
}
|
|
925
1092
|
}
|
|
1093
|
+
if (errors > 0) {
|
|
1094
|
+
process.exit(1);
|
|
1095
|
+
}
|
|
926
1096
|
}
|
|
927
1097
|
async function destroyApp(skipConfirm) {
|
|
928
1098
|
const projectRoot = findProjectRoot();
|
|
@@ -980,19 +1150,54 @@ async function showResource(api, platformDir, resourceType, resourceName) {
|
|
|
980
1150
|
console.log();
|
|
981
1151
|
console.log(pc.bold(` Collection: ${resourceName}`));
|
|
982
1152
|
console.log();
|
|
1153
|
+
let collectionStatus;
|
|
1154
|
+
let addedFields = [];
|
|
1155
|
+
let removedFields = [];
|
|
983
1156
|
if (local && remote) {
|
|
984
|
-
|
|
1157
|
+
const localFieldNames = new Set(local.fields.map((f) => f.name));
|
|
1158
|
+
const remoteFieldNames = new Set(remote.schema.map((f) => f.name));
|
|
1159
|
+
addedFields = [...localFieldNames].filter((n) => !remoteFieldNames.has(n));
|
|
1160
|
+
removedFields = [...remoteFieldNames].filter((n) => !localFieldNames.has(n));
|
|
1161
|
+
collectionStatus = addedFields.length > 0 || removedFields.length > 0 ? "changed" : "synced";
|
|
985
1162
|
} else if (local) {
|
|
986
|
-
|
|
1163
|
+
collectionStatus = "local-only";
|
|
987
1164
|
} else {
|
|
988
|
-
|
|
1165
|
+
collectionStatus = "remote-only";
|
|
989
1166
|
}
|
|
1167
|
+
const statusDisplay = {
|
|
1168
|
+
"synced": pc.green("synced"),
|
|
1169
|
+
"changed": pc.yellow("changed"),
|
|
1170
|
+
"local-only": pc.yellow("local only"),
|
|
1171
|
+
"remote-only": pc.cyan("remote only")
|
|
1172
|
+
};
|
|
1173
|
+
console.log(` Status: ${statusDisplay[collectionStatus]}`);
|
|
990
1174
|
console.log();
|
|
991
|
-
const
|
|
1175
|
+
const addedSet = new Set(addedFields);
|
|
1176
|
+
const removedSet = new Set(removedFields);
|
|
992
1177
|
console.log(pc.bold(" Fields:"));
|
|
993
|
-
|
|
994
|
-
const
|
|
995
|
-
|
|
1178
|
+
if (local) {
|
|
1179
|
+
for (const field of local.fields) {
|
|
1180
|
+
const req = field.required ? pc.red("*") : "";
|
|
1181
|
+
if (addedSet.has(field.name)) {
|
|
1182
|
+
console.log(` ${pc.green("+")} ${field.name}${req} ${pc.dim(`(${field.type})`)}`);
|
|
1183
|
+
} else {
|
|
1184
|
+
console.log(` ${field.name}${req} ${pc.dim(`(${field.type})`)}`);
|
|
1185
|
+
}
|
|
1186
|
+
}
|
|
1187
|
+
}
|
|
1188
|
+
if (remote) {
|
|
1189
|
+
for (const field of remote.schema) {
|
|
1190
|
+
if (removedSet.has(field.name)) {
|
|
1191
|
+
const req = field.required ? pc.red("*") : "";
|
|
1192
|
+
console.log(` ${pc.red("-")} ${field.name}${req} ${pc.dim(`(${field.type})`)}`);
|
|
1193
|
+
}
|
|
1194
|
+
}
|
|
1195
|
+
}
|
|
1196
|
+
if (!local && remote) {
|
|
1197
|
+
for (const field of remote.schema) {
|
|
1198
|
+
const req = field.required ? pc.red("*") : "";
|
|
1199
|
+
console.log(` ${field.name}${req} ${pc.dim(`(${field.type})`)}`);
|
|
1200
|
+
}
|
|
996
1201
|
}
|
|
997
1202
|
console.log();
|
|
998
1203
|
} else if (resourceType === "automations") {
|
|
@@ -1119,6 +1324,14 @@ async function plan(args) {
|
|
|
1119
1324
|
const color = change.type === "create" ? pc.green : change.type === "update" ? pc.yellow : pc.red;
|
|
1120
1325
|
const details = change.details ? ` (${change.details})` : "";
|
|
1121
1326
|
console.log(` ${color(icon)} ${change.resource}: ${change.name}${pc.dim(details)}`);
|
|
1327
|
+
if (change.fieldDetails && change.fieldDetails.length > 0) {
|
|
1328
|
+
for (const field of change.fieldDetails) {
|
|
1329
|
+
const fColor = field.action === "+" ? pc.green : pc.red;
|
|
1330
|
+
const req = field.required ? "*" : "";
|
|
1331
|
+
console.log(` ${fColor(field.action)} ${field.name}${req} ${pc.dim(`(${field.type})`)}`);
|
|
1332
|
+
}
|
|
1333
|
+
console.log();
|
|
1334
|
+
}
|
|
1122
1335
|
}
|
|
1123
1336
|
console.log();
|
|
1124
1337
|
console.log(pc.dim(` Run 'lumera apply' to apply these changes.`));
|
|
@@ -1145,11 +1358,12 @@ async function apply(args) {
|
|
|
1145
1358
|
return;
|
|
1146
1359
|
}
|
|
1147
1360
|
let collections;
|
|
1361
|
+
let totalErrors = 0;
|
|
1148
1362
|
if (!type || type === "collections") {
|
|
1149
1363
|
const localCollections = loadLocalCollections(platformDir, name || void 0);
|
|
1150
1364
|
if (localCollections.length > 0) {
|
|
1151
1365
|
console.log(pc.bold(" Collections:"));
|
|
1152
|
-
await applyCollections(api, localCollections);
|
|
1366
|
+
totalErrors += await applyCollections(api, localCollections);
|
|
1153
1367
|
console.log();
|
|
1154
1368
|
} else if (name) {
|
|
1155
1369
|
console.log(pc.red(` Collection "${name}" not found locally`));
|
|
@@ -1169,7 +1383,7 @@ async function apply(args) {
|
|
|
1169
1383
|
const localAutomations = loadLocalAutomations(platformDir, name || void 0);
|
|
1170
1384
|
if (localAutomations.length > 0) {
|
|
1171
1385
|
console.log(pc.bold(" Automations:"));
|
|
1172
|
-
await applyAutomations(api, localAutomations);
|
|
1386
|
+
totalErrors += await applyAutomations(api, localAutomations);
|
|
1173
1387
|
console.log();
|
|
1174
1388
|
} else if (name) {
|
|
1175
1389
|
console.log(pc.red(` Automation "${name}" not found locally`));
|
|
@@ -1180,7 +1394,7 @@ async function apply(args) {
|
|
|
1180
1394
|
const localHooks = loadLocalHooks(platformDir, name || void 0);
|
|
1181
1395
|
if (localHooks.length > 0) {
|
|
1182
1396
|
console.log(pc.bold(" Hooks:"));
|
|
1183
|
-
await applyHooks(api, localHooks, collections);
|
|
1397
|
+
totalErrors += await applyHooks(api, localHooks, collections);
|
|
1184
1398
|
console.log();
|
|
1185
1399
|
} else if (name) {
|
|
1186
1400
|
console.log(pc.red(` Hook "${name}" not found locally`));
|
|
@@ -1198,6 +1412,11 @@ async function apply(args) {
|
|
|
1198
1412
|
} catch {
|
|
1199
1413
|
}
|
|
1200
1414
|
}
|
|
1415
|
+
if (totalErrors > 0) {
|
|
1416
|
+
console.log(pc.red(` Failed with ${totalErrors} error${totalErrors > 1 ? "s" : ""}.`));
|
|
1417
|
+
console.log();
|
|
1418
|
+
process.exit(1);
|
|
1419
|
+
}
|
|
1201
1420
|
console.log(pc.green(" Done!"));
|
|
1202
1421
|
console.log();
|
|
1203
1422
|
}
|
|
@@ -1242,13 +1461,14 @@ async function destroy(args) {
|
|
|
1242
1461
|
const api = createApiClient();
|
|
1243
1462
|
const { type, name } = parseResource(args[0]);
|
|
1244
1463
|
const skipConfirm = args.includes("--confirm");
|
|
1464
|
+
const forceCycles = args.includes("--force-cycles");
|
|
1245
1465
|
console.log();
|
|
1246
1466
|
console.log(pc.red(pc.bold(" Destroy")));
|
|
1247
1467
|
console.log();
|
|
1248
1468
|
if (type === "app") {
|
|
1249
1469
|
await destroyApp(skipConfirm);
|
|
1250
1470
|
} else {
|
|
1251
|
-
await destroyResources(api, platformDir, type || void 0, name || void 0, skipConfirm);
|
|
1471
|
+
await destroyResources(api, platformDir, type || void 0, name || void 0, skipConfirm, forceCycles);
|
|
1252
1472
|
}
|
|
1253
1473
|
console.log();
|
|
1254
1474
|
}
|
|
@@ -1260,16 +1480,26 @@ async function list(args) {
|
|
|
1260
1480
|
loadEnv();
|
|
1261
1481
|
const platformDir = getPlatformDir();
|
|
1262
1482
|
const api = createApiClient();
|
|
1263
|
-
const
|
|
1483
|
+
const showAll = args.includes("--all");
|
|
1484
|
+
const positionalArgs = args.filter((a) => !a.startsWith("--"));
|
|
1485
|
+
const filterType = positionalArgs[0];
|
|
1264
1486
|
console.log();
|
|
1265
1487
|
console.log(pc.cyan(pc.bold(" Resources")));
|
|
1266
1488
|
console.log();
|
|
1267
|
-
const
|
|
1268
|
-
|
|
1489
|
+
const allResources = await listResources(api, platformDir, filterType);
|
|
1490
|
+
const remoteOnlyCount = allResources.filter((r) => r.status === "remote-only").length;
|
|
1491
|
+
const resources = showAll ? allResources : allResources.filter((r) => r.status !== "remote-only");
|
|
1492
|
+
if (resources.length === 0 && remoteOnlyCount === 0) {
|
|
1269
1493
|
console.log(pc.dim(" No resources found"));
|
|
1270
1494
|
console.log();
|
|
1271
1495
|
return;
|
|
1272
1496
|
}
|
|
1497
|
+
if (resources.length === 0 && remoteOnlyCount > 0) {
|
|
1498
|
+
console.log(pc.dim(" No local resources found"));
|
|
1499
|
+
console.log(pc.dim(` ${remoteOnlyCount} remote-only resource(s) hidden. Use --all to show.`));
|
|
1500
|
+
console.log();
|
|
1501
|
+
return;
|
|
1502
|
+
}
|
|
1273
1503
|
const byType = /* @__PURE__ */ new Map();
|
|
1274
1504
|
for (const r of resources) {
|
|
1275
1505
|
if (!byType.has(r.type)) byType.set(r.type, []);
|
|
@@ -1306,13 +1536,17 @@ async function list(args) {
|
|
|
1306
1536
|
const synced = resources.filter((r) => r.status === "synced").length;
|
|
1307
1537
|
const changed = resources.filter((r) => r.status === "changed").length;
|
|
1308
1538
|
const localOnly = resources.filter((r) => r.status === "local-only").length;
|
|
1309
|
-
const
|
|
1539
|
+
const displayedRemoteOnly = resources.filter((r) => r.status === "remote-only").length;
|
|
1310
1540
|
const summary = [];
|
|
1311
1541
|
if (synced > 0) summary.push(pc.green(`${synced} synced`));
|
|
1312
1542
|
if (changed > 0) summary.push(pc.yellow(`${changed} changed`));
|
|
1313
1543
|
if (localOnly > 0) summary.push(pc.cyan(`${localOnly} local-only`));
|
|
1314
|
-
if (
|
|
1544
|
+
if (displayedRemoteOnly > 0) summary.push(pc.dim(`${displayedRemoteOnly} remote-only`));
|
|
1315
1545
|
console.log(` ${summary.join(" | ")}`);
|
|
1546
|
+
console.log(pc.dim(` ${pc.green("\u2713")} synced ${pc.yellow("~")} changed ${pc.cyan("+")} local-only ? remote-only`));
|
|
1547
|
+
if (!showAll && remoteOnlyCount > 0) {
|
|
1548
|
+
console.log(pc.dim(` ${remoteOnlyCount} remote-only resource(s) hidden. Use --all to show.`));
|
|
1549
|
+
}
|
|
1316
1550
|
console.log();
|
|
1317
1551
|
}
|
|
1318
1552
|
async function show(args) {
|
|
@@ -62,7 +62,28 @@ ${pc.dim("Description:")}
|
|
|
62
62
|
const appName = getAppName(projectRoot);
|
|
63
63
|
const appTitle = getAppTitle(projectRoot);
|
|
64
64
|
const tokenSource = getTokenSource(projectRoot);
|
|
65
|
+
let userEmail;
|
|
66
|
+
let companyName;
|
|
67
|
+
let tokenValid = false;
|
|
68
|
+
let tokenError;
|
|
69
|
+
let token;
|
|
70
|
+
if (tokenSource) {
|
|
71
|
+
try {
|
|
72
|
+
token = getToken(projectRoot);
|
|
73
|
+
const validation = await validateToken(token);
|
|
74
|
+
tokenValid = validation.valid;
|
|
75
|
+
userEmail = validation.user;
|
|
76
|
+
companyName = validation.company;
|
|
77
|
+
tokenError = validation.error;
|
|
78
|
+
} catch {
|
|
79
|
+
}
|
|
80
|
+
}
|
|
65
81
|
console.log();
|
|
82
|
+
if (userEmail || companyName) {
|
|
83
|
+
const identity = [userEmail, companyName].filter(Boolean).join(" @ ");
|
|
84
|
+
console.log(pc.bold(` ${identity}`));
|
|
85
|
+
console.log();
|
|
86
|
+
}
|
|
66
87
|
console.log(pc.bold(`Project: ${appTitle}`));
|
|
67
88
|
console.log(pc.dim(` Name: ${appName}`));
|
|
68
89
|
console.log(pc.dim(` Version: ${pkg.version || "0.0.0"}`));
|
|
@@ -91,23 +112,17 @@ ${pc.dim("Description:")}
|
|
|
91
112
|
const baseUrl = getBaseUrl();
|
|
92
113
|
console.log(pc.dim(` API: ${baseUrl}`));
|
|
93
114
|
if (tokenSource) {
|
|
94
|
-
|
|
95
|
-
try {
|
|
96
|
-
token = getToken(projectRoot);
|
|
97
|
-
} catch {
|
|
115
|
+
if (!token) {
|
|
98
116
|
console.log(pc.yellow(` \u26A0 Token source found but could not read token`));
|
|
99
117
|
console.log(pc.dim(` Run \`lumera login\` to re-authenticate`));
|
|
100
118
|
console.log();
|
|
101
119
|
return;
|
|
102
120
|
}
|
|
103
|
-
|
|
104
|
-
if (validation.valid) {
|
|
121
|
+
if (tokenValid) {
|
|
105
122
|
console.log(pc.green(` \u2713 Authenticated (${tokenSource})`));
|
|
106
|
-
if (validation.user) console.log(pc.dim(` User: ${validation.user}`));
|
|
107
|
-
if (validation.company) console.log(pc.dim(` Company: ${validation.company}`));
|
|
108
123
|
} else {
|
|
109
124
|
console.log(pc.red(` \u2717 Token invalid (${tokenSource})`));
|
|
110
|
-
console.log(pc.dim(` Error: ${
|
|
125
|
+
console.log(pc.dim(` Error: ${tokenError}`));
|
|
111
126
|
console.log(pc.dim(` Run \`lumera login\` to re-authenticate`));
|
|
112
127
|
}
|
|
113
128
|
} else {
|
package/package.json
CHANGED