@lumerahq/cli 0.19.3 → 0.19.4

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.
@@ -2,8 +2,9 @@ import {
2
2
  loadEnv
3
3
  } from "./chunk-2CR762KB.js";
4
4
  import {
5
- createApiClient
6
- } from "./chunk-JKXLKK5I.js";
5
+ createApiClient,
6
+ isApiErrorStatus
7
+ } from "./chunk-UH5X43VV.js";
7
8
  import {
8
9
  findProjectRoot,
9
10
  getAppName
@@ -86,6 +87,10 @@ async function syncResourceShareDeps(projectRoot, api, projectExternalId, opts)
86
87
  try {
87
88
  shares = await listIncomingCollectionShares(api, projectExternalId);
88
89
  } catch (e) {
90
+ if (opts.ignorePermissionDenied && isApiErrorStatus(e, 403)) {
91
+ console.log(pc.yellow(" \u26A0"), "Skipping resource-share sync \u2014 current token cannot list project shares (403).");
92
+ return true;
93
+ }
89
94
  if (!opts.quiet) {
90
95
  console.log(pc.red(" \u2717"), `Failed to list incoming resource shares \u2014 ${e}`);
91
96
  }
@@ -170,7 +175,11 @@ async function syncDeps(projectRoot, options = {}) {
170
175
  const quiet = options.quiet === true;
171
176
  const write = options.write === true;
172
177
  if (flagOn) {
173
- return syncResourceShareDeps(root, api, appName, { write, quiet });
178
+ return syncResourceShareDeps(root, api, appName, {
179
+ write,
180
+ quiet,
181
+ ignorePermissionDenied: options.ignorePermissionDenied === true
182
+ });
174
183
  }
175
184
  if (options.legacyWhenDisabled === false) {
176
185
  return true;
@@ -31,7 +31,7 @@ async function fetchSkillsList() {
31
31
  if (!listRes.ok) {
32
32
  throw new Error(`Failed to fetch skills list: ${listRes.status}`);
33
33
  }
34
- return listRes.json();
34
+ return await listRes.json();
35
35
  }
36
36
  async function fetchSkillContent(slug) {
37
37
  const baseUrl = getBaseUrl();
@@ -25,6 +25,19 @@ while (__pkgDir !== "/") {
25
25
  }
26
26
  var pkg = JSON.parse(readFileSync(resolve(__pkgDir, "package.json"), "utf-8"));
27
27
  var CLI_USER_AGENT = `lumera-cli/${pkg.version}`;
28
+ var ApiError = class extends Error {
29
+ status;
30
+ body;
31
+ constructor(status, body) {
32
+ super(`API request failed: ${status} ${body}`);
33
+ this.name = "ApiError";
34
+ this.status = status;
35
+ this.body = body;
36
+ }
37
+ };
38
+ function isApiErrorStatus(error, status) {
39
+ return error instanceof ApiError && error.status === status;
40
+ }
28
41
  var ApiClient = class {
29
42
  baseUrl;
30
43
  token;
@@ -66,12 +79,12 @@ var ApiClient = class {
66
79
  });
67
80
  if (!response.ok) {
68
81
  const text = await response.text();
69
- throw new Error(`API request failed: ${response.status} ${text}`);
82
+ throw new ApiError(response.status, text);
70
83
  }
71
84
  if (response.status === 204) {
72
85
  return {};
73
86
  }
74
- return response.json();
87
+ return await response.json();
75
88
  }
76
89
  // Collections
77
90
  async getMe() {
@@ -325,5 +338,6 @@ function createApiClient(token, baseUrl, projectExternalId) {
325
338
  }
326
339
 
327
340
  export {
341
+ isApiErrorStatus,
328
342
  createApiClient
329
343
  };
@@ -2,9 +2,9 @@ import {
2
2
  deps,
3
3
  projectResourceDepsEnabled,
4
4
  syncDeps
5
- } from "./chunk-P5HFNAVN.js";
5
+ } from "./chunk-5T22627H.js";
6
6
  import "./chunk-2CR762KB.js";
7
- import "./chunk-JKXLKK5I.js";
7
+ import "./chunk-UH5X43VV.js";
8
8
  import "./chunk-ZH3NVYEQ.js";
9
9
  import "./chunk-FJFIWC7G.js";
10
10
  import "./chunk-PNKVD2UK.js";
@@ -4,13 +4,13 @@ import {
4
4
  import {
5
5
  projectResourceDepsEnabled,
6
6
  syncDeps
7
- } from "./chunk-P5HFNAVN.js";
7
+ } from "./chunk-5T22627H.js";
8
8
  import {
9
9
  loadEnv
10
10
  } from "./chunk-2CR762KB.js";
11
11
  import {
12
12
  createApiClient
13
- } from "./chunk-JKXLKK5I.js";
13
+ } from "./chunk-UH5X43VV.js";
14
14
  import {
15
15
  findProjectRoot,
16
16
  getApiUrl,
@@ -126,7 +126,12 @@ async function dev2(args) {
126
126
  const appTitle = getAppTitle(projectRoot);
127
127
  const apiUrl = getApiUrl();
128
128
  if (await projectResourceDepsEnabled(projectRoot)) {
129
- await syncDeps(projectRoot, { write: true, quiet: true, legacyWhenDisabled: false });
129
+ await syncDeps(projectRoot, {
130
+ write: true,
131
+ quiet: true,
132
+ legacyWhenDisabled: false,
133
+ ignorePermissionDenied: true
134
+ });
130
135
  }
131
136
  if (!flags["skip-setup"]) {
132
137
  const fresh = await isFreshProject(projectRoot);
package/dist/index.js CHANGED
@@ -219,39 +219,39 @@ async function main() {
219
219
  switch (command) {
220
220
  // Resource commands
221
221
  case "plan":
222
- await import("./resources-OP7EECKZ.js").then((m) => m.plan(args.slice(1)));
222
+ await import("./resources-RHF2MDF7.js").then((m) => m.plan(args.slice(1)));
223
223
  break;
224
224
  case "apply":
225
- await import("./resources-OP7EECKZ.js").then((m) => m.apply(args.slice(1)));
225
+ await import("./resources-RHF2MDF7.js").then((m) => m.apply(args.slice(1)));
226
226
  break;
227
227
  case "pull":
228
- await import("./resources-OP7EECKZ.js").then((m) => m.pull(args.slice(1)));
228
+ await import("./resources-RHF2MDF7.js").then((m) => m.pull(args.slice(1)));
229
229
  break;
230
230
  case "destroy":
231
- await import("./resources-OP7EECKZ.js").then((m) => m.destroy(args.slice(1)));
231
+ await import("./resources-RHF2MDF7.js").then((m) => m.destroy(args.slice(1)));
232
232
  break;
233
233
  case "list":
234
- await import("./resources-OP7EECKZ.js").then((m) => m.list(args.slice(1)));
234
+ await import("./resources-RHF2MDF7.js").then((m) => m.list(args.slice(1)));
235
235
  break;
236
236
  case "show":
237
- await import("./resources-OP7EECKZ.js").then((m) => m.show(args.slice(1)));
237
+ await import("./resources-RHF2MDF7.js").then((m) => m.show(args.slice(1)));
238
238
  break;
239
239
  case "diff":
240
- await import("./resources-OP7EECKZ.js").then((m) => m.diff(args.slice(1)));
240
+ await import("./resources-RHF2MDF7.js").then((m) => m.diff(args.slice(1)));
241
241
  break;
242
242
  // Development
243
243
  case "dev":
244
- await import("./dev-R43VQCZD.js").then((m) => m.dev(args.slice(1)));
244
+ await import("./dev-JLSTLIMZ.js").then((m) => m.dev(args.slice(1)));
245
245
  break;
246
246
  case "run":
247
- await import("./run-EJP5WCQU.js").then((m) => m.run(args.slice(1)));
247
+ await import("./run-XJLB4AFD.js").then((m) => m.run(args.slice(1)));
248
248
  break;
249
249
  // Project
250
250
  case "init":
251
- await import("./init-TDIQAOG4.js").then((m) => m.init(args.slice(1)));
251
+ await import("./init-CBZAIERZ.js").then((m) => m.init(args.slice(1)));
252
252
  break;
253
253
  case "register":
254
- await import("./register-JFJADKAJ.js").then((m) => m.register(args.slice(1)));
254
+ await import("./register-N2LF3CCK.js").then((m) => m.register(args.slice(1)));
255
255
  break;
256
256
  case "templates":
257
257
  await import("./templates-LNUOTNLN.js").then((m) => m.templates(subcommand, args.slice(2)));
@@ -264,11 +264,11 @@ async function main() {
264
264
  break;
265
265
  // Skills
266
266
  case "skills":
267
- await import("./skills-TNJHMV4F.js").then((m) => m.skills(subcommand, args.slice(2)));
267
+ await import("./skills-REOKLNEF.js").then((m) => m.skills(subcommand, args.slice(2)));
268
268
  break;
269
269
  // Dependencies
270
270
  case "deps":
271
- await import("./deps-EC3VRNN7.js").then((m) => m.deps(args.slice(1)));
271
+ await import("./deps-GCOH3TXL.js").then((m) => m.deps(args.slice(1)));
272
272
  break;
273
273
  // Auth
274
274
  case "login":
@@ -1,13 +1,13 @@
1
1
  import {
2
2
  installAllSkills,
3
3
  syncClaudeMd
4
- } from "./chunk-AUYOTENF.js";
4
+ } from "./chunk-HU7RYLUF.js";
5
5
  import {
6
6
  spinner
7
7
  } from "./chunk-BHYDYR75.js";
8
8
  import {
9
9
  createApiClient
10
- } from "./chunk-JKXLKK5I.js";
10
+ } from "./chunk-UH5X43VV.js";
11
11
  import {
12
12
  getToken,
13
13
  init_auth,
@@ -287,19 +287,24 @@ async function init(args) {
287
287
  }
288
288
  projectName = response.projectName;
289
289
  }
290
- if (!/^[a-z0-9-]+$/.test(projectName)) {
290
+ if (!projectName) {
291
+ console.log(pc.red(" Error: Project name is required"));
292
+ process.exit(1);
293
+ }
294
+ const finalProjectName = projectName;
295
+ if (!/^[a-z0-9-]+$/.test(finalProjectName)) {
291
296
  console.log(pc.red(" Error: Project name must use lowercase letters, numbers, and hyphens only"));
292
297
  process.exit(1);
293
298
  }
294
299
  if (!directory) {
295
300
  if (nonInteractive) {
296
- directory = projectName;
301
+ directory = finalProjectName;
297
302
  } else {
298
303
  const response = await prompts({
299
304
  type: "text",
300
305
  name: "directory",
301
306
  message: "Where should we create the project?",
302
- initial: projectName
307
+ initial: finalProjectName
303
308
  });
304
309
  if (!response.directory) {
305
310
  console.log(pc.red("Cancelled"));
@@ -308,14 +313,19 @@ async function init(args) {
308
313
  directory = response.directory;
309
314
  }
310
315
  }
311
- const projectTitle = toTitleCase(projectName);
312
- const targetDir = resolve(process.cwd(), directory);
316
+ if (!directory) {
317
+ console.log(pc.red(" Error: Directory is required"));
318
+ process.exit(1);
319
+ }
320
+ const finalDirectory = directory;
321
+ const projectTitle = toTitleCase(finalProjectName);
322
+ const targetDir = resolve(process.cwd(), finalDirectory);
313
323
  if (existsSync(targetDir)) {
314
324
  if (nonInteractive) {
315
325
  if (opts.force) {
316
326
  rmSync(targetDir, { recursive: true });
317
327
  } else {
318
- console.log(pc.red(` Error: Directory ${directory} already exists`));
328
+ console.log(pc.red(` Error: Directory ${finalDirectory} already exists`));
319
329
  console.log(pc.dim(" Use --force (-f) to overwrite"));
320
330
  process.exit(1);
321
331
  }
@@ -323,7 +333,7 @@ async function init(args) {
323
333
  const { overwrite } = await prompts({
324
334
  type: "confirm",
325
335
  name: "overwrite",
326
- message: `Directory ${directory} already exists. Overwrite?`,
336
+ message: `Directory ${finalDirectory} already exists. Overwrite?`,
327
337
  initial: false
328
338
  });
329
339
  if (!overwrite) {
@@ -336,9 +346,9 @@ async function init(args) {
336
346
  mkdirSync(targetDir, { recursive: true });
337
347
  console.log();
338
348
  if (templateName !== "default") {
339
- console.log(pc.dim(` Creating ${projectName} from template ${pc.cyan(templateName)}...`));
349
+ console.log(pc.dim(` Creating ${finalProjectName} from template ${pc.cyan(templateName)}...`));
340
350
  } else {
341
- console.log(pc.dim(` Creating ${projectName}...`));
351
+ console.log(pc.dim(` Creating ${finalProjectName}...`));
342
352
  }
343
353
  console.log();
344
354
  const templatePkgPath = join(templateDir, "package.json");
@@ -346,9 +356,9 @@ async function init(args) {
346
356
  const sourceName = templatePkg.name || "my-lumera-app";
347
357
  const sourceTitle = templatePkg.lumera?.name || toTitleCase(sourceName);
348
358
  const replacements = [
349
- ["{{projectName}}", projectName],
359
+ ["{{projectName}}", finalProjectName],
350
360
  ["{{projectTitle}}", projectTitle],
351
- [sourceName, projectName],
361
+ [sourceName, finalProjectName],
352
362
  [sourceTitle, projectTitle]
353
363
  ];
354
364
  copyDir(templateDir, targetDir, replacements);
@@ -366,7 +376,7 @@ async function init(args) {
366
376
  listFiles(targetDir);
367
377
  if (isGitInstalled()) {
368
378
  const stopGit = spinner("Initializing git repository...");
369
- if (initGitRepo(targetDir, projectName)) {
379
+ if (initGitRepo(targetDir, finalProjectName)) {
370
380
  stopGit(pc.green("\u2713") + pc.dim(" Git repository initialized with initial commit"));
371
381
  } else {
372
382
  stopGit(pc.yellow("\u26A0") + pc.dim(" Failed to initialize git repository"));
@@ -428,7 +438,7 @@ async function init(args) {
428
438
  const api = createApiClient(token);
429
439
  const stopRegister = spinner("Registering project on Lumera...");
430
440
  try {
431
- const project = await api.registerProject(projectName);
441
+ const project = await api.registerProject(finalProjectName);
432
442
  setProjectId(targetDir, project.id);
433
443
  stopRegister(pc.green("\u2713") + pc.dim(` Project registered (${project.id})`));
434
444
  registered = true;
@@ -441,7 +451,7 @@ async function init(args) {
441
451
  console.log();
442
452
  console.log(pc.green(pc.bold(" Done!")), "Next steps:");
443
453
  console.log();
444
- console.log(pc.cyan(` cd ${directory}`));
454
+ console.log(pc.cyan(` cd ${finalDirectory}`));
445
455
  if (!opts.install) {
446
456
  console.log(pc.cyan(" pnpm install"));
447
457
  }
@@ -6,7 +6,7 @@ import {
6
6
  } from "./chunk-BHYDYR75.js";
7
7
  import {
8
8
  createApiClient
9
- } from "./chunk-JKXLKK5I.js";
9
+ } from "./chunk-UH5X43VV.js";
10
10
  import {
11
11
  findProjectRoot,
12
12
  getAppName,
@@ -4,13 +4,13 @@ import {
4
4
  import {
5
5
  projectResourceDepsEnabled,
6
6
  syncDeps
7
- } from "./chunk-P5HFNAVN.js";
7
+ } from "./chunk-5T22627H.js";
8
8
  import {
9
9
  loadEnv
10
10
  } from "./chunk-2CR762KB.js";
11
11
  import {
12
12
  createApiClient
13
- } from "./chunk-JKXLKK5I.js";
13
+ } from "./chunk-UH5X43VV.js";
14
14
  import {
15
15
  findProjectRoot,
16
16
  getApiUrl,
@@ -29,8 +29,95 @@ import "./chunk-PNKVD2UK.js";
29
29
  import pc2 from "picocolors";
30
30
  import prompts from "prompts";
31
31
  import { execFileSync, execSync } from "child_process";
32
- import { existsSync, readdirSync, readFileSync, writeFileSync, mkdirSync } from "fs";
33
- import { join, resolve } from "path";
32
+ import { existsSync as existsSync2, readdirSync as readdirSync2, readFileSync as readFileSync2, writeFileSync, mkdirSync } from "fs";
33
+ import { join as join2, resolve } from "path";
34
+
35
+ // src/lib/lint/index.ts
36
+ import { existsSync, readFileSync, readdirSync } from "fs";
37
+ import { basename, join } from "path";
38
+
39
+ // src/lib/lint/rules/collection-schema.ts
40
+ function isRecord(value) {
41
+ return typeof value === "object" && value !== null && !Array.isArray(value);
42
+ }
43
+ function error(target, message, snippet) {
44
+ return {
45
+ ruleId: "collection-schema",
46
+ target,
47
+ severity: "error",
48
+ message,
49
+ snippet
50
+ };
51
+ }
52
+ var collectionSchemaRule = {
53
+ id: "collection-schema",
54
+ description: "Validates local collection JSON files before planning or applying resources.",
55
+ appliesTo: ["collection"],
56
+ check(target) {
57
+ let parsed;
58
+ try {
59
+ parsed = JSON.parse(target.source);
60
+ } catch (err) {
61
+ const message = err instanceof Error ? err.message : String(err);
62
+ return [error(target, `Invalid JSON: ${message}`)];
63
+ }
64
+ if (!isRecord(parsed)) {
65
+ return [error(target, "Collection file must contain a JSON object.")];
66
+ }
67
+ const issues = [];
68
+ const id = parsed.id;
69
+ const name = parsed.name;
70
+ const fields = parsed.fields;
71
+ const indexes = parsed.indexes;
72
+ if (typeof id !== "string" || id.trim() === "") {
73
+ issues.push(error(target, 'Missing required string field "id".'));
74
+ }
75
+ if (typeof name !== "string" || name.trim() === "") {
76
+ issues.push(error(target, 'Missing required string field "name".'));
77
+ } else {
78
+ if (/\s/.test(name)) {
79
+ issues.push(error(target, `Collection name "${name}" contains spaces; use underscores instead.`));
80
+ }
81
+ if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(name)) {
82
+ issues.push(error(target, `Collection name "${name}" contains invalid characters.`));
83
+ }
84
+ }
85
+ if (!Array.isArray(fields)) {
86
+ issues.push(error(target, 'Missing required array field "fields".'));
87
+ } else {
88
+ for (let i = 0; i < fields.length; i++) {
89
+ const field = fields[i];
90
+ if (!isRecord(field)) {
91
+ issues.push(error(target, `Field at index ${i} must be an object.`));
92
+ continue;
93
+ }
94
+ if (typeof field.name !== "string" || field.name.trim() === "") {
95
+ issues.push(error(target, `Field at index ${i} is missing required string field "name".`));
96
+ }
97
+ if (typeof field.type !== "string" || field.type.trim() === "") {
98
+ issues.push(error(target, `Field ${typeof field.name === "string" ? `"${field.name}"` : `at index ${i}`} is missing required string field "type".`));
99
+ }
100
+ }
101
+ }
102
+ if (indexes !== void 0) {
103
+ if (!Array.isArray(indexes)) {
104
+ issues.push(error(target, 'Optional field "indexes" must be an array when present.'));
105
+ } else {
106
+ for (let i = 0; i < indexes.length; i++) {
107
+ const index = indexes[i];
108
+ if (!isRecord(index)) {
109
+ issues.push(error(target, `Index at index ${i} must be an object.`));
110
+ continue;
111
+ }
112
+ if (!Array.isArray(index.fields) || !index.fields.every((field) => typeof field === "string" && field.trim() !== "")) {
113
+ issues.push(error(target, `Index at index ${i} must include a non-empty string array "fields".`));
114
+ }
115
+ }
116
+ }
117
+ }
118
+ return issues;
119
+ }
120
+ };
34
121
 
35
122
  // src/lib/lint/rules/llm-import.ts
36
123
  var FROM_LUMERA_IMPORT_LLM = /^\s*from\s+lumera\s+import\s+\(?\s*(?:[\w\s,]*?\b)?llm\b/;
@@ -75,25 +162,37 @@ var llmImportRule = {
75
162
  };
76
163
 
77
164
  // src/lib/lint/registry.ts
78
- var ALL_RULES = [llmImportRule];
165
+ var ALL_RULES = [collectionSchemaRule, llmImportRule];
79
166
 
80
167
  // src/lib/lint/format.ts
81
168
  import { relative } from "path";
82
169
  import pc from "picocolors";
83
170
  function printLintWarnings(warnings, projectRoot) {
84
171
  if (warnings.length === 0) return;
172
+ const errors = warnings.filter((w) => w.severity === "error");
173
+ const advisory = warnings.filter((w) => w.severity !== "error");
85
174
  console.log();
86
- console.log(pc.bold(" Warnings:"));
175
+ console.log(pc.bold(errors.length > 0 ? " Lint issues:" : " Warnings:"));
87
176
  for (const w of warnings) {
88
177
  const rel = relative(projectRoot, w.target.filePath);
89
178
  const loc = w.line ? `${rel}:${w.line}` : rel;
90
- console.log(` ${pc.yellow("\u26A0")} ${loc} ${pc.dim(`[${w.ruleId}]`)}`);
179
+ const isError = w.severity === "error";
180
+ const icon = isError ? pc.red("\u2717") : pc.yellow("\u26A0");
181
+ console.log(` ${icon} ${loc} ${pc.dim(`[${w.ruleId}]`)}`);
91
182
  console.log(` ${w.message}`);
92
183
  if (w.snippet) console.log(pc.dim(` > ${w.snippet}`));
93
184
  }
94
185
  console.log();
95
- const n = warnings.length;
96
- console.log(pc.dim(` ${n} warning${n === 1 ? "" : "s"} \u2014 advisory, will not block apply.`));
186
+ if (errors.length > 0) {
187
+ const n = errors.length;
188
+ console.log(pc.red(` ${n} error${n === 1 ? "" : "s"} \u2014 fix these before continuing.`));
189
+ if (advisory.length > 0) {
190
+ console.log(pc.dim(` ${advisory.length} warning${advisory.length === 1 ? "" : "s"} \u2014 advisory.`));
191
+ }
192
+ } else {
193
+ const n = advisory.length;
194
+ console.log(pc.dim(` ${n} warning${n === 1 ? "" : "s"} \u2014 advisory, will not block apply.`));
195
+ }
97
196
  console.log();
98
197
  }
99
198
  function serializeLintWarnings(warnings, projectRoot) {
@@ -101,6 +200,7 @@ function serializeLintWarnings(warnings, projectRoot) {
101
200
  ruleId: w.ruleId,
102
201
  target: { kind: w.target.kind, name: w.target.name, filePath: relative(projectRoot, w.target.filePath) },
103
202
  message: w.message,
203
+ severity: w.severity ?? "warning",
104
204
  line: w.line,
105
205
  snippet: w.snippet
106
206
  }));
@@ -131,6 +231,22 @@ function buildAutomationTargets(localAutomations) {
131
231
  source: a.code
132
232
  }));
133
233
  }
234
+ function buildCollectionTargets(platformDir, _filterName) {
235
+ const collectionsDir = join(platformDir, "collections");
236
+ if (!existsSync(collectionsDir)) return [];
237
+ const targets = [];
238
+ for (const entry of readdirSync(collectionsDir, { withFileTypes: true })) {
239
+ if (!entry.isFile() || !entry.name.endsWith(".json")) continue;
240
+ const filePath = join(collectionsDir, entry.name);
241
+ targets.push({
242
+ kind: "collection",
243
+ name: basename(entry.name, ".json"),
244
+ filePath,
245
+ source: readFileSync(filePath, "utf-8")
246
+ });
247
+ }
248
+ return targets;
249
+ }
134
250
 
135
251
  // src/commands/resources.ts
136
252
  init_auth();
@@ -149,6 +265,13 @@ function safePrintLint(warnings, projectRoot) {
149
265
  if (process.env.LUMERA_DEBUG) console.error("[lint] print failed:", err);
150
266
  }
151
267
  }
268
+ function lintCollectionFiles(projectRoot, platformDir, filterName) {
269
+ const issues = runLint({ projectRoot, targets: buildCollectionTargets(platformDir, filterName) });
270
+ const errors = issues.filter((issue) => issue.severity === "error");
271
+ if (errors.length === 0) return;
272
+ printLintWarnings(issues, projectRoot);
273
+ throw new Error(`Found ${errors.length} collection lint error(s)`);
274
+ }
152
275
  function detectPackageManager() {
153
276
  for (const pm of ["bun", "pnpm", "yarn", "npm"]) {
154
277
  try {
@@ -568,29 +691,29 @@ function parseResource(resourcePath) {
568
691
  return { type, name };
569
692
  }
570
693
  function getPlatformDir() {
571
- if (existsSync(join(process.cwd(), "platform"))) {
572
- return join(process.cwd(), "platform");
694
+ if (existsSync2(join2(process.cwd(), "platform"))) {
695
+ return join2(process.cwd(), "platform");
573
696
  }
574
- if (existsSync(join(process.cwd(), "lumera_platform"))) {
575
- return join(process.cwd(), "lumera_platform");
697
+ if (existsSync2(join2(process.cwd(), "lumera_platform"))) {
698
+ return join2(process.cwd(), "lumera_platform");
576
699
  }
577
- return join(process.cwd(), "platform");
700
+ return join2(process.cwd(), "platform");
578
701
  }
579
702
  function toSafeFilename(name) {
580
703
  return name.replace(/\s+/g, "_").replace(/[^a-zA-Z0-9_-]/g, "").toLowerCase();
581
704
  }
582
705
  function loadLocalCollections(platformDir, filterName) {
583
- const collectionsDir = join(platformDir, "collections");
584
- if (!existsSync(collectionsDir)) {
706
+ const collectionsDir = join2(platformDir, "collections");
707
+ if (!existsSync2(collectionsDir)) {
585
708
  return [];
586
709
  }
587
710
  const collections = [];
588
711
  const errors = [];
589
- for (const file of readdirSync(collectionsDir)) {
712
+ for (const file of readdirSync2(collectionsDir)) {
590
713
  if (!file.endsWith(".json")) continue;
591
- const filePath = join(collectionsDir, file);
714
+ const filePath = join2(collectionsDir, file);
592
715
  try {
593
- const content = readFileSync(filePath, "utf-8");
716
+ const content = readFileSync2(filePath, "utf-8");
594
717
  const collection = JSON.parse(content);
595
718
  if (filterName && collection.name !== filterName && collection.id !== filterName) {
596
719
  continue;
@@ -611,6 +734,27 @@ function loadLocalCollections(platformDir, filterName) {
611
734
  errors.push(`${file}: collection name "${collection.name}" contains invalid characters`);
612
735
  continue;
613
736
  }
737
+ if (!Array.isArray(collection.fields)) {
738
+ errors.push(`${file}: missing fields array`);
739
+ continue;
740
+ }
741
+ for (let i = 0; i < collection.fields.length; i++) {
742
+ const field = collection.fields[i];
743
+ if (!field || typeof field !== "object") {
744
+ errors.push(`${file}: field at index ${i} must be an object`);
745
+ continue;
746
+ }
747
+ if (typeof field.name !== "string" || field.name.trim() === "") {
748
+ errors.push(`${file}: field at index ${i} is missing name`);
749
+ }
750
+ if (typeof field.type !== "string" || field.type.trim() === "") {
751
+ errors.push(`${file}: field ${typeof field.name === "string" ? `"${field.name}"` : `at index ${i}`} is missing type`);
752
+ }
753
+ }
754
+ if (collection.indexes !== void 0 && !Array.isArray(collection.indexes)) {
755
+ errors.push(`${file}: indexes must be an array when present`);
756
+ continue;
757
+ }
614
758
  const existingById = collections.find((c) => c.id === collection.id);
615
759
  if (existingById) {
616
760
  errors.push(`${file}: duplicate collection id "${collection.id}" (also defined in another file)`);
@@ -636,27 +780,27 @@ function loadLocalCollections(platformDir, filterName) {
636
780
  return collections;
637
781
  }
638
782
  function loadLocalAutomations(platformDir, filterName, appName) {
639
- const automationsDir = join(platformDir, "automations");
640
- if (!existsSync(automationsDir)) {
783
+ const automationsDir = join2(platformDir, "automations");
784
+ if (!existsSync2(automationsDir)) {
641
785
  return [];
642
786
  }
643
787
  const automations = [];
644
788
  const errors = [];
645
- for (const entry of readdirSync(automationsDir, { withFileTypes: true })) {
789
+ for (const entry of readdirSync2(automationsDir, { withFileTypes: true })) {
646
790
  if (!entry.isDirectory()) continue;
647
- const automationDir = join(automationsDir, entry.name);
648
- const configPath = join(automationDir, "config.json");
649
- const mainPath = join(automationDir, "main.py");
650
- if (!existsSync(configPath)) {
791
+ const automationDir = join2(automationsDir, entry.name);
792
+ const configPath = join2(automationDir, "config.json");
793
+ const mainPath = join2(automationDir, "main.py");
794
+ if (!existsSync2(configPath)) {
651
795
  errors.push(`${entry.name}: missing config.json`);
652
796
  continue;
653
797
  }
654
- if (!existsSync(mainPath)) {
798
+ if (!existsSync2(mainPath)) {
655
799
  errors.push(`${entry.name}: missing main.py`);
656
800
  continue;
657
801
  }
658
802
  try {
659
- const configContent = readFileSync(configPath, "utf-8");
803
+ const configContent = readFileSync2(configPath, "utf-8");
660
804
  const rawConfig = JSON.parse(configContent);
661
805
  if (filterName && rawConfig.external_id !== filterName && rawConfig.name !== filterName && entry.name !== filterName) {
662
806
  continue;
@@ -683,7 +827,7 @@ function loadLocalAutomations(platformDir, filterName, appName) {
683
827
  errors.push(`${entry.name}: duplicate external_id "${config.external_id}" (also defined in ${existingByExtId.automation.name})`);
684
828
  continue;
685
829
  }
686
- let code = readFileSync(mainPath, "utf-8");
830
+ let code = readFileSync2(mainPath, "utf-8");
687
831
  if (appName) {
688
832
  code = code.replaceAll("{{app}}", appName);
689
833
  }
@@ -702,15 +846,15 @@ function loadLocalAutomations(platformDir, filterName, appName) {
702
846
  return automations;
703
847
  }
704
848
  function loadLocalHooks(platformDir, filterName, appName) {
705
- const hooksDir = join(platformDir, "hooks");
706
- if (!existsSync(hooksDir)) {
849
+ const hooksDir = join2(platformDir, "hooks");
850
+ if (!existsSync2(hooksDir)) {
707
851
  return [];
708
852
  }
709
853
  const hooks = [];
710
- for (const file of readdirSync(hooksDir)) {
854
+ for (const file of readdirSync2(hooksDir)) {
711
855
  if (!file.endsWith(".js") && !file.endsWith(".ts")) continue;
712
- const filePath = join(hooksDir, file);
713
- const content = readFileSync(filePath, "utf-8");
856
+ const filePath = join2(hooksDir, file);
857
+ const content = readFileSync2(filePath, "utf-8");
714
858
  const config = parseHookConfig(content);
715
859
  if (!config) {
716
860
  console.log(pc2.yellow(` \u26A0 Skipping ${file}: could not parse config export`));
@@ -1026,12 +1170,12 @@ async function planAutomations(api, localAutomations) {
1026
1170
  return changes;
1027
1171
  }
1028
1172
  function loadLocalMailboxes(platformDir, filterName) {
1029
- const mailboxesDir = join(platformDir, "mailboxes");
1030
- if (!existsSync(mailboxesDir)) {
1173
+ const mailboxesDir = join2(platformDir, "mailboxes");
1174
+ if (!existsSync2(mailboxesDir)) {
1031
1175
  return [];
1032
1176
  }
1033
1177
  const mailboxes = [];
1034
- for (const file of readdirSync(mailboxesDir)) {
1178
+ for (const file of readdirSync2(mailboxesDir)) {
1035
1179
  if (!file.endsWith(".json")) continue;
1036
1180
  const slug = file.replace(/\.json$/, "").trim();
1037
1181
  if (!slug) {
@@ -1041,10 +1185,10 @@ function loadLocalMailboxes(platformDir, filterName) {
1041
1185
  if (filterName && slug !== filterName) {
1042
1186
  continue;
1043
1187
  }
1044
- const filePath = join(mailboxesDir, file);
1188
+ const filePath = join2(mailboxesDir, file);
1045
1189
  let raw;
1046
1190
  try {
1047
- raw = JSON.parse(readFileSync(filePath, "utf-8"));
1191
+ raw = JSON.parse(readFileSync2(filePath, "utf-8"));
1048
1192
  } catch (e) {
1049
1193
  console.log(pc2.yellow(` \u26A0 Skipping ${file}: invalid JSON (${e.message})`));
1050
1194
  continue;
@@ -1123,8 +1267,8 @@ async function pullMailboxes(api, platformDir, filterName) {
1123
1267
  console.log(pc2.dim(" (no mailboxes)"));
1124
1268
  return;
1125
1269
  }
1126
- const outDir = join(platformDir, "mailboxes");
1127
- if (!existsSync(outDir)) {
1270
+ const outDir = join2(platformDir, "mailboxes");
1271
+ if (!existsSync2(outDir)) {
1128
1272
  mkdirSync(outDir, { recursive: true });
1129
1273
  }
1130
1274
  let count = 0;
@@ -1132,7 +1276,7 @@ async function pullMailboxes(api, platformDir, filterName) {
1132
1276
  if (filterName && mb.slug !== filterName) continue;
1133
1277
  const body = {};
1134
1278
  if (mb.description) body.description = mb.description;
1135
- const file = join(outDir, `${toSafeFilename(mb.slug)}.json`);
1279
+ const file = join2(outDir, `${toSafeFilename(mb.slug)}.json`);
1136
1280
  writeFileSync(file, JSON.stringify(body, null, 2) + "\n");
1137
1281
  console.log(pc2.green(" \u2713"), `pulled mailbox ${mb.slug} ${pc2.dim(`\u2192 ${file}`)}`);
1138
1282
  count++;
@@ -1401,7 +1545,7 @@ async function applyApp(args) {
1401
1545
  await deploy({ token, appName, appTitle, distDir, apiUrl });
1402
1546
  }
1403
1547
  async function pullCollections(api, platformDir, filterName, appName) {
1404
- const collectionsDir = join(platformDir, "collections");
1548
+ const collectionsDir = join2(platformDir, "collections");
1405
1549
  mkdirSync(collectionsDir, { recursive: true });
1406
1550
  const collections = await api.listCollections();
1407
1551
  for (const collection of collections) {
@@ -1445,13 +1589,13 @@ async function pullCollections(api, platformDir, filterName, appName) {
1445
1589
  }).filter((idx) => idx !== null)
1446
1590
  };
1447
1591
  const fileName = toSafeFilename(localName);
1448
- const filePath = join(collectionsDir, `${fileName}.json`);
1592
+ const filePath = join2(collectionsDir, `${fileName}.json`);
1449
1593
  writeFileSync(filePath, JSON.stringify(localFormat, null, 2) + "\n");
1450
1594
  console.log(pc2.green(" \u2713"), `${localName} \u2192 collections/${fileName}.json`);
1451
1595
  }
1452
1596
  }
1453
1597
  async function pullAutomations(api, platformDir, filterName, projectId) {
1454
- const automationsDir = join(platformDir, "automations");
1598
+ const automationsDir = join2(platformDir, "automations");
1455
1599
  mkdirSync(automationsDir, { recursive: true });
1456
1600
  const automations = await api.listAutomations({ include_code: true });
1457
1601
  for (const automation of automations) {
@@ -1462,7 +1606,7 @@ async function pullAutomations(api, platformDir, filterName, projectId) {
1462
1606
  continue;
1463
1607
  }
1464
1608
  const dirName = automation.external_id.replace(/[^a-zA-Z0-9_-]/g, "_");
1465
- const automationDir = join(automationsDir, dirName);
1609
+ const automationDir = join2(automationsDir, dirName);
1466
1610
  mkdirSync(automationDir, { recursive: true });
1467
1611
  const config = {
1468
1612
  external_id: automation.external_id,
@@ -1495,13 +1639,13 @@ async function pullAutomations(api, platformDir, filterName, projectId) {
1495
1639
  }
1496
1640
  } catch {
1497
1641
  }
1498
- writeFileSync(join(automationDir, "config.json"), JSON.stringify(config, null, 2) + "\n");
1499
- writeFileSync(join(automationDir, "main.py"), automation.code || "");
1642
+ writeFileSync(join2(automationDir, "config.json"), JSON.stringify(config, null, 2) + "\n");
1643
+ writeFileSync(join2(automationDir, "main.py"), automation.code || "");
1500
1644
  console.log(pc2.green(" \u2713"), `${automation.name} \u2192 automations/${dirName}/`);
1501
1645
  }
1502
1646
  }
1503
1647
  async function pullHooks(api, platformDir, filterName, appName, projectId) {
1504
- const hooksDir = join(platformDir, "hooks");
1648
+ const hooksDir = join2(platformDir, "hooks");
1505
1649
  mkdirSync(hooksDir, { recursive: true });
1506
1650
  const hooks = await api.listHooks();
1507
1651
  for (const hook of hooks) {
@@ -1523,31 +1667,31 @@ export default async function handler({ record, app, http }) {
1523
1667
  ${hook.script.split("\n").map((line) => " " + line).join("\n")}
1524
1668
  }
1525
1669
  `;
1526
- writeFileSync(join(hooksDir, fileName), content);
1670
+ writeFileSync(join2(hooksDir, fileName), content);
1527
1671
  console.log(pc2.green(" \u2713"), `${hook.name} \u2192 hooks/${fileName}`);
1528
1672
  }
1529
1673
  }
1530
1674
  function loadLocalAgents(platformDir, filterName, appName) {
1531
- const agentsDir = join(platformDir, "agents");
1532
- if (!existsSync(agentsDir)) return [];
1675
+ const agentsDir = join2(platformDir, "agents");
1676
+ if (!existsSync2(agentsDir)) return [];
1533
1677
  const agents = [];
1534
1678
  const errors = [];
1535
- for (const entry of readdirSync(agentsDir, { withFileTypes: true })) {
1679
+ for (const entry of readdirSync2(agentsDir, { withFileTypes: true })) {
1536
1680
  if (!entry.isDirectory()) continue;
1537
- const agentDir = join(agentsDir, entry.name);
1538
- const configPath = join(agentDir, "config.json");
1539
- const promptPath = join(agentDir, "system_prompt.md");
1540
- const policyPath = join(agentDir, "policy.js");
1541
- if (!existsSync(configPath)) {
1681
+ const agentDir = join2(agentsDir, entry.name);
1682
+ const configPath = join2(agentDir, "config.json");
1683
+ const promptPath = join2(agentDir, "system_prompt.md");
1684
+ const policyPath = join2(agentDir, "policy.js");
1685
+ if (!existsSync2(configPath)) {
1542
1686
  errors.push(`${entry.name}: missing config.json`);
1543
1687
  continue;
1544
1688
  }
1545
- if (!existsSync(promptPath)) {
1689
+ if (!existsSync2(promptPath)) {
1546
1690
  errors.push(`${entry.name}: missing system_prompt.md`);
1547
1691
  continue;
1548
1692
  }
1549
1693
  try {
1550
- const config = JSON.parse(readFileSync(configPath, "utf-8"));
1694
+ const config = JSON.parse(readFileSync2(configPath, "utf-8"));
1551
1695
  if (filterName && config.external_id !== filterName && config.name !== filterName && entry.name !== filterName) {
1552
1696
  continue;
1553
1697
  }
@@ -1570,8 +1714,8 @@ function loadLocalAgents(platformDir, filterName, appName) {
1570
1714
  errors.push(`${entry.name}: missing name in config.json`);
1571
1715
  continue;
1572
1716
  }
1573
- let systemPrompt = readFileSync(promptPath, "utf-8");
1574
- let policyScript = existsSync(policyPath) ? readFileSync(policyPath, "utf-8") : "";
1717
+ let systemPrompt = readFileSync2(promptPath, "utf-8");
1718
+ let policyScript = existsSync2(policyPath) ? readFileSync2(policyPath, "utf-8") : "";
1575
1719
  if (appName) {
1576
1720
  systemPrompt = systemPrompt.replaceAll("{{app}}", appName);
1577
1721
  policyScript = policyScript.replaceAll("{{app}}", appName);
@@ -1701,7 +1845,7 @@ async function applyAgents(api, localAgents, projectId) {
1701
1845
  return errors;
1702
1846
  }
1703
1847
  async function pullAgents(api, platformDir, filterName, projectId) {
1704
- const agentsDir = join(platformDir, "agents");
1848
+ const agentsDir = join2(platformDir, "agents");
1705
1849
  mkdirSync(agentsDir, { recursive: true });
1706
1850
  const agents = await api.listAgents(projectId ? { project_id: projectId } : void 0);
1707
1851
  let skillIdToSlug = /* @__PURE__ */ new Map();
@@ -1716,7 +1860,7 @@ async function pullAgents(api, platformDir, filterName, projectId) {
1716
1860
  continue;
1717
1861
  }
1718
1862
  const dirName = agent.external_id.replace(/[^a-zA-Z0-9_-]/g, "_");
1719
- const agentDir = join(agentsDir, dirName);
1863
+ const agentDir = join2(agentsDir, dirName);
1720
1864
  mkdirSync(agentDir, { recursive: true });
1721
1865
  const skillSlugs = [];
1722
1866
  if (agent.skill_ids) {
@@ -1733,10 +1877,10 @@ async function pullAgents(api, platformDir, filterName, projectId) {
1733
1877
  if (agent.model) config.model = agent.model;
1734
1878
  if (skillSlugs.length > 0) config.skills = skillSlugs;
1735
1879
  if (agent.policy_enabled) config.policy_enabled = true;
1736
- writeFileSync(join(agentDir, "config.json"), JSON.stringify(config, null, 2) + "\n");
1737
- writeFileSync(join(agentDir, "system_prompt.md"), agent.system_prompt || "");
1880
+ writeFileSync(join2(agentDir, "config.json"), JSON.stringify(config, null, 2) + "\n");
1881
+ writeFileSync(join2(agentDir, "system_prompt.md"), agent.system_prompt || "");
1738
1882
  if (agent.policy_script) {
1739
- writeFileSync(join(agentDir, "policy.js"), agent.policy_script);
1883
+ writeFileSync(join2(agentDir, "policy.js"), agent.policy_script);
1740
1884
  }
1741
1885
  console.log(pc2.green(" \u2713"), `${agent.name} \u2192 agents/${dirName}/`);
1742
1886
  }
@@ -2374,7 +2518,10 @@ async function plan(args) {
2374
2518
  console.log(pc2.cyan(pc2.bold(" Plan")));
2375
2519
  console.log(pc2.dim(" Comparing local files to remote state..."));
2376
2520
  console.log();
2377
- await syncDeps(projectRoot);
2521
+ if (!type || type === "collections") {
2522
+ lintCollectionFiles(projectRoot, platformDir, name || void 0);
2523
+ }
2524
+ await syncDeps(projectRoot, { ignorePermissionDenied: true });
2378
2525
  const allChanges = [];
2379
2526
  let collections;
2380
2527
  try {
@@ -2509,7 +2656,10 @@ async function apply(args) {
2509
2656
  console.log();
2510
2657
  return;
2511
2658
  }
2512
- await syncDeps(projectRoot);
2659
+ if (!type || type === "collections") {
2660
+ lintCollectionFiles(projectRoot, platformDir, name || void 0);
2661
+ }
2662
+ await syncDeps(projectRoot, { ignorePermissionDenied: true });
2513
2663
  let collections;
2514
2664
  try {
2515
2665
  const remoteCollections = await api.listCollections();
@@ -2540,7 +2690,7 @@ async function apply(args) {
2540
2690
  let willDeployApp = false;
2541
2691
  if (!type) {
2542
2692
  try {
2543
- if (existsSync(join(projectRoot, "dist")) || existsSync(join(projectRoot, "src"))) {
2693
+ if (existsSync2(join2(projectRoot, "dist")) || existsSync2(join2(projectRoot, "src"))) {
2544
2694
  willDeployApp = true;
2545
2695
  }
2546
2696
  } catch {
@@ -2720,7 +2870,7 @@ async function pull(args) {
2720
2870
  console.log();
2721
2871
  if (!name && await projectResourceDepsEnabled(projectRoot)) {
2722
2872
  console.log(pc2.bold(" Resource shares:"));
2723
- await syncDeps(projectRoot, { write: true, legacyWhenDisabled: false });
2873
+ await syncDeps(projectRoot, { write: true, legacyWhenDisabled: false, ignorePermissionDenied: true });
2724
2874
  console.log();
2725
2875
  }
2726
2876
  }
@@ -3,7 +3,7 @@ import {
3
3
  } from "./chunk-2CR762KB.js";
4
4
  import {
5
5
  createApiClient
6
- } from "./chunk-JKXLKK5I.js";
6
+ } from "./chunk-UH5X43VV.js";
7
7
  import {
8
8
  findProjectRoot,
9
9
  getApiUrl,
@@ -8,7 +8,7 @@ import {
8
8
  slugToDirName,
9
9
  slugToFilename,
10
10
  syncClaudeMd
11
- } from "./chunk-AUYOTENF.js";
11
+ } from "./chunk-HU7RYLUF.js";
12
12
  import "./chunk-ZH3NVYEQ.js";
13
13
  import "./chunk-FJFIWC7G.js";
14
14
  import "./chunk-PNKVD2UK.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lumerahq/cli",
3
- "version": "0.19.3",
3
+ "version": "0.19.4",
4
4
  "description": "CLI for building and deploying Lumera apps",
5
5
  "type": "module",
6
6
  "engines": {