@lumerahq/cli 0.24.7-dev.0 → 0.25.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
@@ -1,21 +1,14 @@
1
1
  import {
2
- userContextHeaders
3
- } from "./chunk-FHHWIV4C.js";
4
- import {
5
- getBaseUrl,
6
- getProjectId,
7
- getToken,
8
- init_auth
2
+ getBaseUrl
9
3
  } from "./chunk-JLVVHTBY.js";
10
4
  import {
11
5
  fetchWithRetry
12
6
  } from "./chunk-FJFIWC7G.js";
13
7
 
14
8
  // src/lib/skills.ts
15
- init_auth();
16
9
  import { createHash } from "crypto";
17
- import { existsSync, mkdirSync, readdirSync, readFileSync, rmSync, symlinkSync, lstatSync, statSync, writeFileSync } from "fs";
18
- import { dirname, join, relative } from "path";
10
+ import { existsSync, mkdirSync, readdirSync, readFileSync, symlinkSync, lstatSync, statSync, writeFileSync } from "fs";
11
+ import { join, relative } from "path";
19
12
  import pc from "picocolors";
20
13
  function slugToDirName(slug) {
21
14
  return slug.replace(/-/g, "_");
@@ -27,19 +20,7 @@ function slugToFilename(slug) {
27
20
  return `${slug.replace(/-/g, "_")}.md`;
28
21
  }
29
22
  function hashContent(content) {
30
- return createHash("sha256").update(content).digest("hex");
31
- }
32
- var SKILLS_HASH_MANIFEST_PATH = join(".lumera", "skills-hashes.json");
33
- function writeSkillsHashManifest(projectRoot, skills) {
34
- const hashes = {};
35
- for (const skill of [...skills].sort((a, b) => a.slug < b.slug ? -1 : a.slug > b.slug ? 1 : 0)) {
36
- hashes[skill.slug] = hashContent(skill.content);
37
- }
38
- const manifestPath = join(projectRoot, SKILLS_HASH_MANIFEST_PATH);
39
- mkdirSync(dirname(manifestPath), { recursive: true });
40
- writeFileSync(manifestPath, `${JSON.stringify({ version: 1, skills: hashes }, null, 2)}
41
- `);
42
- rmSync(join(projectRoot, ".lumera", "skills-content-hash"), { force: true });
23
+ return createHash("md5").update(content).digest("hex");
43
24
  }
44
25
  async function fetchSkillsList() {
45
26
  const baseUrl = getBaseUrl();
@@ -63,58 +44,6 @@ async function fetchSkillContent(slug) {
63
44
  }
64
45
  return mdRes.text();
65
46
  }
66
- async function fetchEffectiveSkills(projectRoot) {
67
- const baseUrl = getBaseUrl();
68
- const token = getToken(projectRoot);
69
- const projectId = getProjectId(projectRoot)?.trim();
70
- const url = new URL(`${baseUrl}/api/skills/effective`);
71
- if (projectId) url.searchParams.set("project_id", projectId);
72
- const response = await fetchWithRetry(url.toString(), {
73
- headers: {
74
- Authorization: `Bearer ${token}`,
75
- "X-Lumera-Client": "lumera-cli",
76
- ...userContextHeaders()
77
- }
78
- });
79
- if (!response.ok) {
80
- const detail = (await response.text()).trim();
81
- throw new Error(`Failed to fetch effective skills: ${response.status}${detail ? ` ${detail}` : ""}`);
82
- }
83
- const payload = await response.json();
84
- return (payload.skills ?? []).map((skill) => ({
85
- ...skill,
86
- summary: skill.summary || skill.name,
87
- content_hash: skill.content_hash || hashContent(skill.content)
88
- }));
89
- }
90
- async function fetchPublicSkillFiles() {
91
- const skills = await fetchSkillsList();
92
- const results = await Promise.allSettled(
93
- skills.map(async (skill) => ({
94
- ...skill,
95
- content: await fetchSkillContent(skill.slug)
96
- }))
97
- );
98
- const out = [];
99
- for (const result of results) {
100
- if (result.status !== "fulfilled" || !result.value.content) continue;
101
- out.push({
102
- ...result.value,
103
- content: result.value.content,
104
- content_hash: hashContent(result.value.content),
105
- managed: true
106
- });
107
- }
108
- return out;
109
- }
110
- async function fetchSkillsForProject(projectRoot) {
111
- try {
112
- getToken(projectRoot);
113
- } catch {
114
- return fetchPublicSkillFiles();
115
- }
116
- return fetchEffectiveSkills(projectRoot);
117
- }
118
47
  function getLocalSkills(skillsDir) {
119
48
  const localSkills = /* @__PURE__ */ new Map();
120
49
  if (!existsSync(skillsDir)) {
@@ -197,30 +126,37 @@ function ensureSkillSymlinks(projectRoot) {
197
126
  }
198
127
  async function installAllSkills(targetDir, options) {
199
128
  const verbose = options?.verbose ?? false;
200
- const skills = await fetchSkillsForProject(targetDir);
129
+ const skills = await fetchSkillsList();
201
130
  const skillsDir = join(targetDir, ".agents", "skills");
202
131
  mkdirSync(skillsDir, { recursive: true });
132
+ const results = await Promise.allSettled(
133
+ skills.map(async (skill) => {
134
+ const content = await fetchSkillContent(skill.slug);
135
+ return { skill, content };
136
+ })
137
+ );
203
138
  let installed = 0;
204
139
  let failed = 0;
205
- for (const skill of skills) {
206
- if (!skill.content) {
207
- failed++;
140
+ for (const result of results) {
141
+ if (result.status === "fulfilled" && result.value.content) {
142
+ const { skill, content } = result.value;
143
+ const dirName = slugToDirName(skill.slug);
144
+ const skillDir = join(skillsDir, dirName);
145
+ mkdirSync(skillDir, { recursive: true });
146
+ writeFileSync(join(skillDir, "SKILL.md"), content);
208
147
  if (verbose) {
209
- console.log(pc.yellow(" \u26A0"), pc.dim(`Missing content for ${skill.slug}`));
148
+ console.log(pc.green(" \u2713"), pc.dim(`${dirName}/SKILL.md`));
210
149
  }
211
- continue;
212
- }
213
- const dirName = slugToDirName(skill.slug);
214
- const skillDir = join(skillsDir, dirName);
215
- mkdirSync(skillDir, { recursive: true });
216
- writeFileSync(join(skillDir, "SKILL.md"), skill.content);
217
- if (verbose) {
218
- console.log(pc.green(" \u2713"), pc.dim(`${dirName}/SKILL.md`));
150
+ installed++;
151
+ } else {
152
+ if (verbose) {
153
+ const slug = result.status === "fulfilled" ? result.value.skill.slug : "unknown";
154
+ console.log(pc.yellow(" \u26A0"), pc.dim(`Failed to fetch ${slug}`));
155
+ }
156
+ failed++;
219
157
  }
220
- installed++;
221
158
  }
222
159
  ensureSkillSymlinks(targetDir);
223
- if (failed === 0) writeSkillsHashManifest(targetDir, skills);
224
160
  return { installed, failed };
225
161
  }
226
162
  var SKILLS_START_MARKER = "<!-- LUMERA_SKILLS_START -->";
@@ -281,9 +217,8 @@ export {
281
217
  slugToDirName,
282
218
  slugToFilename,
283
219
  hashContent,
284
- writeSkillsHashManifest,
285
220
  fetchSkillsList,
286
- fetchSkillsForProject,
221
+ fetchSkillContent,
287
222
  getLocalSkills,
288
223
  ensureSkillSymlinks,
289
224
  installAllSkills,
@@ -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.29.0,<0.30.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-NKI3ZKIV.js").then(
270
+ (m) => m.functions(subcommand, args.slice(2))
271
+ );
272
+ break;
257
273
  // Project
258
274
  case "init":
259
- await import("./init-TRZMDKYB.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)));
@@ -272,7 +288,7 @@ async function main() {
272
288
  break;
273
289
  // Skills
274
290
  case "skills":
275
- await import("./skills-O3NZFM7V.js").then((m) => m.skills(subcommand, args.slice(2)));
291
+ await import("./skills-PXZHERAS.js").then((m) => m.skills(subcommand, args.slice(2)));
276
292
  break;
277
293
  // Dependencies
278
294
  case "deps":
@@ -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,7 +1,11 @@
1
+ import {
2
+ listAllTemplates,
3
+ resolveTemplate
4
+ } from "./chunk-H357NP7T.js";
1
5
  import {
2
6
  installAllSkills,
3
7
  syncClaudeMd
4
- } from "./chunk-GFLMEXIK.js";
8
+ } from "./chunk-VL5GDJKU.js";
5
9
  import {
6
10
  spinner
7
11
  } from "./chunk-BHYDYR75.js";
@@ -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 {
@@ -456,18 +461,6 @@ async function init(args) {
456
461
  setProjectId(targetDir, project.id);
457
462
  stopRegister(pc.green("\u2713") + pc.dim(` Project registered (${project.id})`));
458
463
  registered = true;
459
- const stopProjectSkills = spinner("Syncing project skills...");
460
- try {
461
- const { installed, failed } = await installAllSkills(targetDir);
462
- syncClaudeMd(targetDir);
463
- if (failed > 0) {
464
- stopProjectSkills(pc.yellow("\u26A0") + pc.dim(` Synced ${installed} project skills (${failed} failed)`));
465
- } else {
466
- stopProjectSkills(pc.green("\u2713") + pc.dim(` ${installed} project skills synced`));
467
- }
468
- } catch (skillError) {
469
- stopProjectSkills(pc.yellow("\u26A0") + pc.dim(` Failed to sync project skills: ${skillError}`));
470
- }
471
464
  } catch (e) {
472
465
  stopRegister(pc.yellow("\u26A0") + pc.dim(` Could not register project: ${e instanceof Error ? e.message : e}`));
473
466
  }
@@ -479,6 +472,9 @@ async function init(args) {
479
472
  console.log();
480
473
  console.log(pc.cyan(` cd ${finalDirectory}`));
481
474
  if (!opts.install) {
475
+ if (hasPythonProject) {
476
+ console.log(pc.cyan(" uv sync --extra dev --no-install-project"));
477
+ }
482
478
  console.log(pc.cyan(` ${installCommand}`));
483
479
  }
484
480
  if (!registered) {
@@ -493,5 +489,6 @@ async function init(args) {
493
489
  }
494
490
  }
495
491
  export {
492
+ createPythonVenv,
496
493
  init
497
494
  };
@@ -1,16 +1,14 @@
1
1
  import {
2
2
  ensureSkillSymlinks,
3
- fetchSkillsForProject,
3
+ fetchSkillContent,
4
4
  fetchSkillsList,
5
5
  getLocalSkills,
6
6
  hashContent,
7
7
  installAllSkills,
8
8
  slugToDirName,
9
9
  slugToFilename,
10
- syncClaudeMd,
11
- writeSkillsHashManifest
12
- } from "./chunk-GFLMEXIK.js";
13
- import "./chunk-FHHWIV4C.js";
10
+ syncClaudeMd
11
+ } from "./chunk-VL5GDJKU.js";
14
12
  import "./chunk-JLVVHTBY.js";
15
13
  import "./chunk-FJFIWC7G.js";
16
14
  import "./chunk-PNKVD2UK.js";
@@ -29,25 +27,32 @@ function findProjectRoot() {
29
27
  }
30
28
  return null;
31
29
  }
32
- async function computeDiff(projectRoot, skillsDir, filterSlug) {
33
- const skills2 = await fetchSkillsForProject(projectRoot);
30
+ async function computeDiff(skillsDir, filterSlug) {
31
+ const skills2 = await fetchSkillsList();
34
32
  const localSkills = getLocalSkills(skillsDir);
35
33
  const remoteSkillSlugs = /* @__PURE__ */ new Set();
36
34
  const diff = {
37
- resolved: skills2,
38
35
  added: [],
39
36
  updated: [],
40
37
  removed: [],
41
38
  unchanged: []
42
39
  };
43
40
  const skillsToCheck = filterSlug ? skills2.filter((skill) => skill.slug === filterSlug) : skills2;
44
- for (const skill of skillsToCheck) {
41
+ const remoteResults = await Promise.allSettled(
42
+ skillsToCheck.map(async (skill) => {
43
+ const content = await fetchSkillContent(skill.slug);
44
+ return { skill, content };
45
+ })
46
+ );
47
+ for (const result of remoteResults) {
48
+ if (result.status !== "fulfilled" || !result.value.content) continue;
49
+ const { skill, content } = result.value;
45
50
  remoteSkillSlugs.add(skill.slug);
46
51
  const localHash = localSkills.get(skill.slug);
47
52
  if (!localHash) {
48
53
  diff.added.push(skill);
49
54
  } else {
50
- const remoteHash = hashContent(skill.content);
55
+ const remoteHash = hashContent(content);
51
56
  if (localHash !== remoteHash) {
52
57
  diff.updated.push(skill);
53
58
  } else {
@@ -256,7 +261,10 @@ async function update(args, flags) {
256
261
  }
257
262
  const skillsDir = join(projectRoot, ".agents", "skills");
258
263
  if (!existsSync(skillsDir)) {
259
- mkdirSync(skillsDir, { recursive: true });
264
+ console.log(pc.yellow(" \u26A0"), "No skills installed yet");
265
+ console.log(pc.dim(' Run "lumera skills install" first'));
266
+ console.log();
267
+ return;
260
268
  }
261
269
  if (verbose) {
262
270
  console.log(pc.dim(` Project root: ${projectRoot}`));
@@ -267,14 +275,13 @@ async function update(args, flags) {
267
275
  console.log();
268
276
  }
269
277
  try {
270
- const diff = await computeDiff(projectRoot, skillsDir, filterSlug);
278
+ const diff = await computeDiff(skillsDir, filterSlug);
271
279
  const hasChanges = diff.added.length > 0 || diff.updated.length > 0 || diff.removed.length > 0;
272
280
  if (!hasChanges) {
273
281
  console.log(pc.green(" \u2713"), "All skills are up to date");
274
282
  if (verbose && diff.unchanged.length > 0) {
275
283
  console.log(pc.dim(` ${diff.unchanged.length} skills unchanged`));
276
284
  }
277
- if (!filterSlug) writeSkillsHashManifest(projectRoot, diff.resolved);
278
285
  syncClaudeMd(projectRoot);
279
286
  console.log();
280
287
  return;
@@ -322,11 +329,20 @@ async function update(args, flags) {
322
329
  }
323
330
  console.log(pc.dim(" Applying changes..."));
324
331
  console.log();
325
- for (const skill of [...diff.added, ...diff.updated]) {
332
+ const toFetch = [...diff.added, ...diff.updated];
333
+ const fetchResults = await Promise.allSettled(
334
+ toFetch.map(async (skill) => {
335
+ const content = await fetchSkillContent(skill.slug);
336
+ return { skill, content };
337
+ })
338
+ );
339
+ for (const result of fetchResults) {
340
+ if (result.status !== "fulfilled" || !result.value.content) continue;
341
+ const { skill, content } = result.value;
326
342
  const dirName = slugToDirName(skill.slug);
327
343
  const skillDir = join(skillsDir, dirName);
328
344
  mkdirSync(skillDir, { recursive: true });
329
- writeFileSync(join(skillDir, "SKILL.md"), skill.content);
345
+ writeFileSync(join(skillDir, "SKILL.md"), content);
330
346
  const legacyFile = join(skillsDir, slugToFilename(skill.slug));
331
347
  if (existsSync(legacyFile)) {
332
348
  rmSync(legacyFile);
@@ -353,7 +369,6 @@ async function update(args, flags) {
353
369
  }
354
370
  console.log();
355
371
  console.log(pc.green(" \u2713"), `Update complete (${changes.join(", ")})`);
356
- if (!filterSlug) writeSkillsHashManifest(projectRoot, diff.resolved);
357
372
  syncClaudeMd(projectRoot);
358
373
  ensureSkillSymlinks(projectRoot);
359
374
  console.log();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lumerahq/cli",
3
- "version": "0.24.7-dev.0",
3
+ "version": "0.25.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,6 +7,8 @@ __pycache__
7
7
  *.pyc
8
8
  .venv/
9
9
  .lumera/
10
- uv.lock
11
10
  src/routeTree.gen.ts
12
11
  .tanstack/
12
+ output/
13
+ tmp/
14
+ uploads/
@@ -1,5 +1,5 @@
1
1
  {
2
- "$schema": "https://biomejs.dev/schemas/2.4.16/schema.json",
2
+ "$schema": "https://biomejs.dev/schemas/2.5.6/schema.json",
3
3
  "vcs": {
4
4
  "enabled": true,
5
5
  "clientKind": "git",
@@ -7,7 +7,15 @@
7
7
  },
8
8
  "files": {
9
9
  "ignoreUnknown": false,
10
- "includes": ["**", "!!**/node_modules", "!!**/dist", "!!**/*routeTree.gen.ts"]
10
+ "includes": [
11
+ "**",
12
+ "!!**/node_modules",
13
+ "!!**/dist",
14
+ "!!**/*routeTree.gen.ts",
15
+ "!!output",
16
+ "!!tmp",
17
+ "!!uploads"
18
+ ]
11
19
  },
12
20
  "formatter": {
13
21
  "enabled": true,
@@ -18,7 +26,7 @@
18
26
  "linter": {
19
27
  "enabled": true,
20
28
  "rules": {
21
- "recommended": true,
29
+ "preset": "recommended",
22
30
  "suspicious": { "noExplicitAny": "off" },
23
31
  "style": { "noNonNullAssertion": "off" }
24
32
  }
@@ -1,4 +1,5 @@
1
1
  <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 80 80" fill="none">
2
+ <title>Rocket launch</title>
2
3
  <defs>
3
4
  <linearGradient id="bg" x1="0" y1="0" x2="80" y2="80" gradientUnits="userSpaceOnUse">
4
5
  <stop offset="0%" stop-color="#ecfdf5"/>
@@ -35,7 +35,7 @@
35
35
  "sonner": "^2.0.7"
36
36
  },
37
37
  "devDependencies": {
38
- "@biomejs/biome": "^2.4.10",
38
+ "@biomejs/biome": "2.5.6",
39
39
  "@tailwindcss/vite": "^4.2.2",
40
40
  "@tanstack/router-cli": "1.155.0",
41
41
  "@tanstack/router-plugin": "1.155.0",
@@ -0,0 +1 @@
1
+ """Project Function modules."""
@@ -40,8 +40,8 @@ importers:
40
40
  version: 3.6.0
41
41
  devDependencies:
42
42
  '@biomejs/biome':
43
- specifier: ^2.4.10
44
- version: 2.4.16
43
+ specifier: 2.5.6
44
+ version: 2.5.6
45
45
  '@tailwindcss/vite':
46
46
  specifier: ^4.2.2
47
47
  version: 4.3.0(vite@8.0.16(esbuild@0.28.0)(jiti@2.7.0)(tsx@4.22.4))
@@ -192,59 +192,59 @@ packages:
192
192
  '@types/react':
193
193
  optional: true
194
194
 
195
- '@biomejs/biome@2.4.16':
196
- resolution: {integrity: sha512-x9ajFh1zChVybCiM3TN6OD4phAqLgtPZjFrZF+aTMYCPjwBO+k529TX7PPsAqtGNLeV4UgzwQnowEgS7bGmzcA==}
195
+ '@biomejs/biome@2.5.6':
196
+ resolution: {integrity: sha512-lxVNjv7UF6KfhMJfL9gaUHbWdJdHbsAj6OSmwSYNdhRuG67NxNQ4Xdvh3TUxsSK9sBzJBQhEJj3AopmmNJ5pSA==}
197
197
  engines: {node: '>=14.21.3'}
198
198
  hasBin: true
199
199
 
200
- '@biomejs/cli-darwin-arm64@2.4.16':
201
- resolution: {integrity: sha512-wxPvu4XOA85YJk9ixSWUmq/QBHbid85BISbOAqqBM/5xQpPk9ayjk5375tOlSC0BeCwNSbPFafQBm+vBumXq0A==}
200
+ '@biomejs/cli-darwin-arm64@2.5.6':
201
+ resolution: {integrity: sha512-zMOLZP4oMrjh6m1zcSj1ud2awUPgTuMVbmQhYYWL7J8HwCnbHHBvTm7VBTRuY7epT5bez76IpKYQ11ZAqHFlnw==}
202
202
  engines: {node: '>=14.21.3'}
203
203
  cpu: [arm64]
204
204
  os: [darwin]
205
205
 
206
- '@biomejs/cli-darwin-x64@2.4.16':
207
- resolution: {integrity: sha512-xFCqGPwYusQJp4N4NJLi1XJiZqjwFdjhT+KqtNy+Ug3qgfczqnTa6MSDvxJF6TkuDLoYJItMapz6tAf7kCekFw==}
206
+ '@biomejs/cli-darwin-x64@2.5.6':
207
+ resolution: {integrity: sha512-JAC1VqzvO7Th5ZplU0G2uGfkZbxEe9uDDektPAhF0JLusoz1w+T4okp2bkykI0bbaO2vslKiRfj4gU43JaGreA==}
208
208
  engines: {node: '>=14.21.3'}
209
209
  cpu: [x64]
210
210
  os: [darwin]
211
211
 
212
- '@biomejs/cli-linux-arm64-musl@2.4.16':
213
- resolution: {integrity: sha512-oYxnW0ARfJkr72ezzF2OR8N/rtkgLUQeYtF8cFhVswbknHxtTcmzSsanVJP8yQKnGpGpc2ck6c5zLvHahL6Cbg==}
212
+ '@biomejs/cli-linux-arm64-musl@2.5.6':
213
+ resolution: {integrity: sha512-eUa3jeeYvfMt19LBeh6E5PUZpxnTC4JqNWo+EDjTtQjAr2xLGnWaxACtVU1DQqmHYbvThlJzLX+ZsYgrqh2qVw==}
214
214
  engines: {node: '>=14.21.3'}
215
215
  cpu: [arm64]
216
216
  os: [linux]
217
217
  libc: [musl]
218
218
 
219
- '@biomejs/cli-linux-arm64@2.4.16':
220
- resolution: {integrity: sha512-2kFb4//jxfZaP6D+Rj5VkHkxgyD9EoRAVBEQb8PKRv+s4NO2zYNJKXFaJmK1CmhufJOWEfpHKaRbOja7qjmdhQ==}
219
+ '@biomejs/cli-linux-arm64@2.5.6':
220
+ resolution: {integrity: sha512-6XsYwCFkp5sMxl85ffhgeGpGgs6A7dRYFnkceZ7WVxvycuTnGdD5xa534Z3xfrBQ0JCMK/mujT6ZNPJoghedwg==}
221
221
  engines: {node: '>=14.21.3'}
222
222
  cpu: [arm64]
223
223
  os: [linux]
224
224
  libc: [glibc]
225
225
 
226
- '@biomejs/cli-linux-x64-musl@2.4.16':
227
- resolution: {integrity: sha512-iHDS+MCM65DPqWGu+ECC3uoALyj2H7F4nVUPxIPjz/PIl94EUu+EDfGZDzFP+NY1EOPVt9NQvwFqq7HdMmowdg==}
226
+ '@biomejs/cli-linux-x64-musl@2.5.6':
227
+ resolution: {integrity: sha512-2Vp13QdKysH3HIWLaYLhUUwbK+jbZonJD1K+Lr0d0RO4wH7mkYd43vJixEDm8cUWrowoRz4UUHF1nm9Ae7ym8A==}
228
228
  engines: {node: '>=14.21.3'}
229
229
  cpu: [x64]
230
230
  os: [linux]
231
231
  libc: [musl]
232
232
 
233
- '@biomejs/cli-linux-x64@2.4.16':
234
- resolution: {integrity: sha512-NbcBbi/nJqn5baae6wqRXdS7Gadf2uRpehSh6vMSYpG8OhkXl/Xg8aorWrJ+9VWqAT5ml90alLvorkpMW0nBwQ==}
233
+ '@biomejs/cli-linux-x64@2.5.6':
234
+ resolution: {integrity: sha512-Pop9VXCFUhFTMfFefZ39S+u2rOPyNp5iHlxbZRwXGACHLy2r0jjiRgJHmaEKJzL3SyxlVeGShXhvvElvWowonA==}
235
235
  engines: {node: '>=14.21.3'}
236
236
  cpu: [x64]
237
237
  os: [linux]
238
238
  libc: [glibc]
239
239
 
240
- '@biomejs/cli-win32-arm64@2.4.16':
241
- resolution: {integrity: sha512-0rgImMsNb5v/chhkIFe3wu7PEFClS6RBAYUijGL9UsYN3PanSaoK24HSSuSJb1pYbYYVjzAyZTl3gtjJ84BM8A==}
240
+ '@biomejs/cli-win32-arm64@2.5.6':
241
+ resolution: {integrity: sha512-tDGshcm6BdkZOCGnTDX0Y8/U4IfBSlnUU7T56nNDuPEfed+aHg+u8G36NB43fJVl0Os6+QURXIE1yuD7AaEofA==}
242
242
  engines: {node: '>=14.21.3'}
243
243
  cpu: [arm64]
244
244
  os: [win32]
245
245
 
246
- '@biomejs/cli-win32-x64@2.4.16':
247
- resolution: {integrity: sha512-Kp85jgoBHa05gix6UIRjfCDiUV3w/8VIdZ247VyyO2gEjaw12WEVhdIjlxp/AMzXxqxQwbxNTDVZ3Mwd2RG5rw==}
246
+ '@biomejs/cli-win32-x64@2.5.6':
247
+ resolution: {integrity: sha512-WN05KwXnTO/2J45RQPvzZMXf7tZUIofHoR35xIPfCo7pQ2RFidxI8sfb5mGsaTxdMmEOzHzOPRCdA5/fCpc7xQ==}
248
248
  engines: {node: '>=14.21.3'}
249
249
  cpu: [x64]
250
250
  os: [win32]
@@ -2134,39 +2134,39 @@ snapshots:
2134
2134
  optionalDependencies:
2135
2135
  '@types/react': 19.2.16
2136
2136
 
2137
- '@biomejs/biome@2.4.16':
2137
+ '@biomejs/biome@2.5.6':
2138
2138
  optionalDependencies:
2139
- '@biomejs/cli-darwin-arm64': 2.4.16
2140
- '@biomejs/cli-darwin-x64': 2.4.16
2141
- '@biomejs/cli-linux-arm64': 2.4.16
2142
- '@biomejs/cli-linux-arm64-musl': 2.4.16
2143
- '@biomejs/cli-linux-x64': 2.4.16
2144
- '@biomejs/cli-linux-x64-musl': 2.4.16
2145
- '@biomejs/cli-win32-arm64': 2.4.16
2146
- '@biomejs/cli-win32-x64': 2.4.16
2139
+ '@biomejs/cli-darwin-arm64': 2.5.6
2140
+ '@biomejs/cli-darwin-x64': 2.5.6
2141
+ '@biomejs/cli-linux-arm64': 2.5.6
2142
+ '@biomejs/cli-linux-arm64-musl': 2.5.6
2143
+ '@biomejs/cli-linux-x64': 2.5.6
2144
+ '@biomejs/cli-linux-x64-musl': 2.5.6
2145
+ '@biomejs/cli-win32-arm64': 2.5.6
2146
+ '@biomejs/cli-win32-x64': 2.5.6
2147
2147
 
2148
- '@biomejs/cli-darwin-arm64@2.4.16':
2148
+ '@biomejs/cli-darwin-arm64@2.5.6':
2149
2149
  optional: true
2150
2150
 
2151
- '@biomejs/cli-darwin-x64@2.4.16':
2151
+ '@biomejs/cli-darwin-x64@2.5.6':
2152
2152
  optional: true
2153
2153
 
2154
- '@biomejs/cli-linux-arm64-musl@2.4.16':
2154
+ '@biomejs/cli-linux-arm64-musl@2.5.6':
2155
2155
  optional: true
2156
2156
 
2157
- '@biomejs/cli-linux-arm64@2.4.16':
2157
+ '@biomejs/cli-linux-arm64@2.5.6':
2158
2158
  optional: true
2159
2159
 
2160
- '@biomejs/cli-linux-x64-musl@2.4.16':
2160
+ '@biomejs/cli-linux-x64-musl@2.5.6':
2161
2161
  optional: true
2162
2162
 
2163
- '@biomejs/cli-linux-x64@2.4.16':
2163
+ '@biomejs/cli-linux-x64@2.5.6':
2164
2164
  optional: true
2165
2165
 
2166
- '@biomejs/cli-win32-arm64@2.4.16':
2166
+ '@biomejs/cli-win32-arm64@2.5.6':
2167
2167
  optional: true
2168
2168
 
2169
- '@biomejs/cli-win32-x64@2.4.16':
2169
+ '@biomejs/cli-win32-x64@2.5.6':
2170
2170
  optional: true
2171
2171
 
2172
2172
  '@braintree/sanitize-url@7.1.2': {}
@@ -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.29.0,<0.30.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"]
@@ -7,19 +7,13 @@
7
7
  height: 0;
8
8
  }
9
9
  to {
10
- height: var(
11
- --radix-accordion-content-height,
12
- var(--accordion-panel-height, auto)
13
- );
10
+ height: var(--radix-accordion-content-height, var(--accordion-panel-height, auto));
14
11
  }
15
12
  }
16
13
 
17
14
  @keyframes accordion-up {
18
15
  from {
19
- height: var(
20
- --radix-accordion-content-height,
21
- var(--accordion-panel-height, auto)
22
- );
16
+ height: var(--radix-accordion-content-height, var(--accordion-panel-height, auto));
23
17
  }
24
18
  to {
25
19
  height: 0;
@@ -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
  }