@lumerahq/cli 0.33.1 → 0.34.2

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.
@@ -1,8 +1,9 @@
1
1
  import {
2
- RELEASE_SDK_REQUIREMENT,
3
2
  findFunctionsProjectRoot
4
- } from "./chunk-BCSUKRDA.js";
5
- import "./chunk-PNKVD2UK.js";
3
+ } from "./chunk-XZ5VCTGY.js";
4
+ import {
5
+ RELEASE_SDK_REQUIREMENT
6
+ } from "./chunk-5FGDEZSJ.js";
6
7
 
7
8
  // src/commands/project.ts
8
9
  import { spawn } from "child_process";
@@ -13,6 +14,7 @@ var RELEASE_COMMANDS = /* @__PURE__ */ new Set([
13
14
  "inspect",
14
15
  "contract-test",
15
16
  "deploy",
17
+ "deploy-project",
16
18
  "deploy-runtime"
17
19
  ]);
18
20
  function showHelp() {
@@ -22,12 +24,13 @@ ${pc.dim("Usage:")}
22
24
  lumera project release inspect <archive> [--expected-digest <sha256>]
23
25
  lumera project release contract-test <archive> [--mode api|function|all]
24
26
  lumera project release deploy <archive> --project <project-id>
27
+ lumera project release deploy-project --project <project-id>
25
28
  lumera project release deploy-runtime <release-id> --project <project-id>
26
29
 
27
30
  ${pc.dim("Description:")}
28
31
  Build, verify, and contract-test immutable Python 3.13/Linux/ARM64 project
29
- releases. The deploy command uploads an inactive resource and reports the
30
- required deploy-runtime command that activates it.
32
+ releases. deploy-project builds, uploads, and activates the complete project
33
+ runtime used by App APIs, Functions, and Automations.
31
34
  `);
32
35
  }
33
36
  function buildReleaseRunnerArgs(command, args, projectRoot) {
@@ -48,11 +51,11 @@ function buildReleaseRunnerArgs(command, args, projectRoot) {
48
51
  ...args
49
52
  ];
50
53
  }
51
- async function runReleaseRunner(runnerArgs, projectRoot, spawnRunner = (command, args, options) => spawn(command, args, options)) {
54
+ async function runReleaseRunner(runnerArgs, projectRoot, spawnRunner = (command, args, options) => spawn(command, args, options), environment = process.env) {
52
55
  return await new Promise((resolvePromise, reject) => {
53
56
  const child = spawnRunner("uv", runnerArgs, {
54
57
  cwd: projectRoot,
55
- env: process.env,
58
+ env: environment,
56
59
  shell: false,
57
60
  stdio: ["inherit", "pipe", "inherit"]
58
61
  });
@@ -116,8 +119,9 @@ async function project(subcommand, args, dependencies = {}) {
116
119
  process.exitCode = 1;
117
120
  }
118
121
  }
122
+
119
123
  export {
120
124
  buildReleaseRunnerArgs,
121
- project,
122
- runReleaseRunner
125
+ runReleaseRunner,
126
+ project
123
127
  };
@@ -0,0 +1,12 @@
1
+ // src/lib/functions-sdk.ts
2
+ var FUNCTIONS_SDK_VERSION_RANGE = ">=0.45.0,<0.46.0";
3
+ var FUNCTIONS_SDK_COMPATIBILITY_RANGE = ">=0.34.0,<0.46.0";
4
+ var FUNCTIONS_SDK_REQUIREMENT = `lumera[functions]${FUNCTIONS_SDK_COMPATIBILITY_RANGE}`;
5
+ var DEFAULT_FUNCTIONS_SDK_REQUIREMENT = `lumera[functions]${FUNCTIONS_SDK_VERSION_RANGE}`;
6
+ var RELEASE_SDK_REQUIREMENT = `lumera[release]${FUNCTIONS_SDK_VERSION_RANGE}`;
7
+
8
+ export {
9
+ FUNCTIONS_SDK_REQUIREMENT,
10
+ DEFAULT_FUNCTIONS_SDK_REQUIREMENT,
11
+ RELEASE_SDK_REQUIREMENT
12
+ };
@@ -1,3 +1,11 @@
1
+ import {
2
+ parseProjectTOML,
3
+ projectConfigTable
4
+ } from "./chunk-UGAIWMQV.js";
5
+ import {
6
+ buildReleaseRunnerArgs,
7
+ runReleaseRunner
8
+ } from "./chunk-2EHJH2QU.js";
1
9
  import {
2
10
  userContextHeaders
3
11
  } from "./chunk-W57NUJFJ.js";
@@ -8,168 +16,10 @@ import {
8
16
  // src/lib/deploy.ts
9
17
  import { execSync, spawn } from "child_process";
10
18
  import { createConnection } from "net";
11
- import { existsSync as existsSync2, readFileSync } from "fs";
12
- import { resolve as resolve2, dirname } from "path";
13
- import archiver2 from "archiver";
14
- import pc from "picocolors";
15
-
16
- // src/lib/app-runtime-deploy.ts
17
- import { execFileSync } from "child_process";
18
- import {
19
- cpSync,
20
- existsSync,
21
- mkdtempSync,
22
- rmSync
23
- } from "fs";
24
- import { tmpdir } from "os";
25
- import { basename, join, relative, resolve, sep } from "path";
19
+ import { existsSync, readFileSync } from "fs";
20
+ import { resolve, dirname } from "path";
26
21
  import archiver from "archiver";
27
- var APP_RUNTIME_RELATIVE_DIR = join("app-api", "lambda");
28
- var APP_RUNTIME_PYTHON_VERSION = "3.13";
29
- var APP_RUNTIME_PYTHON_PLATFORM = "aarch64-manylinux_2_34";
30
- var MAX_APP_RUNTIME_ZIP_BYTES = 45 * 1024 * 1024;
31
- var APP_RUNTIME_TARGET_ARGS = [
32
- "--python-version",
33
- APP_RUNTIME_PYTHON_VERSION,
34
- "--python-platform",
35
- APP_RUNTIME_PYTHON_PLATFORM
36
- ];
37
- var EXCLUDED_SOURCE_DIRECTORIES = /* @__PURE__ */ new Set([
38
- ".git",
39
- ".venv",
40
- "venv",
41
- "__pycache__",
42
- ".pytest_cache",
43
- ".ruff_cache",
44
- ".lambda-build",
45
- "build",
46
- "dist"
47
- ]);
48
- function resolveAppRuntimeDirectory(projectRoot) {
49
- const runtimeDir = resolve(projectRoot, APP_RUNTIME_RELATIVE_DIR);
50
- if (!existsSync(runtimeDir)) return void 0;
51
- if (!existsSync(join(runtimeDir, "server.py"))) {
52
- throw new Error(`App API directory exists but is missing ${join(APP_RUNTIME_RELATIVE_DIR, "server.py")}`);
53
- }
54
- return runtimeDir;
55
- }
56
- function shouldCopyRuntimeSource(runtimeDir, sourcePath) {
57
- const sourceRelativePath = relative(runtimeDir, sourcePath);
58
- if (!sourceRelativePath) return true;
59
- const segments = sourceRelativePath.split(sep);
60
- if (segments.some((segment) => EXCLUDED_SOURCE_DIRECTORIES.has(segment))) return false;
61
- if (segments.some((segment) => segment === ".env" || segment.startsWith(".env."))) return false;
62
- const name = basename(sourcePath);
63
- if (name === "lambda.zip") return false;
64
- return !name.endsWith(".pyc") && !name.endsWith(".pyo");
65
- }
66
- function copyRuntimeSources(runtimeDir, buildDir) {
67
- cpSync(runtimeDir, buildDir, {
68
- recursive: true,
69
- filter: (candidate) => shouldCopyRuntimeSource(runtimeDir, candidate)
70
- });
71
- }
72
- function ensureUVAvailable(runCommand) {
73
- try {
74
- runCommand("uv", ["--version"], { stdio: "ignore" });
75
- } catch (error) {
76
- throw new Error(
77
- "uv is required to package App APIs. Install uv: https://docs.astral.sh/uv/",
78
- { cause: error }
79
- );
80
- }
81
- }
82
- async function createZip(directory) {
83
- return new Promise((resolveZip, reject) => {
84
- const chunks = [];
85
- const archive = archiver("zip", { zlib: { level: 9 } });
86
- archive.on("data", (chunk) => chunks.push(chunk));
87
- archive.on("end", () => resolveZip(Buffer.concat(chunks)));
88
- archive.on("error", reject);
89
- archive.directory(directory, false);
90
- archive.finalize();
91
- });
92
- }
93
- async function packageAppRuntime(runtimeDir, options = {}) {
94
- if (!existsSync(join(runtimeDir, "server.py"))) {
95
- throw new Error(`App API must contain server.py at ${join(runtimeDir, "server.py")}`);
96
- }
97
- const requirementsPath = join(runtimeDir, "requirements.txt");
98
- const pyprojectPath = join(runtimeDir, "pyproject.toml");
99
- if (!existsSync(requirementsPath) && !existsSync(pyprojectPath)) {
100
- throw new Error("App API must contain requirements.txt or pyproject.toml");
101
- }
102
- const buildDir = mkdtempSync(join(tmpdir(), "lumera-app-runtime-"));
103
- const runCommand = options.runCommand ?? execFileSync;
104
- try {
105
- ensureUVAvailable(runCommand);
106
- let installRequirementsPath = requirementsPath;
107
- let generatedRequirements = false;
108
- if (!existsSync(requirementsPath)) {
109
- installRequirementsPath = join(buildDir, "requirements.generated.txt");
110
- generatedRequirements = true;
111
- runCommand(
112
- "uv",
113
- [
114
- "pip",
115
- "compile",
116
- "pyproject.toml",
117
- ...APP_RUNTIME_TARGET_ARGS,
118
- "-o",
119
- installRequirementsPath
120
- ],
121
- {
122
- cwd: runtimeDir,
123
- stdio: "inherit"
124
- }
125
- );
126
- }
127
- runCommand("uv", [
128
- "pip",
129
- "install",
130
- "--target",
131
- buildDir,
132
- ...APP_RUNTIME_TARGET_ARGS,
133
- "--only-binary=:all:",
134
- "-r",
135
- installRequirementsPath
136
- ], { stdio: "inherit" });
137
- if (generatedRequirements) {
138
- rmSync(installRequirementsPath, { force: true });
139
- }
140
- copyRuntimeSources(runtimeDir, buildDir);
141
- const lambdaZip = await createZip(buildDir);
142
- if (lambdaZip.length > MAX_APP_RUNTIME_ZIP_BYTES) {
143
- throw new Error(
144
- `App API package is too large (${lambdaZip.length} bytes > ${MAX_APP_RUNTIME_ZIP_BYTES}); remove heavy dependencies or split the app`
145
- );
146
- }
147
- return lambdaZip;
148
- } finally {
149
- rmSync(buildDir, { recursive: true, force: true });
150
- }
151
- }
152
- async function deployAppRuntime(options) {
153
- const formData = new FormData();
154
- formData.append("lambda_zip", new Blob([new Uint8Array(options.lambdaZip)]), "lambda.zip");
155
- formData.append("version", options.version);
156
- const res = await fetchWithRetry(`${options.apiUrl}/app-runtimes/current-project/deploy`, {
157
- method: "POST",
158
- headers: {
159
- Authorization: `Bearer ${options.token}`,
160
- "X-Lumera-Client": "lumera-cli",
161
- "X-Lumera-Project": options.projectExternalId,
162
- ...userContextHeaders()
163
- },
164
- body: formData
165
- });
166
- if (!res.ok) {
167
- throw new Error(`App API deploy failed: ${await res.text()}`);
168
- }
169
- await res.arrayBuffer();
170
- }
171
-
172
- // src/lib/deploy.ts
22
+ import pc from "picocolors";
173
23
  function formatTimestampVersion(date = /* @__PURE__ */ new Date()) {
174
24
  const pad = (value) => String(value).padStart(2, "0");
175
25
  return [
@@ -183,7 +33,7 @@ function formatTimestampVersion(date = /* @__PURE__ */ new Date()) {
183
33
  ].join("") + "Z";
184
34
  }
185
35
  function resolveVersion(cwd, gitRelease = resolveGitReleaseState(cwd)) {
186
- const pkgPath = resolve2(cwd, "package.json");
36
+ const pkgPath = resolve(cwd, "package.json");
187
37
  try {
188
38
  const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
189
39
  if (pkg?.version && pkg.version !== "0.0.0") return String(pkg.version);
@@ -195,13 +45,13 @@ function resolveVersion(cwd, gitRelease = resolveGitReleaseState(cwd)) {
195
45
  function resolveUIVersion(cwd) {
196
46
  try {
197
47
  const installed = JSON.parse(
198
- readFileSync(resolve2(cwd, "node_modules/@lumerahq/ui/package.json"), "utf-8")
48
+ readFileSync(resolve(cwd, "node_modules/@lumerahq/ui/package.json"), "utf-8")
199
49
  );
200
50
  if (installed?.version) return String(installed.version);
201
51
  } catch {
202
52
  }
203
53
  try {
204
- const pkg = JSON.parse(readFileSync(resolve2(cwd, "package.json"), "utf-8"));
54
+ const pkg = JSON.parse(readFileSync(resolve(cwd, "package.json"), "utf-8"));
205
55
  const spec = pkg?.dependencies?.["@lumerahq/ui"] ?? pkg?.devDependencies?.["@lumerahq/ui"];
206
56
  if (!spec) return "none";
207
57
  const cleaned = String(spec).replace(/^[\^~>=<\s]+/, "");
@@ -237,53 +87,84 @@ function resolveGitReleaseState(cwd) {
237
87
  }
238
88
  }
239
89
  async function createTarball(distDir) {
240
- return new Promise((resolve3, reject) => {
90
+ return new Promise((resolve2, reject) => {
241
91
  const chunks = [];
242
- const archive = archiver2("tar", { gzip: true });
92
+ const archive = archiver("tar", { gzip: true });
243
93
  archive.on("data", (chunk) => chunks.push(chunk));
244
- archive.on("end", () => resolve3(Buffer.concat(chunks)));
94
+ archive.on("end", () => resolve2(Buffer.concat(chunks)));
245
95
  archive.on("error", reject);
246
96
  archive.directory(distDir, false);
247
97
  archive.finalize();
248
98
  });
249
99
  }
250
- async function packageResolvedAppRuntime(appRuntimeDir) {
251
- console.log(pc.dim("Packaging App API..."));
252
- const lambdaZip = await packageAppRuntime(appRuntimeDir);
253
- console.log(pc.dim(`App API package: ${(lambdaZip.length / 1024).toFixed(1)} KB`));
254
- return lambdaZip;
255
- }
256
- async function packageOptionalAppRuntime(projectRoot) {
257
- const appRuntimeDir = resolveAppRuntimeDirectory(projectRoot);
258
- return appRuntimeDir ? packageResolvedAppRuntime(appRuntimeDir) : void 0;
259
- }
260
- async function packageRequiredAppRuntime(projectRoot) {
261
- const appRuntimeDir = resolveAppRuntimeDirectory(projectRoot);
262
- if (!appRuntimeDir) {
263
- throw new Error("App API not found. Expected app-api/lambda/server.py");
100
+ function resolveConfiguredProjectRuntime(projectRoot) {
101
+ const legacyPath = resolve(projectRoot, "app-api", "lambda");
102
+ if (existsSync(legacyPath)) {
103
+ throw new Error(
104
+ "Legacy app-api/lambda deployment has been retired. Run `lumera migrate app-api`, commit the migrated project, and deploy again."
105
+ );
106
+ }
107
+ const configPath = resolve(projectRoot, "pyproject.toml");
108
+ if (!existsSync(configPath)) return { kind: "none" };
109
+ const document = parseProjectTOML(readFileSync(configPath, "utf8"));
110
+ const release = projectConfigTable(document, "tool", "lumera", "release");
111
+ const functions = projectConfigTable(document, "tool", "lumera", "functions");
112
+ const rawEntrypoint = release["app-entrypoint"];
113
+ if (rawEntrypoint !== void 0 && (typeof rawEntrypoint !== "string" || !rawEntrypoint.trim())) {
114
+ throw new Error("tool.lumera.release.app-entrypoint must be module:attribute");
115
+ }
116
+ const modules = functions.modules ?? [];
117
+ if (!Array.isArray(modules) || !modules.every((module) => typeof module === "string")) {
118
+ throw new Error("tool.lumera.functions.modules must be a string array");
264
119
  }
265
- return packageResolvedAppRuntime(appRuntimeDir);
120
+ const entrypoint = typeof rawEntrypoint === "string" ? rawEntrypoint.trim() : void 0;
121
+ const hasFunctions = modules.length > 0;
122
+ if (entrypoint || hasFunctions) {
123
+ return { kind: "release", appEntrypoint: entrypoint };
124
+ }
125
+ return { kind: "none" };
266
126
  }
267
127
  function resolveDeploymentVersion(projectRoot, gitRelease, explicitVersion) {
268
128
  return explicitVersion || process.env.LUMERA_APP_VERSION?.trim() || resolveVersion(projectRoot, gitRelease);
269
129
  }
270
- async function publishAppRuntime(options) {
271
- console.log(pc.cyan("Deploying App API..."));
272
- await deployAppRuntime(options);
273
- console.log(pc.green("App API deployed!"));
130
+ async function deployProjectRuntime(options) {
131
+ console.log(pc.cyan("Deploying project runtime..."));
132
+ const result = await runReleaseRunner(
133
+ buildReleaseRunnerArgs(
134
+ "deploy-project",
135
+ ["--project", options.projectExternalId],
136
+ options.projectRoot
137
+ ),
138
+ options.projectRoot,
139
+ void 0,
140
+ {
141
+ ...process.env,
142
+ LUMERA_BASE_URL: options.apiUrl,
143
+ LUMERA_TOKEN: options.token
144
+ }
145
+ );
146
+ if (result.code !== 0 || !result.envelope.ok) {
147
+ const error = result.envelope.error;
148
+ throw new Error(error?.message || "Project runtime deployment failed");
149
+ }
150
+ console.log(pc.green("Project runtime deployed!"));
274
151
  }
275
152
  async function deployAppAPI(options) {
276
153
  const gitRelease = resolveGitReleaseState(options.projectRoot);
277
154
  assertDeployableGitRelease(gitRelease);
278
- const lambdaZip = await packageRequiredAppRuntime(options.projectRoot);
155
+ const runtime = resolveConfiguredProjectRuntime(options.projectRoot);
156
+ if (runtime.kind === "none" || !runtime.appEntrypoint) {
157
+ throw new Error(
158
+ "App API not found. Configure tool.lumera.release.app-entrypoint."
159
+ );
160
+ }
279
161
  const version = resolveDeploymentVersion(options.projectRoot, gitRelease, options.version);
280
162
  console.log(pc.dim(`Version: ${version}`));
281
- await publishAppRuntime({
163
+ await deployProjectRuntime({
282
164
  apiUrl: options.apiUrl,
283
165
  token: options.token,
284
166
  projectExternalId: options.appName,
285
- version,
286
- lambdaZip
167
+ projectRoot: options.projectRoot
287
168
  });
288
169
  return version;
289
170
  }
@@ -319,13 +200,13 @@ async function deploy(options) {
319
200
  accessLevel,
320
201
  skipAppAPI
321
202
  } = options;
322
- if (!existsSync2(distDir)) {
203
+ if (!existsSync(distDir)) {
323
204
  throw new Error(`dist directory not found: ${distDir}`);
324
205
  }
325
206
  const projectDir = dirname(distDir);
326
207
  const gitRelease = resolveGitReleaseState(projectDir);
327
208
  assertDeployableGitRelease(gitRelease);
328
- const lambdaZip = skipAppAPI ? void 0 : await packageOptionalAppRuntime(projectDir);
209
+ const projectRuntime = skipAppAPI ? { kind: "none" } : resolveConfiguredProjectRuntime(projectDir);
329
210
  console.log(pc.dim("Ensuring app record..."));
330
211
  await ensureAppRecord(apiUrl, token, appName, appTitle, accessLevel);
331
212
  console.log(pc.cyan(`Preparing ${appTitle}...`));
@@ -339,13 +220,12 @@ async function deploy(options) {
339
220
  if (gitRelease.sha) formData.append("git_sha", gitRelease.sha);
340
221
  const uiVersion = resolveUIVersion(projectDir);
341
222
  if (uiVersion) formData.append("ui_version", uiVersion);
342
- if (lambdaZip) {
343
- await publishAppRuntime({
223
+ if (projectRuntime.kind !== "none") {
224
+ await deployProjectRuntime({
344
225
  apiUrl,
345
226
  token,
346
227
  projectExternalId: appName,
347
- version,
348
- lambdaZip
228
+ projectRoot: projectDir
349
229
  });
350
230
  }
351
231
  console.log(pc.cyan(`Deploying ${appTitle} frontend...`));
@@ -391,19 +271,19 @@ async function registerDevApp(apiBase, token, appName, appTitle, appUrl) {
391
271
  }
392
272
  }
393
273
  function isPortInUse(port, host = "127.0.0.1") {
394
- return new Promise((resolve3) => {
274
+ return new Promise((resolve2) => {
395
275
  const socket = createConnection({ port, host });
396
276
  socket.once("connect", () => {
397
277
  socket.destroy();
398
- resolve3(true);
278
+ resolve2(true);
399
279
  });
400
280
  socket.once("error", () => {
401
281
  socket.destroy();
402
- resolve3(false);
282
+ resolve2(false);
403
283
  });
404
284
  socket.setTimeout(1e3, () => {
405
285
  socket.destroy();
406
- resolve3(false);
286
+ resolve2(false);
407
287
  });
408
288
  });
409
289
  }
@@ -423,7 +303,7 @@ function parsePackageManagerSpecifier(value) {
423
303
  }
424
304
  function packageManagerFromPackageJson(projectRoot) {
425
305
  try {
426
- const pkg = JSON.parse(readFileSync(resolve2(projectRoot, "package.json"), "utf-8"));
306
+ const pkg = JSON.parse(readFileSync(resolve(projectRoot, "package.json"), "utf-8"));
427
307
  return parsePackageManagerSpecifier(pkg.packageManager);
428
308
  } catch {
429
309
  return void 0;
@@ -453,7 +333,7 @@ function detectRunner(projectRoot) {
453
333
  ];
454
334
  for (const { pm, file } of lockfileManagers) {
455
335
  const runner = runnerForPackageManager(pm);
456
- if (existsSync2(resolve2(projectRoot, file)) && commandAvailable(runner[0])) return runner;
336
+ if (existsSync(resolve(projectRoot, file)) && commandAvailable(runner[0])) return runner;
457
337
  }
458
338
  for (const pm of ["pnpm", "bun", "yarn", "npm"]) {
459
339
  const runner = runnerForPackageManager(pm);
@@ -506,8 +386,8 @@ async function dev(options) {
506
386
  );
507
387
  const runner = detectRunner(projectRoot);
508
388
  const viteArgs = ["vite", "--port", String(port), "--strictPort"];
509
- const sandboxConfig = resolve2(projectRoot, "vite.sandbox.config.ts");
510
- if (existsSync2(sandboxConfig)) {
389
+ const sandboxConfig = resolve(projectRoot, "vite.sandbox.config.ts");
390
+ if (existsSync(sandboxConfig)) {
511
391
  viteArgs.push("--config", "vite.sandbox.config.ts");
512
392
  }
513
393
  if (host) {
@@ -0,0 +1,26 @@
1
+ // src/lib/project-runtime-config.ts
2
+ import { parse } from "smol-toml";
3
+ function parseProjectTOML(source) {
4
+ try {
5
+ return parse(source);
6
+ } catch (error) {
7
+ throw new Error(`Invalid pyproject.toml: ${error instanceof Error ? error.message : String(error)}`);
8
+ }
9
+ }
10
+ function projectConfigTable(document, ...path) {
11
+ let table = document;
12
+ for (let index = 0; index < path.length; index++) {
13
+ const value = table[path[index]];
14
+ if (value === void 0) return {};
15
+ if (value === null || typeof value !== "object" || Array.isArray(value) || Object.getPrototypeOf(value) !== Object.prototype && Object.getPrototypeOf(value) !== null) {
16
+ throw new Error(`pyproject.toml ${path.slice(0, index + 1).join(".")} must be a table`);
17
+ }
18
+ table = value;
19
+ }
20
+ return table;
21
+ }
22
+
23
+ export {
24
+ parseProjectTOML,
25
+ projectConfigTable
26
+ };
@@ -1,17 +1,12 @@
1
+ import {
2
+ FUNCTIONS_SDK_REQUIREMENT
3
+ } from "./chunk-5FGDEZSJ.js";
4
+
1
5
  // src/commands/functions.ts
2
6
  import { spawn } from "child_process";
3
7
  import { existsSync } from "fs";
4
8
  import { join, resolve } from "path";
5
9
  import pc from "picocolors";
6
-
7
- // src/lib/functions-sdk.ts
8
- var FUNCTIONS_SDK_VERSION_RANGE = ">=0.43.0,<0.44.0";
9
- var FUNCTIONS_SDK_COMPATIBILITY_RANGE = ">=0.34.0,<0.44.0";
10
- var FUNCTIONS_SDK_REQUIREMENT = `lumera[functions]${FUNCTIONS_SDK_COMPATIBILITY_RANGE}`;
11
- var DEFAULT_FUNCTIONS_SDK_REQUIREMENT = `lumera[functions]${FUNCTIONS_SDK_VERSION_RANGE}`;
12
- var RELEASE_SDK_REQUIREMENT = `lumera[release]${FUNCTIONS_SDK_VERSION_RANGE}`;
13
-
14
- // src/commands/functions.ts
15
10
  var SUPPORTED_SUBCOMMANDS = /* @__PURE__ */ new Set(["list", "inspect", "invoke", "test"]);
16
11
  var FUNCTIONS_RUNNER_PROTOCOL_VERSION = 1;
17
12
  function errorMessage(error, fallback) {
@@ -223,7 +218,6 @@ async function functions(subcommand, args, dependencies = {}) {
223
218
  }
224
219
 
225
220
  export {
226
- RELEASE_SDK_REQUIREMENT,
227
221
  emitFunctionCliFailure,
228
222
  findFunctionsProjectRoot,
229
223
  buildFunctionRunnerArgs,
@@ -1,6 +1,10 @@
1
1
  import {
2
2
  dev
3
- } from "./chunk-3MAALYAP.js";
3
+ } from "./chunk-P6GDSHYC.js";
4
+ import "./chunk-UGAIWMQV.js";
5
+ import "./chunk-2EHJH2QU.js";
6
+ import "./chunk-XZ5VCTGY.js";
7
+ import "./chunk-5FGDEZSJ.js";
4
8
  import {
5
9
  syncDeps
6
10
  } from "./chunk-5JNNUIVJ.js";
@@ -4,7 +4,8 @@ import {
4
4
  findFunctionsProjectRoot,
5
5
  functions,
6
6
  runFunctionRunner
7
- } from "./chunk-BCSUKRDA.js";
7
+ } from "./chunk-XZ5VCTGY.js";
8
+ import "./chunk-5FGDEZSJ.js";
8
9
  import "./chunk-PNKVD2UK.js";
9
10
  export {
10
11
  buildFunctionRunnerArgs,
package/dist/index.js CHANGED
@@ -242,40 +242,40 @@ async function main() {
242
242
  switch (command) {
243
243
  // Resource commands
244
244
  case "plan":
245
- await import("./resources-IY3XSYJH.js").then((m) => m.plan(args.slice(1)));
245
+ await import("./resources-IGHWATTH.js").then((m) => m.plan(args.slice(1)));
246
246
  break;
247
247
  case "apply":
248
- await import("./resources-IY3XSYJH.js").then((m) => m.apply(args.slice(1)));
248
+ await import("./resources-IGHWATTH.js").then((m) => m.apply(args.slice(1)));
249
249
  break;
250
250
  case "pull":
251
- await import("./resources-IY3XSYJH.js").then((m) => m.pull(args.slice(1)));
251
+ await import("./resources-IGHWATTH.js").then((m) => m.pull(args.slice(1)));
252
252
  break;
253
253
  case "destroy":
254
- await import("./resources-IY3XSYJH.js").then((m) => m.destroy(args.slice(1)));
254
+ await import("./resources-IGHWATTH.js").then((m) => m.destroy(args.slice(1)));
255
255
  break;
256
256
  case "list":
257
- await import("./resources-IY3XSYJH.js").then((m) => m.list(args.slice(1)));
257
+ await import("./resources-IGHWATTH.js").then((m) => m.list(args.slice(1)));
258
258
  break;
259
259
  case "show":
260
- await import("./resources-IY3XSYJH.js").then((m) => m.show(args.slice(1)));
260
+ await import("./resources-IGHWATTH.js").then((m) => m.show(args.slice(1)));
261
261
  break;
262
262
  case "diff":
263
- await import("./resources-IY3XSYJH.js").then((m) => m.diff(args.slice(1)));
263
+ await import("./resources-IGHWATTH.js").then((m) => m.diff(args.slice(1)));
264
264
  break;
265
265
  // Development
266
266
  case "dev":
267
- await import("./dev-CGQTW7TN.js").then((m) => m.dev(args.slice(1)));
267
+ await import("./dev-24PCZUAT.js").then((m) => m.dev(args.slice(1)));
268
268
  break;
269
269
  case "run":
270
270
  await import("./run-4XFA5ZDJ.js").then((m) => m.run(args.slice(1)));
271
271
  break;
272
272
  case "functions":
273
- await import("./functions-BAJ3PCNO.js").then(
273
+ await import("./functions-5OOHGQT7.js").then(
274
274
  (m) => m.functions(subcommand, args.slice(2))
275
275
  );
276
276
  break;
277
277
  case "project":
278
- await import("./project-O7ZZQXFN.js").then(
278
+ await import("./project-ISKXDHGJ.js").then(
279
279
  (m) => m.project(subcommand, args.slice(2))
280
280
  );
281
281
  break;
@@ -293,7 +293,7 @@ async function main() {
293
293
  await import("./status-NPNIY3QP.js").then((m) => m.status(args.slice(1)));
294
294
  break;
295
295
  case "migrate":
296
- await import("./migrate-NEG3HKH4.js").then((m) => m.migrate(args.slice(1)));
296
+ await import("./migrate-DHNR3ESM.js").then((m) => m.migrate(args.slice(1)));
297
297
  break;
298
298
  // Skills
299
299
  case "skills":
@@ -1,3 +1,10 @@
1
+ import {
2
+ parseProjectTOML,
3
+ projectConfigTable
4
+ } from "./chunk-UGAIWMQV.js";
5
+ import {
6
+ DEFAULT_FUNCTIONS_SDK_REQUIREMENT
7
+ } from "./chunk-5FGDEZSJ.js";
1
8
  import {
2
9
  detectProjectVersion,
3
10
  findProjectRoot
@@ -6,12 +13,15 @@ import "./chunk-PNKVD2UK.js";
6
13
 
7
14
  // src/commands/migrate.ts
8
15
  import pc from "picocolors";
9
- import { existsSync, mkdirSync, renameSync, readdirSync, readFileSync, writeFileSync, rmSync } from "fs";
10
- import { join } from "path";
16
+ import { execFileSync } from "child_process";
17
+ import { existsSync, mkdirSync, renameSync, readdirSync, readFileSync, writeFileSync, rmSync, rmdirSync } from "fs";
18
+ import { join, resolve } from "path";
19
+ import { stringify } from "smol-toml";
11
20
  function showHelp() {
12
21
  console.log(`
13
22
  ${pc.dim("Usage:")}
14
23
  lumera migrate [options]
24
+ lumera migrate app-api [--dry-run]
15
25
 
16
26
  ${pc.dim("Description:")}
17
27
  Upgrade legacy project structure (v0) to the current structure (v1).
@@ -24,8 +34,103 @@ ${pc.dim("Options:")}
24
34
  ${pc.dim("Examples:")}
25
35
  lumera migrate # Migrate to v1 structure
26
36
  lumera migrate --dry-run # Preview migration changes
37
+ lumera migrate app-api # Move a ZIP App API into the project release
27
38
  `);
28
39
  }
40
+ function addReleaseAppEntrypoint(pyproject, projectRoot) {
41
+ const document = parseProjectTOML(pyproject);
42
+ const release = projectConfigTable(document, "tool", "lumera", "release");
43
+ const functions = projectConfigTable(document, "tool", "lumera", "functions");
44
+ if (release["app-entrypoint"] !== void 0) {
45
+ throw new Error("pyproject.toml already defines a release app-entrypoint");
46
+ }
47
+ for (const [section, config] of [["release", release], ["functions", functions]]) {
48
+ const source = config["source-root"];
49
+ if (source !== void 0 && (typeof source !== "string" || !source.trim() || resolve(projectRoot, source) !== resolve(projectRoot, "platform"))) {
50
+ throw new Error(`Cannot migrate App API: tool.lumera.${section}.source-root must resolve to platform. Align the source roots before migrating.`);
51
+ }
52
+ }
53
+ const tool = document.tool ??= {};
54
+ const lumera = tool.lumera ??= {};
55
+ lumera.release = {
56
+ ...release,
57
+ "source-root": "platform",
58
+ "app-entrypoint": "app.server:app",
59
+ "runtime-abi": release["runtime-abi"] ?? "1.0"
60
+ };
61
+ return stringify(document);
62
+ }
63
+ function rewriteAppImports(source, modules) {
64
+ let rewritten = source;
65
+ for (const moduleName of modules) {
66
+ const escaped = moduleName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
67
+ rewritten = rewritten.replace(
68
+ new RegExp(`^(\\s*)import\\s+${escaped}(\\s*(?:#.*)?)$`, "gm"),
69
+ `$1from app import ${moduleName}$2`
70
+ );
71
+ rewritten = rewritten.replace(
72
+ new RegExp(`^(\\s*)from\\s+${escaped}\\s+import\\s+`, "gm"),
73
+ `$1from app.${moduleName} import `
74
+ );
75
+ }
76
+ return rewritten;
77
+ }
78
+ function migrateAppAPI(projectRoot, dryRun) {
79
+ const legacyDir = join(projectRoot, "app-api", "lambda");
80
+ const destination = join(projectRoot, "platform", "app");
81
+ const pyprojectPath = join(projectRoot, "pyproject.toml");
82
+ if (!existsSync(join(legacyDir, "server.py"))) {
83
+ throw new Error("Legacy App API not found at app-api/lambda/server.py");
84
+ }
85
+ if (existsSync(destination)) {
86
+ throw new Error("platform/app already exists; resolve it before migrating the ZIP App API");
87
+ }
88
+ if (!existsSync(pyprojectPath)) {
89
+ throw new Error("pyproject.toml is required for a project release");
90
+ }
91
+ addReleaseAppEntrypoint(readFileSync(pyprojectPath, "utf8"), projectRoot);
92
+ console.log(pc.bold("App API migration plan:"));
93
+ console.log(pc.yellow(" \u2192 app-api/lambda/ \u2192 platform/app/"));
94
+ console.log(pc.blue(" \u270E rewrite local Python imports as app package imports"));
95
+ console.log(pc.blue(" \u270E merge requirements.txt into pyproject.toml and uv.lock"));
96
+ console.log(pc.blue(' \u270E configure app-entrypoint = "app.server:app"'));
97
+ if (dryRun) {
98
+ console.log(pc.dim("\nDry run - no changes made."));
99
+ return;
100
+ }
101
+ const requirements = join(legacyDir, "requirements.txt");
102
+ if (existsSync(requirements) && readFileSync(requirements, "utf8").trim()) {
103
+ execFileSync("uv", ["add", "-r", requirements], { cwd: projectRoot, stdio: "inherit" });
104
+ }
105
+ execFileSync("uv", ["add", DEFAULT_FUNCTIONS_SDK_REQUIREMENT], {
106
+ cwd: projectRoot,
107
+ stdio: "inherit"
108
+ });
109
+ mkdirSync(join(projectRoot, "platform"), { recursive: true });
110
+ renameSync(legacyDir, destination);
111
+ rmSync(join(destination, "requirements.txt"), { force: true });
112
+ if (!existsSync(join(destination, "__init__.py"))) {
113
+ writeFileSync(join(destination, "__init__.py"), "", "utf8");
114
+ }
115
+ const modules = new Set(
116
+ readdirSync(destination, { withFileTypes: true }).filter((entry) => entry.isFile() && entry.name.endsWith(".py")).map((entry) => entry.name.slice(0, -3)).filter((name) => name !== "__init__")
117
+ );
118
+ for (const entry of readdirSync(destination, { withFileTypes: true })) {
119
+ if (!entry.isFile() || !entry.name.endsWith(".py")) continue;
120
+ const path = join(destination, entry.name);
121
+ writeFileSync(path, rewriteAppImports(readFileSync(path, "utf8"), modules), "utf8");
122
+ }
123
+ writeFileSync(
124
+ pyprojectPath,
125
+ addReleaseAppEntrypoint(readFileSync(pyprojectPath, "utf8"), projectRoot),
126
+ "utf8"
127
+ );
128
+ const legacyParent = join(projectRoot, "app-api");
129
+ if (readdirSync(legacyParent).length === 0) {
130
+ rmdirSync(legacyParent);
131
+ }
132
+ console.log(pc.green("\nApp API migration complete. Review imports and paths relative to the project root, commit the migrated project, then run `lumera apply app`."));
133
+ }
29
134
  async function migrate(args) {
30
135
  if (args.includes("--help") || args.includes("-h")) {
31
136
  showHelp();
@@ -39,6 +144,10 @@ async function migrate(args) {
39
144
  console.error(pc.red("Not in a Lumera project directory."));
40
145
  process.exit(1);
41
146
  }
147
+ if (args[0] === "app-api") {
148
+ migrateAppAPI(projectRoot, dryRun);
149
+ return;
150
+ }
42
151
  const version = detectProjectVersion(projectRoot);
43
152
  if (version === 1) {
44
153
  console.log(pc.green("Project is already using v1 structure."));
@@ -0,0 +1,13 @@
1
+ import {
2
+ buildReleaseRunnerArgs,
3
+ project,
4
+ runReleaseRunner
5
+ } from "./chunk-2EHJH2QU.js";
6
+ import "./chunk-XZ5VCTGY.js";
7
+ import "./chunk-5FGDEZSJ.js";
8
+ import "./chunk-PNKVD2UK.js";
9
+ export {
10
+ buildReleaseRunnerArgs,
11
+ project,
12
+ runReleaseRunner
13
+ };
@@ -1,7 +1,11 @@
1
1
  import {
2
2
  deploy,
3
3
  deployAppAPI
4
- } from "./chunk-3MAALYAP.js";
4
+ } from "./chunk-P6GDSHYC.js";
5
+ import "./chunk-UGAIWMQV.js";
6
+ import "./chunk-2EHJH2QU.js";
7
+ import "./chunk-XZ5VCTGY.js";
8
+ import "./chunk-5FGDEZSJ.js";
5
9
  import {
6
10
  syncDeps
7
11
  } from "./chunk-5JNNUIVJ.js";
@@ -1563,8 +1567,8 @@ ${pc2.dim("Resources:")}
1563
1567
 
1564
1568
  ${pc2.dim("Options:")}
1565
1569
  --yes, -y Skip confirmation prompt (for CI/CD)
1566
- --skip-build Skip the frontend build; still package the App API
1567
- --skip-app-api Publish only the frontend, even if an App API exists
1570
+ --skip-build Skip the frontend build; still deploy the project runtime
1571
+ --skip-app-api Publish only the frontend without deploying the project runtime
1568
1572
  --no-app Apply resources only; do not build/deploy the app
1569
1573
  --resources-only Alias for --no-app
1570
1574
  --package-manager Package manager for app builds (pnpm, bun, yarn, npm)
@@ -1576,7 +1580,7 @@ ${pc2.dim("Examples:")}
1576
1580
  lumera apply agents -y # Apply agents without confirmation
1577
1581
  lumera apply app # Deploy frontend and App API
1578
1582
  lumera apply app-api # Deploy only the App API
1579
- lumera apply app --skip-build # Reuse dist; package and deploy App API
1583
+ lumera apply app --skip-build # Reuse dist; deploy the project runtime
1580
1584
  lumera apply app --skip-app-api # Deploy only the frontend
1581
1585
  lumera apply --no-app -y # Apply resources without app build/deploy
1582
1586
  lumera apply app --package-manager pnpm
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lumerahq/cli",
3
- "version": "0.33.1",
3
+ "version": "0.34.2",
4
4
  "description": "CLI for building and deploying Lumera apps",
5
5
  "type": "module",
6
6
  "engines": {
@@ -25,7 +25,8 @@
25
25
  "dotenv": "^16.4.7",
26
26
  "open": "^10.1.0",
27
27
  "picocolors": "^1.1.1",
28
- "prompts": "^2.4.2"
28
+ "prompts": "^2.4.2",
29
+ "smol-toml": "^1.9.0"
29
30
  },
30
31
  "devDependencies": {
31
32
  "@types/archiver": "^6.0.3",
@@ -1,56 +1,38 @@
1
1
  # {{projectTitle}}
2
2
 
3
- > Status: new project — nothing built yet.
4
-
5
- <!--
6
- AGENT CONTRACT — keep this file true.
7
- This is the project's durable memory, shared by every coding agent that works
8
- here. It records what cannot be re-derived from the code: why things are the
9
- way they are, what the business needs, what must not break.
10
- - Update it in the same turn as the change that makes it stale.
11
- - Small focused edits; delete stale lines rather than appending duplicates.
12
- - Leave a section empty rather than padding it.
13
- - Keep this file concise and always under 100 KB. Before an update would cross
14
- that limit—or if the file is already over it—compact it in the same turn by
15
- removing stale, superseded, duplicated, transient, and re-derivable detail
16
- while preserving user-written notes.
17
- - Do NOT put here: platform documentation (skills and prompts carry that),
18
- code inventories, secrets/credentials, or transient chat summaries.
19
- -->
20
-
21
3
  ## What this is
22
4
 
23
- One paragraph: the problem this solves, for whom, and its current shape
5
+ One short paragraph: the problem this solves, for whom, and its current shape
24
6
  (app / automation / agent / one-off analysis). Update when scope changes.
25
7
 
26
- ## Business context
27
-
28
- - Who uses it, when, and for what workflow (roles, cadence — e.g. month-end)
29
- - Domain terms with tenant-specific meaning
30
- - Source-of-truth data and how it arrives (uploads, mailbox, webhooks, integrations)
31
-
32
- ## Data
33
-
34
- - What each collection represents; non-obvious field/status semantics
35
- - Identifiers that map to external systems, and their formats
36
-
37
- ## Decisions
38
-
39
- - YYYY-MM-DD — decision — why.
40
-
41
8
  ## Working agreements
42
9
 
43
- - User preferences: review gates, deploy expectations, formatting
44
-
45
- ## Correctness
46
-
47
- - What "correct" means here: validations, reconciliations, tolerances, approvals
48
- - Known edge cases; behavior when required data is missing
49
-
50
- ## Gotchas
51
-
52
- - Things that look wrong but are intentional; workarounds and why
53
-
54
- ## Follow-ups
55
-
56
- - [ ] Open questions and deferred work
10
+ Keep critical safety rules and user preferences that apply to every task here.
11
+
12
+ ## Managing memory
13
+
14
+ - Keep this file short: ideally a few KB, always under 10 KB. Put detailed
15
+ knowledge in `memories/<topic>.md` and link it from the index below.
16
+ - Save information that helps future agents: useful context, user preferences,
17
+ important decisions and their reasons, proven approaches, and lessons from
18
+ mistakes. Prefer knowledge that saves the user from repeating instructions or
19
+ future agents from rediscovering hard-won solutions. If there is nothing worth
20
+ remembering, do not update memory.
21
+ - Distill what matters, not everything that happened. Keep routine progress and
22
+ temporary task state in the conversation.
23
+ - Include useful paths, commands, and verification steps when they make a lesson
24
+ easier to reuse. For recurring problems, capture the symptom, cause, and proven
25
+ fix. Be honest about uncertainty. Do not turn a one-off request into a standing
26
+ rule. Check changeable facts against their source.
27
+ - Read only relevant topics when needed; search if necessary. Do not load every
28
+ topic or use automatic file imports.
29
+ - Organize by topic, not turn or date. Update existing notes, merge duplicates,
30
+ and remove outdated material. Keep memory files in version control and check
31
+ this file's size and links after changes.
32
+ - Keep secrets out of memory. Link to source documents rather than copying them.
33
+ Data the app needs belongs in collections or app configuration.
34
+
35
+ ## Memory index
36
+
37
+ Add one relative Markdown link with a one-line description per topic after
38
+ creating the file. No topics recorded yet.
File without changes
@@ -4,7 +4,7 @@ version = "0.1.0"
4
4
  description = "{{projectTitle}} - Lumera custom app"
5
5
  requires-python = ">=3.11"
6
6
  dependencies = [
7
- "lumera[functions]>=0.43.0,<0.44.0",
7
+ "lumera[functions]>=0.45.0,<0.46.0",
8
8
  ]
9
9
 
10
10
  [project.optional-dependencies]
@@ -17,3 +17,7 @@ dev = [
17
17
  source-root = "platform"
18
18
  modules = []
19
19
  test-paths = ["tests/functions"]
20
+
21
+ [tool.lumera.release]
22
+ source-root = "platform"
23
+ runtime-abi = "1.0"