@lumerahq/cli 0.35.2 → 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-E32DYKLL.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-E32DYKLL.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-E32DYKLL.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-E32DYKLL.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-E32DYKLL.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-E32DYKLL.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-E32DYKLL.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,
@@ -474,16 +474,18 @@ var collectionSchemaRule = {
474
474
  if (typeof field.type !== "string" || field.type.trim() === "") {
475
475
  issues.push(error(target, `Field ${typeof field.name === "string" ? `"${field.name}"` : `at index ${i}`} is missing required string field "type".`));
476
476
  }
477
- if (field.type === "lumera_file" || field.type === "file") {
477
+ const legacyFile = field.type === "lumera_file" || field.type === "file";
478
+ if (legacyFile || field.type === "lumera_file_v2" || field.type === "file_v2") {
478
479
  const label = typeof field.name === "string" ? `"${field.name}"` : `at index ${i}`;
479
480
  const ignored = [
480
481
  ...isRecord3(field.options) && field.options.maxSelect !== void 0 ? ['"options.maxSelect"'] : [],
481
482
  ...field.multiple !== void 0 ? ['"multiple"'] : []
482
483
  ];
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." : "";
484
486
  issues.push(warning(
485
487
  target,
486
- `File field ${label} sets ${ignored.join(" and ")}, which collection files ignore; it deploys with maxSelect ${field.maxSelect ?? 1}. Set top-level "maxSelect" instead. A file field's maxSelect cannot change after it is first deployed.`
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}`
487
489
  ));
488
490
  }
489
491
  }
@@ -1373,6 +1375,7 @@ var FIELD_OPTION_KEYS_BY_TYPE = {
1373
1375
  relation: ["cascadeDelete", "minSelect", "maxSelect"],
1374
1376
  json: ["maxSize"],
1375
1377
  lumera_file: ["maxSelect", "maxSize", "mimeTypes", "protected"],
1378
+ lumera_file_v2: ["maxSelect", "maxSize", "mimeTypes", "protected"],
1376
1379
  editor: ["maxSize", "convertURLs"],
1377
1380
  date: ["min", "max"],
1378
1381
  select: ["values", "maxSelect"],
@@ -1384,6 +1387,7 @@ var FIELD_OPTION_DEFAULTS = {
1384
1387
  relation: { cascadeDelete: false, minSelect: 0, maxSelect: 1 },
1385
1388
  json: { maxSize: 0 },
1386
1389
  lumera_file: { maxSelect: 1, maxSize: 0, mimeTypes: [], protected: false },
1390
+ lumera_file_v2: { maxSelect: 1, maxSize: 0, mimeTypes: [], protected: false },
1387
1391
  editor: { maxSize: 0, convertURLs: false },
1388
1392
  date: { min: "", max: "" },
1389
1393
  select: { values: [], maxSelect: 1 },
@@ -2098,6 +2102,7 @@ function mapFieldType(type) {
2098
2102
  select: "select",
2099
2103
  relation: "relation",
2100
2104
  file: "lumera_file",
2105
+ file_v2: "lumera_file_v2",
2101
2106
  json: "json",
2102
2107
  editor: "editor",
2103
2108
  currency: "currency"
@@ -2145,10 +2150,76 @@ function lumeraFileMaxSelectErrors(local, remote) {
2145
2150
  const requestedMaxSelect = Math.max(Number(normalizeFieldOption("lumera_file", "maxSelect", field.maxSelect)), 1);
2146
2151
  if (deployedMaxSelect === requestedMaxSelect) continue;
2147
2152
  const requested = field.maxSelect === void 0 ? "omitting maxSelect (defaults to 1)" : `maxSelect ${requestedMaxSelect}`;
2148
- errors.push(`File field "${field.name}" is deployed with maxSelect ${deployedMaxSelect}; ${requested} would change it, which is not supported. Keep maxSelect ${deployedMaxSelect}.`);
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.`);
2149
2154
  }
2150
2155
  return errors;
2151
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
+ }
2152
2223
  function localIndexesToSql(local, appName) {
2153
2224
  if (!local.indexes || local.indexes.length === 0) return [];
2154
2225
  return local.indexes.map((idx) => buildIndexSql(local, idx, appName)).sort();
@@ -2209,6 +2280,7 @@ async function planCollections(api, localCollections, appName, projectId) {
2209
2280
  }
2210
2281
  }
2211
2282
  if (!remote) {
2283
+ const createWarnings = legacyFileFieldWarnings(local.fields);
2212
2284
  changes.push({
2213
2285
  type: "create",
2214
2286
  resource: "collection",
@@ -2223,7 +2295,8 @@ async function planCollections(api, localCollections, appName, projectId) {
2223
2295
  name: f.name,
2224
2296
  type: f.type,
2225
2297
  required: f.required
2226
- }))
2298
+ })),
2299
+ ...createWarnings.length > 0 ? { warnings: createWarnings } : {}
2227
2300
  });
2228
2301
  } else {
2229
2302
  const localFieldNames = new Set(local.fields.map((f) => f.name));
@@ -2262,7 +2335,11 @@ async function planCollections(api, localCollections, appName, projectId) {
2262
2335
  const f = remoteFieldMap.get(name);
2263
2336
  fieldDetails.push({ action: "-", name: f.name, type: f.type, required: f.required });
2264
2337
  }
2265
- const errors = lumeraFileMaxSelectErrors(local, remote);
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
+ ];
2266
2343
  changes.push({
2267
2344
  type: "update",
2268
2345
  resource: "collection",
@@ -2270,7 +2347,8 @@ async function planCollections(api, localCollections, appName, projectId) {
2270
2347
  name: local.name,
2271
2348
  details: details.join(", "),
2272
2349
  fieldDetails,
2273
- ...errors.length > 0 ? { errors } : {}
2350
+ ...errors.length > 0 ? { errors } : {},
2351
+ ...warnings.length > 0 ? { warnings } : {}
2274
2352
  });
2275
2353
  }
2276
2354
  }
@@ -4570,6 +4648,7 @@ async function plan(args) {
4570
4648
  warnings.push(`Collection "${change.name}" will have ${removals.length} field(s) removed.`);
4571
4649
  }
4572
4650
  }
4651
+ warnings.push(...change.warnings ?? []);
4573
4652
  }
4574
4653
  const validationErrors = planValidationErrors + collectionErrorCount;
4575
4654
  console.log(JSON.stringify({ changes: allChanges, warnings, validationErrors, lintWarnings: serializeLintWarnings(lintWarnings, projectRoot) }));
@@ -4595,6 +4674,10 @@ async function plan(args) {
4595
4674
  for (const message of change.errors) console.log(pc2.red(` \u2717 ${message}`));
4596
4675
  console.log();
4597
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
+ }
4598
4681
  if (change.textDiffs && change.textDiffs.length > 0) {
4599
4682
  const maxDiffLines = 20;
4600
4683
  for (const td of change.textDiffs) {
@@ -4769,6 +4852,7 @@ async function apply(args) {
4769
4852
  const color = change.type === "create" ? pc2.green : pc2.yellow;
4770
4853
  const details = change.details ? ` (${change.details})` : "";
4771
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}`));
4772
4856
  }
4773
4857
  if (willDeployApp) console.log(` ${pc2.blue("\u25CF")} app: frontend build + deploy`);
4774
4858
  console.log();
@@ -5354,12 +5438,14 @@ export {
5354
5438
  convertCollectionToApiFormat,
5355
5439
  destroy,
5356
5440
  diff,
5441
+ fieldTypeChangeErrors,
5357
5442
  lintAgentFiles,
5358
5443
  list,
5359
5444
  loadLocalCollections,
5360
5445
  loadLocalSkills,
5361
5446
  localProjectSkillSlug,
5362
5447
  lumeraFileMaxSelectErrors,
5448
+ lumeraFileV2TighteningWarnings,
5363
5449
  plan,
5364
5450
  planAgents,
5365
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.2",
3
+ "version": "0.36.0-dev.0",
4
4
  "description": "CLI for building and deploying Lumera apps",
5
5
  "type": "module",
6
6
  "engines": {