@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.
- package/README.md +118 -0
- package/dist/auth-7RGL7GXU.js +311 -0
- package/dist/chunk-2CR762KB.js +18 -0
- package/dist/chunk-AVKPM7C4.js +199 -0
- package/dist/chunk-D2BLSEGR.js +59 -0
- package/dist/chunk-NDLYGKS6.js +77 -0
- package/dist/chunk-V2XXMMEI.js +147 -0
- package/dist/dev-UTZC4ZJ7.js +87 -0
- package/dist/index.js +157 -0
- package/dist/init-OQCIET53.js +363 -0
- package/dist/migrate-2DZ6RQ5K.js +190 -0
- package/dist/resources-PNK3NESI.js +1350 -0
- package/dist/run-4NDI2CN4.js +257 -0
- package/dist/skills-56EUKHGY.js +414 -0
- package/dist/status-BEVUV6RY.js +131 -0
- package/package.json +37 -0
- package/templates/default/CLAUDE.md +245 -0
- package/templates/default/README.md +59 -0
- package/templates/default/biome.json +33 -0
- package/templates/default/index.html +13 -0
- package/templates/default/package.json.hbs +46 -0
- package/templates/default/platform/automations/.gitkeep +0 -0
- package/templates/default/platform/collections/example_items.json +28 -0
- package/templates/default/platform/hooks/.gitkeep +0 -0
- package/templates/default/pyproject.toml.hbs +14 -0
- package/templates/default/scripts/seed-demo.py +35 -0
- package/templates/default/src/components/Sidebar.tsx +84 -0
- package/templates/default/src/components/StatCard.tsx +31 -0
- package/templates/default/src/components/layout.tsx +13 -0
- package/templates/default/src/lib/queries.ts +27 -0
- package/templates/default/src/main.tsx +137 -0
- package/templates/default/src/routes/__root.tsx +10 -0
- package/templates/default/src/routes/index.tsx +90 -0
- package/templates/default/src/routes/settings.tsx +25 -0
- package/templates/default/src/styles.css +40 -0
- package/templates/default/tsconfig.json +23 -0
- 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
|
+
};
|