@lumerahq/cli 0.19.21 → 0.19.23

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.
@@ -7,6 +7,55 @@ import {
7
7
  fetchWithRetry
8
8
  } from "./chunk-FJFIWC7G.js";
9
9
 
10
+ // src/lib/name-limits.ts
11
+ var MAX_PROJECT_NAME_LENGTH = 30;
12
+ var MAX_COLLECTION_NAME_LENGTH = 30;
13
+ var MAX_POSTGRES_IDENTIFIER_BYTES = 63;
14
+ var PROJECT_EXTERNAL_ID_REGEX = /^[a-z0-9][a-z0-9-]*[a-z0-9]$|^[a-z0-9]$/;
15
+ var RESERVED_PROJECT_EXTERNAL_IDS = /* @__PURE__ */ new Set([
16
+ "collection",
17
+ "collections",
18
+ "default",
19
+ "admin",
20
+ "api",
21
+ "system",
22
+ "internal",
23
+ "platform",
24
+ "lumera",
25
+ "lm"
26
+ ]);
27
+ function lengthOf(value) {
28
+ return Array.from(value).length;
29
+ }
30
+ function validateProjectNameLength(name, label = "App name") {
31
+ if (lengthOf(name.trim()) > MAX_PROJECT_NAME_LENGTH) {
32
+ return `${label} must be ${MAX_PROJECT_NAME_LENGTH} characters or less`;
33
+ }
34
+ return null;
35
+ }
36
+ function validateProjectExternalId(externalId) {
37
+ const trimmed = externalId.trim();
38
+ const lengthError = validateProjectNameLength(trimmed, "App ID");
39
+ if (lengthError) return lengthError;
40
+ if (!PROJECT_EXTERNAL_ID_REGEX.test(trimmed)) {
41
+ return "App ID must use lowercase letters, numbers, and hyphens only";
42
+ }
43
+ if (RESERVED_PROJECT_EXTERNAL_IDS.has(trimmed)) {
44
+ return `App ID "${trimmed}" is a reserved name`;
45
+ }
46
+ return null;
47
+ }
48
+ function validateCollectionNameLength(name) {
49
+ const trimmed = name.trim();
50
+ if (lengthOf(trimmed) > MAX_COLLECTION_NAME_LENGTH) {
51
+ return `collection name must be ${MAX_COLLECTION_NAME_LENGTH} characters or less`;
52
+ }
53
+ if (new TextEncoder().encode(trimmed).byteLength > MAX_POSTGRES_IDENTIFIER_BYTES) {
54
+ return `collection name must be ${MAX_POSTGRES_IDENTIFIER_BYTES} bytes or less`;
55
+ }
56
+ return null;
57
+ }
58
+
10
59
  // src/lib/api.ts
11
60
  init_auth();
12
61
  import { readFileSync } from "fs";
@@ -282,6 +331,10 @@ var ApiClient = class {
282
331
  // Projects — register/lookup via PocketBase records API
283
332
  async registerProject(name, externalId) {
284
333
  const extId = externalId || name;
334
+ const nameError = validateProjectNameLength(name, "App name");
335
+ if (nameError) throw new Error(nameError);
336
+ const externalIdError = validateProjectExternalId(extId);
337
+ if (externalIdError) throw new Error(externalIdError);
285
338
  const existing = await this.getProjectByExternalId(extId);
286
339
  if (existing) {
287
340
  return existing;
@@ -364,6 +417,9 @@ function createApiClient(token, baseUrl, projectExternalId) {
364
417
  }
365
418
 
366
419
  export {
420
+ validateProjectNameLength,
421
+ validateProjectExternalId,
422
+ validateCollectionNameLength,
367
423
  userContextHeaders,
368
424
  ApiError,
369
425
  isApiErrorStatus,
@@ -4,7 +4,7 @@ import {
4
4
  import {
5
5
  createApiClient,
6
6
  isApiErrorStatus
7
- } from "./chunk-ML7XB7Z2.js";
7
+ } from "./chunk-JMRD5WIY.js";
8
8
  import {
9
9
  findProjectRoot,
10
10
  getAppName
@@ -1,6 +1,8 @@
1
1
  import {
2
- userContextHeaders
3
- } from "./chunk-ML7XB7Z2.js";
2
+ userContextHeaders,
3
+ validateProjectExternalId,
4
+ validateProjectNameLength
5
+ } from "./chunk-JMRD5WIY.js";
4
6
  import {
5
7
  fetchWithRetry
6
8
  } from "./chunk-FJFIWC7G.js";
@@ -63,6 +65,10 @@ async function createTarball(distDir) {
63
65
  });
64
66
  }
65
67
  async function ensureAppRecord(apiBase, token, appName, appTitle) {
68
+ const appIdError = validateProjectExternalId(appName);
69
+ if (appIdError) throw new Error(appIdError);
70
+ const appNameError = validateProjectNameLength(appTitle, "App name");
71
+ if (appNameError) throw new Error(appNameError);
66
72
  const filterParam = encodeURIComponent(JSON.stringify({ external_id: appName }));
67
73
  const searchRes = await fetchWithRetry(
68
74
  `${apiBase}/pb/collections/lm_custom_apps/records?filter=${filterParam}`,
@@ -1,9 +1,9 @@
1
1
  import {
2
2
  deps,
3
3
  syncDeps
4
- } from "./chunk-S7FJU4GM.js";
4
+ } from "./chunk-RH4FQF6O.js";
5
5
  import "./chunk-2CR762KB.js";
6
- import "./chunk-ML7XB7Z2.js";
6
+ import "./chunk-JMRD5WIY.js";
7
7
  import "./chunk-ZH3NVYEQ.js";
8
8
  import "./chunk-FJFIWC7G.js";
9
9
  import "./chunk-PNKVD2UK.js";
@@ -1,15 +1,15 @@
1
1
  import {
2
2
  dev
3
- } from "./chunk-PY4J2U2M.js";
3
+ } from "./chunk-XT4EGGK7.js";
4
4
  import {
5
5
  syncDeps
6
- } from "./chunk-S7FJU4GM.js";
6
+ } from "./chunk-RH4FQF6O.js";
7
7
  import {
8
8
  loadEnv
9
9
  } from "./chunk-2CR762KB.js";
10
10
  import {
11
11
  createApiClient
12
- } from "./chunk-ML7XB7Z2.js";
12
+ } from "./chunk-JMRD5WIY.js";
13
13
  import {
14
14
  findProjectRoot,
15
15
  getApiUrl,
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-FLFD64X2.js").then((m) => m.plan(args.slice(1)));
222
+ await import("./resources-Z5ZMPWTK.js").then((m) => m.plan(args.slice(1)));
223
223
  break;
224
224
  case "apply":
225
- await import("./resources-FLFD64X2.js").then((m) => m.apply(args.slice(1)));
225
+ await import("./resources-Z5ZMPWTK.js").then((m) => m.apply(args.slice(1)));
226
226
  break;
227
227
  case "pull":
228
- await import("./resources-FLFD64X2.js").then((m) => m.pull(args.slice(1)));
228
+ await import("./resources-Z5ZMPWTK.js").then((m) => m.pull(args.slice(1)));
229
229
  break;
230
230
  case "destroy":
231
- await import("./resources-FLFD64X2.js").then((m) => m.destroy(args.slice(1)));
231
+ await import("./resources-Z5ZMPWTK.js").then((m) => m.destroy(args.slice(1)));
232
232
  break;
233
233
  case "list":
234
- await import("./resources-FLFD64X2.js").then((m) => m.list(args.slice(1)));
234
+ await import("./resources-Z5ZMPWTK.js").then((m) => m.list(args.slice(1)));
235
235
  break;
236
236
  case "show":
237
- await import("./resources-FLFD64X2.js").then((m) => m.show(args.slice(1)));
237
+ await import("./resources-Z5ZMPWTK.js").then((m) => m.show(args.slice(1)));
238
238
  break;
239
239
  case "diff":
240
- await import("./resources-FLFD64X2.js").then((m) => m.diff(args.slice(1)));
240
+ await import("./resources-Z5ZMPWTK.js").then((m) => m.diff(args.slice(1)));
241
241
  break;
242
242
  // Development
243
243
  case "dev":
244
- await import("./dev-O6TXHGAK.js").then((m) => m.dev(args.slice(1)));
244
+ await import("./dev-EQK27BUM.js").then((m) => m.dev(args.slice(1)));
245
245
  break;
246
246
  case "run":
247
- await import("./run-NR3725J2.js").then((m) => m.run(args.slice(1)));
247
+ await import("./run-LHHX7UBN.js").then((m) => m.run(args.slice(1)));
248
248
  break;
249
249
  // Project
250
250
  case "init":
251
- await import("./init-HUV5ZJAE.js").then((m) => m.init(args.slice(1)));
251
+ await import("./init-FTDMPFR3.js").then((m) => m.init(args.slice(1)));
252
252
  break;
253
253
  case "register":
254
- await import("./register-OZDOFUPF.js").then((m) => m.register(args.slice(1)));
254
+ await import("./register-BQCVUPWS.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)));
@@ -268,7 +268,7 @@ async function main() {
268
268
  break;
269
269
  // Dependencies
270
270
  case "deps":
271
- await import("./deps-H4QYLGM7.js").then((m) => m.deps(args.slice(1)));
271
+ await import("./deps-VTLXKCQ7.js").then((m) => m.deps(args.slice(1)));
272
272
  break;
273
273
  // Auth
274
274
  case "login":
@@ -6,8 +6,9 @@ import {
6
6
  spinner
7
7
  } from "./chunk-BHYDYR75.js";
8
8
  import {
9
- createApiClient
10
- } from "./chunk-ML7XB7Z2.js";
9
+ createApiClient,
10
+ validateProjectNameLength
11
+ } from "./chunk-JMRD5WIY.js";
11
12
  import {
12
13
  getToken,
13
14
  init_auth,
@@ -26,7 +27,7 @@ import pc from "picocolors";
26
27
  import prompts from "prompts";
27
28
  import { execSync } from "child_process";
28
29
  import { copyFileSync, existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync, rmSync, readlinkSync, symlinkSync } from "fs";
29
- import { join, resolve } from "path";
30
+ import { dirname, join, resolve } from "path";
30
31
  function toTitleCase(str) {
31
32
  return str.split(/[-_]/).map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join(" ");
32
33
  }
@@ -42,6 +43,84 @@ function processTemplate(content, replacements) {
42
43
  var TEMPLATE_EXCLUDE = /* @__PURE__ */ new Set(["template.json"]);
43
44
  var TEMPLATE_RENAMES = /* @__PURE__ */ new Map([["_gitignore", ".gitignore"]]);
44
45
  var TEMPLATE_RAW_COPY = /* @__PURE__ */ new Set(["pnpm-lock.yaml"]);
46
+ var STUDIO_PROBLEM_FIRST_FLAG = "studio_problem_first";
47
+ var MEMORY_AGENTS_SECTION = `## Project Memory
48
+
49
+ This project may use repo-backed memory in \`memory/\`.
50
+
51
+ Before substantial work, read \`memory/README.md\` if it exists, then any
52
+ relevant memory files.
53
+
54
+ Update memory when the user confirms a durable project decision, workflow
55
+ rule, data contract, naming convention, recurring assumption, or correction
56
+ that should survive future sessions.
57
+
58
+ Do not store secrets, credentials, tokens, private keys, or raw sensitive
59
+ source files in memory.
60
+
61
+ If a memory item affects app behavior, do not rely only on repo memory.
62
+ Persist it to the app's runtime source of truth, such as collections or config,
63
+ and expose it in the app UI or a clear review/edit path.
64
+
65
+ Summarize memory updates in your final response.
66
+ `;
67
+ var MEMORY_FILES = /* @__PURE__ */ new Map([
68
+ ["README.md", `# Project Memory
69
+
70
+ Repo-backed memory keeps durable project context available across agent
71
+ sessions. Keep it concise and reviewable.
72
+
73
+ Use this folder for:
74
+ - confirmed project decisions
75
+ - durable workflow rules and mappings
76
+ - data contracts and source-file assumptions
77
+ - runbooks for recurring work
78
+ - open questions that should carry forward
79
+
80
+ Do not store secrets, credentials, tokens, private keys, or raw sensitive source
81
+ files here.
82
+
83
+ If a memory item affects app behavior, also persist it to the app's runtime
84
+ source of truth, such as collections or config, and expose it in the app UI or a
85
+ clear review/edit path.
86
+ `],
87
+ ["project-context.md", `# Project Context
88
+
89
+ Summarize what this app does, who uses it, and the recurring workflow it
90
+ supports.
91
+ `],
92
+ ["decisions.md", `# Decisions
93
+
94
+ Record durable decisions that future sessions should preserve.
95
+
96
+ | Date | Decision | Reason |
97
+ | --- | --- | --- |
98
+ `],
99
+ ["open-questions.md", `# Open Questions
100
+
101
+ Track unresolved questions that should be answered before durable workflow
102
+ behavior is finalized.
103
+ `],
104
+ ["rules/README.md", `# Rules
105
+
106
+ Document durable business rules, validation rules, thresholds, and exception
107
+ handling expectations.
108
+ `],
109
+ ["mappings/README.md", `# Mappings
110
+
111
+ Document source-to-target mappings, normalization rules, and user-approved
112
+ corrections.
113
+ `],
114
+ ["data-contracts/README.md", `# Data Contracts
115
+
116
+ Document expected source files, required columns, data types, and known source
117
+ system assumptions.
118
+ `],
119
+ ["runbooks/README.md", `# Runbooks
120
+
121
+ Document repeatable operating steps for recurring workflows.
122
+ `]
123
+ ]);
45
124
  function copyDir(src, dest, replacements, isRoot = true) {
46
125
  if (!existsSync(dest)) {
47
126
  mkdirSync(dest, { recursive: true });
@@ -81,6 +160,35 @@ function ensureClaudeInstructionsLink(projectRoot) {
81
160
  writeFileSync(claudeMdPath, readFileSync(agentsMdPath, "utf-8"));
82
161
  }
83
162
  }
163
+ async function isStudioProblemFirstScaffoldEnabled(targetDir) {
164
+ try {
165
+ const token = getToken(targetDir);
166
+ const api = createApiClient(token);
167
+ const me = await api.getMe();
168
+ return me.user?.feature_flags?.[STUDIO_PROBLEM_FIRST_FLAG] === true;
169
+ } catch {
170
+ return false;
171
+ }
172
+ }
173
+ function writeFileIfMissing(path, content) {
174
+ if (existsSync(path)) return;
175
+ mkdirSync(dirname(path), { recursive: true });
176
+ writeFileSync(path, content);
177
+ }
178
+ function applyProblemFirstMemoryScaffold(projectRoot) {
179
+ const agentsMdPath = join(projectRoot, "AGENTS.md");
180
+ if (existsSync(agentsMdPath)) {
181
+ const agentsMd = readFileSync(agentsMdPath, "utf-8");
182
+ if (!agentsMd.includes("## Project Memory")) {
183
+ writeFileSync(agentsMdPath, `${agentsMd.trimEnd()}
184
+
185
+ ${MEMORY_AGENTS_SECTION}`);
186
+ }
187
+ }
188
+ for (const [relativePath, content] of MEMORY_FILES) {
189
+ writeFileIfMissing(join(projectRoot, "memory", relativePath), content);
190
+ }
191
+ }
84
192
  function isGitInstalled() {
85
193
  try {
86
194
  execSync("git --version", { stdio: "ignore" });
@@ -280,6 +388,8 @@ async function init(args) {
280
388
  initial: "my-app",
281
389
  validate: (value) => {
282
390
  if (!value) return "Project name is required";
391
+ const lengthError = validateProjectNameLength(value, "Project name");
392
+ if (lengthError) return lengthError;
283
393
  if (!/^[a-z0-9-]+$/.test(value)) {
284
394
  return "Use lowercase letters, numbers, and hyphens only";
285
395
  }
@@ -301,6 +411,11 @@ async function init(args) {
301
411
  console.log(pc.red(" Error: Project name must use lowercase letters, numbers, and hyphens only"));
302
412
  process.exit(1);
303
413
  }
414
+ const projectNameLengthError = validateProjectNameLength(finalProjectName, "Project name");
415
+ if (projectNameLengthError) {
416
+ console.log(pc.red(` Error: ${projectNameLengthError}`));
417
+ process.exit(1);
418
+ }
304
419
  if (!directory) {
305
420
  if (nonInteractive) {
306
421
  directory = finalProjectName;
@@ -366,7 +481,11 @@ async function init(args) {
366
481
  [sourceName, finalProjectName],
367
482
  [sourceTitle, projectTitle]
368
483
  ];
484
+ const problemFirstScaffold = await isStudioProblemFirstScaffoldEnabled(targetDir);
369
485
  copyDir(templateDir, targetDir, replacements);
486
+ if (problemFirstScaffold) {
487
+ applyProblemFirstMemoryScaffold(targetDir);
488
+ }
370
489
  ensureClaudeInstructionsLink(targetDir);
371
490
  const installCommand = existsSync(join(targetDir, "pnpm-lock.yaml")) ? "pnpm install --frozen-lockfile" : "pnpm install";
372
491
  function listFiles(dir, prefix = "") {
@@ -5,8 +5,9 @@ import {
5
5
  spinner
6
6
  } from "./chunk-BHYDYR75.js";
7
7
  import {
8
- createApiClient
9
- } from "./chunk-ML7XB7Z2.js";
8
+ createApiClient,
9
+ validateProjectExternalId
10
+ } from "./chunk-JMRD5WIY.js";
10
11
  import {
11
12
  findProjectRoot,
12
13
  getAppName,
@@ -45,6 +46,11 @@ async function register(args) {
45
46
  const projectRoot = findProjectRoot();
46
47
  loadEnv(projectRoot);
47
48
  const appName = getAppName(projectRoot);
49
+ const appNameError = validateProjectExternalId(appName);
50
+ if (appNameError) {
51
+ console.error(pc.red(` ${appNameError}`));
52
+ process.exit(1);
53
+ }
48
54
  const existingId = getProjectId(projectRoot);
49
55
  if (existingId && !force) {
50
56
  console.log();
@@ -1,16 +1,17 @@
1
1
  import {
2
2
  deploy
3
- } from "./chunk-PY4J2U2M.js";
3
+ } from "./chunk-XT4EGGK7.js";
4
4
  import {
5
5
  syncDeps
6
- } from "./chunk-S7FJU4GM.js";
6
+ } from "./chunk-RH4FQF6O.js";
7
7
  import {
8
8
  loadEnv
9
9
  } from "./chunk-2CR762KB.js";
10
10
  import {
11
11
  ApiError,
12
- createApiClient
13
- } from "./chunk-ML7XB7Z2.js";
12
+ createApiClient,
13
+ validateCollectionNameLength
14
+ } from "./chunk-JMRD5WIY.js";
14
15
  import {
15
16
  findProjectRoot,
16
17
  getApiUrl,
@@ -1261,6 +1262,11 @@ function loadLocalCollections(platformDir, filterName) {
1261
1262
  errors.push(`${file}: missing name field`);
1262
1263
  continue;
1263
1264
  }
1265
+ const collectionNameError = validateCollectionNameLength(collection.name);
1266
+ if (collectionNameError) {
1267
+ errors.push(`${file}: ${collectionNameError}`);
1268
+ continue;
1269
+ }
1264
1270
  if (/\s/.test(collection.name)) {
1265
1271
  errors.push(`${file}: collection name "${collection.name}" contains spaces - use underscores instead`);
1266
1272
  continue;
@@ -3,7 +3,7 @@ import {
3
3
  } from "./chunk-2CR762KB.js";
4
4
  import {
5
5
  createApiClient
6
- } from "./chunk-ML7XB7Z2.js";
6
+ } from "./chunk-JMRD5WIY.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.19.21",
3
+ "version": "0.19.23",
4
4
  "description": "CLI for building and deploying Lumera apps",
5
5
  "type": "module",
6
6
  "engines": {