@lumerahq/cli 0.7.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.
Files changed (37) hide show
  1. package/README.md +118 -0
  2. package/dist/auth-7RGL7GXU.js +311 -0
  3. package/dist/chunk-2CR762KB.js +18 -0
  4. package/dist/chunk-AVKPM7C4.js +199 -0
  5. package/dist/chunk-D2BLSEGR.js +59 -0
  6. package/dist/chunk-NDLYGKS6.js +77 -0
  7. package/dist/chunk-V2XXMMEI.js +147 -0
  8. package/dist/dev-UTZC4ZJ7.js +87 -0
  9. package/dist/index.js +157 -0
  10. package/dist/init-OQCIET53.js +363 -0
  11. package/dist/migrate-2DZ6RQ5K.js +190 -0
  12. package/dist/resources-PNK3NESI.js +1350 -0
  13. package/dist/run-4NDI2CN4.js +257 -0
  14. package/dist/skills-56EUKHGY.js +414 -0
  15. package/dist/status-BEVUV6RY.js +131 -0
  16. package/package.json +37 -0
  17. package/templates/default/CLAUDE.md +245 -0
  18. package/templates/default/README.md +59 -0
  19. package/templates/default/biome.json +33 -0
  20. package/templates/default/index.html +13 -0
  21. package/templates/default/package.json.hbs +46 -0
  22. package/templates/default/platform/automations/.gitkeep +0 -0
  23. package/templates/default/platform/collections/example_items.json +28 -0
  24. package/templates/default/platform/hooks/.gitkeep +0 -0
  25. package/templates/default/pyproject.toml.hbs +14 -0
  26. package/templates/default/scripts/seed-demo.py +35 -0
  27. package/templates/default/src/components/Sidebar.tsx +84 -0
  28. package/templates/default/src/components/StatCard.tsx +31 -0
  29. package/templates/default/src/components/layout.tsx +13 -0
  30. package/templates/default/src/lib/queries.ts +27 -0
  31. package/templates/default/src/main.tsx +137 -0
  32. package/templates/default/src/routes/__root.tsx +10 -0
  33. package/templates/default/src/routes/index.tsx +90 -0
  34. package/templates/default/src/routes/settings.tsx +25 -0
  35. package/templates/default/src/styles.css +40 -0
  36. package/templates/default/tsconfig.json +23 -0
  37. package/templates/default/vite.config.ts +27 -0
@@ -0,0 +1,1350 @@
1
+ import {
2
+ deploy
3
+ } from "./chunk-AVKPM7C4.js";
4
+ import {
5
+ createApiClient
6
+ } from "./chunk-V2XXMMEI.js";
7
+ import {
8
+ loadEnv
9
+ } from "./chunk-2CR762KB.js";
10
+ import {
11
+ getToken
12
+ } from "./chunk-NDLYGKS6.js";
13
+ import {
14
+ findProjectRoot,
15
+ getApiUrl,
16
+ getAppName,
17
+ getAppTitle
18
+ } from "./chunk-D2BLSEGR.js";
19
+
20
+ // src/commands/resources.ts
21
+ import pc from "picocolors";
22
+ import prompts from "prompts";
23
+ import { existsSync, readdirSync, readFileSync, writeFileSync, mkdirSync } from "fs";
24
+ import { join, resolve } from "path";
25
+ function showPlanHelp() {
26
+ console.log(`
27
+ ${pc.dim("Usage:")}
28
+ lumera plan [resource]
29
+
30
+ ${pc.dim("Description:")}
31
+ Preview changes between local files and remote state.
32
+
33
+ ${pc.dim("Resources:")}
34
+ (none) Plan all resources
35
+ collections Plan only collections
36
+ collections/<name> Plan single collection
37
+ automations Plan only automations
38
+ automations/<name> Plan single automation
39
+ hooks Plan only hooks
40
+ app Plan app deployment
41
+
42
+ ${pc.dim("Examples:")}
43
+ lumera plan # Plan all resources
44
+ lumera plan collections # Plan only collections
45
+ lumera plan automations/sync # Plan single automation
46
+ `);
47
+ }
48
+ function showApplyHelp() {
49
+ console.log(`
50
+ ${pc.dim("Usage:")}
51
+ lumera apply [resource]
52
+
53
+ ${pc.dim("Description:")}
54
+ Create or update resources from local files.
55
+
56
+ ${pc.dim("Resources:")}
57
+ (none) Apply all resources
58
+ collections Apply only collections
59
+ collections/<name> Apply single collection
60
+ automations Apply only automations
61
+ automations/<name> Apply single automation
62
+ hooks Apply only hooks
63
+ app Deploy the frontend app
64
+
65
+ ${pc.dim("Options:")}
66
+ --skip-build Skip build step when applying app
67
+
68
+ ${pc.dim("Examples:")}
69
+ lumera apply # Apply everything
70
+ lumera apply collections # Apply all collections
71
+ lumera apply collections/users # Apply single collection
72
+ lumera apply app # Deploy frontend
73
+ lumera apply app --skip-build # Deploy without rebuilding
74
+ `);
75
+ }
76
+ function showPullHelp() {
77
+ console.log(`
78
+ ${pc.dim("Usage:")}
79
+ lumera pull [resource]
80
+
81
+ ${pc.dim("Description:")}
82
+ Download remote state to local files.
83
+
84
+ ${pc.dim("Resources:")}
85
+ (none) Pull all resources
86
+ collections Pull only collections
87
+ collections/<name> Pull single collection
88
+ automations Pull only automations
89
+ automations/<name> Pull single automation
90
+ hooks Pull only hooks
91
+
92
+ ${pc.dim("Examples:")}
93
+ lumera pull # Pull all resources
94
+ lumera pull collections # Pull only collections
95
+ lumera pull automations/sync # Pull single automation
96
+ `);
97
+ }
98
+ function showDestroyHelp() {
99
+ console.log(`
100
+ ${pc.dim("Usage:")}
101
+ lumera destroy [resource]
102
+
103
+ ${pc.dim("Description:")}
104
+ Delete resources from remote.
105
+
106
+ ${pc.dim("Resources:")}
107
+ (none) Destroy all resources
108
+ collections Destroy only collections
109
+ collections/<name> Destroy single collection
110
+ automations Destroy only automations
111
+ automations/<name> Destroy single automation
112
+ hooks Destroy only hooks
113
+ app Delete app registration
114
+
115
+ ${pc.dim("Options:")}
116
+ --confirm Skip confirmation prompt
117
+
118
+ ${pc.dim("Examples:")}
119
+ lumera destroy # Destroy everything
120
+ lumera destroy collections/users # Destroy single collection
121
+ lumera destroy app # Delete app registration
122
+ `);
123
+ }
124
+ function showListHelp() {
125
+ console.log(`
126
+ ${pc.dim("Usage:")}
127
+ lumera list [type]
128
+
129
+ ${pc.dim("Description:")}
130
+ List resources with status (synced, changed, local-only, remote-only).
131
+
132
+ ${pc.dim("Types:")}
133
+ (none) List all resources
134
+ collections List only collections
135
+ automations List only automations
136
+ hooks List only hooks
137
+
138
+ ${pc.dim("Examples:")}
139
+ lumera list # List all resources
140
+ lumera list collections # List only collections
141
+ `);
142
+ }
143
+ function showShowHelp() {
144
+ console.log(`
145
+ ${pc.dim("Usage:")}
146
+ lumera show <resource>
147
+
148
+ ${pc.dim("Description:")}
149
+ Show details of a single resource.
150
+
151
+ ${pc.dim("Resources:")}
152
+ collections/<name> Show collection details
153
+ automations/<name> Show automation details
154
+ hooks/<name> Show hook details
155
+ app Show app details
156
+
157
+ ${pc.dim("Examples:")}
158
+ lumera show collections/users # Show collection details
159
+ lumera show automations/sync # Show automation details
160
+ lumera show app # Show app details
161
+ `);
162
+ }
163
+ function parseResource(resourcePath) {
164
+ if (!resourcePath) {
165
+ return { type: null, name: null };
166
+ }
167
+ const parts = resourcePath.split("/");
168
+ const type = parts[0];
169
+ const name = parts.slice(1).join("/") || null;
170
+ if (!["collections", "automations", "hooks", "app"].includes(type)) {
171
+ return { type: null, name: null };
172
+ }
173
+ return { type, name };
174
+ }
175
+ function getPlatformDir() {
176
+ if (existsSync(join(process.cwd(), "platform"))) {
177
+ return join(process.cwd(), "platform");
178
+ }
179
+ if (existsSync(join(process.cwd(), "lumera_platform"))) {
180
+ return join(process.cwd(), "lumera_platform");
181
+ }
182
+ return join(process.cwd(), "platform");
183
+ }
184
+ function toSafeFilename(name) {
185
+ return name.replace(/\s+/g, "_").replace(/[^a-zA-Z0-9_-]/g, "").toLowerCase();
186
+ }
187
+ function loadLocalCollections(platformDir, filterName) {
188
+ const collectionsDir = join(platformDir, "collections");
189
+ if (!existsSync(collectionsDir)) {
190
+ return [];
191
+ }
192
+ const collections = [];
193
+ const errors = [];
194
+ for (const file of readdirSync(collectionsDir)) {
195
+ if (!file.endsWith(".json")) continue;
196
+ const filePath = join(collectionsDir, file);
197
+ try {
198
+ const content = readFileSync(filePath, "utf-8");
199
+ const collection = JSON.parse(content);
200
+ if (filterName && collection.name !== filterName && collection.id !== filterName) {
201
+ continue;
202
+ }
203
+ if (!collection.id) {
204
+ errors.push(`${file}: missing id field`);
205
+ continue;
206
+ }
207
+ if (!collection.name) {
208
+ errors.push(`${file}: missing name field`);
209
+ continue;
210
+ }
211
+ if (/\s/.test(collection.name)) {
212
+ errors.push(`${file}: collection name "${collection.name}" contains spaces - use underscores instead`);
213
+ continue;
214
+ }
215
+ if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(collection.name)) {
216
+ errors.push(`${file}: collection name "${collection.name}" contains invalid characters`);
217
+ continue;
218
+ }
219
+ collections.push(collection);
220
+ } catch (e) {
221
+ errors.push(`${file}: failed to parse - ${e}`);
222
+ }
223
+ }
224
+ if (errors.length > 0) {
225
+ console.log(pc.red(" Collection errors:"));
226
+ for (const err of errors) {
227
+ console.log(pc.red(` \u2717 ${err}`));
228
+ }
229
+ throw new Error(`Found ${errors.length} collection error(s)`);
230
+ }
231
+ return collections;
232
+ }
233
+ function loadLocalAutomations(platformDir, filterName) {
234
+ const automationsDir = join(platformDir, "automations");
235
+ if (!existsSync(automationsDir)) {
236
+ return [];
237
+ }
238
+ const automations = [];
239
+ const errors = [];
240
+ for (const entry of readdirSync(automationsDir, { withFileTypes: true })) {
241
+ if (!entry.isDirectory()) continue;
242
+ const automationDir = join(automationsDir, entry.name);
243
+ const configPath = join(automationDir, "config.json");
244
+ const mainPath = join(automationDir, "main.py");
245
+ if (!existsSync(configPath)) {
246
+ errors.push(`${entry.name}: missing config.json`);
247
+ continue;
248
+ }
249
+ if (!existsSync(mainPath)) {
250
+ errors.push(`${entry.name}: missing main.py`);
251
+ continue;
252
+ }
253
+ try {
254
+ const configContent = readFileSync(configPath, "utf-8");
255
+ const config = JSON.parse(configContent);
256
+ if (filterName && config.external_id !== filterName && config.name !== filterName && entry.name !== filterName) {
257
+ continue;
258
+ }
259
+ if (!config.external_id) {
260
+ errors.push(`${entry.name}: missing external_id in config.json`);
261
+ continue;
262
+ }
263
+ if (!config.name) {
264
+ errors.push(`${entry.name}: missing name in config.json`);
265
+ continue;
266
+ }
267
+ if (!config.inputs?.schema) {
268
+ errors.push(`${entry.name}: missing inputs.schema in config.json`);
269
+ continue;
270
+ }
271
+ const code = readFileSync(mainPath, "utf-8");
272
+ automations.push({ automation: config, code });
273
+ } catch (e) {
274
+ errors.push(`${entry.name}: failed to parse config.json - ${e}`);
275
+ }
276
+ }
277
+ if (errors.length > 0) {
278
+ console.log(pc.red(" Automation errors:"));
279
+ for (const err of errors) {
280
+ console.log(pc.red(` \u2717 ${err}`));
281
+ }
282
+ throw new Error(`Found ${errors.length} automation error(s)`);
283
+ }
284
+ return automations;
285
+ }
286
+ function loadLocalHooks(platformDir, filterName) {
287
+ const hooksDir = join(platformDir, "hooks");
288
+ if (!existsSync(hooksDir)) {
289
+ return [];
290
+ }
291
+ const hooks = [];
292
+ for (const file of readdirSync(hooksDir)) {
293
+ if (!file.endsWith(".js") && !file.endsWith(".ts")) continue;
294
+ const filePath = join(hooksDir, file);
295
+ const content = readFileSync(filePath, "utf-8");
296
+ const config = parseHookConfig(content);
297
+ if (!config) {
298
+ console.log(pc.yellow(` \u26A0 Skipping ${file}: could not parse config export`));
299
+ continue;
300
+ }
301
+ if (!config.external_id) {
302
+ console.log(pc.yellow(` \u26A0 Skipping ${file}: missing external_id in config`));
303
+ continue;
304
+ }
305
+ if (filterName && config.external_id !== filterName && file.replace(/\.(js|ts)$/, "") !== filterName) {
306
+ continue;
307
+ }
308
+ const script = extractHookScript(content);
309
+ hooks.push({ hook: config, script, fileName: file });
310
+ }
311
+ return hooks;
312
+ }
313
+ function parseHookConfig(content) {
314
+ const configMatch = content.match(/export\s+const\s+config\s*[=:]\s*(\{[\s\S]*?\});?/);
315
+ if (!configMatch) return null;
316
+ try {
317
+ let configStr = configMatch[1].replace(/'/g, '"').replace(/(\w+):/g, '"$1":').replace(/,\s*}/g, "}");
318
+ return JSON.parse(configStr);
319
+ } catch {
320
+ const externalId = content.match(/external_id:\s*['"]([^'"]+)['"]/)?.[1];
321
+ const collection = content.match(/collection:\s*['"]([^'"]+)['"]/)?.[1];
322
+ const trigger = content.match(/trigger:\s*['"]([^'"]+)['"]/)?.[1];
323
+ const enabled = content.match(/enabled:\s*(true|false)/)?.[1];
324
+ if (!externalId || !collection || !trigger) return null;
325
+ return {
326
+ external_id: externalId,
327
+ collection,
328
+ trigger,
329
+ enabled: enabled !== "false"
330
+ };
331
+ }
332
+ }
333
+ function extractHookScript(content) {
334
+ const handlerMatch = content.match(
335
+ /export\s+default\s+(?:async\s+)?function\s*(?:\w+)?\s*\([^)]*\)\s*\{([\s\S]*)\}[\s\n]*$/
336
+ );
337
+ if (handlerMatch) {
338
+ return handlerMatch[1].trim();
339
+ }
340
+ const simpleMatch = content.match(
341
+ /export\s+default\s+async\s+function[^{]*\{([\s\S]*)\}[\s\n]*$/
342
+ );
343
+ if (simpleMatch) {
344
+ return simpleMatch[1].trim();
345
+ }
346
+ return content.replace(/export\s+const\s+config[\s\S]*?;/, "").trim();
347
+ }
348
+ function convertCollectionToApiFormat(local) {
349
+ const schema = local.fields.map((field) => {
350
+ const apiField = {
351
+ name: field.name,
352
+ type: mapFieldType(field.type),
353
+ required: field.required
354
+ };
355
+ const options = {};
356
+ if (field.type === "select" && field.values) {
357
+ options.values = field.values;
358
+ options.maxSelect = field.multiple ? field.values.length : 1;
359
+ }
360
+ if (field.type === "relation" && field.collection) {
361
+ options.collectionId = field.collection;
362
+ options.maxSelect = field.multiple ? 999 : 1;
363
+ }
364
+ if (field.max !== void 0) options.max = field.max;
365
+ if (field.min !== void 0) options.min = field.min;
366
+ if (Object.keys(options).length > 0) {
367
+ apiField.options = options;
368
+ }
369
+ return apiField;
370
+ });
371
+ const indexes = local.indexes?.map((idx) => {
372
+ const fields = idx.fields.join(", ");
373
+ const unique = idx.unique ? "UNIQUE " : "";
374
+ const indexName = `idx_${local.id}_${idx.fields.join("_")}`;
375
+ return `CREATE ${unique}INDEX ${indexName} ON ${local.name} (${fields})`;
376
+ });
377
+ return { id: local.id, name: local.name, schema, indexes };
378
+ }
379
+ function mapFieldType(type) {
380
+ const typeMap = {
381
+ text: "text",
382
+ number: "number",
383
+ bool: "bool",
384
+ email: "text",
385
+ url: "text",
386
+ date: "date",
387
+ select: "select",
388
+ relation: "relation",
389
+ file: "lumera_file",
390
+ json: "json",
391
+ editor: "editor"
392
+ };
393
+ return typeMap[type] || type;
394
+ }
395
+ async function planCollections(api, localCollections) {
396
+ const changes = [];
397
+ const remoteCollections = await api.listCollections();
398
+ const remoteById = new Map(remoteCollections.map((c) => [c.id, c]));
399
+ for (const local of localCollections) {
400
+ const remote = remoteById.get(local.id);
401
+ if (!remote) {
402
+ changes.push({
403
+ type: "create",
404
+ resource: "collection",
405
+ id: local.id,
406
+ name: local.name,
407
+ details: `${local.fields.length} fields`
408
+ });
409
+ } else {
410
+ const localFieldNames = new Set(local.fields.map((f) => f.name));
411
+ const remoteFieldNames = new Set(remote.schema.map((f) => f.name));
412
+ const added = [...localFieldNames].filter((n) => !remoteFieldNames.has(n));
413
+ const removed = [...remoteFieldNames].filter((n) => !localFieldNames.has(n));
414
+ if (added.length > 0 || removed.length > 0) {
415
+ const details = [];
416
+ if (added.length > 0) details.push(`+${added.length} fields`);
417
+ if (removed.length > 0) details.push(`-${removed.length} fields`);
418
+ changes.push({
419
+ type: "update",
420
+ resource: "collection",
421
+ id: local.id,
422
+ name: local.name,
423
+ details: details.join(", ")
424
+ });
425
+ }
426
+ }
427
+ }
428
+ return changes;
429
+ }
430
+ async function planAutomations(api, localAutomations) {
431
+ const changes = [];
432
+ const remoteAutomations = await api.listAutomations();
433
+ const remoteByExternalId = new Map(remoteAutomations.filter((a) => a.external_id).map((a) => [a.external_id, a]));
434
+ for (const { automation, code } of localAutomations) {
435
+ const remote = remoteByExternalId.get(automation.external_id);
436
+ if (!remote) {
437
+ changes.push({
438
+ type: "create",
439
+ resource: "automation",
440
+ id: automation.external_id,
441
+ name: automation.name
442
+ });
443
+ } else {
444
+ const codeChanged = remote.code !== code;
445
+ const nameChanged = remote.name !== automation.name;
446
+ const descChanged = (remote.description || "") !== (automation.description || "");
447
+ if (codeChanged || nameChanged || descChanged) {
448
+ const details = [];
449
+ if (codeChanged) details.push("code");
450
+ if (nameChanged) details.push("name");
451
+ if (descChanged) details.push("description");
452
+ changes.push({
453
+ type: "update",
454
+ resource: "automation",
455
+ id: automation.external_id,
456
+ name: automation.name,
457
+ details: `changed: ${details.join(", ")}`
458
+ });
459
+ }
460
+ }
461
+ }
462
+ return changes;
463
+ }
464
+ async function planHooks(api, localHooks, collections) {
465
+ const changes = [];
466
+ const remoteHooks = await api.listHooks();
467
+ const remoteByExternalId = new Map(remoteHooks.filter((h) => h.external_id).map((h) => [h.external_id, h]));
468
+ for (const { hook, script, fileName } of localHooks) {
469
+ const remote = remoteByExternalId.get(hook.external_id);
470
+ const collectionId = collections.get(hook.collection);
471
+ if (!collectionId) {
472
+ console.log(pc.yellow(` \u26A0 Skipping ${fileName}: collection '${hook.collection}' not found`));
473
+ continue;
474
+ }
475
+ if (!remote) {
476
+ changes.push({
477
+ type: "create",
478
+ resource: "hook",
479
+ id: hook.external_id,
480
+ name: `${hook.collection}.${hook.trigger}`
481
+ });
482
+ } else {
483
+ const scriptChanged = remote.script.trim() !== script.trim();
484
+ const eventChanged = remote.event !== hook.trigger;
485
+ if (scriptChanged || eventChanged) {
486
+ const details = [];
487
+ if (scriptChanged) details.push("script");
488
+ if (eventChanged) details.push("trigger");
489
+ changes.push({
490
+ type: "update",
491
+ resource: "hook",
492
+ id: hook.external_id,
493
+ name: `${hook.collection}.${hook.trigger}`,
494
+ details: `changed: ${details.join(", ")}`
495
+ });
496
+ }
497
+ }
498
+ }
499
+ return changes;
500
+ }
501
+ async function applyCollections(api, localCollections) {
502
+ for (const local of localCollections) {
503
+ const apiFormat = convertCollectionToApiFormat(local);
504
+ try {
505
+ await api.ensureCollection(local.name, apiFormat);
506
+ console.log(pc.green(" \u2713"), `${local.name}`);
507
+ } catch (e) {
508
+ console.log(pc.red(" \u2717"), `${local.name}: ${e}`);
509
+ }
510
+ }
511
+ }
512
+ async function applyAutomations(api, localAutomations) {
513
+ const remoteAutomations = await api.listAutomations();
514
+ const remoteByExternalId = new Map(remoteAutomations.filter((a) => a.external_id).map((a) => [a.external_id, a]));
515
+ for (const { automation, code } of localAutomations) {
516
+ const remote = remoteByExternalId.get(automation.external_id);
517
+ const payload = {
518
+ external_id: automation.external_id,
519
+ name: automation.name,
520
+ description: automation.description,
521
+ code
522
+ };
523
+ if (automation.inputs?.schema) {
524
+ payload.input_schema = automation.inputs.schema;
525
+ }
526
+ try {
527
+ let automationId;
528
+ if (remote) {
529
+ await api.updateAutomation(remote.id, payload);
530
+ automationId = remote.id;
531
+ console.log(pc.green(" \u2713"), `${automation.name} (updated)`);
532
+ } else {
533
+ const created = await api.createAutomation(payload);
534
+ automationId = created.id;
535
+ console.log(pc.green(" \u2713"), `${automation.name} (created)`);
536
+ }
537
+ if (automation.inputs?.presets) {
538
+ await syncPresets(api, automationId, automation.inputs.presets);
539
+ }
540
+ if (automation.schedule) {
541
+ await setSchedule(api, automationId, automation.schedule, automation.inputs?.presets || {});
542
+ } else if (remote?.schedule) {
543
+ await api.updateAutomation(automationId, { schedule: "", schedule_tz: "" });
544
+ console.log(pc.dim(` Cleared schedule`));
545
+ }
546
+ } catch (e) {
547
+ console.log(pc.red(" \u2717"), `${automation.name}: ${e}`);
548
+ }
549
+ }
550
+ }
551
+ async function syncPresets(api, automationId, localPresets) {
552
+ const remotePresets = await api.listPresets(automationId);
553
+ const remoteByName = new Map(remotePresets.map((p) => [p.name, p]));
554
+ for (const [presetKey, preset] of Object.entries(localPresets)) {
555
+ const presetName = preset.label || presetKey;
556
+ const existing = remoteByName.get(presetName);
557
+ try {
558
+ if (existing) {
559
+ await api.updatePreset(existing.id, { name: presetName, inputs: preset.inputs });
560
+ console.log(pc.dim(` Updated preset: ${presetName}`));
561
+ } else {
562
+ await api.createPreset(automationId, { name: presetName, inputs: preset.inputs });
563
+ console.log(pc.dim(` Created preset: ${presetName}`));
564
+ }
565
+ } catch (e) {
566
+ console.log(pc.yellow(` \u26A0 Failed to sync preset ${presetName}: ${e}`));
567
+ }
568
+ }
569
+ }
570
+ async function setSchedule(api, automationId, schedule, localPresets) {
571
+ const presetName = localPresets[schedule.preset]?.label || schedule.preset;
572
+ const remotePresets = await api.listPresets(automationId);
573
+ const preset = remotePresets.find((p) => p.name === presetName);
574
+ if (!preset) {
575
+ console.log(pc.yellow(` \u26A0 Schedule preset '${schedule.preset}' not found, skipping schedule`));
576
+ return;
577
+ }
578
+ try {
579
+ await api.updateAutomation(automationId, {
580
+ schedule: schedule.cron,
581
+ schedule_tz: schedule.timezone || "UTC",
582
+ schedule_preset_id: preset.id
583
+ });
584
+ console.log(pc.dim(` Set schedule: ${schedule.cron}`));
585
+ } catch (e) {
586
+ console.log(pc.yellow(` \u26A0 Failed to set schedule: ${e}`));
587
+ }
588
+ }
589
+ async function applyHooks(api, localHooks, collections) {
590
+ const remoteHooks = await api.listHooks();
591
+ const remoteByExternalId = new Map(remoteHooks.filter((h) => h.external_id).map((h) => [h.external_id, h]));
592
+ for (const { hook, script, fileName } of localHooks) {
593
+ const remote = remoteByExternalId.get(hook.external_id);
594
+ const collectionId = collections.get(hook.collection);
595
+ if (!collectionId) {
596
+ console.log(pc.yellow(` \u26A0 Skipping ${fileName}: collection '${hook.collection}' not found`));
597
+ continue;
598
+ }
599
+ const payload = {
600
+ external_id: hook.external_id,
601
+ collection_id: collectionId,
602
+ event: hook.trigger,
603
+ name: hook.name || `${hook.collection}.${hook.trigger}`,
604
+ script,
605
+ enabled: hook.enabled !== false,
606
+ metadata: hook.metadata
607
+ };
608
+ try {
609
+ if (remote) {
610
+ await api.updateHook(remote.id, payload);
611
+ console.log(pc.green(" \u2713"), `${payload.name} (updated)`);
612
+ } else {
613
+ await api.createHook(payload);
614
+ console.log(pc.green(" \u2713"), `${payload.name} (created)`);
615
+ }
616
+ } catch (e) {
617
+ console.log(pc.red(" \u2717"), `${payload.name}: ${e}`);
618
+ }
619
+ }
620
+ }
621
+ async function applyApp(args) {
622
+ const skipBuild = args.includes("--skip-build");
623
+ const projectRoot = findProjectRoot();
624
+ loadEnv(projectRoot);
625
+ const token = getToken(projectRoot);
626
+ const appName = getAppName(projectRoot);
627
+ const appTitle = getAppTitle(projectRoot);
628
+ const apiUrl = getApiUrl();
629
+ if (!skipBuild) {
630
+ console.log(pc.dim(" Building..."));
631
+ const { execSync } = await import("child_process");
632
+ try {
633
+ execSync("pnpm build", { cwd: projectRoot, stdio: "inherit" });
634
+ } catch {
635
+ throw new Error("Build failed");
636
+ }
637
+ }
638
+ const distDir = resolve(projectRoot, "dist");
639
+ await deploy({ token, appName, appTitle, distDir, apiUrl });
640
+ }
641
+ async function pullCollections(api, platformDir, filterName) {
642
+ const collectionsDir = join(platformDir, "collections");
643
+ mkdirSync(collectionsDir, { recursive: true });
644
+ const collections = await api.listCollections();
645
+ for (const collection of collections) {
646
+ if (collection.system || collection.managed) continue;
647
+ if (filterName && collection.name !== filterName && collection.id !== filterName) {
648
+ continue;
649
+ }
650
+ const localFormat = {
651
+ id: collection.id,
652
+ name: collection.name,
653
+ fields: collection.schema.map((field) => {
654
+ const localField = {
655
+ name: field.name,
656
+ type: field.type,
657
+ required: field.required
658
+ };
659
+ if (field.options) {
660
+ if (field.options.values) localField.values = field.options.values;
661
+ if (field.options.collectionId) localField.collection = field.options.collectionId;
662
+ if (field.options.maxSelect && field.options.maxSelect > 1) localField.multiple = true;
663
+ }
664
+ return localField;
665
+ }),
666
+ indexes: collection.indexes?.map((idx) => {
667
+ const match = idx.match(/CREATE\s+(UNIQUE\s+)?INDEX\s+\w+\s+ON\s+\w+\s+\(([^)]+)\)/i);
668
+ if (match) {
669
+ return { fields: match[2].split(",").map((f) => f.trim()), unique: !!match[1] };
670
+ }
671
+ return null;
672
+ }).filter((idx) => idx !== null)
673
+ };
674
+ const fileName = toSafeFilename(collection.name);
675
+ const filePath = join(collectionsDir, `${fileName}.json`);
676
+ writeFileSync(filePath, JSON.stringify(localFormat, null, 2) + "\n");
677
+ console.log(pc.green(" \u2713"), `${collection.name} \u2192 collections/${fileName}.json`);
678
+ }
679
+ }
680
+ async function pullAutomations(api, platformDir, filterName) {
681
+ const automationsDir = join(platformDir, "automations");
682
+ mkdirSync(automationsDir, { recursive: true });
683
+ const automations = await api.listAutomations();
684
+ for (const automation of automations) {
685
+ if (!automation.external_id || automation.managed) continue;
686
+ if (filterName && automation.external_id !== filterName && automation.name !== filterName) {
687
+ continue;
688
+ }
689
+ const dirName = automation.external_id.replace(/[^a-zA-Z0-9_-]/g, "_");
690
+ const automationDir = join(automationsDir, dirName);
691
+ mkdirSync(automationDir, { recursive: true });
692
+ const config = {
693
+ external_id: automation.external_id,
694
+ name: automation.name,
695
+ description: automation.description
696
+ };
697
+ if (automation.input_schema) {
698
+ try {
699
+ const schema = typeof automation.input_schema === "string" ? JSON.parse(automation.input_schema) : automation.input_schema;
700
+ config.inputs = { schema };
701
+ } catch {
702
+ }
703
+ }
704
+ try {
705
+ const presets = await api.listPresets(automation.id);
706
+ if (presets.length > 0) {
707
+ if (!config.inputs) config.inputs = {};
708
+ config.inputs.presets = {};
709
+ for (const preset of presets) {
710
+ const presetKey = preset.name.toLowerCase().replace(/\s+/g, "_").replace(/[^a-z0-9_]/g, "");
711
+ config.inputs.presets[presetKey] = { label: preset.name, inputs: preset.inputs };
712
+ if (automation.schedule && automation.schedule_preset_id === preset.id) {
713
+ config.schedule = {
714
+ cron: automation.schedule,
715
+ timezone: automation.schedule_tz || "UTC",
716
+ preset: presetKey
717
+ };
718
+ }
719
+ }
720
+ }
721
+ } catch {
722
+ }
723
+ writeFileSync(join(automationDir, "config.json"), JSON.stringify(config, null, 2) + "\n");
724
+ writeFileSync(join(automationDir, "main.py"), automation.code || "");
725
+ console.log(pc.green(" \u2713"), `${automation.name} \u2192 automations/${dirName}/`);
726
+ }
727
+ }
728
+ async function pullHooks(api, platformDir, filterName) {
729
+ const hooksDir = join(platformDir, "hooks");
730
+ mkdirSync(hooksDir, { recursive: true });
731
+ const hooks = await api.listHooks();
732
+ for (const hook of hooks) {
733
+ if (!hook.external_id) continue;
734
+ if (filterName && hook.external_id !== filterName) {
735
+ continue;
736
+ }
737
+ const fileName = `${hook.external_id.replace(/[^a-zA-Z0-9_-]/g, "_")}.js`;
738
+ const content = `export const config = {
739
+ external_id: '${hook.external_id}',
740
+ collection: '${hook.collection_name}',
741
+ trigger: '${hook.event}',
742
+ enabled: ${hook.enabled},
743
+ };
744
+
745
+ export default async function handler({ record, app, http }) {
746
+ ${hook.script.split("\n").map((line) => " " + line).join("\n")}
747
+ }
748
+ `;
749
+ writeFileSync(join(hooksDir, fileName), content);
750
+ console.log(pc.green(" \u2713"), `${hook.name} \u2192 hooks/${fileName}`);
751
+ }
752
+ }
753
+ async function listResources(api, platformDir, filterType) {
754
+ const results = [];
755
+ if (!filterType || filterType === "collections") {
756
+ const localCollections = loadLocalCollections(platformDir);
757
+ const remoteCollections = await api.listCollections();
758
+ const remoteByName = new Map(remoteCollections.filter((c) => !c.system && !c.managed).map((c) => [c.name, c]));
759
+ const localNames = new Set(localCollections.map((c) => c.name));
760
+ for (const local of localCollections) {
761
+ const remote = remoteByName.get(local.name);
762
+ if (!remote) {
763
+ results.push({ name: local.name, type: "collections", status: "local-only" });
764
+ } else {
765
+ const localFieldNames = new Set(local.fields.map((f) => f.name));
766
+ const remoteFieldNames = new Set(remote.schema.map((f) => f.name));
767
+ const added = [...localFieldNames].filter((n) => !remoteFieldNames.has(n));
768
+ const removed = [...remoteFieldNames].filter((n) => !localFieldNames.has(n));
769
+ if (added.length > 0 || removed.length > 0) {
770
+ const details = [];
771
+ if (added.length > 0) details.push(`+${added.join(", ")}`);
772
+ if (removed.length > 0) details.push(`-${removed.join(", ")}`);
773
+ results.push({ name: local.name, type: "collections", status: "changed", details: details.join(" ") });
774
+ } else {
775
+ results.push({ name: local.name, type: "collections", status: "synced" });
776
+ }
777
+ }
778
+ }
779
+ for (const remote of remoteCollections) {
780
+ if (remote.system || remote.managed) continue;
781
+ if (!localNames.has(remote.name)) {
782
+ results.push({ name: remote.name, type: "collections", status: "remote-only" });
783
+ }
784
+ }
785
+ }
786
+ if (!filterType || filterType === "automations") {
787
+ const localAutomations = loadLocalAutomations(platformDir);
788
+ const remoteAutomations = await api.listAutomations();
789
+ const remoteByExternalId = new Map(remoteAutomations.filter((a) => a.external_id && !a.managed).map((a) => [a.external_id, a]));
790
+ const localIds = new Set(localAutomations.map((a) => a.automation.external_id));
791
+ for (const { automation, code } of localAutomations) {
792
+ const remote = remoteByExternalId.get(automation.external_id);
793
+ if (!remote) {
794
+ results.push({ name: automation.name, type: "automations", status: "local-only" });
795
+ } else {
796
+ const codeChanged = remote.code !== code;
797
+ const nameChanged = remote.name !== automation.name;
798
+ if (codeChanged || nameChanged) {
799
+ results.push({ name: automation.name, type: "automations", status: "changed", details: codeChanged ? "code" : "name" });
800
+ } else {
801
+ results.push({ name: automation.name, type: "automations", status: "synced" });
802
+ }
803
+ }
804
+ }
805
+ for (const remote of remoteAutomations) {
806
+ if (!remote.external_id || remote.managed) continue;
807
+ if (!localIds.has(remote.external_id)) {
808
+ results.push({ name: remote.name, type: "automations", status: "remote-only" });
809
+ }
810
+ }
811
+ }
812
+ if (!filterType || filterType === "hooks") {
813
+ const localHooks = loadLocalHooks(platformDir);
814
+ const remoteHooks = await api.listHooks();
815
+ const remoteByExternalId = new Map(remoteHooks.filter((h) => h.external_id).map((h) => [h.external_id, h]));
816
+ const localIds = new Set(localHooks.map((h) => h.hook.external_id));
817
+ for (const { hook, script } of localHooks) {
818
+ const remote = remoteByExternalId.get(hook.external_id);
819
+ if (!remote) {
820
+ results.push({ name: `${hook.collection}.${hook.trigger}`, type: "hooks", status: "local-only" });
821
+ } else {
822
+ const scriptChanged = remote.script.trim() !== script.trim();
823
+ if (scriptChanged) {
824
+ results.push({ name: `${hook.collection}.${hook.trigger}`, type: "hooks", status: "changed", details: "script" });
825
+ } else {
826
+ results.push({ name: `${hook.collection}.${hook.trigger}`, type: "hooks", status: "synced" });
827
+ }
828
+ }
829
+ }
830
+ for (const remote of remoteHooks) {
831
+ if (!remote.external_id) continue;
832
+ if (!localIds.has(remote.external_id)) {
833
+ results.push({ name: remote.name, type: "hooks", status: "remote-only" });
834
+ }
835
+ }
836
+ }
837
+ return results;
838
+ }
839
+ async function destroyResources(api, platformDir, resourceType, resourceName, skipConfirm) {
840
+ const toDelete = [];
841
+ if (!resourceType || resourceType === "collections") {
842
+ const localCollections = loadLocalCollections(platformDir, resourceName || void 0);
843
+ const remoteCollections = await api.listCollections();
844
+ const remoteByName = new Map(remoteCollections.map((c) => [c.name, c]));
845
+ for (const local of localCollections) {
846
+ const remote = remoteByName.get(local.name);
847
+ if (remote) {
848
+ toDelete.push({ type: "collection", id: local.id, name: local.name, remoteId: remote.id });
849
+ }
850
+ }
851
+ }
852
+ if (!resourceType || resourceType === "automations") {
853
+ try {
854
+ const localAutomations = loadLocalAutomations(platformDir, resourceName || void 0);
855
+ const remoteAutomations = await api.listAutomations();
856
+ const remoteByExternalId = new Map(remoteAutomations.filter((a) => a.external_id).map((a) => [a.external_id, a]));
857
+ for (const { automation } of localAutomations) {
858
+ const remote = remoteByExternalId.get(automation.external_id);
859
+ if (remote) {
860
+ toDelete.push({ type: "automation", id: automation.external_id, name: automation.name, remoteId: remote.id });
861
+ }
862
+ }
863
+ } catch {
864
+ }
865
+ }
866
+ if (!resourceType || resourceType === "hooks") {
867
+ const localHooks = loadLocalHooks(platformDir, resourceName || void 0);
868
+ const remoteHooks = await api.listHooks();
869
+ const remoteByExternalId = new Map(remoteHooks.filter((h) => h.external_id).map((h) => [h.external_id, h]));
870
+ for (const { hook } of localHooks) {
871
+ const remote = remoteByExternalId.get(hook.external_id);
872
+ if (remote) {
873
+ toDelete.push({ type: "hook", id: hook.external_id, name: `${hook.collection}.${hook.trigger}`, remoteId: remote.id });
874
+ }
875
+ }
876
+ }
877
+ if (toDelete.length === 0) {
878
+ console.log(pc.green(" \u2713 No resources found to delete"));
879
+ return;
880
+ }
881
+ console.log(pc.bold(" Resources to delete:"));
882
+ console.log();
883
+ for (const item of toDelete) {
884
+ console.log(pc.red(` - ${item.type}: ${item.name}`));
885
+ }
886
+ console.log();
887
+ if (!skipConfirm) {
888
+ const { confirmed } = await prompts({
889
+ type: "confirm",
890
+ name: "confirmed",
891
+ message: `Delete ${toDelete.length} resource(s)?`,
892
+ initial: false
893
+ });
894
+ if (!confirmed) {
895
+ console.log(pc.dim(" Cancelled"));
896
+ return;
897
+ }
898
+ }
899
+ const hooks = toDelete.filter((r) => r.type === "hook");
900
+ const automations = toDelete.filter((r) => r.type === "automation");
901
+ const collections = toDelete.filter((r) => r.type === "collection");
902
+ for (const resource of hooks) {
903
+ try {
904
+ await api.deleteHook(resource.remoteId);
905
+ console.log(pc.green(" \u2713"), `Deleted hook: ${resource.name}`);
906
+ } catch (e) {
907
+ console.log(pc.red(" \u2717"), `Failed to delete hook ${resource.name}: ${e}`);
908
+ }
909
+ }
910
+ for (const resource of automations) {
911
+ try {
912
+ await api.deleteAutomation(resource.remoteId);
913
+ console.log(pc.green(" \u2713"), `Deleted automation: ${resource.name}`);
914
+ } catch (e) {
915
+ console.log(pc.red(" \u2717"), `Failed to delete automation ${resource.name}: ${e}`);
916
+ }
917
+ }
918
+ for (const resource of collections) {
919
+ try {
920
+ await api.deleteCollection(resource.remoteId);
921
+ console.log(pc.green(" \u2713"), `Deleted collection: ${resource.name}`);
922
+ } catch (e) {
923
+ console.log(pc.red(" \u2717"), `Failed to delete collection ${resource.name}: ${e}`);
924
+ }
925
+ }
926
+ }
927
+ async function destroyApp(skipConfirm) {
928
+ const projectRoot = findProjectRoot();
929
+ loadEnv(projectRoot);
930
+ const token = getToken(projectRoot);
931
+ const appName = getAppName(projectRoot);
932
+ let apiUrl = getApiUrl().replace(/\/+$/, "").replace(/\/api$/, "");
933
+ const filterParam = encodeURIComponent(JSON.stringify({ external_id: appName }));
934
+ const searchRes = await fetch(
935
+ `${apiUrl}/api/pb/collections/lm_custom_apps/records?filter=${filterParam}`,
936
+ { headers: { Authorization: `Bearer ${token}` } }
937
+ );
938
+ if (!searchRes.ok) {
939
+ throw new Error(`Failed to find app: ${await searchRes.text()}`);
940
+ }
941
+ const data = await searchRes.json();
942
+ const appRecord = data.items?.[0];
943
+ if (!appRecord) {
944
+ console.log(pc.yellow(` App "${appName}" not found in Lumera.`));
945
+ return;
946
+ }
947
+ console.log(pc.dim(` App to delete: ${appRecord.name} (${appRecord.external_id})`));
948
+ console.log();
949
+ if (!skipConfirm) {
950
+ const { confirmed } = await prompts({
951
+ type: "confirm",
952
+ name: "confirmed",
953
+ message: `Delete app "${appRecord.name}" from Lumera?`,
954
+ initial: false
955
+ });
956
+ if (!confirmed) {
957
+ console.log(pc.dim(" Cancelled"));
958
+ return;
959
+ }
960
+ }
961
+ const deleteRes = await fetch(
962
+ `${apiUrl}/api/pb/collections/lm_custom_apps/records/${appRecord.id}`,
963
+ { method: "DELETE", headers: { Authorization: `Bearer ${token}` } }
964
+ );
965
+ if (!deleteRes.ok) {
966
+ throw new Error(`Failed to delete app: ${await deleteRes.text()}`);
967
+ }
968
+ console.log(pc.green(" \u2713"), `App "${appRecord.name}" deleted from Lumera.`);
969
+ }
970
+ async function showResource(api, platformDir, resourceType, resourceName) {
971
+ if (resourceType === "collections") {
972
+ const localCollections = loadLocalCollections(platformDir, resourceName);
973
+ const remoteCollections = await api.listCollections();
974
+ const local = localCollections[0];
975
+ const remote = remoteCollections.find((c) => c.name === resourceName || c.id === resourceName);
976
+ if (!local && !remote) {
977
+ console.log(pc.red(` Collection "${resourceName}" not found`));
978
+ process.exit(1);
979
+ }
980
+ console.log();
981
+ console.log(pc.bold(` Collection: ${resourceName}`));
982
+ console.log();
983
+ if (local && remote) {
984
+ console.log(` Status: ${pc.green("synced")}`);
985
+ } else if (local) {
986
+ console.log(` Status: ${pc.yellow("local only")}`);
987
+ } else {
988
+ console.log(` Status: ${pc.cyan("remote only")}`);
989
+ }
990
+ console.log();
991
+ const fields = local?.fields || remote?.schema || [];
992
+ console.log(pc.bold(" Fields:"));
993
+ for (const field of fields) {
994
+ const req = field.required ? pc.red("*") : "";
995
+ console.log(` ${field.name}${req} ${pc.dim(`(${field.type})`)}`);
996
+ }
997
+ console.log();
998
+ } else if (resourceType === "automations") {
999
+ const localAutomations = loadLocalAutomations(platformDir, resourceName);
1000
+ const remoteAutomations = await api.listAutomations();
1001
+ const local = localAutomations[0];
1002
+ const remote = remoteAutomations.find((a) => a.external_id === resourceName || a.name === resourceName);
1003
+ if (!local && !remote) {
1004
+ console.log(pc.red(` Automation "${resourceName}" not found`));
1005
+ process.exit(1);
1006
+ }
1007
+ console.log();
1008
+ console.log(pc.bold(` Automation: ${local?.automation.name || remote?.name}`));
1009
+ console.log();
1010
+ if (local && remote) {
1011
+ console.log(` Status: ${pc.green("synced")}`);
1012
+ } else if (local) {
1013
+ console.log(` Status: ${pc.yellow("local only")}`);
1014
+ } else {
1015
+ console.log(` Status: ${pc.cyan("remote only")}`);
1016
+ }
1017
+ if (local?.automation.description || remote?.description) {
1018
+ console.log(` Description: ${local?.automation.description || remote?.description}`);
1019
+ }
1020
+ console.log();
1021
+ } else if (resourceType === "hooks") {
1022
+ const localHooks = loadLocalHooks(platformDir, resourceName);
1023
+ const remoteHooks = await api.listHooks();
1024
+ const local = localHooks[0];
1025
+ const remote = remoteHooks.find((h) => h.external_id === resourceName);
1026
+ if (!local && !remote) {
1027
+ console.log(pc.red(` Hook "${resourceName}" not found`));
1028
+ process.exit(1);
1029
+ }
1030
+ console.log();
1031
+ console.log(pc.bold(` Hook: ${local?.hook.external_id || remote?.external_id}`));
1032
+ console.log();
1033
+ console.log(` Collection: ${local?.hook.collection || remote?.collection_name}`);
1034
+ console.log(` Trigger: ${local?.hook.trigger || remote?.event}`);
1035
+ console.log(` Enabled: ${local?.hook.enabled !== false || remote?.enabled}`);
1036
+ console.log();
1037
+ } else if (resourceType === "app") {
1038
+ const projectRoot = findProjectRoot();
1039
+ loadEnv(projectRoot);
1040
+ const token = getToken(projectRoot);
1041
+ const appName = getAppName(projectRoot);
1042
+ const appTitle = getAppTitle(projectRoot);
1043
+ let apiUrl = getApiUrl().replace(/\/+$/, "").replace(/\/api$/, "");
1044
+ console.log();
1045
+ console.log(pc.bold(` App: ${appTitle || appName}`));
1046
+ console.log();
1047
+ console.log(` External ID: ${appName}`);
1048
+ const filterParam = encodeURIComponent(JSON.stringify({ external_id: appName }));
1049
+ const searchRes = await fetch(
1050
+ `${apiUrl}/api/pb/collections/lm_custom_apps/records?filter=${filterParam}`,
1051
+ { headers: { Authorization: `Bearer ${token}` } }
1052
+ );
1053
+ if (searchRes.ok) {
1054
+ const data = await searchRes.json();
1055
+ const appRecord = data.items?.[0];
1056
+ if (appRecord) {
1057
+ console.log(` Status: ${pc.green("deployed")}`);
1058
+ if (appRecord.hosting_type) console.log(` Hosting: ${appRecord.hosting_type}`);
1059
+ if (appRecord.current_version) console.log(` Version: ${appRecord.current_version}`);
1060
+ if (appRecord.deployed_at) console.log(` Deployed: ${appRecord.deployed_at}`);
1061
+ } else {
1062
+ console.log(` Status: ${pc.yellow("not deployed")}`);
1063
+ }
1064
+ }
1065
+ console.log();
1066
+ }
1067
+ }
1068
+ async function plan(args) {
1069
+ if (args.includes("--help") || args.includes("-h")) {
1070
+ showPlanHelp();
1071
+ return;
1072
+ }
1073
+ loadEnv();
1074
+ const platformDir = getPlatformDir();
1075
+ const api = createApiClient();
1076
+ const { type, name } = parseResource(args[0]);
1077
+ console.log();
1078
+ console.log(pc.cyan(pc.bold(" Plan")));
1079
+ console.log(pc.dim(" Comparing local files to remote state..."));
1080
+ console.log();
1081
+ const allChanges = [];
1082
+ let collections;
1083
+ try {
1084
+ const remoteCollections = await api.listCollections();
1085
+ collections = new Map(remoteCollections.map((c) => [c.name, c.id]));
1086
+ } catch {
1087
+ collections = /* @__PURE__ */ new Map();
1088
+ }
1089
+ if (!type || type === "collections") {
1090
+ const localCollections = loadLocalCollections(platformDir, name || void 0);
1091
+ if (localCollections.length > 0) {
1092
+ const changes = await planCollections(api, localCollections);
1093
+ allChanges.push(...changes);
1094
+ }
1095
+ }
1096
+ if (!type || type === "automations") {
1097
+ const localAutomations = loadLocalAutomations(platformDir, name || void 0);
1098
+ if (localAutomations.length > 0) {
1099
+ const changes = await planAutomations(api, localAutomations);
1100
+ allChanges.push(...changes);
1101
+ }
1102
+ }
1103
+ if (!type || type === "hooks") {
1104
+ const localHooks = loadLocalHooks(platformDir, name || void 0);
1105
+ if (localHooks.length > 0) {
1106
+ const changes = await planHooks(api, localHooks, collections);
1107
+ allChanges.push(...changes);
1108
+ }
1109
+ }
1110
+ if (allChanges.length === 0) {
1111
+ console.log(pc.green(" \u2713 No changes detected"));
1112
+ console.log();
1113
+ return;
1114
+ }
1115
+ console.log(pc.bold(" Changes:"));
1116
+ console.log();
1117
+ for (const change of allChanges) {
1118
+ const icon = change.type === "create" ? "+" : change.type === "update" ? "~" : "-";
1119
+ const color = change.type === "create" ? pc.green : change.type === "update" ? pc.yellow : pc.red;
1120
+ const details = change.details ? ` (${change.details})` : "";
1121
+ console.log(` ${color(icon)} ${change.resource}: ${change.name}${pc.dim(details)}`);
1122
+ }
1123
+ console.log();
1124
+ console.log(pc.dim(` Run 'lumera apply' to apply these changes.`));
1125
+ console.log();
1126
+ }
1127
+ async function apply(args) {
1128
+ if (args.includes("--help") || args.includes("-h")) {
1129
+ showApplyHelp();
1130
+ return;
1131
+ }
1132
+ loadEnv();
1133
+ const platformDir = getPlatformDir();
1134
+ const api = createApiClient();
1135
+ const { type, name } = parseResource(args[0]);
1136
+ console.log();
1137
+ console.log(pc.cyan(pc.bold(" Apply")));
1138
+ console.log();
1139
+ if (type === "app") {
1140
+ console.log(pc.bold(" App:"));
1141
+ await applyApp(args);
1142
+ console.log();
1143
+ console.log(pc.green(" Done!"));
1144
+ console.log();
1145
+ return;
1146
+ }
1147
+ let collections;
1148
+ if (!type || type === "collections") {
1149
+ const localCollections = loadLocalCollections(platformDir, name || void 0);
1150
+ if (localCollections.length > 0) {
1151
+ console.log(pc.bold(" Collections:"));
1152
+ await applyCollections(api, localCollections);
1153
+ console.log();
1154
+ } else if (name) {
1155
+ console.log(pc.red(` Collection "${name}" not found locally`));
1156
+ process.exit(1);
1157
+ }
1158
+ }
1159
+ try {
1160
+ const remoteCollections = await api.listCollections();
1161
+ collections = new Map(remoteCollections.map((c) => [c.name, c.id]));
1162
+ for (const c of remoteCollections) {
1163
+ collections.set(c.id, c.id);
1164
+ }
1165
+ } catch {
1166
+ collections = /* @__PURE__ */ new Map();
1167
+ }
1168
+ if (!type || type === "automations") {
1169
+ const localAutomations = loadLocalAutomations(platformDir, name || void 0);
1170
+ if (localAutomations.length > 0) {
1171
+ console.log(pc.bold(" Automations:"));
1172
+ await applyAutomations(api, localAutomations);
1173
+ console.log();
1174
+ } else if (name) {
1175
+ console.log(pc.red(` Automation "${name}" not found locally`));
1176
+ process.exit(1);
1177
+ }
1178
+ }
1179
+ if (!type || type === "hooks") {
1180
+ const localHooks = loadLocalHooks(platformDir, name || void 0);
1181
+ if (localHooks.length > 0) {
1182
+ console.log(pc.bold(" Hooks:"));
1183
+ await applyHooks(api, localHooks, collections);
1184
+ console.log();
1185
+ } else if (name) {
1186
+ console.log(pc.red(` Hook "${name}" not found locally`));
1187
+ process.exit(1);
1188
+ }
1189
+ }
1190
+ if (!type) {
1191
+ try {
1192
+ const projectRoot = findProjectRoot();
1193
+ if (existsSync(join(projectRoot, "dist")) || existsSync(join(projectRoot, "src"))) {
1194
+ console.log(pc.bold(" App:"));
1195
+ await applyApp(args);
1196
+ console.log();
1197
+ }
1198
+ } catch {
1199
+ }
1200
+ }
1201
+ console.log(pc.green(" Done!"));
1202
+ console.log();
1203
+ }
1204
+ async function pull(args) {
1205
+ if (args.includes("--help") || args.includes("-h")) {
1206
+ showPullHelp();
1207
+ return;
1208
+ }
1209
+ loadEnv();
1210
+ const platformDir = getPlatformDir();
1211
+ const api = createApiClient();
1212
+ const { type, name } = parseResource(args[0]);
1213
+ console.log();
1214
+ console.log(pc.cyan(pc.bold(" Pull")));
1215
+ console.log(pc.dim(` Downloading remote state to ${platformDir}/...`));
1216
+ console.log();
1217
+ if (!type || type === "collections") {
1218
+ console.log(pc.bold(" Collections:"));
1219
+ await pullCollections(api, platformDir, name || void 0);
1220
+ console.log();
1221
+ }
1222
+ if (!type || type === "automations") {
1223
+ console.log(pc.bold(" Automations:"));
1224
+ await pullAutomations(api, platformDir, name || void 0);
1225
+ console.log();
1226
+ }
1227
+ if (!type || type === "hooks") {
1228
+ console.log(pc.bold(" Hooks:"));
1229
+ await pullHooks(api, platformDir, name || void 0);
1230
+ console.log();
1231
+ }
1232
+ console.log(pc.green(" Done!"));
1233
+ console.log();
1234
+ }
1235
+ async function destroy(args) {
1236
+ if (args.includes("--help") || args.includes("-h")) {
1237
+ showDestroyHelp();
1238
+ return;
1239
+ }
1240
+ loadEnv();
1241
+ const platformDir = getPlatformDir();
1242
+ const api = createApiClient();
1243
+ const { type, name } = parseResource(args[0]);
1244
+ const skipConfirm = args.includes("--confirm");
1245
+ console.log();
1246
+ console.log(pc.red(pc.bold(" Destroy")));
1247
+ console.log();
1248
+ if (type === "app") {
1249
+ await destroyApp(skipConfirm);
1250
+ } else {
1251
+ await destroyResources(api, platformDir, type || void 0, name || void 0, skipConfirm);
1252
+ }
1253
+ console.log();
1254
+ }
1255
+ async function list(args) {
1256
+ if (args.includes("--help") || args.includes("-h")) {
1257
+ showListHelp();
1258
+ return;
1259
+ }
1260
+ loadEnv();
1261
+ const platformDir = getPlatformDir();
1262
+ const api = createApiClient();
1263
+ const filterType = args[0];
1264
+ console.log();
1265
+ console.log(pc.cyan(pc.bold(" Resources")));
1266
+ console.log();
1267
+ const resources = await listResources(api, platformDir, filterType);
1268
+ if (resources.length === 0) {
1269
+ console.log(pc.dim(" No resources found"));
1270
+ console.log();
1271
+ return;
1272
+ }
1273
+ const byType = /* @__PURE__ */ new Map();
1274
+ for (const r of resources) {
1275
+ if (!byType.has(r.type)) byType.set(r.type, []);
1276
+ byType.get(r.type).push(r);
1277
+ }
1278
+ for (const [type, items] of byType) {
1279
+ console.log(pc.bold(` ${type.charAt(0).toUpperCase() + type.slice(1)}:`));
1280
+ for (const item of items) {
1281
+ let icon;
1282
+ let color;
1283
+ switch (item.status) {
1284
+ case "synced":
1285
+ icon = "\u2713";
1286
+ color = pc.green;
1287
+ break;
1288
+ case "changed":
1289
+ icon = "~";
1290
+ color = pc.yellow;
1291
+ break;
1292
+ case "local-only":
1293
+ icon = "+";
1294
+ color = pc.cyan;
1295
+ break;
1296
+ case "remote-only":
1297
+ icon = "?";
1298
+ color = pc.dim;
1299
+ break;
1300
+ }
1301
+ const details = item.details ? pc.dim(` (${item.details})`) : "";
1302
+ console.log(` ${color(icon)} ${item.name}${details}`);
1303
+ }
1304
+ console.log();
1305
+ }
1306
+ const synced = resources.filter((r) => r.status === "synced").length;
1307
+ const changed = resources.filter((r) => r.status === "changed").length;
1308
+ const localOnly = resources.filter((r) => r.status === "local-only").length;
1309
+ const remoteOnly = resources.filter((r) => r.status === "remote-only").length;
1310
+ const summary = [];
1311
+ if (synced > 0) summary.push(pc.green(`${synced} synced`));
1312
+ if (changed > 0) summary.push(pc.yellow(`${changed} changed`));
1313
+ if (localOnly > 0) summary.push(pc.cyan(`${localOnly} local-only`));
1314
+ if (remoteOnly > 0) summary.push(pc.dim(`${remoteOnly} remote-only`));
1315
+ console.log(` ${summary.join(" | ")}`);
1316
+ console.log();
1317
+ }
1318
+ async function show(args) {
1319
+ if (args.includes("--help") || args.includes("-h") || args.length === 0) {
1320
+ showShowHelp();
1321
+ if (args.length === 0) process.exit(1);
1322
+ return;
1323
+ }
1324
+ loadEnv();
1325
+ const platformDir = getPlatformDir();
1326
+ const api = createApiClient();
1327
+ const { type, name } = parseResource(args[0]);
1328
+ if (!type) {
1329
+ console.log(pc.red(` Invalid resource path: ${args[0]}`));
1330
+ console.log(pc.dim(" Use format: <type>/<name> (e.g., collections/users)"));
1331
+ process.exit(1);
1332
+ }
1333
+ if (type === "app") {
1334
+ await showResource(api, platformDir, "app", "");
1335
+ } else if (!name) {
1336
+ console.log(pc.red(` Resource name required`));
1337
+ console.log(pc.dim(" Use format: <type>/<name> (e.g., collections/users)"));
1338
+ process.exit(1);
1339
+ } else {
1340
+ await showResource(api, platformDir, type, name);
1341
+ }
1342
+ }
1343
+ export {
1344
+ apply,
1345
+ destroy,
1346
+ list,
1347
+ plan,
1348
+ pull,
1349
+ show
1350
+ };