@zaflun/lumio-cli 0.2.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 +864 -121
  2. package/package.json +2 -2
package/dist/index.js CHANGED
@@ -75,7 +75,7 @@ function registerLogin(program2) {
75
75
  const state = crypto.randomBytes(16).toString("hex");
76
76
  const port = 9876;
77
77
  const redirectUri = `http://localhost:${port}/callback`;
78
- await new Promise((resolve9, reject) => {
78
+ await new Promise((resolve10, reject) => {
79
79
  const server = http.createServer(async (req, res) => {
80
80
  if (!req.url?.startsWith("/callback")) {
81
81
  res.writeHead(404);
@@ -118,7 +118,7 @@ function registerLogin(program2) {
118
118
  return;
119
119
  }
120
120
  server.close();
121
- resolve9();
121
+ resolve10();
122
122
  });
123
123
  server.listen(port, async () => {
124
124
  const pkceUrl = `${options.apiUrl}/v1/auth/cli/authorize?code_challenge=${challenge}&code_challenge_method=S256&redirect_uri=${encodeURIComponent(redirectUri)}&state=${state}`;
@@ -288,24 +288,136 @@ function registerInit(program2) {
288
288
  }
289
289
  } catch {
290
290
  }
291
- const answers = await inquirer.prompt([
291
+ const baseAnswers = await inquirer.prompt([
292
292
  { type: "input", name: "name", message: "Extension name:", default: name },
293
293
  { type: "input", name: "description", message: "Description:" },
294
294
  {
295
295
  type: "list",
296
296
  name: "category",
297
297
  message: "Category:",
298
- choices: ["widget", "extension", "overlay", "bot_module", "integration", "theme"]
299
- },
300
- {
301
- type: "checkbox",
302
- name: "targets",
303
- message: "Targets:",
304
- choices: ["editor", "layer", "interactive"],
305
- default: ["editor", "layer"]
306
- },
307
- { type: "confirm", name: "server", message: "Server functions?", default: false }
298
+ choices: ["widget", "extension", "overlay", "bot_module", "integration", "theme", "automation_node"]
299
+ }
308
300
  ]);
301
+ const answers = { ...baseAnswers, targets: [], server: false };
302
+ if (answers.category === "bot_module") {
303
+ const { platforms } = await inquirer.prompt([{
304
+ type: "checkbox",
305
+ name: "platforms",
306
+ message: "Which platforms should this bot module support?",
307
+ choices: ["twitch", "youtube", "kick", "trovo", "discord"],
308
+ default: ["twitch"],
309
+ validate: (input) => input.length > 0 || "Select at least one platform"
310
+ }]);
311
+ answers.platforms = platforms;
312
+ answers.targets = [];
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;
347
+ } else {
348
+ const targetAndServer = await inquirer.prompt([
349
+ {
350
+ type: "checkbox",
351
+ name: "targets",
352
+ message: "Targets:",
353
+ choices: ["editor", "layer", "interactive"],
354
+ default: ["editor", "layer"]
355
+ },
356
+ { type: "confirm", name: "server", message: "Server functions?", default: false }
357
+ ]);
358
+ answers.targets = targetAndServer.targets;
359
+ answers.server = targetAndServer.server;
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
+ }
309
421
  const createBody = {
310
422
  name: answers.name,
311
423
  description: answers.description,
@@ -327,34 +439,120 @@ function registerInit(program2) {
327
439
  version: "0.1.0",
328
440
  targets: answers.targets,
329
441
  server: answers.server,
330
- permissions: [],
442
+ permissions: answers.category === "bot_module" ? ["chat:read", "chat:send"] : [],
331
443
  egress: { allowHosts: [], allowPorts: [443] },
332
444
  visibility: "private",
333
445
  pricing: { type: "free" }
334
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
+ }
464
+ if (answers.category === "bot_module") {
465
+ config.platforms = answers.platforms;
466
+ config.triggers = {
467
+ commands: [{ name: "hello", description: "Say hello", cooldownGlobal: 5, cooldownUser: 10, minRole: "everyone" }],
468
+ keywords: [],
469
+ patterns: [],
470
+ events: [],
471
+ timers: []
472
+ };
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
+ }
335
503
  fs2.writeFileSync(
336
504
  path2.join(projectDir, "lumio.config.json"),
337
505
  JSON.stringify(config, null, 2)
338
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/";
339
511
  const pkg = {
340
512
  name: answers.name.toLowerCase().replace(/\s+/g, "-"),
341
513
  version: "0.1.0",
342
514
  private: true,
343
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
+ },
344
523
  dependencies: {
345
524
  "@zaflun/lumio-sdk": "latest",
346
- "react": "^19.1.0",
347
- "react-dom": "^19.1.0"
525
+ ...needsReact ? { "react": "^19.2.5" } : {}
348
526
  },
349
527
  devDependencies: {
350
- "typescript": "^5.8.3",
351
- "@types/react": "^19.1.0"
528
+ "typescript": "^6.0.3",
529
+ ...needsReact ? { "@types/react": "^19.1.0" } : {},
530
+ ...useTailwind ? { "tailwindcss": "^4.3.0" } : {}
352
531
  }
353
532
  };
354
533
  fs2.writeFileSync(
355
534
  path2.join(projectDir, "package.json"),
356
535
  JSON.stringify(pkg, null, 2)
357
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);
358
556
  const changelog = `# Changelog
359
557
 
360
558
  ## [0.1.0] - ${(/* @__PURE__ */ new Date()).toISOString().split("T")[0]}
@@ -364,8 +562,83 @@ function registerInit(program2) {
364
562
  `;
365
563
  fs2.writeFileSync(path2.join(projectDir, "CHANGELOG.md"), changelog);
366
564
  fs2.mkdirSync(path2.join(projectDir, "src"), { recursive: true });
367
- for (const target of answers.targets) {
368
- const content = `import { Lumio, CompactView, Text } from "@zaflun/lumio-sdk";
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
+ }
576
+ if (answers.category === "bot_module") {
577
+ fs2.mkdirSync(path2.join(projectDir, "server"), { recursive: true });
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';
579
+ fs2.writeFileSync(path2.join(projectDir, "server", "functions.ts"), serverFunctions);
580
+ const schema = 'import { defineSchema } from "@zaflun/lumio-sdk/server";\n\nexport default defineSchema({});\n';
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);
623
+ } else {
624
+ for (const target of answers.targets) {
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";
369
642
 
370
643
  function App() {
371
644
  return (
@@ -377,14 +650,15 @@ function App() {
377
650
 
378
651
  Lumio.render(<App />, { target: "${target}" });
379
652
  `;
380
- fs2.writeFileSync(path2.join(projectDir, "src", `${target}.tsx`), content);
381
- }
382
- if (answers.server) {
383
- fs2.mkdirSync(path2.join(projectDir, "server"), { recursive: true });
384
- const schema = 'import { defineSchema, defineTable, v } from "@zaflun/lumio-sdk/server";\n\nexport default defineSchema({\n items: defineTable({\n text: v.string(),\n completed: v.boolean(),\n }).storage("instance"),\n});\n';
385
- fs2.writeFileSync(path2.join(projectDir, "server", "schema.ts"), schema);
386
- const functions = 'import { queryRows, insertRow, patchRow, deleteRow, arg } from "@zaflun/lumio-sdk/server";\n\nexport const getItems = queryRows("items");\nexport const addItem = insertRow("items", { text: arg("text"), completed: false }, { editorOnly: true });\nexport const toggleItem = patchRow("items", arg("id"), { completed: arg("completed") }, { editorOnly: true });\nexport const removeItem = deleteRow("items", arg("id"), { editorOnly: true });\n';
387
- fs2.writeFileSync(path2.join(projectDir, "server", "functions.ts"), functions);
653
+ fs2.writeFileSync(path2.join(projectDir, "src", `${target}.tsx`), content);
654
+ }
655
+ if (answers.server) {
656
+ fs2.mkdirSync(path2.join(projectDir, "server"), { recursive: true });
657
+ const schema = 'import { defineSchema, defineTable, v } from "@zaflun/lumio-sdk/server";\n\nexport default defineSchema({\n items: defineTable({\n text: v.string(),\n completed: v.boolean(),\n }).storage("instance"),\n});\n';
658
+ fs2.writeFileSync(path2.join(projectDir, "server", "schema.ts"), schema);
659
+ const functions = 'import { queryRows, insertRow, patchRow, deleteRow, arg } from "@zaflun/lumio-sdk/server";\n\nexport const getItems = queryRows("items");\nexport const addItem = insertRow("items", { text: arg("text"), completed: false }, { editorOnly: true });\nexport const toggleItem = patchRow("items", arg("id"), { completed: arg("completed") }, { editorOnly: true });\nexport const removeItem = deleteRow("items", arg("id"), { editorOnly: true });\n';
660
+ fs2.writeFileSync(path2.join(projectDir, "server", "functions.ts"), functions);
661
+ }
388
662
  }
389
663
  console.log(`
390
664
  Done! Created ${projectDir}/`);
@@ -393,17 +667,6 @@ Done! Created ${projectDir}/`);
393
667
  }
394
668
 
395
669
  // src/commands/dev.ts
396
- function registerDev(program2) {
397
- program2.command("dev").description("Start local dev server with hot-reload and mock events").option("-p, --port <port>", "Dev server port", "3100").action(async (options) => {
398
- console.log(`Starting Lumio dev server on port ${options.port}...`);
399
- console.log("Mock events enabled. Hot-reload active.");
400
- console.log("Open http://localhost:" + options.port + " in your browser.");
401
- console.log("\nDev server implementation requires @zaflun/lumio-sdk and Vite.");
402
- console.log("Full implementation coming in Phase 12 (Live-Code-Editor).");
403
- });
404
- }
405
-
406
- // src/commands/build.ts
407
670
  import * as fs3 from "fs";
408
671
  import * as path3 from "path";
409
672
 
@@ -417,7 +680,7 @@ var ConfigError = class extends Error {
417
680
  function parseConfigFile(jsonString) {
418
681
  const raw = JSON.parse(jsonString);
419
682
  return {
420
- extensionId: raw.extensionId ?? "",
683
+ extensionId: raw.extensionId ?? raw.extension_id ?? "",
421
684
  name: raw.name ?? "",
422
685
  description: raw.description,
423
686
  category: raw.category ?? "widget",
@@ -429,9 +692,21 @@ function parseConfigFile(jsonString) {
429
692
  allowHosts: raw.egress?.allowHosts ?? [],
430
693
  allowPorts: raw.egress?.allowPorts ?? []
431
694
  },
695
+ platforms: raw.platforms,
696
+ triggers: raw.triggers,
432
697
  fonts: raw.fonts,
698
+ sounds: raw.sounds,
433
699
  visibility: raw.visibility ?? "private",
434
- 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
435
710
  };
436
711
  }
437
712
  function validateConfig(config) {
@@ -441,7 +716,32 @@ function validateConfig(config) {
441
716
  if (!config.name) {
442
717
  throw new ConfigError("name is required in lumio.config.json");
443
718
  }
444
- if (!config.targets || config.targets.length === 0) {
719
+ if (config.category === "bot_module") {
720
+ if (!config.platforms || config.platforms.length === 0) {
721
+ throw new ConfigError("At least one platform is required for bot_module (twitch, youtube, kick, trovo, discord)");
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
+ }
744
+ } else if (!config.targets || config.targets.length === 0) {
445
745
  throw new ConfigError("At least one target is required (editor, layer, or interactive)");
446
746
  }
447
747
  if (!config.version) {
@@ -449,28 +749,274 @@ function validateConfig(config) {
449
749
  }
450
750
  }
451
751
 
752
+ // src/commands/dev-bot-module.ts
753
+ import * as readline from "readline";
754
+ import chalk from "chalk";
755
+ function parseUserPrefix(input) {
756
+ const defaultUser = {
757
+ id: "user-001",
758
+ name: "dev_user",
759
+ displayName: "Dev User",
760
+ platform: "twitch",
761
+ role: "everyone",
762
+ isMod: false,
763
+ isVip: false,
764
+ isSub: false
765
+ };
766
+ if (!input.startsWith("@")) {
767
+ return { user: defaultUser, message: input };
768
+ }
769
+ const spaceIdx = input.indexOf(" ");
770
+ if (spaceIdx === -1) return { user: defaultUser, message: input };
771
+ const prefix = input.substring(1, spaceIdx);
772
+ const message = input.substring(spaceIdx + 1);
773
+ const parts = prefix.split(":");
774
+ const name = parts[parts.length - 1];
775
+ const rolePrefix = parts.length > 1 ? parts[0] : "everyone";
776
+ return {
777
+ user: {
778
+ id: `user-${name}`,
779
+ name,
780
+ displayName: name,
781
+ platform: "twitch",
782
+ role: rolePrefix,
783
+ isMod: rolePrefix === "mod",
784
+ isVip: rolePrefix === "vip",
785
+ isSub: ["sub", "vip", "mod", "broadcaster"].includes(rolePrefix)
786
+ },
787
+ message
788
+ };
789
+ }
790
+ async function runBotModuleDevServer(config) {
791
+ console.log(chalk.bold("\n\u{1F916} Bot Module Dev Server"));
792
+ console.log(`Extension: ${config.name}`);
793
+ console.log(`Platforms: ${config.platforms?.join(", ") ?? "none"}`);
794
+ console.log(chalk.dim("\nType chat messages. Prefix with @username or @role:username."));
795
+ console.log(chalk.dim("Examples: !hello | @sub:viewer !points 100 | @mod:admin gg"));
796
+ console.log(chalk.dim("Type !quit to exit.\n"));
797
+ const rl = readline.createInterface({
798
+ input: process.stdin,
799
+ output: process.stdout,
800
+ prompt: chalk.cyan("> ")
801
+ });
802
+ rl.prompt();
803
+ rl.on("line", async (line) => {
804
+ const trimmed = line.trim();
805
+ if (!trimmed) {
806
+ rl.prompt();
807
+ return;
808
+ }
809
+ if (trimmed === "!quit") {
810
+ rl.close();
811
+ return;
812
+ }
813
+ const { user, message } = parseUserPrefix(trimmed);
814
+ console.log(chalk.dim(` [${user.role}:${user.name}] ${message}`));
815
+ console.log(chalk.yellow(" \u2192 Handler execution not yet wired (requires esbuild + V8)"));
816
+ rl.prompt();
817
+ });
818
+ rl.on("close", () => {
819
+ console.log(chalk.dim("\nBot Module Dev Server stopped."));
820
+ process.exit(0);
821
+ });
822
+ }
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
+
933
+ // src/commands/dev.ts
934
+ function registerDev(program2) {
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) => {
936
+ const configPath = path3.resolve("lumio.config.json");
937
+ if (options.botModule || options.automationNode || fs3.existsSync(configPath)) {
938
+ const configExists = fs3.existsSync(configPath);
939
+ if (configExists) {
940
+ const config = parseConfigFile(fs3.readFileSync(configPath, "utf-8"));
941
+ if (options.botModule || config.category === "bot_module") {
942
+ await runBotModuleDevServer(config);
943
+ return;
944
+ }
945
+ if (options.automationNode || config.category === "automation_node") {
946
+ await runAutomationNodeDevServer(config);
947
+ return;
948
+ }
949
+ } else if (options.botModule || options.automationNode) {
950
+ console.error("lumio.config.json not found. Run `lumio init` first.");
951
+ process.exit(1);
952
+ }
953
+ }
954
+ console.log(`Starting Lumio dev server on port ${options.port}...`);
955
+ console.log("Mock events enabled. Hot-reload active.");
956
+ console.log("Open http://localhost:" + options.port + " in your browser.");
957
+ console.log("\nDev server implementation requires @zaflun/lumio-sdk and Vite.");
958
+ console.log("Full implementation coming in Phase 12 (Live-Code-Editor).");
959
+ });
960
+ }
961
+
452
962
  // src/commands/build.ts
963
+ import * as fs4 from "fs";
964
+ import * as path4 from "path";
965
+ import { execFile } from "child_process";
966
+ import { promisify } from "util";
967
+ var execFileAsync = promisify(execFile);
453
968
  function registerBuild(program2) {
454
969
  program2.command("build").description("Bundle extension as standalone HTML + source archive").action(async () => {
455
- const configPath = path3.resolve("lumio.config.json");
456
- if (!fs3.existsSync(configPath)) {
970
+ const configPath = path4.resolve("lumio.config.json");
971
+ if (!fs4.existsSync(configPath)) {
457
972
  console.error("lumio.config.json not found. Run `lumio init` first.");
458
973
  process.exit(1);
459
974
  }
460
- const configJson = fs3.readFileSync(configPath, "utf-8");
975
+ const configJson = fs4.readFileSync(configPath, "utf-8");
461
976
  const config = parseConfigFile(configJson);
462
977
  validateConfig(config);
463
- const changelogPath = path3.resolve("CHANGELOG.md");
464
- if (!fs3.existsSync(changelogPath)) {
978
+ const changelogPath = path4.resolve("CHANGELOG.md");
979
+ if (!fs4.existsSync(changelogPath)) {
465
980
  console.warn("Warning: No CHANGELOG.md found. It is recommended to maintain a changelog.");
466
981
  }
467
982
  console.log(`Building ${config.name} v${config.version}...`);
468
- const distDir = path3.resolve("dist");
469
- fs3.mkdirSync(distDir, { recursive: true });
983
+ const distDir = path4.resolve("dist");
984
+ fs4.mkdirSync(distDir, { recursive: true });
470
985
  const { build } = await import("esbuild");
986
+ if (config.category === "bot_module" && config.targets.length === 0) {
987
+ console.log(" Bot module \u2014 no UI targets to bundle.");
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
+ }
471
1017
  for (const target of config.targets) {
472
- const entryPoint = path3.resolve("src", `${target}.tsx`);
473
- if (!fs3.existsSync(entryPoint)) {
1018
+ const entryPoint = path4.resolve("src", `${target}.tsx`);
1019
+ if (!fs4.existsSync(entryPoint)) {
474
1020
  console.warn(`Warning: Entry point ${entryPoint} not found, skipping target "${target}".`);
475
1021
  continue;
476
1022
  }
@@ -478,11 +1024,39 @@ function registerBuild(program2) {
478
1024
  entryPoints: [entryPoint],
479
1025
  bundle: true,
480
1026
  minify: true,
481
- format: "esm",
482
- outfile: path3.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}`,
483
1034
  jsx: "automatic",
484
1035
  target: "es2022",
485
- 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"],
486
1060
  define: {
487
1061
  "process.env.NODE_ENV": '"production"'
488
1062
  }
@@ -490,19 +1064,23 @@ function registerBuild(program2) {
490
1064
  console.log(` Built: dist/${target}.js`);
491
1065
  }
492
1066
  const { default: archiver } = await import("archiver");
493
- const sourceArchivePath = path3.join(distDir, "source.tar.gz");
494
- const output = fs3.createWriteStream(sourceArchivePath);
1067
+ const sourceArchivePath = path4.join(distDir, "source.tar.gz");
1068
+ const output = fs4.createWriteStream(sourceArchivePath);
495
1069
  const archive = archiver("tar", { gzip: true });
496
1070
  archive.pipe(output);
497
- archive.directory("src/", "src");
498
- if (config.server && fs3.existsSync("server")) {
499
- archive.directory("server/", "server");
500
- }
501
- archive.file("lumio.config.json", { name: "lumio.config.json" });
502
- archive.file("package.json", { name: "package.json" });
503
- if (fs3.existsSync("CHANGELOG.md")) {
504
- archive.file("CHANGELOG.md", { name: "CHANGELOG.md" });
505
- }
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
+ });
506
1084
  await archive.finalize();
507
1085
  console.log(" Archive: dist/source.tar.gz");
508
1086
  console.log("Build complete.");
@@ -510,8 +1088,8 @@ function registerBuild(program2) {
510
1088
  }
511
1089
 
512
1090
  // src/commands/deploy.ts
513
- import * as fs4 from "fs";
514
- import * as path4 from "path";
1091
+ import * as fs5 from "fs";
1092
+ import * as path5 from "path";
515
1093
 
516
1094
  // src/changelog/parser.ts
517
1095
  var VERSION_HEADER_REGEX = /^## \[([^\]]+)\](?:\s*-\s*(\d{4}-\d{2}-\d{2}))?/;
@@ -549,77 +1127,205 @@ function getVersionEntry(changelog, version) {
549
1127
  }
550
1128
 
551
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;
552
1177
  function registerDeploy(program2) {
553
- 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) => {
554
1179
  const store = new CredentialStore();
555
- const token = store.getToken();
1180
+ const token = options.token ?? store.getToken();
556
1181
  if (!token) {
557
- console.error("Not logged in. Run `lumio login` first.");
1182
+ console.error("Not logged in. Run `lumio login` or provide --token.");
558
1183
  process.exit(1);
559
1184
  }
560
- const configPath = path4.resolve("lumio.config.json");
561
- if (!fs4.existsSync(configPath)) {
1185
+ const configPath = path5.resolve("lumio.config.json");
1186
+ if (!fs5.existsSync(configPath)) {
562
1187
  console.error("lumio.config.json not found. Run `lumio init` first.");
563
1188
  process.exit(1);
564
1189
  }
565
- const configJson = fs4.readFileSync(configPath, "utf-8");
1190
+ const configJson = fs5.readFileSync(configPath, "utf-8");
566
1191
  const config = parseConfigFile(configJson);
567
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
+ }
568
1221
  const version = options.version ?? config.version;
569
1222
  let releaseNotes = options.description ?? "";
570
1223
  if (!releaseNotes) {
571
- const changelogPath = path4.resolve("CHANGELOG.md");
572
- if (fs4.existsSync(changelogPath)) {
573
- const changelog = fs4.readFileSync(changelogPath, "utf-8");
1224
+ const changelogPath = path5.resolve("CHANGELOG.md");
1225
+ if (fs5.existsSync(changelogPath)) {
1226
+ const changelog = fs5.readFileSync(changelogPath, "utf-8");
574
1227
  const entry = getVersionEntry(changelog, version);
575
1228
  if (entry) {
576
1229
  releaseNotes = entry.content;
577
- } else if (!LumioApiClient.isCI()) {
1230
+ } else if (!LumioApiClient.isCI() && !options.system) {
578
1231
  console.error(`No changelog entry found for version ${version}.`);
579
1232
  console.error("Add an entry to CHANGELOG.md or use -d to provide release notes.");
580
1233
  process.exit(1);
581
1234
  }
582
1235
  }
583
1236
  }
584
- const distDir = path4.resolve("dist");
585
- if (!fs4.existsSync(distDir)) {
1237
+ if (!fs5.existsSync(distDir)) {
586
1238
  console.error("No dist/ directory found. Run `lumio build` first.");
587
1239
  process.exit(1);
588
1240
  }
589
1241
  console.log(`Deploying ${config.name} v${version}...`);
590
- const client = new LumioApiClient(store.getApiUrl(), token);
591
- const formData = new FormData();
592
- const sourceArchive = path4.join(distDir, "source.tar.gz");
593
- if (fs4.existsSync(sourceArchive)) {
594
- const sourceBuffer = fs4.readFileSync(sourceArchive);
595
- 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);
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);
596
1264
  }
1265
+ const formData = new FormData();
597
1266
  const manifest = JSON.parse(configJson);
598
1267
  manifest.version = version;
599
1268
  formData.append("version", version);
600
1269
  formData.append("manifest", JSON.stringify(manifest));
601
1270
  formData.append("release_notes", releaseNotes);
602
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...");
603
1301
  try {
604
- await client.uploadFile(
605
- `/v1/developer/extensions/${config.extensionId}/versions`,
606
- formData
607
- );
608
- console.log("Upload successful. Status: draft");
609
- 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`.");
610
1306
  if (LumioApiClient.isCI()) {
611
1307
  process.exit(0);
612
1308
  }
613
1309
  } catch (err) {
614
- 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
+ }
615
1321
  process.exit(1);
616
1322
  }
617
1323
  });
618
1324
  }
619
1325
 
620
1326
  // src/commands/status.ts
621
- import * as fs5 from "fs";
622
- import * as path5 from "path";
1327
+ import * as fs6 from "fs";
1328
+ import * as path6 from "path";
623
1329
  function registerStatus(program2) {
624
1330
  program2.command("status").description("Show review status of the latest version").action(async () => {
625
1331
  const store = new CredentialStore();
@@ -628,12 +1334,12 @@ function registerStatus(program2) {
628
1334
  console.error("Not logged in. Run `lumio login` first.");
629
1335
  process.exit(1);
630
1336
  }
631
- const configPath = path5.resolve("lumio.config.json");
632
- if (!fs5.existsSync(configPath)) {
1337
+ const configPath = path6.resolve("lumio.config.json");
1338
+ if (!fs6.existsSync(configPath)) {
633
1339
  console.error("lumio.config.json not found.");
634
1340
  process.exit(1);
635
1341
  }
636
- const config = parseConfigFile(fs5.readFileSync(configPath, "utf-8"));
1342
+ const config = parseConfigFile(fs6.readFileSync(configPath, "utf-8"));
637
1343
  const client = new LumioApiClient(store.getApiUrl(), token);
638
1344
  try {
639
1345
  const versions = await client.get(
@@ -662,8 +1368,8 @@ function registerStatus(program2) {
662
1368
  }
663
1369
 
664
1370
  // src/commands/logs.ts
665
- import * as fs6 from "fs";
666
- import * as path6 from "path";
1371
+ import * as fs7 from "fs";
1372
+ import * as path7 from "path";
667
1373
  function registerLogs(program2) {
668
1374
  program2.command("logs").description("Stream server function logs via SSE").option("-f, --follow", "Follow log output", true).option("-n, --lines <count>", "Number of recent lines", "50").action(async (options) => {
669
1375
  const store = new CredentialStore();
@@ -672,12 +1378,12 @@ function registerLogs(program2) {
672
1378
  console.error("Not logged in. Run `lumio login` first.");
673
1379
  process.exit(1);
674
1380
  }
675
- const configPath = path6.resolve("lumio.config.json");
676
- if (!fs6.existsSync(configPath)) {
1381
+ const configPath = path7.resolve("lumio.config.json");
1382
+ if (!fs7.existsSync(configPath)) {
677
1383
  console.error("lumio.config.json not found.");
678
1384
  process.exit(1);
679
1385
  }
680
- const config = parseConfigFile(fs6.readFileSync(configPath, "utf-8"));
1386
+ const config = parseConfigFile(fs7.readFileSync(configPath, "utf-8"));
681
1387
  const apiUrl = store.getApiUrl();
682
1388
  const logsUrl = `${apiUrl}/v1/developer/extensions/${config.extensionId}/logs?lines=${options.lines}`;
683
1389
  console.log(`Streaming logs for ${config.name}...`);
@@ -714,8 +1420,8 @@ function registerLogs(program2) {
714
1420
  }
715
1421
 
716
1422
  // src/commands/env.ts
717
- import * as fs7 from "fs";
718
- import * as path7 from "path";
1423
+ import * as fs8 from "fs";
1424
+ import * as path8 from "path";
719
1425
  function validateSecretKey(key) {
720
1426
  return key.length > 0 && /^[A-Z0-9_]+$/.test(key);
721
1427
  }
@@ -724,12 +1430,12 @@ function formatSecretList(secrets) {
724
1430
  return secrets.map((s) => ` ${s.key} (created ${s.created_at})`).join("\n");
725
1431
  }
726
1432
  function loadConfig() {
727
- const configPath = path7.resolve("lumio.config.json");
728
- if (!fs7.existsSync(configPath)) {
1433
+ const configPath = path8.resolve("lumio.config.json");
1434
+ if (!fs8.existsSync(configPath)) {
729
1435
  console.error("lumio.config.json not found. Run `lumio init` first.");
730
1436
  process.exit(1);
731
1437
  }
732
- return parseConfigFile(fs7.readFileSync(configPath, "utf-8"));
1438
+ return parseConfigFile(fs8.readFileSync(configPath, "utf-8"));
733
1439
  }
734
1440
  function requireClient() {
735
1441
  const store = new CredentialStore();
@@ -771,8 +1477,27 @@ function registerEnv(program2) {
771
1477
  }
772
1478
 
773
1479
  // src/commands/run.ts
774
- import * as fs8 from "fs";
775
- import * as path8 from "path";
1480
+ import * as fs9 from "fs";
1481
+ import * as path9 from "path";
1482
+ var VALID_FUNCTION_TYPES = [
1483
+ "query",
1484
+ "mutation",
1485
+ "action",
1486
+ "command",
1487
+ "keyword",
1488
+ "pattern",
1489
+ "event",
1490
+ "timer",
1491
+ "moderate"
1492
+ ];
1493
+ var BOT_HANDLER_TYPES = [
1494
+ "command",
1495
+ "keyword",
1496
+ "pattern",
1497
+ "event",
1498
+ "timer",
1499
+ "moderate"
1500
+ ];
776
1501
  function parseFunctionRef(ref) {
777
1502
  const colonIndex = ref.indexOf(":");
778
1503
  if (colonIndex === -1) {
@@ -780,20 +1505,22 @@ function parseFunctionRef(ref) {
780
1505
  }
781
1506
  const type = ref.slice(0, colonIndex);
782
1507
  const name = ref.slice(colonIndex + 1);
783
- if (!["query", "mutation", "action"].includes(type)) {
784
- throw new Error(`Function type must be query, mutation, or action. Got: "${type}"`);
1508
+ if (!VALID_FUNCTION_TYPES.includes(type)) {
1509
+ throw new Error(
1510
+ `Function type must be one of: ${VALID_FUNCTION_TYPES.join(", ")}. Got: "${type}"`
1511
+ );
785
1512
  }
786
1513
  return { type, name };
787
1514
  }
788
1515
  function registerRun(program2) {
789
- program2.command("run <type:name> [args]").description("Execute a server function locally (e.g. lumio run query:getPlayers)").action(async (typeAndName, argsJson) => {
1516
+ program2.command("run <type:name> [args]").description("Execute a server function locally (e.g. lumio run query:getPlayers)").option("--user <name>", "Mock user name for bot handlers", "testuser").option("--role <role>", "Mock user role for bot handlers", "everyone").option("--message <text>", "Mock chat message for bot handlers").option("--platform <platform>", "Mock platform for bot handlers", "twitch").action(async (typeAndName, argsJson, options) => {
790
1517
  const fnRef = parseFunctionRef(typeAndName);
791
- const configPath = path8.resolve("lumio.config.json");
792
- if (!fs8.existsSync(configPath)) {
1518
+ const configPath = path9.resolve("lumio.config.json");
1519
+ if (!fs9.existsSync(configPath)) {
793
1520
  console.error("lumio.config.json not found. Run `lumio init` first.");
794
1521
  process.exit(1);
795
1522
  }
796
- const config = parseConfigFile(fs8.readFileSync(configPath, "utf-8"));
1523
+ const config = parseConfigFile(fs9.readFileSync(configPath, "utf-8"));
797
1524
  const store = new CredentialStore();
798
1525
  const token = store.getToken();
799
1526
  if (!token) {
@@ -802,10 +1529,26 @@ function registerRun(program2) {
802
1529
  }
803
1530
  const client = new LumioApiClient(store.getApiUrl(), token);
804
1531
  const args = argsJson ? JSON.parse(argsJson) : {};
1532
+ const body = { type: fnRef.type, args };
1533
+ if (BOT_HANDLER_TYPES.includes(fnRef.type)) {
1534
+ body.user = {
1535
+ id: "test-user-001",
1536
+ name: options.user,
1537
+ displayName: options.user,
1538
+ platform: options.platform,
1539
+ role: options.role,
1540
+ isMod: options.role === "moderator",
1541
+ isVip: options.role === "vip",
1542
+ isSub: ["subscriber", "vip", "moderator", "broadcaster"].includes(options.role)
1543
+ };
1544
+ if (options.message) {
1545
+ body.message = { text: options.message, platformMessageId: "test-msg-001" };
1546
+ }
1547
+ }
805
1548
  try {
806
1549
  const result = await client.post(
807
1550
  `/v1/extension-installs/${config.extensionId}/execute/${fnRef.name}`,
808
- { type: fnRef.type, args }
1551
+ body
809
1552
  );
810
1553
  console.log(JSON.stringify(result, null, 2));
811
1554
  } catch (err) {
@@ -816,16 +1559,16 @@ function registerRun(program2) {
816
1559
  }
817
1560
 
818
1561
  // src/commands/dashboard.ts
819
- import * as fs9 from "fs";
820
- import * as path9 from "path";
1562
+ import * as fs10 from "fs";
1563
+ import * as path10 from "path";
821
1564
  function registerDashboard(program2) {
822
1565
  program2.command("dashboard").description("Open the extension dashboard in browser").action(async () => {
823
- const configPath = path9.resolve("lumio.config.json");
824
- if (!fs9.existsSync(configPath)) {
1566
+ const configPath = path10.resolve("lumio.config.json");
1567
+ if (!fs10.existsSync(configPath)) {
825
1568
  console.error("lumio.config.json not found. Run `lumio init` first.");
826
1569
  process.exit(1);
827
1570
  }
828
- const config = parseConfigFile(fs9.readFileSync(configPath, "utf-8"));
1571
+ const config = parseConfigFile(fs10.readFileSync(configPath, "utf-8"));
829
1572
  const baseUrl = process.env.LUMIO_WEB_URL ?? "https://lumio.vision";
830
1573
  const url = `${baseUrl}/dashboard/extensions/${config.extensionId}`;
831
1574
  console.log(`Opening ${url}...`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zaflun/lumio-cli",
3
- "version": "0.2.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.1.0"
25
+ "@zaflun/lumio-extension-types": "0.3.2"
26
26
  },
27
27
  "devDependencies": {
28
28
  "@types/archiver": "^6.0.3",