@lumerahq/cli 0.35.1 → 0.36.0-dev.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.
@@ -8,7 +8,7 @@ import {
8
8
  } from "./chunk-2EHJH2QU.js";
9
9
  import {
10
10
  userContextHeaders
11
- } from "./chunk-2A2DSLIE.js";
11
+ } from "./chunk-RV7WQAK3.js";
12
12
  import {
13
13
  fetchWithRetry
14
14
  } from "./chunk-FJFIWC7G.js";
@@ -4,7 +4,7 @@ import {
4
4
  import {
5
5
  createApiClient,
6
6
  isApiErrorStatus
7
- } from "./chunk-2A2DSLIE.js";
7
+ } from "./chunk-RV7WQAK3.js";
8
8
  import {
9
9
  findProjectRoot,
10
10
  getAppName
@@ -171,6 +171,13 @@ var ApiClient = class {
171
171
  async getCollection(id) {
172
172
  return this.request(`/api/pb/collections/${id}`);
173
173
  }
174
+ /** Runs a read-only SQL query in the project's scope. */
175
+ async querySql(sql) {
176
+ return this.request("/api/pb/sql", {
177
+ method: "POST",
178
+ body: JSON.stringify({ sql })
179
+ });
180
+ }
174
181
  async ensureCollection(name, def) {
175
182
  return this.request(`/api/pb/collections/${name}`, {
176
183
  method: "PUT",
@@ -2,9 +2,9 @@ import {
2
2
  deps,
3
3
  syncDeps,
4
4
  syncResourceShareDeps
5
- } from "./chunk-A27HZEU6.js";
5
+ } from "./chunk-MFAEFPHY.js";
6
6
  import "./chunk-2CR762KB.js";
7
- import "./chunk-2A2DSLIE.js";
7
+ import "./chunk-RV7WQAK3.js";
8
8
  import "./chunk-JLVVHTBY.js";
9
9
  import "./chunk-FJFIWC7G.js";
10
10
  import "./chunk-PNKVD2UK.js";
@@ -1,19 +1,19 @@
1
1
  import {
2
2
  dev
3
- } from "./chunk-5TQ3BQ2X.js";
3
+ } from "./chunk-DXHA72QY.js";
4
4
  import "./chunk-UGAIWMQV.js";
5
5
  import "./chunk-2EHJH2QU.js";
6
6
  import "./chunk-XZ5VCTGY.js";
7
7
  import "./chunk-5FGDEZSJ.js";
8
8
  import {
9
9
  syncDeps
10
- } from "./chunk-A27HZEU6.js";
10
+ } from "./chunk-MFAEFPHY.js";
11
11
  import {
12
12
  loadEnv
13
13
  } from "./chunk-2CR762KB.js";
14
14
  import {
15
15
  createApiClient
16
- } from "./chunk-2A2DSLIE.js";
16
+ } from "./chunk-RV7WQAK3.js";
17
17
  import {
18
18
  findProjectRoot,
19
19
  getApiUrl,
package/dist/index.js CHANGED
@@ -243,32 +243,32 @@ async function main() {
243
243
  switch (command) {
244
244
  // Resource commands
245
245
  case "plan":
246
- await import("./resources-X7K436TK.js").then((m) => m.plan(args.slice(1)));
246
+ await import("./resources-IW5JT7A3.js").then((m) => m.plan(args.slice(1)));
247
247
  break;
248
248
  case "apply":
249
- await import("./resources-X7K436TK.js").then((m) => m.apply(args.slice(1)));
249
+ await import("./resources-IW5JT7A3.js").then((m) => m.apply(args.slice(1)));
250
250
  break;
251
251
  case "pull":
252
- await import("./resources-X7K436TK.js").then((m) => m.pull(args.slice(1)));
252
+ await import("./resources-IW5JT7A3.js").then((m) => m.pull(args.slice(1)));
253
253
  break;
254
254
  case "destroy":
255
- await import("./resources-X7K436TK.js").then((m) => m.destroy(args.slice(1)));
255
+ await import("./resources-IW5JT7A3.js").then((m) => m.destroy(args.slice(1)));
256
256
  break;
257
257
  case "list":
258
- await import("./resources-X7K436TK.js").then((m) => m.list(args.slice(1)));
258
+ await import("./resources-IW5JT7A3.js").then((m) => m.list(args.slice(1)));
259
259
  break;
260
260
  case "show":
261
- await import("./resources-X7K436TK.js").then((m) => m.show(args.slice(1)));
261
+ await import("./resources-IW5JT7A3.js").then((m) => m.show(args.slice(1)));
262
262
  break;
263
263
  case "diff":
264
- await import("./resources-X7K436TK.js").then((m) => m.diff(args.slice(1)));
264
+ await import("./resources-IW5JT7A3.js").then((m) => m.diff(args.slice(1)));
265
265
  break;
266
266
  // Development
267
267
  case "dev":
268
- await import("./dev-E2O6R7WW.js").then((m) => m.dev(args.slice(1)));
268
+ await import("./dev-NEHWUIWB.js").then((m) => m.dev(args.slice(1)));
269
269
  break;
270
270
  case "run":
271
- await import("./run-BJ7PXSEH.js").then((m) => m.run(args.slice(1)));
271
+ await import("./run-TMJPC4PK.js").then((m) => m.run(args.slice(1)));
272
272
  break;
273
273
  case "functions":
274
274
  await import("./functions-5OOHGQT7.js").then(
@@ -282,10 +282,10 @@ async function main() {
282
282
  break;
283
283
  // Project
284
284
  case "init":
285
- await import("./init-MJEVA5HK.js").then((m) => m.init(args.slice(1)));
285
+ await import("./init-LZAXKILN.js").then((m) => m.init(args.slice(1)));
286
286
  break;
287
287
  case "register":
288
- await import("./register-RUDM4ITV.js").then((m) => m.register(args.slice(1)));
288
+ await import("./register-MOO266NL.js").then((m) => m.register(args.slice(1)));
289
289
  break;
290
290
  case "templates":
291
291
  await import("./templates-LNUOTNLN.js").then((m) => m.templates(subcommand, args.slice(2)));
@@ -302,7 +302,7 @@ async function main() {
302
302
  break;
303
303
  // Dependencies
304
304
  case "deps":
305
- await import("./deps-DLJUDFNK.js").then((m) => m.deps(args.slice(1)));
305
+ await import("./deps-OUTYSIZU.js").then((m) => m.deps(args.slice(1)));
306
306
  break;
307
307
  // Feature flags (read the in-sandbox snapshot)
308
308
  case "flags":
@@ -12,7 +12,7 @@ import {
12
12
  import {
13
13
  createApiClient,
14
14
  validateProjectNameLength
15
- } from "./chunk-2A2DSLIE.js";
15
+ } from "./chunk-RV7WQAK3.js";
16
16
  import {
17
17
  getToken,
18
18
  init_auth,
@@ -7,7 +7,7 @@ import {
7
7
  import {
8
8
  createApiClient,
9
9
  validateProjectExternalId
10
- } from "./chunk-2A2DSLIE.js";
10
+ } from "./chunk-RV7WQAK3.js";
11
11
  import {
12
12
  findProjectRoot,
13
13
  getAppName,
@@ -1,14 +1,14 @@
1
1
  import {
2
2
  deploy,
3
3
  deployAppAPI
4
- } from "./chunk-5TQ3BQ2X.js";
4
+ } from "./chunk-DXHA72QY.js";
5
5
  import "./chunk-UGAIWMQV.js";
6
6
  import "./chunk-2EHJH2QU.js";
7
7
  import "./chunk-XZ5VCTGY.js";
8
8
  import "./chunk-5FGDEZSJ.js";
9
9
  import {
10
10
  syncDeps
11
- } from "./chunk-A27HZEU6.js";
11
+ } from "./chunk-MFAEFPHY.js";
12
12
  import {
13
13
  loadEnv
14
14
  } from "./chunk-2CR762KB.js";
@@ -17,7 +17,7 @@ import {
17
17
  createApiClient,
18
18
  isApiErrorStatus,
19
19
  validateCollectionNameLength
20
- } from "./chunk-2A2DSLIE.js";
20
+ } from "./chunk-RV7WQAK3.js";
21
21
  import {
22
22
  findProjectRoot,
23
23
  getApiUrl,
@@ -382,7 +382,7 @@ var automationMainGuardRule = {
382
382
  if (entrypoint === null) return [];
383
383
  const guard = findTopLevelMainGuard(target.source);
384
384
  if (guard === null) return [];
385
- const warning2 = {
385
+ const warning3 = {
386
386
  ruleId: "automation-main-guard",
387
387
  target,
388
388
  severity: "warning",
@@ -390,7 +390,7 @@ var automationMainGuardRule = {
390
390
  snippet: guard.snippet,
391
391
  message: `Function automations must not contain a top-level __main__ guard. Lumera invokes "${entrypoint}" automatically; remove the guard.`
392
392
  };
393
- return [warning2];
393
+ return [warning3];
394
394
  }
395
395
  };
396
396
 
@@ -407,6 +407,14 @@ function error(target, message, snippet) {
407
407
  snippet
408
408
  };
409
409
  }
410
+ function warning(target, message) {
411
+ return {
412
+ ruleId: "collection-schema",
413
+ target,
414
+ severity: "warning",
415
+ message
416
+ };
417
+ }
410
418
  var collectionSchemaRule = {
411
419
  id: "collection-schema",
412
420
  description: "Validates local collection JSON files before planning or applying resources.",
@@ -466,6 +474,21 @@ var collectionSchemaRule = {
466
474
  if (typeof field.type !== "string" || field.type.trim() === "") {
467
475
  issues.push(error(target, `Field ${typeof field.name === "string" ? `"${field.name}"` : `at index ${i}`} is missing required string field "type".`));
468
476
  }
477
+ const legacyFile = field.type === "lumera_file" || field.type === "file";
478
+ if (legacyFile || field.type === "lumera_file_v2" || field.type === "file_v2") {
479
+ const label = typeof field.name === "string" ? `"${field.name}"` : `at index ${i}`;
480
+ const ignored = [
481
+ ...isRecord3(field.options) && field.options.maxSelect !== void 0 ? ['"options.maxSelect"'] : [],
482
+ ...field.multiple !== void 0 ? ['"multiple"'] : []
483
+ ];
484
+ if (ignored.length > 0) {
485
+ const fixed = legacyFile ? " A lumera_file field's maxSelect cannot change after it is first deployed; use lumera_file_v2 if it needs to." : "";
486
+ issues.push(warning(
487
+ target,
488
+ `File field ${label} sets ${ignored.join(" and ")}, which collection files ignore; it deploys with maxSelect ${field.maxSelect ?? 1}. Set top-level "maxSelect" instead.${fixed}`
489
+ ));
490
+ }
491
+ }
469
492
  }
470
493
  }
471
494
  if (audit !== void 0 && typeof audit !== "boolean") {
@@ -817,7 +840,7 @@ function error2(target, message, line) {
817
840
  ...line !== void 0 ? { line } : {}
818
841
  };
819
842
  }
820
- function warning(target, message, line) {
843
+ function warning2(target, message, line) {
821
844
  return {
822
845
  ruleId: "hook-verify",
823
846
  target,
@@ -897,7 +920,7 @@ var hookVerifyRule = {
897
920
  if (errors.length > 0) return errors;
898
921
  const advisories = [];
899
922
  if (finding.legacyHandlerPattern) {
900
- advisories.push(warning(target, LEGACY_HANDLER_MIGRATION_HINT));
923
+ advisories.push(warning2(target, LEGACY_HANDLER_MIGRATION_HINT));
901
924
  }
902
925
  let script = finding.scriptBody ?? "";
903
926
  if (ctx.appName) {
@@ -1193,16 +1216,19 @@ async function safeLintPass(label, build) {
1193
1216
  return null;
1194
1217
  }
1195
1218
  }
1196
- async function lintCollectionFiles(projectRoot, platformDir, filterName) {
1219
+ async function lintCollectionFiles(projectRoot, platformDir, filterName, printWarnings = true) {
1197
1220
  const result = await safeLintPass(
1198
1221
  "collection",
1199
1222
  async () => runLint({ projectRoot, targets: buildCollectionTargets(platformDir, filterName) })
1200
1223
  );
1201
- if (!result || result.issues.length === 0) return;
1202
- safePrintLint(result.issues, projectRoot);
1224
+ if (!result || result.issues.length === 0) return [];
1225
+ if (printWarnings || result.errors.length > 0) {
1226
+ safePrintLint(result.issues, projectRoot);
1227
+ }
1203
1228
  if (result.errors.length > 0) {
1204
1229
  throw new Error(`Found ${result.errors.length} collection lint error(s)`);
1205
1230
  }
1231
+ return result.issues;
1206
1232
  }
1207
1233
  async function lintHookFiles(api, projectRoot, platformDir, appName, filterName) {
1208
1234
  const result = await safeLintPass("hook", async () => {
@@ -1349,6 +1375,7 @@ var FIELD_OPTION_KEYS_BY_TYPE = {
1349
1375
  relation: ["cascadeDelete", "minSelect", "maxSelect"],
1350
1376
  json: ["maxSize"],
1351
1377
  lumera_file: ["maxSelect", "maxSize", "mimeTypes", "protected"],
1378
+ lumera_file_v2: ["maxSelect", "maxSize", "mimeTypes", "protected"],
1352
1379
  editor: ["maxSize", "convertURLs"],
1353
1380
  date: ["min", "max"],
1354
1381
  select: ["values", "maxSelect"],
@@ -1360,6 +1387,7 @@ var FIELD_OPTION_DEFAULTS = {
1360
1387
  relation: { cascadeDelete: false, minSelect: 0, maxSelect: 1 },
1361
1388
  json: { maxSize: 0 },
1362
1389
  lumera_file: { maxSelect: 1, maxSize: 0, mimeTypes: [], protected: false },
1390
+ lumera_file_v2: { maxSelect: 1, maxSize: 0, mimeTypes: [], protected: false },
1363
1391
  editor: { maxSize: 0, convertURLs: false },
1364
1392
  date: { min: "", max: "" },
1365
1393
  select: { values: [], maxSelect: 1 },
@@ -2074,6 +2102,7 @@ function mapFieldType(type) {
2074
2102
  select: "select",
2075
2103
  relation: "relation",
2076
2104
  file: "lumera_file",
2105
+ file_v2: "lumera_file_v2",
2077
2106
  json: "json",
2078
2107
  editor: "editor",
2079
2108
  currency: "currency"
@@ -2111,6 +2140,86 @@ function fieldsDiffer(local, remote) {
2111
2140
  }
2112
2141
  return false;
2113
2142
  }
2143
+ function lumeraFileMaxSelectErrors(local, remote) {
2144
+ const remoteFields = new Map(remote.schema.map((f) => [f.name, f]));
2145
+ const errors = [];
2146
+ for (const field of local.fields) {
2147
+ const deployed = remoteFields.get(field.name);
2148
+ if (mapFieldType(field.type) !== "lumera_file" || deployed?.type !== "lumera_file") continue;
2149
+ const deployedMaxSelect = Math.max(Number(normalizeFieldOption("lumera_file", "maxSelect", deployed.options?.maxSelect)), 1);
2150
+ const requestedMaxSelect = Math.max(Number(normalizeFieldOption("lumera_file", "maxSelect", field.maxSelect)), 1);
2151
+ if (deployedMaxSelect === requestedMaxSelect) continue;
2152
+ const requested = field.maxSelect === void 0 ? "omitting maxSelect (defaults to 1)" : `maxSelect ${requestedMaxSelect}`;
2153
+ errors.push(`File field "${field.name}" is deployed with maxSelect ${deployedMaxSelect}; ${requested} would change it, which is not supported. Keep maxSelect ${deployedMaxSelect}. To change the limit, move the field to lumera_file_v2.`);
2154
+ }
2155
+ return errors;
2156
+ }
2157
+ function fieldTypeChangeErrors(local, remote) {
2158
+ const remoteFields = new Map(remote.schema.map((f) => [f.name, f]));
2159
+ const errors = [];
2160
+ for (const field of local.fields) {
2161
+ const deployed = remoteFields.get(field.name);
2162
+ const requestedType = mapFieldType(field.type);
2163
+ if (!deployed || deployed.type === requestedType) continue;
2164
+ errors.push(`Field "${field.name}" is deployed as ${deployed.type}; changing it to ${requestedType} would drop its data, which is not supported. Add a field with a new name instead.`);
2165
+ }
2166
+ return errors;
2167
+ }
2168
+ function legacyFileFieldWarnings(fields) {
2169
+ return fields.filter((field) => mapFieldType(field.type) === "lumera_file").map((field) => `File field "${field.name}" uses lumera_file; use lumera_file_v2 for new file fields so maxSelect can change later.`);
2170
+ }
2171
+ function quoteSqlIdentifier(name) {
2172
+ return `"${name.replaceAll('"', '""')}"`;
2173
+ }
2174
+ async function countRecords(api, sql) {
2175
+ try {
2176
+ const total = Number((await api.querySql(sql)).rows?.[0]?.total);
2177
+ return Number.isFinite(total) ? total : void 0;
2178
+ } catch {
2179
+ return void 0;
2180
+ }
2181
+ }
2182
+ function readOnlyRecords(count, condition, fix) {
2183
+ if (count === void 0) return `Records ${condition} become read-only until ${fix}.`;
2184
+ return `${count} record${count === 1 ? "" : "s"} ${condition} ${count === 1 ? "becomes" : "become"} read-only until ${fix}.`;
2185
+ }
2186
+ async function lumeraFileV2TighteningWarnings(api, local, remote) {
2187
+ const remoteFields = new Map(remote.schema.map((f) => [f.name, f]));
2188
+ const warnings = [];
2189
+ for (const field of local.fields) {
2190
+ const deployed = remoteFields.get(field.name);
2191
+ if (mapFieldType(field.type) !== "lumera_file_v2" || deployed?.type !== "lumera_file_v2") continue;
2192
+ const option = (key, value) => normalizeFieldOption("lumera_file_v2", key, value);
2193
+ const label = `${local.name}.${field.name}`;
2194
+ const from = `FROM ${quoteSqlIdentifier(remote.name)} WHERE`;
2195
+ const column = quoteSqlIdentifier(field.name);
2196
+ const deployedMaxSelect = Math.max(Number(option("maxSelect", deployed.options?.maxSelect)), 1);
2197
+ const requestedMaxSelect = Math.max(Number(option("maxSelect", field.maxSelect)), 1);
2198
+ if (requestedMaxSelect < deployedMaxSelect) {
2199
+ const count = await countRecords(api, `SELECT count(*) AS total ${from} jsonb_array_length(${column}) > ${requestedMaxSelect}`);
2200
+ if (count !== 0) {
2201
+ warnings.push(`${label}: maxSelect ${deployedMaxSelect} \u2192 ${requestedMaxSelect}. ${readOnlyRecords(count, `with more than ${requestedMaxSelect} file${requestedMaxSelect === 1 ? "" : "s"}`, "trimmed or deleted")}`);
2202
+ }
2203
+ }
2204
+ if (field.required && !deployed.required) {
2205
+ const count = await countRecords(api, `SELECT count(*) AS total ${from} jsonb_array_length(${column}) = 0`);
2206
+ if (count !== 0) {
2207
+ warnings.push(`${label}: now required. ${readOnlyRecords(count, "with no files", "a file is added or they are deleted")}`);
2208
+ }
2209
+ }
2210
+ const deployedMaxSize = Number(option("maxSize", deployed.options?.maxSize));
2211
+ const requestedMaxSize = Number(option("maxSize", field.maxSize));
2212
+ if (requestedMaxSize > 0 && (deployedMaxSize === 0 || requestedMaxSize < deployedMaxSize)) {
2213
+ warnings.push(`${label}: maxSize lowered to ${requestedMaxSize} bytes. ${readOnlyRecords(void 0, "with larger files", "fixed or deleted")}`);
2214
+ }
2215
+ const deployedTypes = option("mimeTypes", deployed.options?.mimeTypes);
2216
+ const requestedTypes = option("mimeTypes", field.mimeTypes);
2217
+ if (requestedTypes.length > 0 && (deployedTypes.length === 0 || deployedTypes.some((type) => !requestedTypes.includes(type)))) {
2218
+ warnings.push(`${label}: mimeTypes narrowed. ${readOnlyRecords(void 0, "with other file types", "fixed or deleted")}`);
2219
+ }
2220
+ }
2221
+ return warnings;
2222
+ }
2114
2223
  function localIndexesToSql(local, appName) {
2115
2224
  if (!local.indexes || local.indexes.length === 0) return [];
2116
2225
  return local.indexes.map((idx) => buildIndexSql(local, idx, appName)).sort();
@@ -2171,6 +2280,7 @@ async function planCollections(api, localCollections, appName, projectId) {
2171
2280
  }
2172
2281
  }
2173
2282
  if (!remote) {
2283
+ const createWarnings = legacyFileFieldWarnings(local.fields);
2174
2284
  changes.push({
2175
2285
  type: "create",
2176
2286
  resource: "collection",
@@ -2185,7 +2295,8 @@ async function planCollections(api, localCollections, appName, projectId) {
2185
2295
  name: f.name,
2186
2296
  type: f.type,
2187
2297
  required: f.required
2188
- }))
2298
+ })),
2299
+ ...createWarnings.length > 0 ? { warnings: createWarnings } : {}
2189
2300
  });
2190
2301
  } else {
2191
2302
  const localFieldNames = new Set(local.fields.map((f) => f.name));
@@ -2224,13 +2335,20 @@ async function planCollections(api, localCollections, appName, projectId) {
2224
2335
  const f = remoteFieldMap.get(name);
2225
2336
  fieldDetails.push({ action: "-", name: f.name, type: f.type, required: f.required });
2226
2337
  }
2338
+ const errors = [...fieldTypeChangeErrors(local, remote), ...lumeraFileMaxSelectErrors(local, remote)];
2339
+ const warnings = [
2340
+ ...legacyFileFieldWarnings(added.map((name) => localFieldMap.get(name))),
2341
+ ...await lumeraFileV2TighteningWarnings(api, local, remote)
2342
+ ];
2227
2343
  changes.push({
2228
2344
  type: "update",
2229
2345
  resource: "collection",
2230
2346
  id: local.id,
2231
2347
  name: local.name,
2232
2348
  details: details.join(", "),
2233
- fieldDetails
2349
+ fieldDetails,
2350
+ ...errors.length > 0 ? { errors } : {},
2351
+ ...warnings.length > 0 ? { warnings } : {}
2234
2352
  });
2235
2353
  }
2236
2354
  }
@@ -4422,8 +4540,9 @@ async function plan(args) {
4422
4540
  console.log(pc2.cyan(pc2.bold(" Plan")));
4423
4541
  console.log(pc2.dim(" Comparing local files to remote state..."));
4424
4542
  console.log();
4543
+ let collectionLintWarnings = [];
4425
4544
  if (!type || type === "collections") {
4426
- await lintCollectionFiles(projectRoot, platformDir, name || void 0);
4545
+ collectionLintWarnings = await lintCollectionFiles(projectRoot, platformDir, name || void 0, false);
4427
4546
  }
4428
4547
  if (!type || type === "hooks") {
4429
4548
  await lintHookFiles(api, projectRoot, platformDir, appName, name || void 0);
@@ -4498,6 +4617,7 @@ async function plan(args) {
4498
4617
  }
4499
4618
  }
4500
4619
  const lintWarnings = [
4620
+ ...collectionLintWarnings,
4501
4621
  ...agentLintWarnings,
4502
4622
  ...await safeLint(projectRoot, localAutomations)
4503
4623
  ];
@@ -4518,6 +4638,7 @@ async function plan(args) {
4518
4638
  safePrintLint(lintWarnings, projectRoot);
4519
4639
  return;
4520
4640
  }
4641
+ const collectionErrorCount = allChanges.reduce((count, change) => count + (change.errors?.length ?? 0), 0);
4521
4642
  if (jsonOutput) {
4522
4643
  const warnings = [];
4523
4644
  for (const change of allChanges) {
@@ -4527,9 +4648,11 @@ async function plan(args) {
4527
4648
  warnings.push(`Collection "${change.name}" will have ${removals.length} field(s) removed.`);
4528
4649
  }
4529
4650
  }
4651
+ warnings.push(...change.warnings ?? []);
4530
4652
  }
4531
- console.log(JSON.stringify({ changes: allChanges, warnings, validationErrors: planValidationErrors, lintWarnings: serializeLintWarnings(lintWarnings, projectRoot) }));
4532
- if (planValidationErrors > 0) process.exitCode = 1;
4653
+ const validationErrors = planValidationErrors + collectionErrorCount;
4654
+ console.log(JSON.stringify({ changes: allChanges, warnings, validationErrors, lintWarnings: serializeLintWarnings(lintWarnings, projectRoot) }));
4655
+ if (validationErrors > 0) process.exitCode = 1;
4533
4656
  return;
4534
4657
  }
4535
4658
  console.log(pc2.bold(" Changes:"));
@@ -4547,6 +4670,14 @@ async function plan(args) {
4547
4670
  }
4548
4671
  console.log();
4549
4672
  }
4673
+ if (change.errors && change.errors.length > 0) {
4674
+ for (const message of change.errors) console.log(pc2.red(` \u2717 ${message}`));
4675
+ console.log();
4676
+ }
4677
+ if (change.warnings && change.warnings.length > 0) {
4678
+ for (const message of change.warnings) console.log(pc2.yellow(` \u26A0 ${message}`));
4679
+ console.log();
4680
+ }
4550
4681
  if (change.textDiffs && change.textDiffs.length > 0) {
4551
4682
  const maxDiffLines = 20;
4552
4683
  for (const td of change.textDiffs) {
@@ -4572,6 +4703,11 @@ async function plan(args) {
4572
4703
  safePrintLint(lintWarnings, projectRoot);
4573
4704
  if (planValidationErrors > 0) {
4574
4705
  console.log(pc2.red(` \u2717 Fix ${planValidationErrors} agent skill validation error${planValidationErrors === 1 ? "" : "s"} before applying.`));
4706
+ }
4707
+ if (collectionErrorCount > 0) {
4708
+ console.log(pc2.red(` \u2717 Fix ${collectionErrorCount} collection error${collectionErrorCount === 1 ? "" : "s"} before applying.`));
4709
+ }
4710
+ if (planValidationErrors > 0 || collectionErrorCount > 0) {
4575
4711
  process.exitCode = 1;
4576
4712
  } else {
4577
4713
  console.log(pc2.dim(` Run 'lumera apply' to apply these changes.`));
@@ -4679,6 +4815,15 @@ async function apply(args) {
4679
4815
  console.log();
4680
4816
  process.exit(1);
4681
4817
  }
4818
+ const collectionErrors = allChanges.flatMap((change) => (change.errors ?? []).map((message) => `${change.resource} ${change.name}: ${message}`));
4819
+ if (collectionErrors.length > 0) {
4820
+ console.log();
4821
+ for (const message of collectionErrors) console.log(pc2.red(` \u2717 ${message}`));
4822
+ console.log();
4823
+ console.log(pc2.red(` \u2717 Apply aborted: fix ${collectionErrors.length} collection error${collectionErrors.length === 1 ? "" : "s"} first. Nothing was applied.`));
4824
+ console.log();
4825
+ process.exit(1);
4826
+ }
4682
4827
  let willDeployApp = false;
4683
4828
  if (!type && !skipApp) {
4684
4829
  try {
@@ -4707,6 +4852,7 @@ async function apply(args) {
4707
4852
  const color = change.type === "create" ? pc2.green : pc2.yellow;
4708
4853
  const details = change.details ? ` (${change.details})` : "";
4709
4854
  console.log(` ${color(icon)} ${change.resource}: ${change.name}${pc2.dim(details)}`);
4855
+ for (const message of change.warnings ?? []) console.log(pc2.yellow(` \u26A0 ${message}`));
4710
4856
  }
4711
4857
  if (willDeployApp) console.log(` ${pc2.blue("\u25CF")} app: frontend build + deploy`);
4712
4858
  console.log();
@@ -5292,11 +5438,14 @@ export {
5292
5438
  convertCollectionToApiFormat,
5293
5439
  destroy,
5294
5440
  diff,
5441
+ fieldTypeChangeErrors,
5295
5442
  lintAgentFiles,
5296
5443
  list,
5297
5444
  loadLocalCollections,
5298
5445
  loadLocalSkills,
5299
5446
  localProjectSkillSlug,
5447
+ lumeraFileMaxSelectErrors,
5448
+ lumeraFileV2TighteningWarnings,
5300
5449
  plan,
5301
5450
  planAgents,
5302
5451
  planCollections,
@@ -3,7 +3,7 @@ import {
3
3
  } from "./chunk-2CR762KB.js";
4
4
  import {
5
5
  createApiClient
6
- } from "./chunk-2A2DSLIE.js";
6
+ } from "./chunk-RV7WQAK3.js";
7
7
  import {
8
8
  findProjectRoot,
9
9
  getApiUrl,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lumerahq/cli",
3
- "version": "0.35.1",
3
+ "version": "0.36.0-dev.0",
4
4
  "description": "CLI for building and deploying Lumera apps",
5
5
  "type": "module",
6
6
  "engines": {