@lumerahq/cli 0.24.7 → 0.26.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.
package/README.md CHANGED
@@ -33,10 +33,22 @@ lumera destroy # Delete remote resources
33
33
 
34
34
  lumera run <target> # Run script, automation, or invoke agent
35
35
 
36
+ lumera functions list # List locally configured Functions
37
+ lumera functions inspect invoices:approve
38
+ lumera functions invoke invoices:approve --input input.json --local
39
+ lumera functions test # Validate contracts and run Function tests
40
+
36
41
  lumera flags list # List this sandbox's feature flags
37
42
  lumera flags get <key> # Print one flag's value (--default <v> if unset)
38
43
  ```
39
44
 
45
+ Function commands are local-only in this release. Each command starts a fresh
46
+ SDK runner through `uv` and supplies the compatible Functions SDK as an
47
+ isolated overlay, so an older project lock cannot select a pre-Functions SDK.
48
+ `invoke` requires `--local` so it cannot be mistaken for a deployed invocation.
49
+ Function modules and test locations are configured under
50
+ `[tool.lumera.functions]` in `pyproject.toml`.
51
+
40
52
  ## Scaffolding Projects
41
53
 
42
54
  ### Interactive Mode
@@ -54,7 +66,7 @@ For CI/CD or scripted environments, use `-y` (or `--yes`) flag:
54
66
  lumera init my-app -y # Creates ./my-app
55
67
  lumera init my-app -y --dir ./apps # Creates ./apps
56
68
  lumera init my-app -y --force # Overwrites if directory exists
57
- lumera init my-app -y --no-install # Skip pnpm install
69
+ lumera init my-app -y --no-install # Defer Python and pnpm dependencies
58
70
  ```
59
71
 
60
72
  ### Init Options
@@ -0,0 +1,212 @@
1
+ import "./chunk-PNKVD2UK.js";
2
+
3
+ // src/commands/functions.ts
4
+ import { spawn } from "child_process";
5
+ import { existsSync } from "fs";
6
+ import { join, resolve } from "path";
7
+ import pc from "picocolors";
8
+ var SUPPORTED_SUBCOMMANDS = /* @__PURE__ */ new Set(["list", "inspect", "invoke", "test"]);
9
+ var FUNCTIONS_SDK_REQUIREMENT = "lumera[functions]>=0.30.0,<0.31.0";
10
+ var FUNCTIONS_RUNNER_PROTOCOL_VERSION = 1;
11
+ function errorMessage(error, fallback) {
12
+ return error instanceof Error && error.message ? error.message : fallback;
13
+ }
14
+ function isFunctionProtocolEnvelope(output) {
15
+ try {
16
+ const payload = JSON.parse(output);
17
+ return typeof payload === "object" && payload !== null && !Array.isArray(payload) && "protocol_version" in payload && payload.protocol_version === FUNCTIONS_RUNNER_PROTOCOL_VERSION && "ok" in payload && typeof payload.ok === "boolean";
18
+ } catch {
19
+ return false;
20
+ }
21
+ }
22
+ function emitFunctionCliFailure(code, message, write = (value) => {
23
+ process.stdout.write(value);
24
+ }) {
25
+ write(
26
+ `${JSON.stringify({
27
+ protocol_version: FUNCTIONS_RUNNER_PROTOCOL_VERSION,
28
+ ok: false,
29
+ error: { code, message, retryable: false }
30
+ })}
31
+ `
32
+ );
33
+ process.exitCode = 1;
34
+ }
35
+ function findFunctionsProjectRoot(startDir = process.cwd()) {
36
+ let directory = resolve(startDir);
37
+ while (true) {
38
+ if (existsSync(join(directory, "pyproject.toml"))) {
39
+ return directory;
40
+ }
41
+ const parent = resolve(directory, "..");
42
+ if (parent === directory) break;
43
+ directory = parent;
44
+ }
45
+ throw new Error(
46
+ "Could not find Functions project root (no pyproject.toml found)"
47
+ );
48
+ }
49
+ function showHelp() {
50
+ console.log(`
51
+ ${pc.dim("Usage:")}
52
+ lumera functions list
53
+ lumera functions inspect <function-id>
54
+ lumera functions invoke <function-id> --input <path|-> --local
55
+ lumera functions test [-- <pytest-options>]
56
+
57
+ ${pc.dim("Description:")}
58
+ Discover, inspect, invoke, and test configured Functions in a fresh local
59
+ Python process. Phase 1 does not deploy or invoke Functions remotely.
60
+
61
+ ${pc.dim("Options:")}
62
+ --local Required for invoke; remote invocation is unavailable
63
+ --input <path|-> Read a JSON object from a project file or stdin (-)
64
+ --help, -h Show this help
65
+
66
+ ${pc.dim("Examples:")}
67
+ lumera functions list
68
+ lumera functions inspect invoices:approve
69
+ lumera functions invoke invoices:approve --input input.json --local
70
+ cat input.json | lumera functions invoke invoices:approve --input - --local
71
+ lumera functions test
72
+ lumera functions test -- -k approve
73
+ `);
74
+ }
75
+ function buildFunctionRunnerArgs(subcommand, args, projectRoot) {
76
+ if (!SUPPORTED_SUBCOMMANDS.has(subcommand)) {
77
+ throw new Error(`Unknown functions subcommand: ${subcommand}`);
78
+ }
79
+ if (subcommand === "invoke" && !args.includes("--local")) {
80
+ throw new Error(
81
+ "Function invocation is local-only in Phase 1; pass --local explicitly."
82
+ );
83
+ }
84
+ const runnerPrefix = ["run"];
85
+ if (existsSync(join(projectRoot, "uv.lock"))) {
86
+ runnerPrefix.push("--locked");
87
+ }
88
+ runnerPrefix.push("--with", FUNCTIONS_SDK_REQUIREMENT);
89
+ if (subcommand === "test") {
90
+ runnerPrefix.push("--with", "pytest>=8.0,<10.0");
91
+ }
92
+ return [
93
+ ...runnerPrefix,
94
+ "python",
95
+ "-m",
96
+ "lumera.functions",
97
+ "--project-root",
98
+ projectRoot,
99
+ subcommand,
100
+ ...args
101
+ ];
102
+ }
103
+ async function runFunctionRunner(runnerArgs, projectRoot, spawnRunner = (command, args, options) => spawn(command, args, options), writeOutput = (value) => {
104
+ process.stdout.write(value);
105
+ }, writeDiagnostic = (value) => {
106
+ process.stderr.write(value);
107
+ }) {
108
+ return await new Promise((resolve2, reject) => {
109
+ const child = spawnRunner("uv", runnerArgs, {
110
+ cwd: projectRoot,
111
+ env: process.env,
112
+ shell: false,
113
+ // Buffer the single protocol value so a uv failure before Python starts
114
+ // cannot leave machine callers with stderr and no JSON response.
115
+ stdio: ["inherit", "pipe", "inherit"]
116
+ });
117
+ let protocolOutput = "";
118
+ child.stdout?.setEncoding("utf8");
119
+ child.stdout?.on("data", (chunk) => {
120
+ protocolOutput += chunk;
121
+ });
122
+ let settled = false;
123
+ const forwardSigint = () => child.kill("SIGINT");
124
+ const forwardSigterm = () => child.kill("SIGTERM");
125
+ const cleanup = () => {
126
+ process.off("SIGINT", forwardSigint);
127
+ process.off("SIGTERM", forwardSigterm);
128
+ };
129
+ process.once("SIGINT", forwardSigint);
130
+ process.once("SIGTERM", forwardSigterm);
131
+ child.once("error", (error) => {
132
+ if (settled) return;
133
+ settled = true;
134
+ cleanup();
135
+ if (error.code === "ENOENT") {
136
+ reject(
137
+ new Error(
138
+ "uv is not installed. Install it from https://docs.astral.sh/uv/"
139
+ )
140
+ );
141
+ return;
142
+ }
143
+ reject(new Error(`Failed to start the Function runner: ${error.message}`));
144
+ });
145
+ child.once("close", (code) => {
146
+ if (settled) return;
147
+ settled = true;
148
+ cleanup();
149
+ if (!isFunctionProtocolEnvelope(protocolOutput)) {
150
+ if (protocolOutput) writeDiagnostic(protocolOutput);
151
+ reject(
152
+ new Error(
153
+ `Function runner exited with status ${code ?? 1} before emitting a valid protocol envelope`
154
+ )
155
+ );
156
+ return;
157
+ }
158
+ writeOutput(protocolOutput);
159
+ resolve2(code ?? 1);
160
+ });
161
+ });
162
+ }
163
+ async function functions(subcommand, args, dependencies = {}) {
164
+ if (subcommand === void 0 || subcommand === "help" || subcommand === "--help" || subcommand === "-h" || args.includes("--help") || args.includes("-h")) {
165
+ showHelp();
166
+ return;
167
+ }
168
+ const findProjectRoot = dependencies.findProjectRoot ?? (() => findFunctionsProjectRoot());
169
+ const buildRunnerArgs = dependencies.buildRunnerArgs ?? buildFunctionRunnerArgs;
170
+ const runRunner = dependencies.runRunner ?? runFunctionRunner;
171
+ const emitFailure = dependencies.emitFailure ?? emitFunctionCliFailure;
172
+ let projectRoot;
173
+ try {
174
+ projectRoot = findProjectRoot();
175
+ } catch (error) {
176
+ emitFailure(
177
+ "function_discovery_error",
178
+ errorMessage(error, "Could not locate the Functions project")
179
+ );
180
+ return;
181
+ }
182
+ let runnerArgs;
183
+ try {
184
+ runnerArgs = buildRunnerArgs(subcommand, args, projectRoot);
185
+ } catch (error) {
186
+ emitFailure(
187
+ "invalid_arguments",
188
+ errorMessage(error, "Invalid Functions command arguments")
189
+ );
190
+ return;
191
+ }
192
+ let exitCode;
193
+ try {
194
+ exitCode = await runRunner(runnerArgs, projectRoot);
195
+ } catch (error) {
196
+ emitFailure(
197
+ "function_runner_unavailable",
198
+ errorMessage(error, "The local Function runner is unavailable")
199
+ );
200
+ return;
201
+ }
202
+ if (exitCode !== 0) {
203
+ process.exitCode = exitCode;
204
+ }
205
+ }
206
+ export {
207
+ buildFunctionRunnerArgs,
208
+ emitFunctionCliFailure,
209
+ findFunctionsProjectRoot,
210
+ functions,
211
+ runFunctionRunner
212
+ };
package/dist/index.js CHANGED
@@ -10,6 +10,11 @@ import { dirname as dirname2, join as join2 } from "path";
10
10
  import { fileURLToPath } from "url";
11
11
  import pc from "picocolors";
12
12
 
13
+ // src/lib/command-output.ts
14
+ function commandOwnsStdout(command2) {
15
+ return command2 === "functions";
16
+ }
17
+
13
18
  // src/lib/update-check.ts
14
19
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
15
20
  import { homedir } from "os";
@@ -79,6 +84,7 @@ if (jsonMode) process.env.LUMERA_JSON = "1";
79
84
  var args = rawArgs.filter((a) => a !== "--json");
80
85
  var command = args[0];
81
86
  var subcommand = args[1];
87
+ var ownsStdout = commandOwnsStdout(command);
82
88
  var COMMANDS = [
83
89
  "plan",
84
90
  "apply",
@@ -89,6 +95,7 @@ var COMMANDS = [
89
95
  "diff",
90
96
  "dev",
91
97
  "run",
98
+ "functions",
92
99
  "init",
93
100
  "register",
94
101
  "templates",
@@ -150,6 +157,7 @@ ${pc.dim("Resource Commands:")}
150
157
  ${pc.dim("Development:")}
151
158
  ${pc.cyan("dev")} Start dev server
152
159
  ${pc.cyan("run")} <target> Run script, trigger automation, or invoke agent
160
+ ${pc.cyan("functions")} <command> Develop and test Functions locally
153
161
 
154
162
  ${pc.dim("Project:")}
155
163
  ${pc.cyan("init")} [name] Scaffold a new project
@@ -194,6 +202,9 @@ ${pc.dim("Examples:")}
194
202
  lumera run scripts/seed.py # Run a script
195
203
  lumera run automations/sync # Trigger automation
196
204
  lumera run agents/support "Hello" # Invoke an agent
205
+ lumera functions list # List locally configured Functions
206
+ lumera functions invoke invoices:approve --input input.json --local
207
+ lumera functions test # Validate Function contracts and tests
197
208
  lumera dev # Start dev server
198
209
  lumera flags list # List this sandbox's feature flags
199
210
  lumera flags get studio_browser --default false # one flag (fallback if unset)
@@ -222,7 +233,7 @@ async function main() {
222
233
  }
223
234
  }
224
235
  const startTime = performance.now();
225
- const updateCheck = checkForUpdate(VERSION);
236
+ const updateCheck = ownsStdout ? Promise.resolve(null) : checkForUpdate(VERSION);
226
237
  try {
227
238
  switch (command) {
228
239
  // Resource commands
@@ -254,9 +265,14 @@ async function main() {
254
265
  case "run":
255
266
  await import("./run-WHVUVIYB.js").then((m) => m.run(args.slice(1)));
256
267
  break;
268
+ case "functions":
269
+ await import("./functions-PVOVWMON.js").then(
270
+ (m) => m.functions(subcommand, args.slice(2))
271
+ );
272
+ break;
257
273
  // Project
258
274
  case "init":
259
- await import("./init-B4DXTC7L.js").then((m) => m.init(args.slice(1)));
275
+ await import("./init-CUB3KH6M.js").then((m) => m.init(args.slice(1)));
260
276
  break;
261
277
  case "register":
262
278
  await import("./register-HRLBT4FI.js").then((m) => m.register(args.slice(1)));
@@ -309,14 +325,14 @@ async function main() {
309
325
  process.exit(1);
310
326
  }
311
327
  }
312
- if (!jsonMode) {
328
+ if (!jsonMode && !ownsStdout) {
313
329
  const elapsed = performance.now() - startTime;
314
330
  if (elapsed >= 500) {
315
331
  console.log(pc.dim(`
316
332
  Done in ${formatElapsed(elapsed)}`));
317
333
  }
318
334
  }
319
- if (!jsonMode) {
335
+ if (!jsonMode && !ownsStdout) {
320
336
  try {
321
337
  const update = await Promise.race([
322
338
  updateCheck,
@@ -1,3 +1,7 @@
1
+ import {
2
+ listAllTemplates,
3
+ resolveTemplate
4
+ } from "./chunk-H357NP7T.js";
1
5
  import {
2
6
  installAllSkills,
3
7
  syncClaudeMd
@@ -15,10 +19,6 @@ import {
15
19
  setProjectId
16
20
  } from "./chunk-JLVVHTBY.js";
17
21
  import "./chunk-FJFIWC7G.js";
18
- import {
19
- listAllTemplates,
20
- resolveTemplate
21
- } from "./chunk-H357NP7T.js";
22
22
  import "./chunk-PNKVD2UK.js";
23
23
 
24
24
  // src/commands/init.ts
@@ -130,14 +130,11 @@ function installUv() {
130
130
  return false;
131
131
  }
132
132
  }
133
- function createPythonVenv(targetDir) {
134
- try {
135
- execSync("uv venv", { cwd: targetDir, stdio: "ignore" });
136
- execSync("uv pip install lumera", { cwd: targetDir, stdio: "ignore" });
137
- return true;
138
- } catch {
139
- return false;
140
- }
133
+ function createPythonVenv(targetDir, runCommand = execSync) {
134
+ runCommand("uv sync --extra dev --no-install-project", {
135
+ cwd: targetDir,
136
+ stdio: "ignore"
137
+ });
141
138
  }
142
139
  function detectEditor() {
143
140
  const envEditor = process.env.VISUAL || process.env.EDITOR;
@@ -377,6 +374,7 @@ async function init(args) {
377
374
  copyDir(templateDir, targetDir, replacements);
378
375
  ensureClaudeInstructionsLink(targetDir);
379
376
  const installCommand = existsSync(join(targetDir, "pnpm-lock.yaml")) ? "pnpm install --frozen-lockfile" : "pnpm install";
377
+ const hasPythonProject = existsSync(join(targetDir, "pyproject.toml"));
380
378
  function listFiles(dir, prefix = "") {
381
379
  for (const entry of readdirSync(dir, { withFileTypes: true })) {
382
380
  const relativePath = prefix + entry.name;
@@ -388,6 +386,31 @@ async function init(args) {
388
386
  }
389
387
  }
390
388
  listFiles(targetDir);
389
+ if (opts.install && hasPythonProject) {
390
+ if (!isUvInstalled()) {
391
+ const stopUv = spinner("Installing uv (Python package manager)...");
392
+ if (installUv()) {
393
+ stopUv(pc.green("\u2713") + pc.dim(" uv installed successfully"));
394
+ } else {
395
+ stopUv(pc.red("\u2717") + pc.dim(" Failed to install uv"));
396
+ rmSync(targetDir, { recursive: true, force: true });
397
+ throw new Error(
398
+ "uv is required to resolve this Python project, so the incomplete generated directory was removed. Install uv from https://docs.astral.sh/uv/ and retry initialization, or pass --no-install to defer Python and JavaScript dependencies."
399
+ );
400
+ }
401
+ }
402
+ const stopVenv = spinner("Creating Python venv with Lumera SDK...");
403
+ try {
404
+ createPythonVenv(targetDir);
405
+ stopVenv(pc.green("\u2713") + pc.dim(" Python venv created (.venv/) with lumera SDK"));
406
+ } catch {
407
+ stopVenv(pc.red("\u2717") + pc.dim(" Failed to resolve the Python environment"));
408
+ rmSync(targetDir, { recursive: true, force: true });
409
+ throw new Error(
410
+ "Python dependency resolution failed, so the incomplete generated directory was removed. Verify the declared Lumera SDK is available and retry initialization, or pass --no-install to defer Python and JavaScript dependencies."
411
+ );
412
+ }
413
+ }
391
414
  if (isGitInstalled()) {
392
415
  const stopGit = spinner("Initializing git repository...");
393
416
  if (initGitRepo(targetDir, finalProjectName)) {
@@ -398,24 +421,6 @@ async function init(args) {
398
421
  } else {
399
422
  console.log(pc.yellow(" \u26A0"), pc.dim("Git not found \u2014 skipping repository initialization"));
400
423
  }
401
- let uvAvailable = isUvInstalled();
402
- if (!uvAvailable) {
403
- const stopUv = spinner("Installing uv (Python package manager)...");
404
- if (installUv()) {
405
- stopUv(pc.green("\u2713") + pc.dim(" uv installed successfully"));
406
- uvAvailable = true;
407
- } else {
408
- stopUv(pc.yellow("\u26A0") + pc.dim(" Failed to install uv \u2014 install manually: https://docs.astral.sh/uv/"));
409
- }
410
- }
411
- if (uvAvailable) {
412
- const stopVenv = spinner("Creating Python venv with Lumera SDK...");
413
- if (createPythonVenv(targetDir)) {
414
- stopVenv(pc.green("\u2713") + pc.dim(" Python venv created (.venv/) with lumera SDK"));
415
- } else {
416
- stopVenv(pc.yellow("\u26A0") + pc.dim(" Failed to create Python venv"));
417
- }
418
- }
419
424
  if (opts.install) {
420
425
  const stopInstall = spinner("Installing dependencies...");
421
426
  try {
@@ -467,6 +472,9 @@ async function init(args) {
467
472
  console.log();
468
473
  console.log(pc.cyan(` cd ${finalDirectory}`));
469
474
  if (!opts.install) {
475
+ if (hasPythonProject) {
476
+ console.log(pc.cyan(" uv sync --extra dev --no-install-project"));
477
+ }
470
478
  console.log(pc.cyan(` ${installCommand}`));
471
479
  }
472
480
  if (!registered) {
@@ -481,5 +489,6 @@ async function init(args) {
481
489
  }
482
490
  }
483
491
  export {
492
+ createPythonVenv,
484
493
  init
485
494
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lumerahq/cli",
3
- "version": "0.24.7",
3
+ "version": "0.26.0",
4
4
  "description": "CLI for building and deploying Lumera apps",
5
5
  "type": "module",
6
6
  "engines": {
@@ -13,6 +13,16 @@ lumera apply # Deploy resources
13
13
 
14
14
  - `platform/collections/` — Collection schemas (JSON)
15
15
  - `platform/automations/` — Python automations
16
+ - `platform/functions/` — Typed Python Functions configured in `pyproject.toml`
16
17
  - `platform/hooks/` — JavaScript hooks
17
18
  - `src/` — React frontend (TanStack Router + Query)
18
19
  - `scripts/` — Utility scripts
20
+
21
+ Add each Function module to `[tool.lumera.functions].modules`, then use
22
+ `lumera functions list`, `lumera functions invoke <id> --local`, and
23
+ `lumera functions test` for the local development loop. With dependency
24
+ installation enabled, scaffolding runs `uv sync` and includes `uv.lock` in the
25
+ initial commit. `--no-install` (used by Studio bootstrap) defers this until the
26
+ first Functions command or an explicit `uv sync`. Refresh and commit the lock
27
+ after changing Python dependencies so every developer and later release build
28
+ resolves the same SDK and dependency graph.
@@ -7,7 +7,6 @@ __pycache__
7
7
  *.pyc
8
8
  .venv/
9
9
  .lumera/
10
- uv.lock
11
10
  src/routeTree.gen.ts
12
11
  .tanstack/
13
12
  output/
@@ -0,0 +1 @@
1
+ """Project Function modules."""
@@ -4,11 +4,16 @@ version = "0.1.0"
4
4
  description = "{{projectTitle}} - Lumera custom app"
5
5
  requires-python = ">=3.11"
6
6
  dependencies = [
7
- "lumera",
7
+ "lumera[functions]>=0.30.0,<0.31.0",
8
8
  ]
9
9
 
10
10
  [project.optional-dependencies]
11
11
  dev = [
12
12
  "ruff",
13
- "pytest",
13
+ "pytest>=8.0,<10.0",
14
14
  ]
15
+
16
+ [tool.lumera.functions]
17
+ source-root = "platform"
18
+ modules = []
19
+ test-paths = ["tests/functions"]
@@ -3,5 +3,5 @@
3
3
  "title": "Blank Starter",
4
4
  "description": "A minimal blank canvas with React frontend and Lumera platform scaffolding. No pre-built collections or agents — start from scratch.",
5
5
  "category": "General",
6
- "version": "1.3.0"
6
+ "version": "1.4.0"
7
7
  }