@lumerahq/cli 0.10.1 → 0.11.1

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.
@@ -5,34 +5,61 @@ import {
5
5
  import {
6
6
  listAllTemplates,
7
7
  resolveTemplate
8
- } from "./chunk-WTDV3MTG.js";
8
+ } from "./chunk-CHRKCAIZ.js";
9
9
  import "./chunk-D2BLSEGR.js";
10
10
 
11
11
  // src/commands/init.ts
12
- import pc from "picocolors";
12
+ import pc2 from "picocolors";
13
13
  import prompts from "prompts";
14
14
  import { execSync } from "child_process";
15
15
  import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync, rmSync } from "fs";
16
16
  import { join, resolve } from "path";
17
+
18
+ // src/lib/spinner.ts
19
+ import pc from "picocolors";
20
+ var frames = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
21
+ function spinner(message) {
22
+ if (!process.stdout.isTTY) {
23
+ process.stdout.write(` ${message}
24
+ `);
25
+ return (doneMessage) => {
26
+ if (doneMessage) {
27
+ process.stdout.write(` ${doneMessage}
28
+ `);
29
+ }
30
+ };
31
+ }
32
+ let i = 0;
33
+ const interval = setInterval(() => {
34
+ const frame = frames[i % frames.length];
35
+ process.stdout.write(`\r ${pc.cyan(frame)} ${message}`);
36
+ i++;
37
+ }, 80);
38
+ return (doneMessage) => {
39
+ clearInterval(interval);
40
+ process.stdout.write(`\r${" ".repeat(message.length + 10)}\r`);
41
+ if (doneMessage) {
42
+ process.stdout.write(` ${doneMessage}
43
+ `);
44
+ }
45
+ };
46
+ }
47
+
48
+ // src/commands/init.ts
17
49
  function toTitleCase(str) {
18
50
  return str.split(/[-_]/).map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join(" ");
19
51
  }
20
- var TEMPLATE_DEFAULTS = {
21
- projectName: "my-lumera-app",
22
- projectTitle: "My Lumera App"
23
- };
24
- function processTemplate(content, vars) {
52
+ function processTemplate(content, replacements) {
25
53
  let result = content;
26
- for (const [key, value] of Object.entries(vars)) {
27
- const defaultValue = TEMPLATE_DEFAULTS[key];
28
- if (defaultValue && defaultValue !== value) {
29
- result = result.replaceAll(defaultValue, value);
54
+ for (const [from, to] of replacements) {
55
+ if (from !== to) {
56
+ result = result.replaceAll(from, to);
30
57
  }
31
58
  }
32
59
  return result;
33
60
  }
34
61
  var TEMPLATE_EXCLUDE = /* @__PURE__ */ new Set(["template.json"]);
35
- function copyDir(src, dest, vars, isRoot = true) {
62
+ function copyDir(src, dest, replacements, isRoot = true) {
36
63
  if (!existsSync(dest)) {
37
64
  mkdirSync(dest, { recursive: true });
38
65
  }
@@ -41,10 +68,10 @@ function copyDir(src, dest, vars, isRoot = true) {
41
68
  const srcPath = join(src, entry.name);
42
69
  const destPath = join(dest, entry.name);
43
70
  if (entry.isDirectory()) {
44
- copyDir(srcPath, destPath, vars, false);
71
+ copyDir(srcPath, destPath, replacements, false);
45
72
  } else {
46
73
  const content = readFileSync(srcPath, "utf-8");
47
- const processed = processTemplate(content, vars);
74
+ const processed = processTemplate(content, replacements);
48
75
  writeFileSync(destPath, processed);
49
76
  }
50
77
  }
@@ -82,13 +109,13 @@ function installUv() {
82
109
  try {
83
110
  try {
84
111
  execSync("curl -LsSf https://astral.sh/uv/install.sh | sh", {
85
- stdio: "inherit",
112
+ stdio: "ignore",
86
113
  shell: "/bin/bash"
87
114
  });
88
115
  return true;
89
116
  } catch {
90
117
  execSync("wget -qO- https://astral.sh/uv/install.sh | sh", {
91
- stdio: "inherit",
118
+ stdio: "ignore",
92
119
  shell: "/bin/bash"
93
120
  });
94
121
  return true;
@@ -106,12 +133,40 @@ function createPythonVenv(targetDir) {
106
133
  return false;
107
134
  }
108
135
  }
136
+ function detectEditor() {
137
+ const envEditor = process.env.VISUAL || process.env.EDITOR;
138
+ if (envEditor) return envEditor;
139
+ const editors = ["cursor", "code", "zed", "subl"];
140
+ for (const editor of editors) {
141
+ try {
142
+ execSync(`which ${editor}`, { stdio: "ignore" });
143
+ return editor;
144
+ } catch {
145
+ continue;
146
+ }
147
+ }
148
+ return null;
149
+ }
150
+ function openInEditor(targetDir) {
151
+ const editor = detectEditor();
152
+ if (!editor) {
153
+ console.log(pc2.yellow(" \u26A0"), pc2.dim("No editor detected. Set VISUAL or EDITOR env var."));
154
+ return;
155
+ }
156
+ try {
157
+ execSync(`${editor} "${targetDir}"`, { stdio: "ignore" });
158
+ console.log(pc2.green(" \u2713"), pc2.dim(`Opened in ${editor}`));
159
+ } catch {
160
+ console.log(pc2.yellow(" \u26A0"), pc2.dim(`Failed to open in ${editor}`));
161
+ }
162
+ }
109
163
  function parseArgs(args) {
110
164
  const result = {
111
165
  projectName: void 0,
112
166
  directory: void 0,
113
167
  template: void 0,
114
- install: false,
168
+ install: true,
169
+ open: false,
115
170
  yes: false,
116
171
  force: false,
117
172
  help: false
@@ -120,8 +175,10 @@ function parseArgs(args) {
120
175
  const arg = args[i];
121
176
  if (arg === "--help" || arg === "-h") {
122
177
  result.help = true;
123
- } else if (arg === "--install" || arg === "-i") {
124
- result.install = true;
178
+ } else if (arg === "--no-install") {
179
+ result.install = false;
180
+ } else if (arg === "--open" || arg === "-o") {
181
+ result.open = true;
125
182
  } else if (arg === "--yes" || arg === "-y") {
126
183
  result.yes = true;
127
184
  } else if (arg === "--force" || arg === "-f") {
@@ -138,25 +195,26 @@ function parseArgs(args) {
138
195
  }
139
196
  function showHelp() {
140
197
  console.log(`
141
- ${pc.dim("Usage:")}
198
+ ${pc2.dim("Usage:")}
142
199
  lumera init [name] [options]
143
200
 
144
- ${pc.dim("Description:")}
201
+ ${pc2.dim("Description:")}
145
202
  Scaffold a new Lumera project from a template.
146
203
 
147
- ${pc.dim("Options:")}
148
- --template, -t <name> Template to use (run ${pc.cyan("lumera templates")} to see options)
204
+ ${pc2.dim("Options:")}
205
+ --template, -t <name> Template to use (run ${pc2.cyan("lumera templates")} to see options)
149
206
  --yes, -y Non-interactive mode (requires project name)
150
207
  --dir, -d <path> Target directory (defaults to project name)
151
208
  --force, -f Overwrite existing directory without prompting
152
- --install, -i Install dependencies after scaffolding
209
+ --no-install Skip dependency installation
210
+ --open, -o Open project in editor after scaffolding
153
211
  --help, -h Show this help
154
212
 
155
- ${pc.dim("Examples:")}
213
+ ${pc2.dim("Examples:")}
156
214
  lumera init my-app # Interactive mode
157
215
  lumera init my-app -t invoice-processing # Use a specific template
158
216
  lumera init my-app -y # Non-interactive (default template)
159
- lumera init my-app -t invoice-processing -y -i # Full non-interactive setup
217
+ lumera init my-app -t invoice-processing -y -o # Full non-interactive + open editor
160
218
  `);
161
219
  }
162
220
  async function init(args) {
@@ -166,34 +224,36 @@ async function init(args) {
166
224
  return;
167
225
  }
168
226
  console.log();
169
- console.log(pc.cyan(pc.bold(" Create Lumera App")));
227
+ console.log(pc2.cyan(pc2.bold(" Create Lumera App")));
170
228
  console.log();
171
229
  let projectName = opts.projectName;
172
230
  let directory = opts.directory;
173
231
  const nonInteractive = opts.yes;
174
232
  if (nonInteractive && !projectName) {
175
- console.log(pc.red(" Error: Project name is required in non-interactive mode"));
176
- console.log(pc.dim(" Usage: lumera init <name> -y"));
233
+ console.log(pc2.red(" Error: Project name is required in non-interactive mode"));
234
+ console.log(pc2.dim(" Usage: lumera init <name> -y"));
177
235
  process.exit(1);
178
236
  }
179
237
  let templateName = opts.template;
180
238
  if (!templateName && !nonInteractive) {
181
239
  try {
240
+ const stop = spinner("Fetching templates...");
182
241
  const available = await listAllTemplates();
242
+ stop();
183
243
  if (available.length > 1) {
184
244
  const response = await prompts({
185
245
  type: "select",
186
246
  name: "template",
187
247
  message: "Choose a template",
188
248
  choices: available.map((t) => ({
189
- title: `${t.title} ${pc.dim(`(${t.name})`)}`,
249
+ title: `${t.title} ${pc2.dim(`(${t.name})`)}`,
190
250
  description: t.description,
191
251
  value: t.name
192
252
  })),
193
253
  initial: 0
194
254
  });
195
255
  if (!response.template) {
196
- console.log(pc.red("Cancelled"));
256
+ console.log(pc2.red("Cancelled"));
197
257
  process.exit(1);
198
258
  }
199
259
  templateName = response.template;
@@ -204,13 +264,15 @@ async function init(args) {
204
264
  if (!templateName) {
205
265
  templateName = "default";
206
266
  }
267
+ const stopResolve = spinner("Resolving template...");
207
268
  const templateDir = await resolveTemplate(templateName);
269
+ stopResolve();
208
270
  if (!projectName) {
209
271
  const response = await prompts({
210
272
  type: "text",
211
273
  name: "projectName",
212
274
  message: "What is your project name?",
213
- initial: "my-lumera-app",
275
+ initial: "my-app",
214
276
  validate: (value) => {
215
277
  if (!value) return "Project name is required";
216
278
  if (!/^[a-z0-9-]+$/.test(value)) {
@@ -220,13 +282,13 @@ async function init(args) {
220
282
  }
221
283
  });
222
284
  if (!response.projectName) {
223
- console.log(pc.red("Cancelled"));
285
+ console.log(pc2.red("Cancelled"));
224
286
  process.exit(1);
225
287
  }
226
288
  projectName = response.projectName;
227
289
  }
228
290
  if (!/^[a-z0-9-]+$/.test(projectName)) {
229
- console.log(pc.red(" Error: Project name must use lowercase letters, numbers, and hyphens only"));
291
+ console.log(pc2.red(" Error: Project name must use lowercase letters, numbers, and hyphens only"));
230
292
  process.exit(1);
231
293
  }
232
294
  if (!directory) {
@@ -240,7 +302,7 @@ async function init(args) {
240
302
  initial: projectName
241
303
  });
242
304
  if (!response.directory) {
243
- console.log(pc.red("Cancelled"));
305
+ console.log(pc2.red("Cancelled"));
244
306
  process.exit(1);
245
307
  }
246
308
  directory = response.directory;
@@ -253,8 +315,8 @@ async function init(args) {
253
315
  if (opts.force) {
254
316
  rmSync(targetDir, { recursive: true });
255
317
  } else {
256
- console.log(pc.red(` Error: Directory ${directory} already exists`));
257
- console.log(pc.dim(" Use --force (-f) to overwrite"));
318
+ console.log(pc2.red(` Error: Directory ${directory} already exists`));
319
+ console.log(pc2.dim(" Use --force (-f) to overwrite"));
258
320
  process.exit(1);
259
321
  }
260
322
  } else {
@@ -265,7 +327,7 @@ async function init(args) {
265
327
  initial: false
266
328
  });
267
329
  if (!overwrite) {
268
- console.log(pc.red("Cancelled"));
330
+ console.log(pc2.red("Cancelled"));
269
331
  process.exit(1);
270
332
  }
271
333
  rmSync(targetDir, { recursive: true });
@@ -274,77 +336,75 @@ async function init(args) {
274
336
  mkdirSync(targetDir, { recursive: true });
275
337
  console.log();
276
338
  if (templateName !== "default") {
277
- console.log(pc.dim(` Creating ${projectName} from template ${pc.cyan(templateName)}...`));
339
+ console.log(pc2.dim(` Creating ${projectName} from template ${pc2.cyan(templateName)}...`));
278
340
  } else {
279
- console.log(pc.dim(` Creating ${projectName}...`));
341
+ console.log(pc2.dim(` Creating ${projectName}...`));
280
342
  }
281
343
  console.log();
282
- const vars = {
283
- projectName,
284
- projectTitle
285
- };
286
- copyDir(templateDir, targetDir, vars);
344
+ const templatePkgPath = join(templateDir, "package.json");
345
+ const templatePkg = existsSync(templatePkgPath) ? JSON.parse(readFileSync(templatePkgPath, "utf-8")) : { name: "my-lumera-app" };
346
+ const sourceName = templatePkg.name || "my-lumera-app";
347
+ const sourceTitle = templatePkg.lumera?.name || toTitleCase(sourceName);
348
+ const replacements = [
349
+ [sourceName, projectName],
350
+ [sourceTitle, projectTitle]
351
+ ];
352
+ copyDir(templateDir, targetDir, replacements);
287
353
  function listFiles(dir, prefix = "") {
288
354
  for (const entry of readdirSync(dir, { withFileTypes: true })) {
289
355
  const relativePath = prefix + entry.name;
290
356
  if (entry.isDirectory()) {
291
357
  listFiles(join(dir, entry.name), relativePath + "/");
292
358
  } else {
293
- console.log(pc.green(" \u2713"), pc.dim(relativePath));
359
+ console.log(pc2.green(" \u2713"), pc2.dim(relativePath));
294
360
  }
295
361
  }
296
362
  }
297
363
  listFiles(targetDir);
298
364
  if (isGitInstalled()) {
299
- console.log();
300
- console.log(pc.dim(" Initializing git repository..."));
365
+ const stopGit = spinner("Initializing git repository...");
301
366
  if (initGitRepo(targetDir, projectName)) {
302
- console.log(pc.green(" \u2713"), pc.dim("Git repository initialized with initial commit"));
367
+ stopGit(pc2.green("\u2713") + pc2.dim(" Git repository initialized with initial commit"));
303
368
  } else {
304
- console.log(pc.yellow(" \u26A0"), pc.dim("Failed to initialize git repository"));
369
+ stopGit(pc2.yellow("\u26A0") + pc2.dim(" Failed to initialize git repository"));
305
370
  }
306
371
  } else {
307
- console.log();
308
- console.log(pc.yellow(" \u26A0"), pc.dim("Git not found - skipping repository initialization"));
372
+ console.log(pc2.yellow(" \u26A0"), pc2.dim("Git not found \u2014 skipping repository initialization"));
309
373
  }
310
374
  let uvAvailable = isUvInstalled();
311
375
  if (!uvAvailable) {
312
- console.log();
313
- console.log(pc.dim(" Installing uv (Python package manager)..."));
376
+ const stopUv = spinner("Installing uv (Python package manager)...");
314
377
  if (installUv()) {
315
- console.log(pc.green(" \u2713"), pc.dim("uv installed successfully"));
378
+ stopUv(pc2.green("\u2713") + pc2.dim(" uv installed successfully"));
316
379
  uvAvailable = true;
317
380
  } else {
318
- console.log(pc.yellow(" \u26A0"), pc.dim("Failed to install uv - install manually: https://docs.astral.sh/uv/"));
381
+ stopUv(pc2.yellow("\u26A0") + pc2.dim(" Failed to install uv \u2014 install manually: https://docs.astral.sh/uv/"));
319
382
  }
320
383
  }
321
384
  if (uvAvailable) {
322
- console.log();
323
- console.log(pc.dim(" Creating Python venv with Lumera SDK..."));
385
+ const stopVenv = spinner("Creating Python venv with Lumera SDK...");
324
386
  if (createPythonVenv(targetDir)) {
325
- console.log(pc.green(" \u2713"), pc.dim("Python venv created (.venv/) with lumera SDK"));
387
+ stopVenv(pc2.green("\u2713") + pc2.dim(" Python venv created (.venv/) with lumera SDK"));
326
388
  } else {
327
- console.log(pc.yellow(" \u26A0"), pc.dim("Failed to create Python venv"));
389
+ stopVenv(pc2.yellow("\u26A0") + pc2.dim(" Failed to create Python venv"));
328
390
  }
329
391
  }
330
392
  if (opts.install) {
331
- console.log();
332
- console.log(pc.dim(" Installing dependencies..."));
393
+ const stopInstall = spinner("Installing dependencies...");
333
394
  try {
334
- execSync("pnpm install", { cwd: targetDir, stdio: "inherit" });
335
- console.log(pc.green(" \u2713"), pc.dim("Dependencies installed"));
395
+ execSync("pnpm install", { cwd: targetDir, stdio: "ignore" });
396
+ stopInstall(pc2.green("\u2713") + pc2.dim(" Dependencies installed"));
336
397
  } catch {
337
- console.log(pc.yellow(" \u26A0"), pc.dim("Failed to install dependencies"));
398
+ stopInstall(pc2.yellow("\u26A0") + pc2.dim(" Failed to install dependencies"));
338
399
  }
339
400
  }
340
- console.log();
341
- console.log(pc.dim(" Installing Lumera skills for AI agents..."));
401
+ const stopSkills = spinner("Installing Lumera skills for AI agents...");
342
402
  try {
343
403
  const { installed, failed } = await installAllSkills(targetDir);
344
404
  if (failed > 0) {
345
- console.log(pc.yellow(" \u26A0"), pc.dim(`Installed ${installed} skills (${failed} failed)`));
405
+ stopSkills(pc2.yellow("\u26A0") + pc2.dim(` Installed ${installed} skills (${failed} failed)`));
346
406
  } else {
347
- console.log(pc.green(" \u2713"), pc.dim(`${installed} Lumera skills installed`));
407
+ stopSkills(pc2.green("\u2713") + pc2.dim(` ${installed} Lumera skills installed`));
348
408
  }
349
409
  syncClaudeMd(targetDir);
350
410
  if (isGitInstalled()) {
@@ -357,20 +417,22 @@ async function init(args) {
357
417
  }
358
418
  }
359
419
  } catch (err) {
360
- console.log(pc.yellow(" \u26A0"), pc.dim(`Failed to install skills: ${err}`));
420
+ stopSkills(pc2.yellow("\u26A0") + pc2.dim(` Failed to install skills: ${err}`));
361
421
  }
362
422
  console.log();
363
- console.log(pc.green(pc.bold(" Done!")), "Next steps:");
423
+ console.log(pc2.green(pc2.bold(" Done!")), "Next steps:");
364
424
  console.log();
365
- console.log(pc.cyan(` cd ${directory}`));
425
+ console.log(pc2.cyan(` cd ${directory}`));
366
426
  if (!opts.install) {
367
- console.log(pc.cyan(" pnpm install"));
427
+ console.log(pc2.cyan(" pnpm install"));
368
428
  }
369
- console.log(pc.cyan(" lumera login"));
370
- console.log(pc.cyan(" lumera apply"));
371
- console.log(pc.cyan(" lumera run scripts/seed-demo.py"));
372
- console.log(pc.cyan(" lumera dev"));
429
+ console.log(pc2.cyan(" lumera login"));
430
+ console.log(pc2.cyan(" lumera dev"));
373
431
  console.log();
432
+ if (opts.open) {
433
+ openInEditor(targetDir);
434
+ console.log();
435
+ }
374
436
  }
375
437
  export {
376
438
  init