@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 +57 -0
- package/dist/index.js +7 -7
- package/dist/{resources-64MEBAL5.js → resources-E4XMWX7N.js} +306 -42
- 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:")}
|
|
@@ -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
|
-
|
|
256
|
-
|
|
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
|
|
341
|
-
if (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 (!
|
|
519
|
+
if (!rawConfig.external_id) {
|
|
345
520
|
if (appName) {
|
|
346
|
-
|
|
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 (!
|
|
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
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
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
|
-
|
|
512
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
|
680
|
-
|
|
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} (
|
|
933
|
+
console.log(pc.green(" \u2713"), `${local.name} (schema)`);
|
|
685
934
|
} catch (e) {
|
|
686
|
-
console.log(pc.red(" \u2717"), `${local.name} (
|
|
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 =
|
|
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
|
|
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
|
|
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();
|