@zaflun/lumio-cli 0.3.0 → 0.5.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.
Files changed (2) hide show
  1. package/dist/index.js +613 -41
  2. package/package.json +2 -2
package/dist/index.js CHANGED
@@ -295,7 +295,7 @@ function registerInit(program2) {
295
295
  type: "list",
296
296
  name: "category",
297
297
  message: "Category:",
298
- choices: ["widget", "extension", "overlay", "bot_module", "integration", "theme"]
298
+ choices: ["widget", "extension", "overlay", "bot_module", "integration", "theme", "automation_node"]
299
299
  }
300
300
  ]);
301
301
  const answers = { ...baseAnswers, targets: [], server: false };
@@ -311,6 +311,39 @@ function registerInit(program2) {
311
311
  answers.platforms = platforms;
312
312
  answers.targets = [];
313
313
  answers.server = true;
314
+ } else if (answers.category === "automation_node") {
315
+ const nodeAnswers = await inquirer.prompt([
316
+ {
317
+ type: "list",
318
+ name: "nodeType",
319
+ message: "Node type:",
320
+ choices: [
321
+ { name: "Action \u2014 perform work when the automation runs", value: "action" },
322
+ { name: "Trigger \u2014 start an automation from an external event", value: "trigger" },
323
+ { name: "Logic \u2014 conditional branching", value: "logic" }
324
+ ]
325
+ }
326
+ ]);
327
+ let triggerMode;
328
+ if (nodeAnswers.nodeType === "trigger") {
329
+ const triggerAnswers = await inquirer.prompt([
330
+ {
331
+ type: "list",
332
+ name: "triggerMode",
333
+ message: "How does this trigger receive events?",
334
+ choices: [
335
+ { name: "Webhook \u2014 receive HTTP POST from an external service", value: "webhook" },
336
+ { name: "Polling \u2014 periodically check an external service", value: "polling" },
337
+ { name: "Both \u2014 support webhook and polling", value: "both" }
338
+ ]
339
+ }
340
+ ]);
341
+ triggerMode = triggerAnswers.triggerMode;
342
+ }
343
+ answers.targets = ["editor"];
344
+ answers.server = true;
345
+ answers.nodeType = nodeAnswers.nodeType;
346
+ answers.triggerMode = triggerMode;
314
347
  } else {
315
348
  const targetAndServer = await inquirer.prompt([
316
349
  {
@@ -325,6 +358,66 @@ function registerInit(program2) {
325
358
  answers.targets = targetAndServer.targets;
326
359
  answers.server = targetAndServer.server;
327
360
  }
361
+ let editorApproach = "schema";
362
+ if (answers.category !== "bot_module" && answers.category !== "automation_node") {
363
+ const editorChoice = await inquirer.prompt([{
364
+ type: "list",
365
+ name: "approach",
366
+ message: "How should the editor UI be built?",
367
+ choices: [
368
+ { name: "config_schema only \u2014 auto-generated form from schema fields (recommended)", value: "schema" },
369
+ { name: "editor.tsx only \u2014 fully custom React UI in sandbox", value: "custom" },
370
+ { name: "Both \u2014 schema fields + custom editor.tsx for advanced controls", value: "both" }
371
+ ],
372
+ default: "schema"
373
+ }]);
374
+ editorApproach = editorChoice.approach;
375
+ if (editorApproach === "schema") {
376
+ answers.targets = answers.targets.filter((t) => t !== "editor");
377
+ } else if (!answers.targets.includes("editor")) {
378
+ answers.targets.push("editor");
379
+ }
380
+ }
381
+ answers.editorApproach = editorApproach;
382
+ let includeDesigner = false;
383
+ if (answers.category === "widget" || answers.category === "overlay") {
384
+ const designerAnswer = await inquirer.prompt([{
385
+ type: "confirm",
386
+ name: "includeDesigner",
387
+ message: "Include a visual designer? (fullpage design editor with live preview)",
388
+ default: false
389
+ }]);
390
+ includeDesigner = designerAnswer.includeDesigner;
391
+ }
392
+ let useTailwind = false;
393
+ if (answers.category !== "bot_module") {
394
+ const styleAnswer = await inquirer.prompt([
395
+ {
396
+ type: "list",
397
+ name: "styling",
398
+ message: "Styling approach:",
399
+ choices: [
400
+ { name: "Tailwind CSS v4 (recommended)", value: "tailwind" },
401
+ { name: "Plain CSS", value: "css" },
402
+ { name: "Inline styles only", value: "inline" }
403
+ ],
404
+ default: "tailwind"
405
+ }
406
+ ]);
407
+ useTailwind = styleAnswer.styling === "tailwind";
408
+ answers.styling = styleAnswer.styling;
409
+ }
410
+ try {
411
+ await client.get("/v1/developer/profile");
412
+ } catch (err) {
413
+ if (err.status === 404) {
414
+ console.error(
415
+ "\n You need to apply as a developer first.\n Visit https://lumio.vision/account/developer to submit your application.\n"
416
+ );
417
+ process.exit(1);
418
+ }
419
+ throw err;
420
+ }
328
421
  const createBody = {
329
422
  name: answers.name,
330
423
  description: answers.description,
@@ -351,6 +444,23 @@ function registerInit(program2) {
351
444
  visibility: "private",
352
445
  pricing: { type: "free" }
353
446
  };
447
+ if (editorApproach === "schema" || editorApproach === "both") {
448
+ config.config_schema = [
449
+ { key: "title", type: "string", label: "Title", default_value: answers.name, required: false, placeholder: "Enter a title..." },
450
+ { key: "fontSize", type: "number", label: "Font Size", default_value: 16, required: false, min: 8, max: 200, step: 1, unit: "px" },
451
+ { key: "color", type: "color", label: "Color", default_value: "#ffffff", required: false }
452
+ ];
453
+ }
454
+ if (includeDesigner) {
455
+ config.designer_schema = [
456
+ { key: "primaryColor", type: "color", label: "Primary Color", default_value: "#43F7CD", required: false },
457
+ { key: "backgroundColor", type: "color", label: "Background Color", default_value: "#1a1a2e", required: false },
458
+ { key: "fontFamily", type: "font", label: "Font Family", default_value: "Inter", required: false }
459
+ ];
460
+ if (config.config_schema && Array.isArray(config.config_schema)) {
461
+ config.config_schema.push({ key: "design", type: "designer", label: "Widget Designer", required: false, default_value: null });
462
+ }
463
+ }
354
464
  if (answers.category === "bot_module") {
355
465
  config.platforms = answers.platforms;
356
466
  config.triggers = {
@@ -361,29 +471,88 @@ function registerInit(program2) {
361
471
  timers: []
362
472
  };
363
473
  }
474
+ if (answers.category === "automation_node") {
475
+ const nodeType = answers.nodeType;
476
+ const triggerMode = answers.triggerMode;
477
+ config.node_type = nodeType;
478
+ const nodeConfig = {
479
+ input_schema: {
480
+ type: "object",
481
+ properties: {
482
+ value: { type: "string", title: "Input Value" }
483
+ },
484
+ required: ["value"]
485
+ },
486
+ output_schema: {
487
+ type: "object",
488
+ properties: {
489
+ result: { type: "string" }
490
+ }
491
+ }
492
+ };
493
+ if (nodeType === "trigger") {
494
+ nodeConfig.trigger_mode = triggerMode;
495
+ if (triggerMode === "polling" || triggerMode === "both") {
496
+ nodeConfig.poll_interval_seconds = 30;
497
+ }
498
+ }
499
+ nodeConfig.icon = "puzzle";
500
+ nodeConfig.color = "#a855f7";
501
+ config.node = nodeConfig;
502
+ }
364
503
  fs2.writeFileSync(
365
504
  path2.join(projectDir, "lumio.config.json"),
366
505
  JSON.stringify(config, null, 2)
367
506
  );
507
+ const isBotModule = answers.category === "bot_module";
508
+ const isAutomationNode = answers.category === "automation_node";
509
+ const needsReact = !isBotModule || isAutomationNode;
510
+ const lintDir = isBotModule ? "server/" : "src/";
368
511
  const pkg = {
369
512
  name: answers.name.toLowerCase().replace(/\s+/g, "-"),
370
513
  version: "0.1.0",
371
514
  private: true,
372
515
  type: "module",
516
+ scripts: {
517
+ "dev": isBotModule ? "lumio dev --bot-module" : isAutomationNode ? "lumio dev --automation-node" : "lumio dev",
518
+ "build": "lumio build",
519
+ "deploy": "lumio deploy",
520
+ "typecheck": "tsc --noEmit",
521
+ "lint": `eslint ${lintDir}`
522
+ },
373
523
  dependencies: {
374
524
  "@zaflun/lumio-sdk": "latest",
375
- "react": "^19.1.0",
376
- "react-dom": "^19.1.0"
525
+ ...needsReact ? { "react": "^19.2.5" } : {}
377
526
  },
378
527
  devDependencies: {
379
- "typescript": "^5.8.3",
380
- "@types/react": "^19.1.0"
528
+ "typescript": "^6.0.3",
529
+ ...needsReact ? { "@types/react": "^19.1.0" } : {},
530
+ ...useTailwind ? { "tailwindcss": "^4.3.0" } : {}
381
531
  }
382
532
  };
383
533
  fs2.writeFileSync(
384
534
  path2.join(projectDir, "package.json"),
385
535
  JSON.stringify(pkg, null, 2)
386
536
  );
537
+ const tsconfig = {
538
+ compilerOptions: {
539
+ target: "ES2022",
540
+ module: "ESNext",
541
+ moduleResolution: "bundler",
542
+ jsx: "react-jsx",
543
+ strict: true,
544
+ esModuleInterop: true,
545
+ skipLibCheck: true,
546
+ outDir: "dist"
547
+ },
548
+ include: answers.category === "bot_module" ? ["server/**/*"] : ["src/**/*", "server/**/*"]
549
+ };
550
+ fs2.writeFileSync(
551
+ path2.join(projectDir, "tsconfig.json"),
552
+ JSON.stringify(tsconfig, null, 2)
553
+ );
554
+ const eslintConfig = 'import eslintJs from "@eslint/js";\nimport tseslint from "typescript-eslint";\n\nexport default tseslint.config(\n eslintJs.configs.recommended,\n ...tseslint.configs.recommended,\n {\n rules: {\n "quotes": ["error", "double", { avoidEscape: true }],\n },\n },\n);\n';
555
+ fs2.writeFileSync(path2.join(projectDir, "eslint.config.mjs"), eslintConfig);
387
556
  const changelog = `# Changelog
388
557
 
389
558
  ## [0.1.0] - ${(/* @__PURE__ */ new Date()).toISOString().split("T")[0]}
@@ -393,15 +562,83 @@ function registerInit(program2) {
393
562
  `;
394
563
  fs2.writeFileSync(path2.join(projectDir, "CHANGELOG.md"), changelog);
395
564
  fs2.mkdirSync(path2.join(projectDir, "src"), { recursive: true });
565
+ if (useTailwind) {
566
+ fs2.writeFileSync(
567
+ path2.join(projectDir, "src", "globals.css"),
568
+ '@import "tailwindcss";\n'
569
+ );
570
+ } else if (needsReact && answers.styling === "css") {
571
+ fs2.writeFileSync(
572
+ path2.join(projectDir, "src", "globals.css"),
573
+ "/* Add your custom styles here */\n"
574
+ );
575
+ }
396
576
  if (answers.category === "bot_module") {
397
577
  fs2.mkdirSync(path2.join(projectDir, "server"), { recursive: true });
398
578
  const serverFunctions = 'import { command } from "@zaflun/lumio-sdk/server";\n\nexport const hello = command("hello", async (ctx, args) => {\n return { reply: `Hello ${ctx.user?.name ?? "stranger"}!` };\n});\n';
399
579
  fs2.writeFileSync(path2.join(projectDir, "server", "functions.ts"), serverFunctions);
400
580
  const schema = 'import { defineSchema } from "@zaflun/lumio-sdk/server";\n\nexport default defineSchema({});\n';
401
581
  fs2.writeFileSync(path2.join(projectDir, "server", "schema.ts"), schema);
582
+ } else if (answers.category === "automation_node") {
583
+ fs2.mkdirSync(path2.join(projectDir, "server"), { recursive: true });
584
+ const nodeType = answers.nodeType;
585
+ const triggerMode = answers.triggerMode;
586
+ let serverHandler;
587
+ if (nodeType === "action") {
588
+ serverHandler = 'import { defineAutomationAction } from "@zaflun/lumio-sdk/server";\n\ndefineAutomationAction(async (ctx) => {\n const value = ctx.inputData.value;\n\n // TODO: Implement your action logic\n // Use ctx.fetch() for external API calls\n // Use ctx.cache.get/set for temporary storage\n // Use ctx.installConfig for extension-level settings\n // Use ctx.nodeConfig for per-node-instance settings\n\n return {\n output: { result: `Processed: ${value}` },\n status: "success",\n };\n});\n';
589
+ } else if (nodeType === "trigger") {
590
+ const parts = [];
591
+ if (triggerMode === "webhook" || triggerMode === "both") {
592
+ parts.push(" onWebhook: async (ctx) => {\n const payload = ctx.webhookBody;\n\n // TODO: Validate the incoming webhook payload\n // Return { fired: false } to ignore the event\n // Return { fired: true, output: { ... } } to trigger the automation\n\n return {\n fired: true,\n output: {\n result: JSON.stringify(payload),\n },\n };\n }");
593
+ }
594
+ if (triggerMode === "polling" || triggerMode === "both") {
595
+ parts.push(' onPoll: async (ctx) => {\n const lastCheck = await ctx.cache.get("last_check_ts");\n\n // TODO: Check your external service for new events\n // const res = await ctx.fetch("https://api.example.com/events?since=" + lastCheck);\n // const events = await res.json();\n\n await ctx.cache.set("last_check_ts", Date.now().toString());\n\n // Return null or { fired: false } if no new events\n // Return { fired: true, output: { ... } } to trigger the automation\n return null;\n }');
596
+ }
597
+ serverHandler = `import { defineAutomationTrigger } from "@zaflun/lumio-sdk/server";
598
+
599
+ defineAutomationTrigger({
600
+ ${parts.join(",\n")}
601
+ });
602
+ `;
603
+ } else {
604
+ serverHandler = 'import { defineAutomationLogic } from "@zaflun/lumio-sdk/server";\n\ndefineAutomationLogic(async (ctx) => {\n const value = ctx.inputData.value;\n const threshold = ctx.nodeConfig.threshold ?? 50;\n\n // TODO: Implement your branching logic\n // Return the branch key that matches an output handle\n\n if (Number(value) > Number(threshold)) {\n return "above";\n }\n return "below";\n});\n';
605
+ }
606
+ fs2.writeFileSync(path2.join(projectDir, "server", "handler.ts"), serverHandler);
607
+ const editorComponent = `import { Lumio, CompactView, Text } from "@zaflun/lumio-sdk";
608
+ import { useLumioConfig } from "@zaflun/lumio-sdk/hooks";
609
+
610
+ function ConfigPanel() {
611
+ const [config, setConfig] = useLumioConfig();
612
+
613
+ return (
614
+ <CompactView title="${answers.name} \u2014 Settings">
615
+ <Text content="Configure your automation node here." />
616
+ </CompactView>
617
+ );
618
+ }
619
+
620
+ Lumio.render(<ConfigPanel />, { target: "editor" });
621
+ `;
622
+ fs2.writeFileSync(path2.join(projectDir, "src", "editor.tsx"), editorComponent);
402
623
  } else {
403
624
  for (const target of answers.targets) {
404
- const content = `import { Lumio, CompactView, Text } from "@zaflun/lumio-sdk";
625
+ if (target === "editor" && editorApproach === "schema") continue;
626
+ const isEditorTarget = target === "editor";
627
+ const content = isEditorTarget ? `import { Lumio, CompactView, Text } from "@zaflun/lumio-sdk";
628
+ import { useLumioConfig } from "@zaflun/lumio-sdk";
629
+
630
+ function Editor() {
631
+ const [config, setConfig] = useLumioConfig();
632
+
633
+ return (
634
+ <CompactView title="${answers.name} \u2014 Advanced">
635
+ <Text content="Custom editor controls go here." />
636
+ </CompactView>
637
+ );
638
+ }
639
+
640
+ export default Editor;
641
+ ` : `import { Lumio, CompactView, Text } from "@zaflun/lumio-sdk";
405
642
 
406
643
  function App() {
407
644
  return (
@@ -443,7 +680,7 @@ var ConfigError = class extends Error {
443
680
  function parseConfigFile(jsonString) {
444
681
  const raw = JSON.parse(jsonString);
445
682
  return {
446
- extensionId: raw.extensionId ?? "",
683
+ extensionId: raw.extensionId ?? raw.extension_id ?? "",
447
684
  name: raw.name ?? "",
448
685
  description: raw.description,
449
686
  category: raw.category ?? "widget",
@@ -458,8 +695,18 @@ function parseConfigFile(jsonString) {
458
695
  platforms: raw.platforms,
459
696
  triggers: raw.triggers,
460
697
  fonts: raw.fonts,
698
+ sounds: raw.sounds,
461
699
  visibility: raw.visibility ?? "private",
462
- pricing: raw.pricing ?? { type: "free" }
700
+ pricing: raw.pricing ?? { type: "free" },
701
+ nodeType: raw.node_type ?? raw.nodeType,
702
+ node: raw.node ? {
703
+ inputSchema: raw.node.input_schema ?? raw.node.inputSchema ?? {},
704
+ outputSchema: raw.node.output_schema ?? raw.node.outputSchema ?? {},
705
+ triggerMode: raw.node.trigger_mode ?? raw.node.triggerMode,
706
+ pollIntervalSeconds: raw.node.poll_interval_seconds ?? raw.node.pollIntervalSeconds,
707
+ icon: raw.node.icon,
708
+ color: raw.node.color
709
+ } : void 0
463
710
  };
464
711
  }
465
712
  function validateConfig(config) {
@@ -473,6 +720,27 @@ function validateConfig(config) {
473
720
  if (!config.platforms || config.platforms.length === 0) {
474
721
  throw new ConfigError("At least one platform is required for bot_module (twitch, youtube, kick, trovo, discord)");
475
722
  }
723
+ } else if (config.category === "automation_node") {
724
+ if (!config.nodeType) {
725
+ throw new ConfigError("nodeType is required for automation_node extensions (trigger, action, or logic)");
726
+ }
727
+ if (!["trigger", "action", "logic"].includes(config.nodeType)) {
728
+ throw new ConfigError("nodeType must be one of: trigger, action, logic");
729
+ }
730
+ if (!config.node) {
731
+ throw new ConfigError("node configuration is required for automation_node extensions");
732
+ }
733
+ if (config.nodeType === "trigger" && !config.node.triggerMode) {
734
+ throw new ConfigError("node.triggerMode is required for trigger nodes (webhook, polling, or both)");
735
+ }
736
+ if (config.nodeType !== "trigger" && config.node.triggerMode) {
737
+ throw new ConfigError("node.triggerMode is only valid for trigger nodes");
738
+ }
739
+ if (config.node.pollIntervalSeconds !== void 0) {
740
+ if (config.node.pollIntervalSeconds < 10 || config.node.pollIntervalSeconds > 300) {
741
+ throw new ConfigError("node.pollIntervalSeconds must be between 10 and 300");
742
+ }
743
+ }
476
744
  } else if (!config.targets || config.targets.length === 0) {
477
745
  throw new ConfigError("At least one target is required (editor, layer, or interactive)");
478
746
  }
@@ -553,11 +821,120 @@ async function runBotModuleDevServer(config) {
553
821
  });
554
822
  }
555
823
 
824
+ // src/commands/dev-automation-node.ts
825
+ import * as readline2 from "readline";
826
+ import chalk2 from "chalk";
827
+ async function runAutomationNodeDevServer(config) {
828
+ const nodeType = config.nodeType ?? "action";
829
+ console.log(chalk2.bold("\n\u2699\uFE0F Automation Node Dev Server"));
830
+ console.log(`Extension: ${config.name}`);
831
+ console.log(`Node type: ${nodeType}`);
832
+ if (nodeType === "trigger" && config.node?.triggerMode) {
833
+ console.log(`Trigger mode: ${config.node.triggerMode}`);
834
+ }
835
+ console.log(chalk2.dim("\nCommands:"));
836
+ console.log(chalk2.dim(" /execute [json] \u2014 Execute the node with input data"));
837
+ if (nodeType === "trigger") {
838
+ console.log(chalk2.dim(" /webhook [json] \u2014 Simulate a webhook POST payload"));
839
+ console.log(chalk2.dim(" /poll \u2014 Trigger a poll cycle"));
840
+ }
841
+ console.log(chalk2.dim(" /config [json] \u2014 Set node instance config"));
842
+ console.log(chalk2.dim(" /install-config [j] \u2014 Set extension install config"));
843
+ console.log(chalk2.dim(" /quit \u2014 Exit\n"));
844
+ let nodeConfig = {};
845
+ let installConfig = {};
846
+ const rl = readline2.createInterface({
847
+ input: process.stdin,
848
+ output: process.stdout,
849
+ prompt: chalk2.cyan("> ")
850
+ });
851
+ rl.prompt();
852
+ rl.on("line", async (line) => {
853
+ const trimmed = line.trim();
854
+ if (!trimmed) {
855
+ rl.prompt();
856
+ return;
857
+ }
858
+ if (trimmed === "/quit") {
859
+ rl.close();
860
+ return;
861
+ }
862
+ if (trimmed.startsWith("/config ")) {
863
+ try {
864
+ nodeConfig = JSON.parse(trimmed.slice("/config ".length));
865
+ console.log(chalk2.green(" Node config updated."));
866
+ } catch {
867
+ console.log(chalk2.red(" Invalid JSON."));
868
+ }
869
+ rl.prompt();
870
+ return;
871
+ }
872
+ if (trimmed.startsWith("/install-config ")) {
873
+ try {
874
+ installConfig = JSON.parse(trimmed.slice("/install-config ".length));
875
+ console.log(chalk2.green(" Install config updated."));
876
+ } catch {
877
+ console.log(chalk2.red(" Invalid JSON."));
878
+ }
879
+ rl.prompt();
880
+ return;
881
+ }
882
+ if (trimmed.startsWith("/execute")) {
883
+ const jsonStr = trimmed.slice("/execute".length).trim();
884
+ let inputData = {};
885
+ if (jsonStr) {
886
+ try {
887
+ inputData = JSON.parse(jsonStr);
888
+ } catch {
889
+ console.log(chalk2.red(" Invalid JSON."));
890
+ rl.prompt();
891
+ return;
892
+ }
893
+ }
894
+ console.log(chalk2.dim(` [${nodeType}] Executing with input: ${JSON.stringify(inputData)}`));
895
+ console.log(chalk2.dim(` [${nodeType}] Node config: ${JSON.stringify(nodeConfig)}`));
896
+ console.log(chalk2.dim(` [${nodeType}] Install config: ${JSON.stringify(installConfig)}`));
897
+ console.log(chalk2.yellow(" \u2192 Handler execution not yet wired (requires esbuild + V8)"));
898
+ rl.prompt();
899
+ return;
900
+ }
901
+ if (nodeType === "trigger" && trimmed.startsWith("/webhook")) {
902
+ const jsonStr = trimmed.slice("/webhook".length).trim();
903
+ let body = {};
904
+ if (jsonStr) {
905
+ try {
906
+ body = JSON.parse(jsonStr);
907
+ } catch {
908
+ console.log(chalk2.red(" Invalid JSON."));
909
+ rl.prompt();
910
+ return;
911
+ }
912
+ }
913
+ console.log(chalk2.dim(` [trigger:webhook] Received payload: ${JSON.stringify(body)}`));
914
+ console.log(chalk2.yellow(" \u2192 Handler execution not yet wired (requires esbuild + V8)"));
915
+ rl.prompt();
916
+ return;
917
+ }
918
+ if (nodeType === "trigger" && trimmed === "/poll") {
919
+ console.log(chalk2.dim(" [trigger:poll] Polling..."));
920
+ console.log(chalk2.yellow(" \u2192 Handler execution not yet wired (requires esbuild + V8)"));
921
+ rl.prompt();
922
+ return;
923
+ }
924
+ console.log(chalk2.dim(" Unknown command. Type /execute, /webhook, /poll, /config, or /quit."));
925
+ rl.prompt();
926
+ });
927
+ rl.on("close", () => {
928
+ console.log(chalk2.dim("\nAutomation Node Dev Server stopped."));
929
+ process.exit(0);
930
+ });
931
+ }
932
+
556
933
  // src/commands/dev.ts
557
934
  function registerDev(program2) {
558
- program2.command("dev").description("Start local dev server with hot-reload and mock events").option("-p, --port <port>", "Dev server port", "3100").option("--bot-module", "Start interactive bot module chat simulator").action(async (options) => {
935
+ program2.command("dev").description("Start local dev server with hot-reload and mock events").option("-p, --port <port>", "Dev server port", "3100").option("--bot-module", "Start interactive bot module chat simulator").option("--automation-node", "Start automation node dev simulator").action(async (options) => {
559
936
  const configPath = path3.resolve("lumio.config.json");
560
- if (options.botModule || fs3.existsSync(configPath)) {
937
+ if (options.botModule || options.automationNode || fs3.existsSync(configPath)) {
561
938
  const configExists = fs3.existsSync(configPath);
562
939
  if (configExists) {
563
940
  const config = parseConfigFile(fs3.readFileSync(configPath, "utf-8"));
@@ -565,7 +942,11 @@ function registerDev(program2) {
565
942
  await runBotModuleDevServer(config);
566
943
  return;
567
944
  }
568
- } else if (options.botModule) {
945
+ if (options.automationNode || config.category === "automation_node") {
946
+ await runAutomationNodeDevServer(config);
947
+ return;
948
+ }
949
+ } else if (options.botModule || options.automationNode) {
569
950
  console.error("lumio.config.json not found. Run `lumio init` first.");
570
951
  process.exit(1);
571
952
  }
@@ -581,6 +962,9 @@ function registerDev(program2) {
581
962
  // src/commands/build.ts
582
963
  import * as fs4 from "fs";
583
964
  import * as path4 from "path";
965
+ import { execFile } from "child_process";
966
+ import { promisify } from "util";
967
+ var execFileAsync = promisify(execFile);
584
968
  function registerBuild(program2) {
585
969
  program2.command("build").description("Bundle extension as standalone HTML + source archive").action(async () => {
586
970
  const configPath = path4.resolve("lumio.config.json");
@@ -602,6 +986,34 @@ function registerBuild(program2) {
602
986
  if (config.category === "bot_module" && config.targets.length === 0) {
603
987
  console.log(" Bot module \u2014 no UI targets to bundle.");
604
988
  }
989
+ const cssEntryFile = ["src/globals.css", "src/style.css", "src/index.css"].find((f) => fs4.existsSync(path4.resolve(f)));
990
+ let compiledCssPath = null;
991
+ if (cssEntryFile) {
992
+ const cssContent = fs4.readFileSync(path4.resolve(cssEntryFile), "utf-8");
993
+ const usesTailwind = cssContent.includes("tailwindcss") || cssContent.includes("@tailwind");
994
+ if (usesTailwind) {
995
+ console.log(" Tailwind CSS v4 detected \u2014 compiling...");
996
+ compiledCssPath = path4.join(distDir, "styles.css");
997
+ try {
998
+ await execFileAsync("npx", [
999
+ "@tailwindcss/cli",
1000
+ "-i",
1001
+ path4.resolve(cssEntryFile),
1002
+ "-o",
1003
+ compiledCssPath,
1004
+ "--minify"
1005
+ ], { cwd: path4.resolve(".") });
1006
+ console.log(" Compiled: dist/styles.css");
1007
+ } catch {
1008
+ console.warn(" Warning: Tailwind CSS compilation failed. Install tailwindcss v4: npm add -D tailwindcss@^4.3");
1009
+ compiledCssPath = null;
1010
+ }
1011
+ } else {
1012
+ compiledCssPath = path4.join(distDir, "styles.css");
1013
+ fs4.copyFileSync(path4.resolve(cssEntryFile), compiledCssPath);
1014
+ console.log(` Copied: ${cssEntryFile} \u2192 dist/styles.css`);
1015
+ }
1016
+ }
605
1017
  for (const target of config.targets) {
606
1018
  const entryPoint = path4.resolve("src", `${target}.tsx`);
607
1019
  if (!fs4.existsSync(entryPoint)) {
@@ -612,11 +1024,39 @@ function registerBuild(program2) {
612
1024
  entryPoints: [entryPoint],
613
1025
  bundle: true,
614
1026
  minify: true,
615
- format: "esm",
616
- outfile: path4.join(distDir, `${target}.js`),
1027
+ format: "iife",
1028
+ globalName: "LumioExtension",
1029
+ footer: {
1030
+ js: "if(typeof LumioExtension!=='undefined'&&typeof LumioExtension.default==='function'){var _L=(typeof require!=='undefined'?require('@zaflun/lumio-sdk'):{}).Lumio;if(_L)_L.render(LumioExtension.default);}"
1031
+ },
1032
+ outdir: distDir,
1033
+ entryNames: `${target}`,
617
1034
  jsx: "automatic",
618
1035
  target: "es2022",
619
- external: [],
1036
+ loader: {
1037
+ ".css": "css",
1038
+ // Asset file loaders — copies to dist/assets/ with content hash
1039
+ ".png": "file",
1040
+ ".jpg": "file",
1041
+ ".jpeg": "file",
1042
+ ".gif": "file",
1043
+ ".svg": "file",
1044
+ ".webp": "file",
1045
+ ".avif": "file",
1046
+ ".ico": "file",
1047
+ ".mp4": "file",
1048
+ ".webm": "file",
1049
+ ".ogg": "file",
1050
+ ".mp3": "file",
1051
+ ".wav": "file",
1052
+ ".woff": "file",
1053
+ ".woff2": "file",
1054
+ ".ttf": "file",
1055
+ ".otf": "file",
1056
+ ".lottie": "file"
1057
+ },
1058
+ assetNames: "assets/[name]-[hash]",
1059
+ external: ["react", "react-dom", "react/jsx-runtime", "react/jsx-dev-runtime", "@zaflun/lumio-sdk", "@zaflun/lumio-protocol"],
620
1060
  define: {
621
1061
  "process.env.NODE_ENV": '"production"'
622
1062
  }
@@ -628,15 +1068,19 @@ function registerBuild(program2) {
628
1068
  const output = fs4.createWriteStream(sourceArchivePath);
629
1069
  const archive = archiver("tar", { gzip: true });
630
1070
  archive.pipe(output);
631
- archive.directory("src/", "src");
632
- if ((config.server || config.category === "bot_module") && fs4.existsSync("server")) {
633
- archive.directory("server/", "server");
634
- }
635
- archive.file("lumio.config.json", { name: "lumio.config.json" });
636
- archive.file("package.json", { name: "package.json" });
637
- if (fs4.existsSync("CHANGELOG.md")) {
638
- archive.file("CHANGELOG.md", { name: "CHANGELOG.md" });
639
- }
1071
+ archive.glob("**/*", {
1072
+ cwd: path4.resolve("."),
1073
+ ignore: [
1074
+ "node_modules/**",
1075
+ "dist/**",
1076
+ ".git/**",
1077
+ ".next/**",
1078
+ ".lumio-dist/**",
1079
+ "*.tgz",
1080
+ "*.tar.gz"
1081
+ ],
1082
+ dot: true
1083
+ });
640
1084
  await archive.finalize();
641
1085
  console.log(" Archive: dist/source.tar.gz");
642
1086
  console.log("Build complete.");
@@ -683,12 +1127,59 @@ function getVersionEntry(changelog, version) {
683
1127
  }
684
1128
 
685
1129
  // src/commands/deploy.ts
1130
+ function collectFiles(dir, base = dir) {
1131
+ const entries = fs5.readdirSync(dir, { withFileTypes: true });
1132
+ const files = [];
1133
+ for (const entry of entries) {
1134
+ const fullPath = path5.join(dir, entry.name);
1135
+ if (entry.isDirectory()) {
1136
+ files.push(...collectFiles(fullPath, base));
1137
+ } else if (entry.isFile()) {
1138
+ files.push(path5.relative(base, fullPath));
1139
+ }
1140
+ }
1141
+ return files;
1142
+ }
1143
+ var ALLOWED_EXTENSIONS = /* @__PURE__ */ new Set([
1144
+ "js",
1145
+ "css",
1146
+ "html",
1147
+ "map",
1148
+ "png",
1149
+ "jpg",
1150
+ "jpeg",
1151
+ "gif",
1152
+ "svg",
1153
+ "webp",
1154
+ "avif",
1155
+ "ico",
1156
+ "mp4",
1157
+ "webm",
1158
+ "ogg",
1159
+ "mp3",
1160
+ "wav",
1161
+ "m4a",
1162
+ "aac",
1163
+ "flac",
1164
+ "woff",
1165
+ "woff2",
1166
+ "ttf",
1167
+ "otf",
1168
+ "eot",
1169
+ "json",
1170
+ "lottie"
1171
+ ]);
1172
+ var AUDIO_EXTENSIONS = /* @__PURE__ */ new Set(["mp3", "wav", "ogg", "webm", "m4a", "aac", "flac"]);
1173
+ var LARGE_SOUND_THRESHOLD = 5 * 1024 * 1024;
1174
+ var MAX_FILE_SIZE = 10 * 1024 * 1024;
1175
+ var MAX_TOTAL_SIZE = 50 * 1024 * 1024;
1176
+ var MAX_FILES = 200;
686
1177
  function registerDeploy(program2) {
687
- program2.command("deploy").description("Upload extension to Lumio (status: draft)").option("-v, --version <version>", "Version override").option("-d, --description <text>", "Release notes override").option("--breaking", "Mark as breaking change").option("--draft", "Explicitly mark as draft").action(async (options) => {
1178
+ program2.command("deploy").description("Upload extension to Lumio (status: draft)").option("-v, --version <version>", "Version override").option("-d, --description <text>", "Release notes override").option("--breaking", "Mark as breaking change").option("--draft", "Explicitly mark as draft").option("--token <token>", "API token (for CI, skips login)").option("--system", "Deploy as system extension (no developer profile required)").action(async (options) => {
688
1179
  const store = new CredentialStore();
689
- const token = store.getToken();
1180
+ const token = options.token ?? store.getToken();
690
1181
  if (!token) {
691
- console.error("Not logged in. Run `lumio login` first.");
1182
+ console.error("Not logged in. Run `lumio login` or provide --token.");
692
1183
  process.exit(1);
693
1184
  }
694
1185
  const configPath = path5.resolve("lumio.config.json");
@@ -699,6 +1190,34 @@ function registerDeploy(program2) {
699
1190
  const configJson = fs5.readFileSync(configPath, "utf-8");
700
1191
  const config = parseConfigFile(configJson);
701
1192
  validateConfig(config);
1193
+ const distDir = path5.resolve("dist");
1194
+ if (config.sounds && config.sounds.length > 0) {
1195
+ for (const sound of config.sounds) {
1196
+ const soundFile = sound.file;
1197
+ const ext = soundFile.split(".").pop()?.toLowerCase() ?? "";
1198
+ if (!AUDIO_EXTENSIONS.has(ext)) {
1199
+ console.error(
1200
+ `Error: Sound file "${soundFile}" has unsupported extension ".${ext}". Allowed audio formats: ${[...AUDIO_EXTENSIONS].join(", ")}.`
1201
+ );
1202
+ process.exit(1);
1203
+ }
1204
+ const soundAbsPath = path5.join(distDir, "sounds", soundFile);
1205
+ if (fs5.existsSync(distDir) && !fs5.existsSync(soundAbsPath)) {
1206
+ console.error(
1207
+ `Error: Sound file "${soundFile}" declared in lumio.config.json was not found at dist/sounds/${soundFile}. Run \`lumio build\` first.`
1208
+ );
1209
+ process.exit(1);
1210
+ }
1211
+ if (fs5.existsSync(soundAbsPath)) {
1212
+ const stat = fs5.statSync(soundAbsPath);
1213
+ if (stat.size > LARGE_SOUND_THRESHOLD) {
1214
+ console.warn(
1215
+ ` Warning: Sound file "${soundFile}" is ${(stat.size / 1024 / 1024).toFixed(1)} MB. Consider compressing large audio files to reduce load times.`
1216
+ );
1217
+ }
1218
+ }
1219
+ }
1220
+ }
702
1221
  const version = options.version ?? config.version;
703
1222
  let releaseNotes = options.description ?? "";
704
1223
  if (!releaseNotes) {
@@ -708,44 +1227,97 @@ function registerDeploy(program2) {
708
1227
  const entry = getVersionEntry(changelog, version);
709
1228
  if (entry) {
710
1229
  releaseNotes = entry.content;
711
- } else if (!LumioApiClient.isCI()) {
1230
+ } else if (!LumioApiClient.isCI() && !options.system) {
712
1231
  console.error(`No changelog entry found for version ${version}.`);
713
1232
  console.error("Add an entry to CHANGELOG.md or use -d to provide release notes.");
714
1233
  process.exit(1);
715
1234
  }
716
1235
  }
717
1236
  }
718
- const distDir = path5.resolve("dist");
719
1237
  if (!fs5.existsSync(distDir)) {
720
1238
  console.error("No dist/ directory found. Run `lumio build` first.");
721
1239
  process.exit(1);
722
1240
  }
723
1241
  console.log(`Deploying ${config.name} v${version}...`);
724
- const client = new LumioApiClient(store.getApiUrl(), token);
725
- const formData = new FormData();
726
- const sourceArchive = path5.join(distDir, "source.tar.gz");
727
- if (fs5.existsSync(sourceArchive)) {
728
- const sourceBuffer = fs5.readFileSync(sourceArchive);
729
- formData.append("source", new Blob([sourceBuffer]), "source.tar.gz");
1242
+ const apiUrl = process.env.LUMIO_API_URL ?? store.getApiUrl();
1243
+ const client = new LumioApiClient(apiUrl, token);
1244
+ const allFiles = collectFiles(distDir);
1245
+ const sourceFile = "source.tar.gz";
1246
+ const bundleFiles = allFiles.filter((f) => {
1247
+ if (f === sourceFile) return false;
1248
+ if (f === "bundle.zip") return false;
1249
+ const ext = f.split(".").pop()?.toLowerCase() ?? "";
1250
+ return ALLOWED_EXTENSIONS.has(ext);
1251
+ });
1252
+ if (bundleFiles.length === 0) {
1253
+ console.error("No uploadable files found in dist/. Run `lumio build` first.");
1254
+ process.exit(1);
730
1255
  }
1256
+ if (bundleFiles.length > MAX_FILES) {
1257
+ console.error(`Too many files (${bundleFiles.length}). Maximum is ${MAX_FILES}.`);
1258
+ process.exit(1);
1259
+ }
1260
+ const sourceArchive = path5.join(distDir, sourceFile);
1261
+ if (!fs5.existsSync(sourceArchive)) {
1262
+ console.error("Error: source.tar.gz not found in dist/. Run `lumio build` first.");
1263
+ process.exit(1);
1264
+ }
1265
+ const formData = new FormData();
731
1266
  const manifest = JSON.parse(configJson);
732
1267
  manifest.version = version;
733
1268
  formData.append("version", version);
734
1269
  formData.append("manifest", JSON.stringify(manifest));
735
1270
  formData.append("release_notes", releaseNotes);
736
1271
  formData.append("is_breaking", String(!!options.breaking));
1272
+ const sourceBuffer = fs5.readFileSync(sourceArchive);
1273
+ if (sourceBuffer.length === 0) {
1274
+ console.error("Error: source.tar.gz is empty. Run `lumio build` to regenerate.");
1275
+ process.exit(1);
1276
+ }
1277
+ formData.append("source", new Blob([sourceBuffer]), "source.tar.gz");
1278
+ console.log(` Source: source.tar.gz (${(sourceBuffer.length / 1024).toFixed(1)} KB)`);
1279
+ let totalSize = 0;
1280
+ for (const relPath of bundleFiles) {
1281
+ const absPath = path5.join(distDir, relPath);
1282
+ const buffer = fs5.readFileSync(absPath);
1283
+ if (buffer.length === 0) {
1284
+ console.warn(` Warning: Skipping empty file: ${relPath}`);
1285
+ continue;
1286
+ }
1287
+ if (buffer.length > MAX_FILE_SIZE) {
1288
+ console.error(`Error: File '${relPath}' exceeds ${MAX_FILE_SIZE / 1024 / 1024} MB limit.`);
1289
+ process.exit(1);
1290
+ }
1291
+ totalSize += buffer.length;
1292
+ if (totalSize > MAX_TOTAL_SIZE) {
1293
+ console.error(`Error: Total bundle size exceeds ${MAX_TOTAL_SIZE / 1024 / 1024} MB limit.`);
1294
+ process.exit(1);
1295
+ }
1296
+ formData.append("files", new Blob([buffer]), relPath);
1297
+ console.log(` File: ${relPath} (${(buffer.length / 1024).toFixed(1)} KB)`);
1298
+ }
1299
+ console.log(` Total: ${bundleFiles.length} files (${(totalSize / 1024).toFixed(1)} KB)`);
1300
+ console.log(" Uploading...");
737
1301
  try {
738
- await client.uploadFile(
739
- `/v1/developer/extensions/${config.extensionId}/versions`,
740
- formData
741
- );
742
- console.log("Upload successful. Status: draft");
743
- console.log("Submit for review via the dashboard or `lumio status`.");
1302
+ const uploadPath = options.system ? `/v1/system/extensions/${config.extensionId}/versions/upload` : `/v1/developer/extensions/${config.extensionId}/versions/upload`;
1303
+ await client.uploadFile(uploadPath, formData);
1304
+ console.log("\n Upload successful. Status: draft");
1305
+ console.log(" Submit for review via the dashboard or `lumio status`.");
744
1306
  if (LumioApiClient.isCI()) {
745
1307
  process.exit(0);
746
1308
  }
747
1309
  } catch (err) {
748
- console.error("Deploy failed:", err);
1310
+ const status = err.status;
1311
+ const message = err.message ?? String(err);
1312
+ console.error(`
1313
+ Deploy failed${status ? ` (HTTP ${status})` : ""}: ${message}`);
1314
+ if (status === 409) {
1315
+ console.error(" This version already exists. Bump the version in lumio.config.json.");
1316
+ } else if (status === 413) {
1317
+ console.error(" Bundle or source archive exceeds the maximum upload size.");
1318
+ } else if (status === 401 || status === 403) {
1319
+ console.error(" Authentication failed. Run `lumio login` to refresh your token.");
1320
+ }
749
1321
  process.exit(1);
750
1322
  }
751
1323
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zaflun/lumio-cli",
3
- "version": "0.3.0",
3
+ "version": "0.5.2",
4
4
  "description": "Lumio Extension CLI — Build, test, and deploy Lumio extensions",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -22,7 +22,7 @@
22
22
  "inquirer": "^12.6.0",
23
23
  "open": "^10.1.2",
24
24
  "ora": "^8.2.0",
25
- "@zaflun/lumio-extension-types": "0.3.0"
25
+ "@zaflun/lumio-extension-types": "0.3.2"
26
26
  },
27
27
  "devDependencies": {
28
28
  "@types/archiver": "^6.0.3",