@zaflun/lumio-cli 0.6.2 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  // src/program.ts
2
- import { Command } from "commander";
3
- import { createRequire as createRequire2 } from "module";
2
+ import { Command as Command2 } from "commander";
3
+ import { createRequire as createRequire3 } from "module";
4
4
 
5
5
  // src/commands/login.ts
6
6
  import * as http from "http";
@@ -105,13 +105,13 @@ function registerLogin(program2) {
105
105
  return;
106
106
  }
107
107
  const redirectUri = `http://localhost:${port}/callback`;
108
- await new Promise((resolve10, reject) => {
108
+ await new Promise((resolve13, reject) => {
109
109
  let timeout;
110
110
  const server = http.createServer(async (req, res) => {
111
111
  const finish = () => {
112
112
  clearTimeout(timeout);
113
113
  server.close();
114
- resolve10();
114
+ resolve13();
115
115
  };
116
116
  const fail = (err) => {
117
117
  clearTimeout(timeout);
@@ -302,6 +302,22 @@ function registerWhoami(program2) {
302
302
  // src/commands/init.ts
303
303
  import * as fs2 from "fs";
304
304
  import * as path2 from "path";
305
+ var SERVER_SCHEMA_SCAFFOLD = `import { defineSchema, defineTable, v } from "@zaflun/lumio-sdk/server";
306
+
307
+ export default defineSchema({
308
+ items: defineTable({
309
+ text: v.string(),
310
+ completed: v.boolean(),
311
+ }).storage("instance"),
312
+ });
313
+ `;
314
+ var SERVER_FUNCTIONS_SCAFFOLD = `import { queryRows, insertRow, patchRow, deleteRow, arg } from "@zaflun/lumio-sdk/server";
315
+
316
+ export const getItems = queryRows("items", { orderBy: "created_at", orderDir: "asc" });
317
+ export const addItem = insertRow("items", { text: arg("text"), completed: false }, { editorOnly: true });
318
+ export const toggleItem = patchRow("items", arg("id"), { completed: arg("completed") }, { editorOnly: true });
319
+ export const removeItem = deleteRow("items", arg("id"), { editorOnly: true });
320
+ `;
305
321
  function registerInit(program2) {
306
322
  program2.command("init").argument("[name]", "Extension project name").description("Create a new extension project with interactive wizard").action(async (name) => {
307
323
  const store = new CredentialStore();
@@ -403,7 +419,7 @@ function registerInit(program2) {
403
419
  type: "checkbox",
404
420
  name: "targets",
405
421
  message: "Targets:",
406
- choices: ["editor", "layer", "interactive"],
422
+ choices: ["editor", "layer"],
407
423
  default: ["editor", "layer"]
408
424
  },
409
425
  { type: "confirm", name: "server", message: "Server functions?", default: false }
@@ -465,7 +481,7 @@ function registerInit(program2) {
465
481
  } catch (err) {
466
482
  if (err.status === 404) {
467
483
  console.error(
468
- "\n You need to apply as a developer first.\n Visit https://lumio.vision/account/developer to submit your application.\n"
484
+ "\n You need to apply as a developer first.\n Visit https://lumio.vision/dashboard/settings/developer to submit your application.\n"
469
485
  );
470
486
  process.exit(1);
471
487
  }
@@ -579,6 +595,8 @@ function registerInit(program2) {
579
595
  },
580
596
  devDependencies: {
581
597
  "typescript": "^6.0.3",
598
+ // `lumio dev` runs the extension on the devkit's mock host.
599
+ ...!isAutomationNode ? { "@zaflun/lumio-devkit": "latest" } : {},
582
600
  ...needsReact ? { "@types/react": "^19.1.0" } : {},
583
601
  ...useTailwind ? { "tailwindcss": "^4.3.0" } : {}
584
602
  }
@@ -707,10 +725,8 @@ Lumio.render(<App />, { target: "${target}" });
707
725
  }
708
726
  if (answers.server) {
709
727
  fs2.mkdirSync(path2.join(projectDir, "server"), { recursive: true });
710
- 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';
711
- fs2.writeFileSync(path2.join(projectDir, "server", "schema.ts"), schema);
712
- 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';
713
- fs2.writeFileSync(path2.join(projectDir, "server", "functions.ts"), functions);
728
+ fs2.writeFileSync(path2.join(projectDir, "server", "schema.ts"), SERVER_SCHEMA_SCAFFOLD);
729
+ fs2.writeFileSync(path2.join(projectDir, "server", "functions.ts"), SERVER_FUNCTIONS_SCAFFOLD);
714
730
  }
715
731
  }
716
732
  console.log(`
@@ -720,17 +736,20 @@ Done! Created ${projectDir}/`);
720
736
  }
721
737
 
722
738
  // src/commands/dev.ts
723
- import * as fs3 from "fs";
724
- import * as path3 from "path";
739
+ import * as fs9 from "fs";
740
+ import * as path10 from "path";
741
+ import { InvalidArgumentError } from "commander";
742
+ import chalk3 from "chalk";
725
743
 
726
744
  // src/config/parser.ts
727
745
  import { NODE_TYPES, TRIGGER_MODES, VALID_PLATFORMS } from "@zaflun/lumio-extension-types";
728
746
  var ConfigError = class extends Error {
729
- constructor(message) {
730
- super(message);
747
+ constructor(message2) {
748
+ super(message2);
731
749
  this.name = "ConfigError";
732
750
  }
733
751
  };
752
+ var VALID_TARGETS = ["editor", "layer", "designer"];
734
753
  function parseConfigFile(jsonString) {
735
754
  const raw = JSON.parse(jsonString);
736
755
  return {
@@ -806,111 +825,45 @@ function validateConfig(config) {
806
825
  }
807
826
  }
808
827
  } else if (!config.targets || config.targets.length === 0) {
809
- throw new ConfigError("At least one target is required (editor, layer, or interactive)");
828
+ throw new ConfigError("At least one target is required (editor or layer)");
829
+ }
830
+ const unknownTargets = (config.targets ?? []).filter((t) => !VALID_TARGETS.includes(t));
831
+ if (unknownTargets.length > 0) {
832
+ throw new ConfigError(
833
+ `Invalid target(s): ${unknownTargets.join(", ")}. Must be one of: ${VALID_TARGETS.join(", ")}. Viewer input comes through chat commands and channel point rewards handled by a bot module.`
834
+ );
810
835
  }
811
836
  if (!config.version) {
812
837
  throw new ConfigError("version is required in lumio.config.json");
813
838
  }
814
839
  }
815
840
 
816
- // src/commands/dev-bot-module.ts
841
+ // src/commands/dev-automation-node.ts
817
842
  import * as readline from "readline";
818
843
  import chalk from "chalk";
819
- function parseUserPrefix(input) {
820
- const defaultUser = {
821
- id: "user-001",
822
- name: "dev_user",
823
- displayName: "Dev User",
824
- platform: "twitch",
825
- role: "everyone",
826
- isMod: false,
827
- isVip: false,
828
- isSub: false
829
- };
830
- if (!input.startsWith("@")) {
831
- return { user: defaultUser, message: input };
832
- }
833
- const spaceIdx = input.indexOf(" ");
834
- if (spaceIdx === -1) return { user: defaultUser, message: input };
835
- const prefix = input.substring(1, spaceIdx);
836
- const message = input.substring(spaceIdx + 1);
837
- const parts = prefix.split(":");
838
- const name = parts[parts.length - 1];
839
- const rolePrefix = parts.length > 1 ? parts[0] : "everyone";
840
- return {
841
- user: {
842
- id: `user-${name}`,
843
- name,
844
- displayName: name,
845
- platform: "twitch",
846
- role: rolePrefix,
847
- isMod: rolePrefix === "mod",
848
- isVip: rolePrefix === "vip",
849
- isSub: ["sub", "vip", "mod", "broadcaster"].includes(rolePrefix)
850
- },
851
- message
852
- };
853
- }
854
- async function runBotModuleDevServer(config) {
855
- console.log(chalk.bold("\n\u{1F916} Bot Module Dev Server"));
856
- console.log(`Extension: ${config.name}`);
857
- console.log(`Platforms: ${config.platforms?.join(", ") ?? "none"}`);
858
- console.log(chalk.dim("\nType chat messages. Prefix with @username or @role:username."));
859
- console.log(chalk.dim("Examples: !hello | @sub:viewer !points 100 | @mod:admin gg"));
860
- console.log(chalk.dim("Type !quit to exit.\n"));
861
- const rl = readline.createInterface({
862
- input: process.stdin,
863
- output: process.stdout,
864
- prompt: chalk.cyan("> ")
865
- });
866
- rl.prompt();
867
- rl.on("line", async (line) => {
868
- const trimmed = line.trim();
869
- if (!trimmed) {
870
- rl.prompt();
871
- return;
872
- }
873
- if (trimmed === "!quit") {
874
- rl.close();
875
- return;
876
- }
877
- const { user, message } = parseUserPrefix(trimmed);
878
- console.log(chalk.dim(` [${user.role}:${user.name}] ${message}`));
879
- console.log(chalk.yellow(" \u2192 Handler execution not yet wired (requires esbuild + V8)"));
880
- rl.prompt();
881
- });
882
- rl.on("close", () => {
883
- console.log(chalk.dim("\nBot Module Dev Server stopped."));
884
- process.exit(0);
885
- });
886
- }
887
-
888
- // src/commands/dev-automation-node.ts
889
- import * as readline2 from "readline";
890
- import chalk2 from "chalk";
891
844
  async function runAutomationNodeDevServer(config) {
892
845
  const nodeType = config.nodeType ?? "action";
893
- console.log(chalk2.bold("\n\u2699\uFE0F Automation Node Dev Server"));
846
+ console.log(chalk.bold("\n\u2699\uFE0F Automation Node Dev Server"));
894
847
  console.log(`Extension: ${config.name}`);
895
848
  console.log(`Node type: ${nodeType}`);
896
849
  if (nodeType === "trigger" && config.node?.triggerMode) {
897
850
  console.log(`Trigger mode: ${config.node.triggerMode}`);
898
851
  }
899
- console.log(chalk2.dim("\nCommands:"));
900
- console.log(chalk2.dim(" /execute [json] \u2014 Execute the node with input data"));
852
+ console.log(chalk.dim("\nCommands:"));
853
+ console.log(chalk.dim(" /execute [json] \u2014 Execute the node with input data"));
901
854
  if (nodeType === "trigger") {
902
- console.log(chalk2.dim(" /webhook [json] \u2014 Simulate a webhook POST payload"));
903
- console.log(chalk2.dim(" /poll \u2014 Trigger a poll cycle"));
855
+ console.log(chalk.dim(" /webhook [json] \u2014 Simulate a webhook POST payload"));
856
+ console.log(chalk.dim(" /poll \u2014 Trigger a poll cycle"));
904
857
  }
905
- console.log(chalk2.dim(" /config [json] \u2014 Set node instance config"));
906
- console.log(chalk2.dim(" /install-config [j] \u2014 Set extension install config"));
907
- console.log(chalk2.dim(" /quit \u2014 Exit\n"));
858
+ console.log(chalk.dim(" /config [json] \u2014 Set node instance config"));
859
+ console.log(chalk.dim(" /install-config [j] \u2014 Set extension install config"));
860
+ console.log(chalk.dim(" /quit \u2014 Exit\n"));
908
861
  let nodeConfig = {};
909
862
  let installConfig = {};
910
- const rl = readline2.createInterface({
863
+ const rl = readline.createInterface({
911
864
  input: process.stdin,
912
865
  output: process.stdout,
913
- prompt: chalk2.cyan("> ")
866
+ prompt: chalk.cyan("> ")
914
867
  });
915
868
  rl.prompt();
916
869
  rl.on("line", async (line) => {
@@ -926,9 +879,9 @@ async function runAutomationNodeDevServer(config) {
926
879
  if (trimmed.startsWith("/config ")) {
927
880
  try {
928
881
  nodeConfig = JSON.parse(trimmed.slice("/config ".length));
929
- console.log(chalk2.green(" Node config updated."));
882
+ console.log(chalk.green(" Node config updated."));
930
883
  } catch {
931
- console.log(chalk2.red(" Invalid JSON."));
884
+ console.log(chalk.red(" Invalid JSON."));
932
885
  }
933
886
  rl.prompt();
934
887
  return;
@@ -936,9 +889,9 @@ async function runAutomationNodeDevServer(config) {
936
889
  if (trimmed.startsWith("/install-config ")) {
937
890
  try {
938
891
  installConfig = JSON.parse(trimmed.slice("/install-config ".length));
939
- console.log(chalk2.green(" Install config updated."));
892
+ console.log(chalk.green(" Install config updated."));
940
893
  } catch {
941
- console.log(chalk2.red(" Invalid JSON."));
894
+ console.log(chalk.red(" Invalid JSON."));
942
895
  }
943
896
  rl.prompt();
944
897
  return;
@@ -950,15 +903,15 @@ async function runAutomationNodeDevServer(config) {
950
903
  try {
951
904
  inputData = JSON.parse(jsonStr);
952
905
  } catch {
953
- console.log(chalk2.red(" Invalid JSON."));
906
+ console.log(chalk.red(" Invalid JSON."));
954
907
  rl.prompt();
955
908
  return;
956
909
  }
957
910
  }
958
- console.log(chalk2.dim(` [${nodeType}] Executing with input: ${JSON.stringify(inputData)}`));
959
- console.log(chalk2.dim(` [${nodeType}] Node config: ${JSON.stringify(nodeConfig)}`));
960
- console.log(chalk2.dim(` [${nodeType}] Install config: ${JSON.stringify(installConfig)}`));
961
- console.log(chalk2.yellow(" \u2192 Handler execution not yet wired (requires esbuild + V8)"));
911
+ console.log(chalk.dim(` [${nodeType}] Executing with input: ${JSON.stringify(inputData)}`));
912
+ console.log(chalk.dim(` [${nodeType}] Node config: ${JSON.stringify(nodeConfig)}`));
913
+ console.log(chalk.dim(` [${nodeType}] Install config: ${JSON.stringify(installConfig)}`));
914
+ console.log(chalk.yellow(" \u2192 Handler execution not yet wired (requires esbuild + V8)"));
962
915
  rl.prompt();
963
916
  return;
964
917
  }
@@ -969,297 +922,2987 @@ async function runAutomationNodeDevServer(config) {
969
922
  try {
970
923
  body = JSON.parse(jsonStr);
971
924
  } catch {
972
- console.log(chalk2.red(" Invalid JSON."));
925
+ console.log(chalk.red(" Invalid JSON."));
973
926
  rl.prompt();
974
927
  return;
975
928
  }
976
929
  }
977
- console.log(chalk2.dim(` [trigger:webhook] Received payload: ${JSON.stringify(body)}`));
978
- console.log(chalk2.yellow(" \u2192 Handler execution not yet wired (requires esbuild + V8)"));
930
+ console.log(chalk.dim(` [trigger:webhook] Received payload: ${JSON.stringify(body)}`));
931
+ console.log(chalk.yellow(" \u2192 Handler execution not yet wired (requires esbuild + V8)"));
979
932
  rl.prompt();
980
933
  return;
981
934
  }
982
935
  if (nodeType === "trigger" && trimmed === "/poll") {
983
- console.log(chalk2.dim(" [trigger:poll] Polling..."));
984
- console.log(chalk2.yellow(" \u2192 Handler execution not yet wired (requires esbuild + V8)"));
936
+ console.log(chalk.dim(" [trigger:poll] Polling..."));
937
+ console.log(chalk.yellow(" \u2192 Handler execution not yet wired (requires esbuild + V8)"));
985
938
  rl.prompt();
986
939
  return;
987
940
  }
988
- console.log(chalk2.dim(" Unknown command. Type /execute, /webhook, /poll, /config, or /quit."));
941
+ console.log(chalk.dim(" Unknown command. Type /execute, /webhook, /poll, /config, or /quit."));
989
942
  rl.prompt();
990
943
  });
991
944
  rl.on("close", () => {
992
- console.log(chalk2.dim("\nAutomation Node Dev Server stopped."));
945
+ console.log(chalk.dim("\nAutomation Node Dev Server stopped."));
993
946
  process.exit(0);
994
947
  });
995
948
  }
996
949
 
997
- // src/commands/dev.ts
998
- function registerDev(program2) {
999
- 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) => {
1000
- const configPath = path3.resolve("lumio.config.json");
1001
- if (options.botModule || options.automationNode || fs3.existsSync(configPath)) {
1002
- const configExists = fs3.existsSync(configPath);
1003
- if (configExists) {
1004
- const config = parseConfigFile(fs3.readFileSync(configPath, "utf-8"));
1005
- if (options.botModule || config.category === "bot_module") {
1006
- await runBotModuleDevServer(config);
1007
- return;
1008
- }
1009
- if (options.automationNode || config.category === "automation_node") {
1010
- await runAutomationNodeDevServer(config);
1011
- return;
1012
- }
1013
- } else if (options.botModule || options.automationNode) {
1014
- console.error("lumio.config.json not found. Run `lumio init` first.");
1015
- process.exit(1);
1016
- }
1017
- }
1018
- console.log(`Starting Lumio dev server on port ${options.port}...`);
1019
- console.log("Mock events enabled. Hot-reload active.");
1020
- console.log("Open http://localhost:" + options.port + " in your browser.");
1021
- console.log("\nDev server implementation requires @zaflun/lumio-sdk and Vite.");
1022
- console.log("Full implementation coming in Phase 12 (Live-Code-Editor).");
950
+ // src/dev/builder.ts
951
+ import * as fs6 from "fs";
952
+ import * as path6 from "path";
953
+
954
+ // src/build/pipeline.ts
955
+ import * as fs5 from "fs";
956
+ import * as path5 from "path";
957
+ import { execFile } from "child_process";
958
+ import { promisify } from "util";
959
+ import { createRequire } from "module";
960
+
961
+ // src/commands/data-schema.ts
962
+ import * as fs3 from "fs";
963
+ import * as path3 from "path";
964
+ var SCHEMA_FILES = ["server/schema.ts", "server/schema.js", "server/schema.mjs"];
965
+ function findSchemaFile(projectDir) {
966
+ return SCHEMA_FILES.find((f) => fs3.existsSync(path3.join(projectDir, f))) ?? null;
967
+ }
968
+ async function loadDataSchema(projectDir) {
969
+ const schemaFile = findSchemaFile(projectDir);
970
+ if (!schemaFile) return null;
971
+ const { build } = await import("esbuild");
972
+ const result = await build({
973
+ entryPoints: [path3.join(projectDir, schemaFile)],
974
+ absWorkingDir: projectDir,
975
+ bundle: true,
976
+ write: false,
977
+ format: "esm",
978
+ platform: "node",
979
+ target: "es2022",
980
+ logLevel: "silent"
1023
981
  });
982
+ const code = result.outputFiles[0]?.text ?? "";
983
+ const url = `data:text/javascript;base64,${Buffer.from(code).toString("base64")}`;
984
+ const mod = await import(url);
985
+ return toDataSchemaJson(mod.default, schemaFile);
986
+ }
987
+ function toDataSchemaJson(value, schemaFile = "server/schema.ts") {
988
+ const tables = value?.tables;
989
+ if (typeof tables !== "object" || tables === null || Array.isArray(tables)) {
990
+ throw new Error(
991
+ `${schemaFile} must \`export default defineSchema({ ... })\` from "@zaflun/lumio-sdk/server".`
992
+ );
993
+ }
994
+ return JSON.parse(JSON.stringify(value));
995
+ }
996
+ function formatDataSchemaError(body) {
997
+ let parsed;
998
+ try {
999
+ parsed = JSON.parse(body);
1000
+ } catch {
1001
+ return null;
1002
+ }
1003
+ const message2 = typeof parsed.message === "string" ? parsed.message : "";
1004
+ if (parsed.error === "INVALID_DATA_SCHEMA") {
1005
+ return [`Invalid data schema (server/schema.ts): ${message2}`];
1006
+ }
1007
+ if (parsed.error === "DATA_SCHEMA_NOT_ADDITIVE") {
1008
+ const violations = Array.isArray(parsed.details?.violations) ? parsed.details.violations : [];
1009
+ if (violations.length === 0) return [message2];
1010
+ return [
1011
+ "Data schema change is not additive. Published tables only allow new tables, new optional columns and relaxing NOT NULL:",
1012
+ ...violations.map((v) => {
1013
+ const target = v.column ? `${v.table}.${v.column}` : v.table;
1014
+ return ` - ${target}: ${v.rule} (applied: ${v.applied}, declared: ${v.declared})`;
1015
+ })
1016
+ ];
1017
+ }
1018
+ return null;
1024
1019
  }
1025
1020
 
1026
- // src/commands/build.ts
1021
+ // src/commands/functions-manifest.ts
1027
1022
  import * as fs4 from "fs";
1028
1023
  import * as path4 from "path";
1029
- import { execFile } from "child_process";
1030
- import { promisify } from "util";
1031
- import { createRequire } from "module";
1032
- var execFileAsync = promisify(execFile);
1033
- function registerBuild(program2) {
1034
- program2.command("build").description("Bundle extension as standalone HTML + source archive").action(async () => {
1035
- const configPath = path4.resolve("lumio.config.json");
1036
- if (!fs4.existsSync(configPath)) {
1037
- console.error("lumio.config.json not found. Run `lumio init` first.");
1038
- process.exit(1);
1024
+ var FUNCTIONS_FILES = ["server/functions.ts", "server/functions.js", "server/functions.mjs"];
1025
+ var FUNCTIONS_MANIFEST_VERSION = 1;
1026
+ var FUNCTIONS_MANIFEST_FILE = "functions.json";
1027
+ var FILTER_OPERATORS = /* @__PURE__ */ new Set(["=", "!=", ">", ">=", "<", "<=", "like", "in"]);
1028
+ var NAME_PATTERN = /^[A-Za-z_][A-Za-z0-9_]{0,63}$/;
1029
+ var FunctionsManifestError = class extends Error {
1030
+ constructor(message2) {
1031
+ super(message2);
1032
+ this.name = "FunctionsManifestError";
1033
+ }
1034
+ };
1035
+ function findFunctionsFile(projectDir) {
1036
+ return FUNCTIONS_FILES.find((f) => fs4.existsSync(path4.join(projectDir, f))) ?? null;
1037
+ }
1038
+ function combinedServerEntry(handlerEntry, functionsFile, names) {
1039
+ const spec = (file) => JSON.stringify(`./${file.replace(/\\/g, "/")}`);
1040
+ const lines = [`export * from ${spec(handlerEntry)};`];
1041
+ if (names.length > 0) {
1042
+ lines.push(`export { ${names.join(", ")} } from ${spec(functionsFile)};`);
1043
+ }
1044
+ return `${lines.join("\n")}
1045
+ `;
1046
+ }
1047
+ function handlerNames(manifest) {
1048
+ return Object.entries(manifest.functions).filter(([, entry]) => entry.kind === "handler").map(([name]) => name);
1049
+ }
1050
+ async function loadFunctionExports(projectDir) {
1051
+ const file = findFunctionsFile(projectDir);
1052
+ if (!file) return null;
1053
+ const { build } = await import("esbuild");
1054
+ const result = await build({
1055
+ entryPoints: [path4.join(projectDir, file)],
1056
+ absWorkingDir: projectDir,
1057
+ bundle: true,
1058
+ write: false,
1059
+ format: "esm",
1060
+ platform: "node",
1061
+ target: "es2022",
1062
+ logLevel: "silent"
1063
+ });
1064
+ const code = result.outputFiles[0]?.text ?? "";
1065
+ const url = `data:text/javascript;base64,${Buffer.from(code).toString("base64")}`;
1066
+ const mod = await import(url);
1067
+ return { file, exports: { ...mod } };
1068
+ }
1069
+ function buildFunctionsManifest(exports, schema, file = "server/functions.ts") {
1070
+ const functions = {};
1071
+ const seen = /* @__PURE__ */ new Map();
1072
+ for (const name of Object.keys(exports).sort()) {
1073
+ const value = exports[name];
1074
+ const where = `${file}: export "${name}"`;
1075
+ if (name === "default") {
1076
+ throw new FunctionsManifestError(
1077
+ `${file}: a default export is not allowed. Export each function under its name (\`export const getRules = queryRows("rules")\`).`
1078
+ );
1039
1079
  }
1040
- const configJson = fs4.readFileSync(configPath, "utf-8");
1041
- const config = parseConfigFile(configJson);
1042
- validateConfig(config);
1043
- const changelogPath = path4.resolve("CHANGELOG.md");
1044
- if (!fs4.existsSync(changelogPath)) {
1045
- console.warn("Warning: No CHANGELOG.md found. It is recommended to maintain a changelog.");
1080
+ if (!NAME_PATTERN.test(name)) {
1081
+ throw new FunctionsManifestError(
1082
+ `${where}: function names may only use letters, digits and "_" (at most 64 characters).`
1083
+ );
1046
1084
  }
1047
- console.log(`Building ${config.name} v${config.version}...`);
1048
- const distDir = path4.resolve("dist");
1049
- fs4.mkdirSync(distDir, { recursive: true });
1050
- const { build } = await import("esbuild");
1051
- if (config.category === "bot_module" && config.targets.length === 0) {
1052
- console.log(" Bot module \u2014 no UI targets to bundle.");
1085
+ const folded = name.toLowerCase();
1086
+ const other = seen.get(folded);
1087
+ if (other !== void 0) {
1088
+ throw new FunctionsManifestError(
1089
+ `${where}: duplicate function name \u2014 it differs from "${other}" only in case.`
1090
+ );
1053
1091
  }
1054
- if (config.server) {
1055
- const serverEntry = ["server/handler.ts", "server/handler.js", "server/index.ts", "server/index.js"].find((f) => fs4.existsSync(path4.resolve(f)));
1056
- if (serverEntry) {
1057
- await build({
1058
- entryPoints: [path4.resolve(serverEntry)],
1059
- bundle: true,
1060
- minify: true,
1061
- format: "esm",
1062
- outfile: path4.join(distDir, "server.js"),
1063
- platform: "neutral",
1064
- target: "es2022",
1065
- external: ["@zaflun/lumio-sdk", "@zaflun/lumio-protocol"],
1066
- define: {
1067
- "process.env.NODE_ENV": '"production"'
1068
- }
1069
- });
1070
- console.log(" Built: dist/server.js");
1071
- } else {
1072
- console.warn(" Warning: server: true but no server/handler.ts or server/index.ts found.");
1092
+ seen.set(folded, name);
1093
+ const entry = toEntry(value, where);
1094
+ if (entry.table !== void 0) checkTable(entry.table, schema, where);
1095
+ functions[name] = entry;
1096
+ }
1097
+ return { version: FUNCTIONS_MANIFEST_VERSION, functions };
1098
+ }
1099
+ function toEntry(value, where) {
1100
+ const type = value?.__type;
1101
+ switch (type) {
1102
+ case "queryRows": {
1103
+ const def = value;
1104
+ const entry = {
1105
+ kind: "queryRows",
1106
+ category: "query",
1107
+ editorOnly: false,
1108
+ table: tableName(def.table, where)
1109
+ };
1110
+ if (def.filter !== void 0) entry.filter = filters(def.filter, where);
1111
+ if (def.orderBy !== void 0) {
1112
+ if (typeof def.orderBy !== "string" || def.orderBy === "") {
1113
+ throw new FunctionsManifestError(`${where}: orderBy must be a column name.`);
1114
+ }
1115
+ entry.orderBy = def.orderBy;
1073
1116
  }
1074
- }
1075
- const cssEntryFile = ["src/globals.css", "src/style.css", "src/index.css"].find((f) => fs4.existsSync(path4.resolve(f)));
1076
- let compiledCssPath = null;
1077
- if (cssEntryFile) {
1078
- const cssContent = fs4.readFileSync(path4.resolve(cssEntryFile), "utf-8");
1079
- const usesTailwind = cssContent.includes("tailwindcss") || cssContent.includes("@tailwind");
1080
- if (usesTailwind) {
1081
- console.log(" Tailwind CSS v4 detected \u2014 compiling...");
1082
- compiledCssPath = path4.join(distDir, "styles.css");
1083
- try {
1084
- const projectRequire = createRequire(path4.join(path4.resolve("."), "package.json"));
1085
- const cliPkgPath = projectRequire.resolve("@tailwindcss/cli/package.json");
1086
- const cliPkg = JSON.parse(fs4.readFileSync(cliPkgPath, "utf-8"));
1087
- const binRel = typeof cliPkg.bin === "string" ? cliPkg.bin : cliPkg.bin?.tailwindcss ?? Object.values(cliPkg.bin ?? {})[0];
1088
- if (!binRel) {
1089
- throw new Error("@tailwindcss/cli has no bin entry");
1090
- }
1091
- const binPath = path4.join(path4.dirname(cliPkgPath), binRel);
1092
- await execFileAsync(process.execPath, [
1093
- binPath,
1094
- "-i",
1095
- path4.resolve(cssEntryFile),
1096
- "-o",
1097
- compiledCssPath,
1098
- "--minify"
1099
- ], { cwd: path4.resolve(".") });
1100
- console.log(" Compiled: dist/styles.css");
1101
- } catch (err) {
1102
- console.error(" Error: Tailwind CSS compilation failed. Install tailwindcss v4: npm add -D tailwindcss@^4.3 @tailwindcss/cli@^4.3");
1103
- console.error(` ${err instanceof Error ? err.message : String(err)}`);
1104
- process.exit(1);
1117
+ if (def.orderDir !== void 0) {
1118
+ if (def.orderDir !== "asc" && def.orderDir !== "desc") {
1119
+ throw new FunctionsManifestError(`${where}: orderDir must be "asc" or "desc".`);
1105
1120
  }
1106
- } else {
1107
- compiledCssPath = path4.join(distDir, "styles.css");
1108
- fs4.copyFileSync(path4.resolve(cssEntryFile), compiledCssPath);
1109
- console.log(` Copied: ${cssEntryFile} \u2192 dist/styles.css`);
1121
+ entry.orderDir = def.orderDir;
1110
1122
  }
1123
+ if (def.limit !== void 0) entry.limit = count(def.limit, "limit", where);
1124
+ if (def.offset !== void 0) entry.offset = count(def.offset, "offset", where);
1125
+ return entry;
1111
1126
  }
1112
- for (const target of config.targets) {
1113
- const entryPoint = path4.resolve("src", `${target}.tsx`);
1114
- if (!fs4.existsSync(entryPoint)) {
1115
- console.warn(`Warning: Entry point ${entryPoint} not found, skipping target "${target}".`);
1116
- continue;
1127
+ case "insertRow": {
1128
+ const def = value;
1129
+ return {
1130
+ kind: "insertRow",
1131
+ category: "mutation",
1132
+ editorOnly: editorOnly(def.editorOnly, where),
1133
+ table: tableName(def.table, where),
1134
+ values: rowValues(def.values, where)
1135
+ };
1136
+ }
1137
+ case "patchRow": {
1138
+ const def = value;
1139
+ return {
1140
+ kind: "patchRow",
1141
+ category: "mutation",
1142
+ editorOnly: editorOnly(def.editorOnly, where),
1143
+ table: tableName(def.table, where),
1144
+ rowId: rowId(def.rowId, where),
1145
+ values: rowValues(def.values, where)
1146
+ };
1147
+ }
1148
+ case "deleteRow": {
1149
+ const def = value;
1150
+ return {
1151
+ kind: "deleteRow",
1152
+ category: "mutation",
1153
+ editorOnly: editorOnly(def.editorOnly, where),
1154
+ table: tableName(def.table, where),
1155
+ rowId: rowId(def.rowId, where)
1156
+ };
1157
+ }
1158
+ case "query":
1159
+ case "mutation":
1160
+ case "action": {
1161
+ if (typeof value.handler !== "function") {
1162
+ throw new FunctionsManifestError(`${where}: ${type}() needs a handler function.`);
1117
1163
  }
1118
- await build({
1119
- entryPoints: [entryPoint],
1120
- bundle: true,
1121
- minify: true,
1122
- format: "iife",
1123
- globalName: "LumioExtension",
1124
- footer: {
1125
- 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);}"
1126
- },
1127
- outdir: distDir,
1128
- entryNames: `${target}`,
1129
- jsx: "automatic",
1130
- target: "es2022",
1131
- loader: {
1132
- ".css": "css",
1133
- // Asset file loaders — copies to dist/assets/ with content hash
1134
- ".png": "file",
1135
- ".jpg": "file",
1136
- ".jpeg": "file",
1137
- ".gif": "file",
1138
- ".svg": "file",
1139
- ".webp": "file",
1140
- ".avif": "file",
1141
- ".ico": "file",
1142
- ".mp4": "file",
1143
- ".webm": "file",
1144
- ".ogg": "file",
1145
- ".mp3": "file",
1146
- ".wav": "file",
1147
- ".woff": "file",
1148
- ".woff2": "file",
1149
- ".ttf": "file",
1150
- ".otf": "file",
1151
- ".lottie": "file"
1152
- },
1153
- assetNames: "assets/[name]-[hash]",
1154
- external: ["react", "react-dom", "react/jsx-runtime", "react/jsx-dev-runtime", "@zaflun/lumio-sdk", "@zaflun/lumio-protocol"],
1155
- define: {
1156
- "process.env.NODE_ENV": '"production"'
1157
- }
1158
- });
1159
- console.log(` Built: dist/${target}.js`);
1164
+ const flag = editorOnly(value.editorOnly, where);
1165
+ return { kind: "handler", category: type, editorOnly: flag };
1160
1166
  }
1161
- const { TarArchive } = await import("archiver");
1162
- const sourceArchivePath = path4.join(distDir, "source.tar.gz");
1163
- const output = fs4.createWriteStream(sourceArchivePath);
1164
- const archive = new TarArchive({ gzip: true });
1165
- archive.pipe(output);
1166
- archive.glob("**/*", {
1167
- cwd: path4.resolve("."),
1168
- ignore: [
1169
- "node_modules/**",
1170
- "dist/**",
1171
- ".git/**",
1172
- ".git",
1173
- ".next/**",
1174
- ".lumio-dist/**",
1175
- "*.tgz",
1176
- "*.tar.gz",
1177
- // Secrets & credentials must never be uploaded to the store: the
1178
- // archive is visible to reviewers. Exclude env files (except the
1179
- // example), npm/yarn auth, and local CLI state.
1180
- // archiver 8's readdir-glob@3 dropped the `!pattern` un-ignore
1181
- // feature (each ignore entry is now an independent minimatch, so a
1182
- // leading `!` inverts that one matcher and ignores everything else).
1183
- // Keep `.env.example` via an extglob instead of a negated entry.
1184
- "**/.env",
1185
- "**/.env.!(example)",
1186
- "**/.npmrc",
1187
- "**/.yarnrc",
1188
- "**/.yarnrc.yml",
1189
- "**/.lumio/**"
1190
- ],
1191
- dot: true
1192
- });
1193
- await archive.finalize();
1194
- console.log(" Archive: dist/source.tar.gz");
1195
- console.log("Build complete.");
1167
+ case "fetchAction":
1168
+ throw new FunctionsManifestError(
1169
+ `${where}: fetchAction() is not supported in server functions. Call ctx.fetch() from an action() handler instead.`
1170
+ );
1171
+ default:
1172
+ throw new FunctionsManifestError(
1173
+ `${where}: not a server function definition. server/functions.ts may only export queryRows, insertRow, patchRow, deleteRow, query, mutation and action definitions \u2014 move other values into a separate module.`
1174
+ );
1175
+ }
1176
+ }
1177
+ function tableName(value, where) {
1178
+ if (typeof value !== "string" || value === "") {
1179
+ throw new FunctionsManifestError(`${where}: the table name must be a non-empty string.`);
1180
+ }
1181
+ return value;
1182
+ }
1183
+ function checkTable(table, schema, where) {
1184
+ if (!schema) {
1185
+ throw new FunctionsManifestError(
1186
+ `${where}: uses table "${table}", but the extension has no server/schema.ts. Declare the table with defineSchema().`
1187
+ );
1188
+ }
1189
+ if (!Object.prototype.hasOwnProperty.call(schema.tables, table)) {
1190
+ const known = Object.keys(schema.tables);
1191
+ throw new FunctionsManifestError(
1192
+ `${where}: unknown table "${table}" (server/schema.ts declares ${known.length > 0 ? known.map((t) => `"${t}"`).join(", ") : "no tables"}).`
1193
+ );
1194
+ }
1195
+ }
1196
+ function editorOnly(value, where) {
1197
+ if (value === void 0) return false;
1198
+ if (typeof value !== "boolean") {
1199
+ throw new FunctionsManifestError(`${where}: editorOnly must be true or false.`);
1200
+ }
1201
+ return value;
1202
+ }
1203
+ function rowId(value, where) {
1204
+ if (typeof value === "string" && value !== "") return value;
1205
+ const expr = expression(value, where);
1206
+ if (expr?.__expr === "arg") return expr;
1207
+ throw new FunctionsManifestError(`${where}: the row id must be a string or arg("\u2026").`);
1208
+ }
1209
+ function count(value, field, where) {
1210
+ if (typeof value === "number" && Number.isSafeInteger(value) && value >= 0) return value;
1211
+ const expr = expression(value, where);
1212
+ if (expr?.__expr === "arg") return expr;
1213
+ throw new FunctionsManifestError(`${where}: ${field} must be a non-negative integer or arg("\u2026").`);
1214
+ }
1215
+ function filters(value, where) {
1216
+ const list = Array.isArray(value) ? value : [value];
1217
+ return list.map((item) => {
1218
+ const f = item;
1219
+ if (typeof f !== "object" || f === null || f.__expr !== "filter") {
1220
+ throw new FunctionsManifestError(`${where}: filter must be filter(field, op, value) or a list of them.`);
1221
+ }
1222
+ if (typeof f.field !== "string" || f.field === "") {
1223
+ throw new FunctionsManifestError(`${where}: filter field must be a column name.`);
1224
+ }
1225
+ if (typeof f.op !== "string" || !FILTER_OPERATORS.has(f.op)) {
1226
+ throw new FunctionsManifestError(
1227
+ `${where}: unsupported filter operator ${JSON.stringify(f.op)} (use ${[...FILTER_OPERATORS].map((o) => `"${o}"`).join(", ")}).`
1228
+ );
1229
+ }
1230
+ return { field: f.field, op: f.op, value: jsonValue(f.value, `${where} (filter "${f.field}")`) };
1196
1231
  });
1197
1232
  }
1198
-
1199
- // src/commands/deploy.ts
1200
- import * as fs5 from "fs";
1201
- import * as path5 from "path";
1202
-
1203
- // src/changelog/parser.ts
1204
- var VERSION_HEADER_REGEX = /^## \[([^\]]+)\](?:\s*-\s*(\d{4}-\d{2}-\d{2}))?/;
1205
- function parseChangelog(markdown) {
1206
- const lines = markdown.split("\n");
1207
- const entries = [];
1208
- let currentEntry = null;
1209
- const contentLines = [];
1210
- for (const line of lines) {
1211
- const match = line.match(VERSION_HEADER_REGEX);
1212
- if (match) {
1213
- if (currentEntry) {
1214
- currentEntry.content = contentLines.join("\n").trim();
1215
- entries.push(currentEntry);
1216
- contentLines.length = 0;
1217
- }
1218
- currentEntry = {
1219
- version: match[1],
1220
- date: match[2] ?? null,
1221
- content: ""
1222
- };
1223
- } else if (currentEntry) {
1224
- contentLines.push(line);
1233
+ function rowValues(value, where) {
1234
+ if (!isPlainObject(value)) {
1235
+ throw new FunctionsManifestError(`${where}: row values must be an object of column \u2192 value.`);
1236
+ }
1237
+ return jsonValue(value, where);
1238
+ }
1239
+ function expression(value, where) {
1240
+ if (!isPlainObject(value) || !("__expr" in value)) return null;
1241
+ const expr = value.__expr;
1242
+ if (expr === "arg") {
1243
+ if (typeof value.name !== "string" || value.name === "") {
1244
+ throw new FunctionsManifestError(`${where}: arg() needs a parameter name.`);
1225
1245
  }
1246
+ return { __expr: "arg", name: value.name };
1226
1247
  }
1227
- if (currentEntry) {
1228
- currentEntry.content = contentLines.join("\n").trim();
1229
- entries.push(currentEntry);
1248
+ if (expr === "identity") return { __expr: "identity" };
1249
+ if (expr === "secret") {
1250
+ throw new FunctionsManifestError(
1251
+ `${where}: secret() is only available in action() handlers, not in declarative functions.`
1252
+ );
1230
1253
  }
1231
- return entries;
1254
+ throw new FunctionsManifestError(`${where}: ${JSON.stringify(expr)} expression is not allowed here.`);
1232
1255
  }
1233
- function getVersionEntry(changelog, version) {
1234
- const entries = parseChangelog(changelog);
1235
- return entries.find((e) => e.version === version) ?? null;
1256
+ function jsonValue(value, where, stack = []) {
1257
+ if (value === null || typeof value === "string" || typeof value === "boolean") return value;
1258
+ if (typeof value === "number") {
1259
+ if (!Number.isFinite(value)) {
1260
+ throw new FunctionsManifestError(`${where}: ${value} is not a JSON number.`);
1261
+ }
1262
+ return value;
1263
+ }
1264
+ if (value === void 0) {
1265
+ throw new FunctionsManifestError(`${where}: undefined is not a JSON value; use null.`);
1266
+ }
1267
+ if (typeof value !== "object") {
1268
+ throw new FunctionsManifestError(`${where}: a ${typeof value} value cannot be serialised.`);
1269
+ }
1270
+ if (stack.includes(value)) {
1271
+ throw new FunctionsManifestError(`${where}: circular value cannot be serialised.`);
1272
+ }
1273
+ if (Array.isArray(value)) {
1274
+ return value.map((item) => jsonValue(item, where, [...stack, value]));
1275
+ }
1276
+ if (!isPlainObject(value)) {
1277
+ const ctor = value.constructor?.name ?? "object";
1278
+ throw new FunctionsManifestError(`${where}: a ${ctor} value cannot be serialised; use plain JSON.`);
1279
+ }
1280
+ const expr = expression(value, where);
1281
+ if (expr) return expr;
1282
+ const out = {};
1283
+ for (const [key, item] of Object.entries(value)) {
1284
+ out[key] = jsonValue(item, where, [...stack, value]);
1285
+ }
1286
+ return out;
1287
+ }
1288
+ function isPlainObject(value) {
1289
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
1290
+ const proto = Object.getPrototypeOf(value);
1291
+ return proto === Object.prototype || proto === null;
1236
1292
  }
1237
1293
 
1238
- // src/commands/deploy.ts
1239
- function collectFiles(dir, base = dir) {
1240
- const entries = fs5.readdirSync(dir, { withFileTypes: true });
1241
- const files = [];
1242
- for (const entry of entries) {
1243
- const fullPath = path5.join(dir, entry.name);
1244
- if (entry.isDirectory()) {
1245
- files.push(...collectFiles(fullPath, base));
1246
- } else if (entry.isFile()) {
1247
- files.push(path5.relative(base, fullPath));
1294
+ // src/build/pipeline.ts
1295
+ var execFileAsync = promisify(execFile);
1296
+ var HANDLER_FILES = ["server/handler.ts", "server/handler.js", "server/index.ts", "server/index.js"];
1297
+ var CSS_ENTRY_FILES = ["src/globals.css", "src/style.css", "src/index.css"];
1298
+ var TARGET_FOOTER = "if(typeof LumioExtension!=='undefined'&&typeof LumioExtension.default==='function'){var _L=(typeof require!=='undefined'?require('@zaflun/lumio-sdk'):{}).Lumio;if(_L)_L.render(LumioExtension.default);}";
1299
+ var ASSET_LOADERS = Object.fromEntries(
1300
+ [".png", ".jpg", ".jpeg", ".gif", ".svg", ".webp", ".avif", ".ico", ".mp4", ".webm", ".ogg", ".mp3", ".wav", ".woff", ".woff2", ".ttf", ".otf", ".lottie"].map((ext) => [ext, "file"])
1301
+ );
1302
+ function define(mode) {
1303
+ return { "process.env.NODE_ENV": JSON.stringify(mode.dev ? "development" : "production") };
1304
+ }
1305
+ function targetEntry(projectDir, target) {
1306
+ const entry = path5.join(projectDir, "src", `${target}.tsx`);
1307
+ return fs5.existsSync(entry) ? entry : null;
1308
+ }
1309
+ function targetBuildOptions(entryPoint, outdir, target, mode = {}) {
1310
+ return {
1311
+ entryPoints: [entryPoint],
1312
+ bundle: true,
1313
+ minify: !mode.dev,
1314
+ format: "iife",
1315
+ globalName: "LumioExtension",
1316
+ footer: { js: TARGET_FOOTER },
1317
+ // outdir instead of outfile so esbuild can emit asset files alongside JS;
1318
+ // entryNames keeps the target name.
1319
+ outdir,
1320
+ entryNames: target,
1321
+ jsx: "automatic",
1322
+ target: "es2022",
1323
+ loader: { ".css": "css", ...ASSET_LOADERS },
1324
+ assetNames: "assets/[name]-[hash]",
1325
+ external: ["react", "react-dom", "react/jsx-runtime", "react/jsx-dev-runtime", "@zaflun/lumio-sdk", "@zaflun/lumio-protocol"],
1326
+ define: define(mode)
1327
+ };
1328
+ }
1329
+ async function planServerBuild(projectDir, config, outfile, mode = {}) {
1330
+ const functionsFile = config.category !== "automation_node" ? findFunctionsFile(projectDir) : null;
1331
+ const schema = await loadDataSchema(projectDir);
1332
+ let manifest = null;
1333
+ if (functionsFile) {
1334
+ const loaded = await loadFunctionExports(projectDir);
1335
+ manifest = buildFunctionsManifest(loaded?.exports ?? {}, schema, functionsFile);
1336
+ }
1337
+ const handlerEntry = HANDLER_FILES.find((f) => fs5.existsSync(path5.join(projectDir, f))) ?? null;
1338
+ const sfHandlers = manifest ? handlerNames(manifest) : [];
1339
+ const entry = handlerEntry && functionsFile && sfHandlers.length > 0 ? {
1340
+ stdin: {
1341
+ contents: combinedServerEntry(handlerEntry, functionsFile, sfHandlers),
1342
+ resolveDir: projectDir,
1343
+ sourcefile: "lumio-server-entry.ts",
1344
+ loader: "ts"
1248
1345
  }
1346
+ } : handlerEntry || functionsFile ? { entryPoints: [path5.join(projectDir, handlerEntry ?? functionsFile)] } : null;
1347
+ const options = entry ? {
1348
+ ...entry,
1349
+ absWorkingDir: projectDir,
1350
+ bundle: true,
1351
+ // Minified in dev too: the isolate's ESM → script transform (backend
1352
+ // and devkit replica alike) reads esbuild's minified import/export form.
1353
+ minify: true,
1354
+ format: "esm",
1355
+ outfile,
1356
+ platform: "neutral",
1357
+ target: "es2022",
1358
+ external: ["@zaflun/lumio-sdk", "@zaflun/lumio-protocol"],
1359
+ define: define(mode)
1360
+ } : null;
1361
+ return { options, manifest, schema, handlerEntry, functionsFile };
1362
+ }
1363
+ function findCssEntry(projectDir) {
1364
+ return CSS_ENTRY_FILES.find((f) => fs5.existsSync(path5.join(projectDir, f))) ?? null;
1365
+ }
1366
+ async function compileCss(projectDir, outDir, mode = {}) {
1367
+ const cssEntryFile = findCssEntry(projectDir);
1368
+ if (!cssEntryFile) return null;
1369
+ const input = path5.join(projectDir, cssEntryFile);
1370
+ const output = path5.join(outDir, "styles.css");
1371
+ const cssContent = fs5.readFileSync(input, "utf-8");
1372
+ const usesTailwind = cssContent.includes("tailwindcss") || cssContent.includes("@tailwind");
1373
+ if (!usesTailwind) {
1374
+ fs5.copyFileSync(input, output);
1375
+ return { file: output, tailwind: false };
1249
1376
  }
1250
- return files;
1377
+ try {
1378
+ const projectRequire = createRequire(path5.join(projectDir, "package.json"));
1379
+ const cliPkgPath = projectRequire.resolve("@tailwindcss/cli/package.json");
1380
+ const cliPkg = JSON.parse(fs5.readFileSync(cliPkgPath, "utf-8"));
1381
+ const binRel = typeof cliPkg.bin === "string" ? cliPkg.bin : cliPkg.bin?.tailwindcss ?? Object.values(cliPkg.bin ?? {})[0];
1382
+ if (!binRel) throw new Error("@tailwindcss/cli has no bin entry");
1383
+ const binPath = path5.join(path5.dirname(cliPkgPath), binRel);
1384
+ await execFileAsync(process.execPath, [binPath, "-i", input, "-o", output, ...mode.dev ? [] : ["--minify"]], { cwd: projectDir });
1385
+ } catch (err) {
1386
+ throw new CssBuildError(err instanceof Error ? err.message : String(err));
1387
+ }
1388
+ return { file: output, tailwind: true };
1251
1389
  }
1252
- var ALLOWED_EXTENSIONS = /* @__PURE__ */ new Set([
1253
- "js",
1254
- "css",
1255
- "html",
1256
- "map",
1257
- "png",
1258
- "jpg",
1259
- "jpeg",
1260
- "gif",
1261
- "svg",
1262
- "webp",
1390
+ var CssBuildError = class extends Error {
1391
+ hint = "Install tailwindcss v4: npm add -D tailwindcss@^4.3 @tailwindcss/cli@^4.3";
1392
+ constructor(message2) {
1393
+ super(message2);
1394
+ this.name = "CssBuildError";
1395
+ }
1396
+ };
1397
+
1398
+ // src/dev/builder.ts
1399
+ var DEV_OUT_DIR = path6.join(".lumio", "dev");
1400
+ var DEV_TARGETS = ["editor", "layer"];
1401
+ function devTargets(projectDir, config) {
1402
+ const targets = [];
1403
+ const skipped = [];
1404
+ for (const t of config.targets) {
1405
+ if (!DEV_TARGETS.includes(t)) {
1406
+ skipped.push(`target "${t}" is not simulated by lumio dev`);
1407
+ } else if (!targetEntry(projectDir, t)) {
1408
+ skipped.push(`target "${t}": src/${t}.tsx not found`);
1409
+ } else targets.push(t);
1410
+ }
1411
+ return { targets, skipped };
1412
+ }
1413
+ function formatMessages(messages) {
1414
+ return messages.map((m) => m.location ? `${m.location.file}:${m.location.line}:${m.location.column}: ${m.text}` : m.text);
1415
+ }
1416
+ var DevBuilder = class {
1417
+ constructor(projectDir, config, targets, handlers) {
1418
+ this.projectDir = projectDir;
1419
+ this.config = config;
1420
+ this.targets = targets;
1421
+ this.handlers = handlers;
1422
+ this.outDir = path6.join(projectDir, DEV_OUT_DIR);
1423
+ }
1424
+ projectDir;
1425
+ config;
1426
+ targets;
1427
+ handlers;
1428
+ contexts = [];
1429
+ serverContext = null;
1430
+ serverEntryKey = "";
1431
+ watchers = [];
1432
+ serverTimer = null;
1433
+ serverBusy = false;
1434
+ lastTarget = /* @__PURE__ */ new Map();
1435
+ lastServer = "";
1436
+ cssRunning = null;
1437
+ stopped = false;
1438
+ outDir;
1439
+ /** Builds everything once and starts watching. Resolves to the first bundles. */
1440
+ async start() {
1441
+ fs6.mkdirSync(this.outDir, { recursive: true });
1442
+ const { context } = await import("esbuild");
1443
+ const bundles = {};
1444
+ let first = true;
1445
+ for (const target of this.targets) {
1446
+ const entry = targetEntry(this.projectDir, target);
1447
+ const outFile = path6.join(this.outDir, `${target}.js`);
1448
+ const ctx = await context({
1449
+ ...targetBuildOptions(entry, this.outDir, target, { dev: true }),
1450
+ absWorkingDir: this.projectDir,
1451
+ logLevel: "silent",
1452
+ plugins: [
1453
+ this.onEnd(`${target}.js`, (ok) => {
1454
+ if (!ok) return;
1455
+ const code = fs6.readFileSync(outFile, "utf8");
1456
+ if (this.lastTarget.get(target) === code) return;
1457
+ this.lastTarget.set(target, code);
1458
+ if (first) bundles[target] = code;
1459
+ else this.handlers.onTarget(target, code);
1460
+ void this.css();
1461
+ })
1462
+ ]
1463
+ });
1464
+ this.contexts.push(ctx);
1465
+ await ctx.rebuild().catch(() => {
1466
+ });
1467
+ }
1468
+ const server = await this.buildServer(true);
1469
+ await this.css();
1470
+ first = false;
1471
+ for (const ctx of this.contexts) await ctx.watch();
1472
+ if (this.serverContext) await this.serverContext.watch();
1473
+ this.watch(path6.join(this.projectDir, "server"), () => this.scheduleServer());
1474
+ return { bundles, server };
1475
+ }
1476
+ onEnd(key, done) {
1477
+ return {
1478
+ name: "lumio-dev",
1479
+ setup: (build) => {
1480
+ build.onEnd(async (result) => {
1481
+ if (this.stopped) return;
1482
+ const ok = result.errors.length === 0;
1483
+ this.handlers.onStatus(key, ok ? null : formatMessages(result.errors).map((m) => `${key}: ${m}`));
1484
+ await done(ok, result);
1485
+ });
1486
+ }
1487
+ };
1488
+ }
1489
+ watch(dir, onChange) {
1490
+ if (!fs6.existsSync(dir)) return;
1491
+ try {
1492
+ this.watchers.push(fs6.watch(dir, { recursive: true }, () => onChange()));
1493
+ } catch {
1494
+ }
1495
+ }
1496
+ scheduleServer() {
1497
+ if (this.stopped) return;
1498
+ if (this.serverTimer) clearTimeout(this.serverTimer);
1499
+ this.serverTimer = setTimeout(() => {
1500
+ this.serverTimer = null;
1501
+ void this.buildServer(false);
1502
+ }, 120);
1503
+ }
1504
+ /** Plans the server build (functions.json + schema), (re)creates its context when the entry changed, and builds. */
1505
+ async buildServer(first) {
1506
+ const empty = { bundle: null, functions: null, schema: null };
1507
+ if (!this.config.server) return empty;
1508
+ this.serverBusy = true;
1509
+ try {
1510
+ const outfile = path6.join(this.outDir, "server.js");
1511
+ let plan;
1512
+ try {
1513
+ plan = await planServerBuild(this.projectDir, this.config, outfile, { dev: true });
1514
+ this.handlers.onStatus("server", null);
1515
+ } catch (e) {
1516
+ this.handlers.onStatus("server", [`server: ${e instanceof Error ? e.message : String(e)}`]);
1517
+ return empty;
1518
+ }
1519
+ const result = { bundle: null, functions: plan.manifest ? JSON.stringify(plan.manifest) : null, schema: plan.schema };
1520
+ if (!plan.options) {
1521
+ await this.serverContext?.dispose();
1522
+ this.serverContext = null;
1523
+ } else {
1524
+ const key = JSON.stringify({ e: plan.options.entryPoints, s: plan.options.stdin });
1525
+ if (!this.serverContext || key !== this.serverEntryKey) {
1526
+ await this.serverContext?.dispose();
1527
+ const { context } = await import("esbuild");
1528
+ this.serverContext = await context({
1529
+ ...plan.options,
1530
+ logLevel: "silent",
1531
+ // A change inside the bundle graph re-plans: functions.ts may be part of it.
1532
+ plugins: [this.onEnd("server.js", () => {
1533
+ if (!this.serverBusy) this.scheduleServer();
1534
+ })]
1535
+ });
1536
+ this.serverEntryKey = key;
1537
+ if (!first) await this.serverContext.watch();
1538
+ }
1539
+ try {
1540
+ await this.serverContext.rebuild();
1541
+ } catch {
1542
+ if (!first) return result;
1543
+ }
1544
+ result.bundle = this.readServer(outfile);
1545
+ }
1546
+ const fingerprint = JSON.stringify(result);
1547
+ if (!first && fingerprint !== this.lastServer) this.handlers.onServer(result);
1548
+ this.lastServer = fingerprint;
1549
+ return result;
1550
+ } finally {
1551
+ this.serverBusy = false;
1552
+ }
1553
+ }
1554
+ readServer(outfile) {
1555
+ return fs6.existsSync(outfile) ? fs6.readFileSync(outfile, "utf8") : null;
1556
+ }
1557
+ /** Tailwind scans the sources, so the stylesheet follows every target build. */
1558
+ css() {
1559
+ if (this.cssRunning) return this.cssRunning;
1560
+ this.cssRunning = (async () => {
1561
+ try {
1562
+ const css = await compileCss(this.projectDir, this.outDir, { dev: true });
1563
+ this.handlers.onStatus("styles.css", null);
1564
+ if (css) this.handlers.onCss();
1565
+ } catch (e) {
1566
+ const hint = e instanceof CssBuildError ? ` (${e.hint})` : "";
1567
+ this.handlers.onStatus("styles.css", [`styles.css: ${e instanceof Error ? e.message : String(e)}${hint}`]);
1568
+ } finally {
1569
+ this.cssRunning = null;
1570
+ }
1571
+ })();
1572
+ return this.cssRunning;
1573
+ }
1574
+ /** The built stylesheet, or `null`. */
1575
+ stylesheet() {
1576
+ const file = path6.join(this.outDir, "styles.css");
1577
+ return fs6.existsSync(file) ? fs6.readFileSync(file, "utf8") : null;
1578
+ }
1579
+ async stop() {
1580
+ this.stopped = true;
1581
+ if (this.serverTimer) clearTimeout(this.serverTimer);
1582
+ for (const w of this.watchers.splice(0)) w.close();
1583
+ for (const ctx of this.contexts.splice(0)) await ctx.dispose();
1584
+ await this.serverContext?.dispose();
1585
+ this.serverContext = null;
1586
+ }
1587
+ };
1588
+
1589
+ // src/dev/project-modules.ts
1590
+ import * as vm from "vm";
1591
+ import { createRequire as createRequire2 } from "module";
1592
+ import * as path8 from "path";
1593
+
1594
+ // src/dev/resolve.ts
1595
+ import * as fs7 from "fs";
1596
+ import * as path7 from "path";
1597
+ import { pathToFileURL } from "url";
1598
+ function findPackage(fromDir, name) {
1599
+ let dir = path7.resolve(fromDir);
1600
+ for (; ; ) {
1601
+ const candidate = path7.join(dir, "node_modules", name, "package.json");
1602
+ if (fs7.existsSync(candidate)) {
1603
+ const manifest = JSON.parse(fs7.readFileSync(candidate, "utf8"));
1604
+ return {
1605
+ name,
1606
+ version: typeof manifest.version === "string" ? manifest.version : "0.0.0",
1607
+ dir: fs7.realpathSync(path7.dirname(candidate)),
1608
+ manifest
1609
+ };
1610
+ }
1611
+ const parent = path7.dirname(dir);
1612
+ if (parent === dir) return null;
1613
+ dir = parent;
1614
+ }
1615
+ }
1616
+ function entryOf(pkg, subpath = ".") {
1617
+ const exportsField = pkg.manifest.exports;
1618
+ let target;
1619
+ if (typeof exportsField === "string") {
1620
+ target = subpath === "." ? exportsField : void 0;
1621
+ } else if (exportsField && typeof exportsField === "object") {
1622
+ const map = exportsField;
1623
+ const isSubpathMap = Object.keys(map).some((k) => k.startsWith("."));
1624
+ const entry = isSubpathMap ? map[subpath] : subpath === "." ? map : void 0;
1625
+ target = pickCondition(entry);
1626
+ }
1627
+ if (target === void 0 && subpath === ".") {
1628
+ target = pkg.manifest.module ?? pkg.manifest.main ?? "index.js";
1629
+ }
1630
+ if (typeof target !== "string") {
1631
+ throw new Error(`${pkg.name} does not export "${subpath}" (installed ${pkg.version} at ${pkg.dir})`);
1632
+ }
1633
+ return path7.join(pkg.dir, target);
1634
+ }
1635
+ function pickCondition(entry) {
1636
+ if (typeof entry === "string" || entry === void 0 || entry === null) return entry ?? void 0;
1637
+ if (typeof entry !== "object") return void 0;
1638
+ const conditions = entry;
1639
+ for (const key of ["import", "node", "default"]) {
1640
+ if (key in conditions) return pickCondition(conditions[key]);
1641
+ }
1642
+ return void 0;
1643
+ }
1644
+ async function importFile(file) {
1645
+ return await import(pathToFileURL(file).href);
1646
+ }
1647
+ function compareVersions(a, b) {
1648
+ const parse = (v) => v.split("-")[0].split(".").map((n) => Number.parseInt(n, 10) || 0);
1649
+ const [x, y] = [parse(a), parse(b)];
1650
+ for (let i = 0; i < 3; i++) {
1651
+ const d = (x[i] ?? 0) - (y[i] ?? 0);
1652
+ if (d !== 0) return d < 0 ? -1 : 1;
1653
+ }
1654
+ return 0;
1655
+ }
1656
+
1657
+ // src/dev/version.ts
1658
+ var CLI_PROTOCOL_VERSION = "0.9.0";
1659
+
1660
+ // src/dev/project-modules.ts
1661
+ var MissingPackageError = class extends Error {
1662
+ constructor(packageName) {
1663
+ super(`${packageName} is not installed in this project. Run: npm install --save-dev ${packageName}`);
1664
+ this.packageName = packageName;
1665
+ this.name = "MissingPackageError";
1666
+ }
1667
+ packageName;
1668
+ };
1669
+ function need(fromDir, name) {
1670
+ const pkg = findPackage(fromDir, name);
1671
+ if (!pkg) throw new MissingPackageError(name);
1672
+ return pkg;
1673
+ }
1674
+ async function loadProjectModules(projectDir) {
1675
+ const devkitPkg = need(projectDir, "@zaflun/lumio-devkit");
1676
+ const sdkPkg = findPackage(devkitPkg.dir, "@zaflun/lumio-sdk") ?? need(projectDir, "@zaflun/lumio-sdk");
1677
+ const protocolPkg = findPackage(sdkPkg.dir, "@zaflun/lumio-protocol") ?? need(projectDir, "@zaflun/lumio-protocol");
1678
+ const devkit = await importFile(entryOf(devkitPkg));
1679
+ const devkitNode = await importFile(entryOf(devkitPkg, "./node"));
1680
+ const sdk = await importFile(entryOf(sdkPkg));
1681
+ const sdkServer = await importFile(entryOf(sdkPkg, "./server"));
1682
+ const protocol = await importFile(entryOf(protocolPkg));
1683
+ const sdkRequire = createRequire2(path8.join(sdkPkg.dir, "package.json"));
1684
+ const react = sdkRequire("react");
1685
+ const jsxRuntime = sdkRequire("react/jsx-runtime");
1686
+ const warnings = [];
1687
+ if (compareVersions(protocolPkg.version, CLI_PROTOCOL_VERSION) < 0) {
1688
+ warnings.push(
1689
+ `@zaflun/lumio-sdk ${sdkPkg.version} of this project speaks protocol ${protocolPkg.version}, older than ${CLI_PROTOCOL_VERSION} of this CLI. Update: npm install @zaflun/lumio-sdk@latest && npm install --save-dev @zaflun/lumio-devkit@latest`
1690
+ );
1691
+ }
1692
+ const devkitProtocol = findPackage(devkitPkg.dir, "@zaflun/lumio-protocol");
1693
+ if (devkitProtocol && compareVersions(devkitProtocol.version, CLI_PROTOCOL_VERSION) < 0) {
1694
+ warnings.push(
1695
+ `@zaflun/lumio-devkit ${devkitPkg.version} mocks protocol ${devkitProtocol.version}, older than ${CLI_PROTOCOL_VERSION} of this CLI: events and actions added since are missing. Update: npm install --save-dev @zaflun/lumio-devkit@latest`
1696
+ );
1697
+ }
1698
+ return {
1699
+ devkit,
1700
+ devkitNode,
1701
+ react,
1702
+ registry: {
1703
+ react,
1704
+ "react/jsx-runtime": jsxRuntime,
1705
+ "@zaflun/lumio-sdk": sdk,
1706
+ "@zaflun/lumio-sdk/server": sdkServer,
1707
+ "@zaflun/lumio-protocol": protocol
1708
+ },
1709
+ versions: { devkit: devkitPkg.version, sdk: sdkPkg.version, protocol: protocolPkg.version },
1710
+ warnings
1711
+ };
1712
+ }
1713
+ var BundleError = class extends Error {
1714
+ constructor(message2) {
1715
+ super(message2);
1716
+ this.name = "BundleError";
1717
+ }
1718
+ };
1719
+ function evaluateTargetBundle(code, file, modules) {
1720
+ const react = modules.react;
1721
+ let rendered = null;
1722
+ const lumio = {
1723
+ render(input) {
1724
+ if (rendered) throw new Error("Lumio.render() can only be called once");
1725
+ rendered = typeof input === "function" ? react.createElement(input) : input;
1726
+ }
1727
+ };
1728
+ const registry = {
1729
+ ...modules.registry,
1730
+ "@zaflun/lumio-sdk": { ...modules.registry["@zaflun/lumio-sdk"], Lumio: lumio }
1731
+ };
1732
+ const lumioRequire = (id) => {
1733
+ if (!(id in registry)) throw new Error(`[Lumio Runtime] Module "${id}" is not available in the extension sandbox`);
1734
+ return registry[id];
1735
+ };
1736
+ try {
1737
+ const fn = vm.runInThisContext(`(function (require, self) {
1738
+ ${code}
1739
+ })`, { filename: file });
1740
+ fn(lumioRequire, globalThis);
1741
+ } catch (e) {
1742
+ throw new BundleError(`${path8.basename(file)} failed to evaluate: ${e instanceof Error ? e.message : String(e)}`);
1743
+ }
1744
+ if (!rendered) {
1745
+ throw new BundleError(`${path8.basename(file)} rendered nothing: export a default component or call Lumio.render(<App />)`);
1746
+ }
1747
+ return rendered;
1748
+ }
1749
+
1750
+ // src/dev/repl.ts
1751
+ import * as readline2 from "readline";
1752
+ import chalk2 from "chalk";
1753
+
1754
+ // src/dev/errors.ts
1755
+ var CommandSyntaxError = class extends Error {
1756
+ constructor(message2) {
1757
+ super(message2);
1758
+ this.name = "CommandSyntaxError";
1759
+ }
1760
+ };
1761
+
1762
+ // src/dev/load.ts
1763
+ var MAX_COUNT = 1e4;
1764
+ var MAX_RATE_PER_SECOND = 1e3;
1765
+ function parseCount(raw) {
1766
+ const n = Number(raw);
1767
+ if (!Number.isInteger(n) || n < 1 || n > MAX_COUNT) throw new CommandSyntaxError(`--count must be an integer between 1 and ${MAX_COUNT}, got "${raw}"`);
1768
+ return n;
1769
+ }
1770
+ function parseRate(raw) {
1771
+ const m = /^(\d+(?:\.\d+)?)(?:\/(s|sec|m|min|h))?$/.exec(raw.trim());
1772
+ if (!m) throw new CommandSyntaxError(`--rate is a count per time, e.g. 5/s, 120/m or 600/h, got "${raw}"`);
1773
+ const per = { s: 1, sec: 1, m: 60, min: 60, h: 3600 }[m[2] ?? "s"];
1774
+ const perSecond = Number(m[1]) / per;
1775
+ if (!(perSecond > 0) || perSecond > MAX_RATE_PER_SECOND) {
1776
+ throw new CommandSyntaxError(`--rate must be above 0 and at most ${MAX_RATE_PER_SECOND}/s, got "${raw}"`);
1777
+ }
1778
+ return 1e3 / perSecond;
1779
+ }
1780
+ function describeRate(intervalMs) {
1781
+ if (intervalMs <= 0) return "back to back";
1782
+ const perSecond = 1e3 / intervalMs;
1783
+ return perSecond >= 1 ? `${round(perSecond)}/s` : `${round(perSecond * 60)}/m`;
1784
+ }
1785
+ function round(n) {
1786
+ return Math.round(n * 100) / 100;
1787
+ }
1788
+ function seededPicker(seed) {
1789
+ let state = seed >>> 0;
1790
+ return (items) => {
1791
+ if (items.length === 0) throw new CommandSyntaxError("Nothing to pick from");
1792
+ state = state + 1831565813 >>> 0;
1793
+ let t = state;
1794
+ t = Math.imul(t ^ t >>> 15, t | 1);
1795
+ t ^= t + Math.imul(t ^ t >>> 7, t | 61);
1796
+ const r = ((t ^ t >>> 14) >>> 0) / 4294967296;
1797
+ return items[Math.floor(r * items.length)];
1798
+ };
1799
+ }
1800
+ function freshSeed() {
1801
+ return Math.floor(Math.random() * 2 ** 31);
1802
+ }
1803
+ function randomEventPool(eventTypes, platform) {
1804
+ const pool = eventTypes.filter((e) => e.forwarded && (platform === void 0 || e.type.startsWith(`${platform}:`))).map((e) => e.type);
1805
+ if (pool.length === 0) {
1806
+ throw new CommandSyntaxError(platform ? `No ${platform}:* event type is forwarded to extensions` : "No event type is forwarded to extensions");
1807
+ }
1808
+ return pool;
1809
+ }
1810
+ function repeatSpec(cmd, defaultSeed) {
1811
+ const spec = { count: cmd.count ?? 1, intervalMs: cmd.intervalMs ?? 0, random: cmd.random === true, seed: cmd.seed ?? defaultSeed };
1812
+ if (!Number.isInteger(spec.count) || spec.count < 1 || spec.count > MAX_COUNT) throw new CommandSyntaxError(`count must be an integer between 1 and ${MAX_COUNT}`);
1813
+ if (typeof spec.intervalMs !== "number" || !(spec.intervalMs >= 0)) throw new CommandSyntaxError("intervalMs must be 0 or more");
1814
+ const inner = cmd.command;
1815
+ if (typeof inner !== "object" || inner === null || inner.kind !== "event" && inner.kind !== "scenario" && inner.kind !== "chat") {
1816
+ throw new CommandSyntaxError("repeat runs an event, scenario or chat command");
1817
+ }
1818
+ return spec;
1819
+ }
1820
+ function repeatVariants(cmd, spec, pools) {
1821
+ const inner = cmd.command;
1822
+ if (!spec.random) return () => inner;
1823
+ const pick = seededPicker(spec.seed);
1824
+ if (inner.kind === "scenario") {
1825
+ if (pools.scenarios.length === 0) throw new CommandSyntaxError("This devkit has no scenarios");
1826
+ return () => ({ ...inner, name: pick(pools.scenarios) });
1827
+ }
1828
+ if (inner.kind !== "event") throw new CommandSyntaxError("--random picks an event type or a scenario");
1829
+ const pool = randomEventPool(pools.eventTypes, cmd.platform);
1830
+ return () => ({ ...inner, type: pick(pool) });
1831
+ }
1832
+ function defaultSleep(ms, signal) {
1833
+ return new Promise((resolve13) => {
1834
+ if (signal?.aborted) return resolve13();
1835
+ const timer = setTimeout(done, ms);
1836
+ function done() {
1837
+ clearTimeout(timer);
1838
+ signal?.removeEventListener("abort", done);
1839
+ resolve13();
1840
+ }
1841
+ signal?.addEventListener("abort", done, { once: true });
1842
+ });
1843
+ }
1844
+ async function runLoad(spec, run, hooks = {}) {
1845
+ const now = hooks.now ?? Date.now;
1846
+ const sleep = hooks.sleep ?? defaultSleep;
1847
+ const results = [];
1848
+ const start = now();
1849
+ for (let i = 0; i < spec.count; i++) {
1850
+ if (hooks.signal?.aborted) return { results, failed: null, stopped: true };
1851
+ if (i > 0 && spec.intervalMs > 0) {
1852
+ const wait = start + i * spec.intervalMs - now();
1853
+ if (wait > 0) await sleep(wait, hooks.signal);
1854
+ if (hooks.signal?.aborted) return { results, failed: null, stopped: true };
1855
+ }
1856
+ const r = await run(i);
1857
+ results.push(r);
1858
+ if (!r.ok) return { results, failed: r, stopped: false };
1859
+ }
1860
+ return { results, failed: null, stopped: false };
1861
+ }
1862
+ function summarizeLoad(spec, result) {
1863
+ const ok = result.results.filter((r) => r.ok).length;
1864
+ const pace = spec.count > 1 ? ` (${describeRate(spec.intervalMs)})` : "";
1865
+ const random = spec.random ? ` \xB7 random picks, seed ${spec.seed}` : "";
1866
+ if (result.failed) return `failed at ${result.results.length}/${spec.count}, ${ok} sent${pace}${random}`;
1867
+ if (result.stopped) return `stopped after ${ok}/${spec.count}${pace}${random}`;
1868
+ return `sent ${ok}/${spec.count}${pace}${random}`;
1869
+ }
1870
+
1871
+ // src/dev/commands.ts
1872
+ var REPL_HELP = [
1873
+ "Commands (plain text without a / is a chat message):",
1874
+ " /event <type> [field=value \u2026] [--json '{\u2026}'] [--user name] send an event (fields are dotted paths)",
1875
+ " /event @<preset> send a preset from lumio/mocks/events/",
1876
+ " /chat [@role:name] <text> [--platform kick] a chat message (commands, keywords, moderation)",
1877
+ ' /reward "<title>" [--user name] [--input "text"] [--platform kick] redeem a channel-point reward',
1878
+ " /reward-status <redemption-id> <status> fulfilled|canceled (Twitch), accepted|rejected (Kick)",
1879
+ " /music play|pause|skip [n]|seek 1:30|volume 50|shuffle on|repeat track|queue <uri>|device <name>|advance 30s",
1880
+ " /timer <name> fire a declared timer /defer run run queued ctx.defer jobs",
1881
+ " /scenario <name> [key=value \u2026] e.g. /scenario raid viewers=500",
1882
+ " load flags on /event, /scenario and chat: --count N --rate 5/s --random [--platform kick] --seed n",
1883
+ " e.g. /event twitch:follower --count 50 --rate 5/s \xB7 /event --random --count 20 \xB7 /scenario --random",
1884
+ " /storage [key] [json] show or set storage /db [table] [--reset] show ctx.db rows",
1885
+ " /config [key=value \u2026] show or change settings /theme light|dark",
1886
+ " /view editor|layer switch the sandbox view /stop stop running loads /status /help /quit"
1887
+ ];
1888
+ function tokenize(line) {
1889
+ const out = [];
1890
+ let current = "";
1891
+ let quote = null;
1892
+ let inToken = false;
1893
+ for (let i = 0; i < line.length; i++) {
1894
+ const c = line[i];
1895
+ if (quote) {
1896
+ if (c === quote) quote = null;
1897
+ else if (c === "\\" && quote === '"' && i + 1 < line.length) current += line[++i];
1898
+ else current += c;
1899
+ continue;
1900
+ }
1901
+ if (c === '"' || c === "'") {
1902
+ quote = c;
1903
+ inToken = true;
1904
+ } else if (c === "\\" && i + 1 < line.length) {
1905
+ current += line[++i];
1906
+ inToken = true;
1907
+ } else if (/\s/.test(c)) {
1908
+ if (inToken) out.push(current);
1909
+ current = "";
1910
+ inToken = false;
1911
+ } else {
1912
+ current += c;
1913
+ inToken = true;
1914
+ }
1915
+ }
1916
+ if (quote) throw new CommandSyntaxError(`Unclosed ${quote} quote`);
1917
+ if (inToken) out.push(current);
1918
+ return out;
1919
+ }
1920
+ var LOAD_FLAGS = ["count", "rate", "seed"];
1921
+ function splitFlags(tokens, flags, booleans = []) {
1922
+ const words = [];
1923
+ const found = {};
1924
+ const bools = /* @__PURE__ */ new Set();
1925
+ const all = [...flags, ...booleans];
1926
+ for (let i = 0; i < tokens.length; i++) {
1927
+ const t = tokens[i];
1928
+ if (t.startsWith("--")) {
1929
+ const name = t.slice(2);
1930
+ if (!all.includes(name)) throw new CommandSyntaxError(`Unknown option ${t} (options: ${all.map((f) => `--${f}`).join(", ") || "none"})`);
1931
+ if (booleans.includes(name)) {
1932
+ bools.add(name);
1933
+ continue;
1934
+ }
1935
+ if (i + 1 >= tokens.length) throw new CommandSyntaxError(`${t} needs a value`);
1936
+ found[name] = tokens[++i];
1937
+ } else words.push(t);
1938
+ }
1939
+ return { words, flags: found, bools };
1940
+ }
1941
+ function loadFlags(flags, bools) {
1942
+ return { count: flags.count, rate: flags.rate, seed: flags.seed, random: bools.has("random") };
1943
+ }
1944
+ function withLoad(inner, load) {
1945
+ const random = load.random === true;
1946
+ if (load.platform !== void 0 && !random) throw new CommandSyntaxError("--platform filters --random; the event type already names its platform");
1947
+ if (load.count === void 0 && load.rate === void 0 && load.seed === void 0 && !random) return inner;
1948
+ if (load.rate !== void 0 && load.count === void 0) throw new CommandSyntaxError("--rate paces --count runs: add --count N");
1949
+ if (load.seed !== void 0 && !random) throw new CommandSyntaxError("--seed sets the --random picks; event fields follow the session seed (lumio dev --seed)");
1950
+ if (inner.kind !== "event" && inner.kind !== "scenario" && inner.kind !== "chat") {
1951
+ throw new CommandSyntaxError("--count, --rate and --random apply to events, scenarios and chat messages");
1952
+ }
1953
+ if (random) {
1954
+ if (inner.kind === "chat") throw new CommandSyntaxError("--random picks an event type or a scenario, not a chat message");
1955
+ if (inner.kind === "event" && (inner.type !== void 0 || inner.preset !== void 0 || Object.keys(inner.data ?? {}).length > 0)) {
1956
+ throw new CommandSyntaxError("--random picks the event type itself: drop the type, preset and fields (every event gets fresh random fields anyway)");
1957
+ }
1958
+ if (inner.kind === "scenario" && (inner.name !== "" || Object.keys(inner.options ?? {}).length > 0)) {
1959
+ throw new CommandSyntaxError("--random picks the scenario itself: drop the name and options");
1960
+ }
1961
+ }
1962
+ if (random && load.platform !== void 0 && inner.kind !== "event") throw new CommandSyntaxError("--platform filters random event types only");
1963
+ let seed;
1964
+ if (load.seed !== void 0) {
1965
+ seed = Number(load.seed);
1966
+ if (!Number.isInteger(seed) || seed < 0 || seed > 2 ** 31 - 1) throw new CommandSyntaxError(`--seed must be an integer between 0 and ${2 ** 31 - 1}`);
1967
+ }
1968
+ return {
1969
+ kind: "repeat",
1970
+ command: inner,
1971
+ count: load.count === void 0 ? 1 : parseCount(load.count),
1972
+ intervalMs: load.rate === void 0 ? 0 : parseRate(load.rate),
1973
+ ...random ? { random: true } : {},
1974
+ ...load.platform !== void 0 ? { platform: load.platform } : {},
1975
+ ...seed !== void 0 ? { seed } : {}
1976
+ };
1977
+ }
1978
+ function parseValue(raw) {
1979
+ if (raw === "true") return true;
1980
+ if (raw === "false") return false;
1981
+ if (raw === "null") return null;
1982
+ if (/^-?\d+(\.\d+)?$/.test(raw)) return Number(raw);
1983
+ if (/^[[{]/.test(raw)) {
1984
+ try {
1985
+ return JSON.parse(raw);
1986
+ } catch {
1987
+ throw new CommandSyntaxError(`Not valid JSON: ${raw}`);
1988
+ }
1989
+ }
1990
+ return raw;
1991
+ }
1992
+ function setPath(target, dotted, value) {
1993
+ const parts = dotted.split(".");
1994
+ if (parts.some((p) => p === "" || p === "__proto__" || p === "constructor" || p === "prototype")) {
1995
+ throw new CommandSyntaxError(`Invalid field path "${dotted}"`);
1996
+ }
1997
+ let node = target;
1998
+ for (const part of parts.slice(0, -1)) {
1999
+ const next = node[part];
2000
+ if (typeof next !== "object" || next === null || Array.isArray(next)) node[part] = {};
2001
+ node = node[part];
2002
+ }
2003
+ node[parts[parts.length - 1]] = value;
2004
+ }
2005
+ function assignments(words, what) {
2006
+ const out = {};
2007
+ for (const w of words) {
2008
+ const eq = w.indexOf("=");
2009
+ if (eq <= 0) throw new CommandSyntaxError(`Expected ${what} as key=value, got "${w}"`);
2010
+ setPath(out, w.slice(0, eq), parseValue(w.slice(eq + 1)));
2011
+ }
2012
+ return out;
2013
+ }
2014
+ var ROLE_ALIASES = {
2015
+ mod: "moderator",
2016
+ moderator: "moderator",
2017
+ sub: "subscriber",
2018
+ subscriber: "subscriber",
2019
+ vip: "vip",
2020
+ broadcaster: "broadcaster",
2021
+ everyone: "everyone",
2022
+ viewer: "everyone",
2023
+ follower: "follower"
2024
+ };
2025
+ function parseUserPrefix(word) {
2026
+ const body = word.slice(1);
2027
+ const colon = body.indexOf(":");
2028
+ if (colon === -1) return { name: body };
2029
+ const role = ROLE_ALIASES[body.slice(0, colon).toLowerCase()];
2030
+ if (!role) throw new CommandSyntaxError(`Unknown role "${body.slice(0, colon)}" (use ${Object.keys(ROLE_ALIASES).join(", ")})`);
2031
+ return { role, name: body.slice(colon + 1) };
2032
+ }
2033
+ function userFlag(value) {
2034
+ if (value === void 0) return void 0;
2035
+ return value.startsWith("@") ? parseUserPrefix(value) : { name: value };
2036
+ }
2037
+ function parseReplLine(line) {
2038
+ const trimmed = line.trim();
2039
+ if (trimmed === "") throw new CommandSyntaxError("Empty input");
2040
+ if (!trimmed.startsWith("/")) return parseChat(tokenize(trimmed));
2041
+ const [head, ...rest] = tokenize(trimmed);
2042
+ switch (head) {
2043
+ case "/quit":
2044
+ case "/exit":
2045
+ return null;
2046
+ case "/help":
2047
+ return { kind: "help" };
2048
+ case "/status":
2049
+ return { kind: "status" };
2050
+ case "/stop":
2051
+ return { kind: "stop" };
2052
+ case "/event": {
2053
+ const { words, flags, bools } = splitFlags(rest, ["json", "user", "platform", ...LOAD_FLAGS], ["random"]);
2054
+ const load = { ...loadFlags(flags, bools), platform: flags.platform };
2055
+ const [type, ...fields] = words;
2056
+ if (!type && load.random) {
2057
+ if (flags.json !== void 0) throw new CommandSyntaxError("--random picks the event type itself: drop --json");
2058
+ return withLoad({ kind: "event", user: userFlag(flags.user) }, load);
2059
+ }
2060
+ if (!type) throw new CommandSyntaxError("Usage: /event <type> [field=value \u2026] | /event @<preset> | /event --random");
2061
+ if (type.startsWith("@")) {
2062
+ if (fields.length || flags.json) throw new CommandSyntaxError("A preset takes no fields");
2063
+ return withLoad({ kind: "event", preset: type.slice(1), user: userFlag(flags.user) }, load);
2064
+ }
2065
+ const data = assignments(fields, "event fields");
2066
+ if (flags.json !== void 0) {
2067
+ const json = parseValue(flags.json);
2068
+ if (typeof json !== "object" || json === null || Array.isArray(json)) throw new CommandSyntaxError("--json needs a JSON object");
2069
+ Object.assign(data, json);
2070
+ }
2071
+ return withLoad({ kind: "event", type, data, user: userFlag(flags.user) }, load);
2072
+ }
2073
+ case "/chat":
2074
+ return parseChat(rest);
2075
+ case "/reward": {
2076
+ const { words, flags } = splitFlags(rest, ["user", "input", "platform"]);
2077
+ if (words.length !== 1) throw new CommandSyntaxError('Usage: /reward "<title or id>" [--user name] [--input "text"] [--platform twitch|kick]');
2078
+ return { kind: "reward", reward: words[0], user: userFlag(flags.user), input: flags.input, platform: flags.platform };
2079
+ }
2080
+ case "/reward-status": {
2081
+ if (rest.length !== 2) throw new CommandSyntaxError("Usage: /reward-status <redemption-id> <status>");
2082
+ return { kind: "reward-status", id: rest[0], status: rest[1] };
2083
+ }
2084
+ case "/music": {
2085
+ const [op, ...args] = rest;
2086
+ if (!op) throw new CommandSyntaxError("Usage: /music play|pause|skip|seek|volume|shuffle|repeat|queue|device|advance \u2026");
2087
+ return { kind: "music", op, args };
2088
+ }
2089
+ case "/timer":
2090
+ if (rest.length !== 1) throw new CommandSyntaxError("Usage: /timer <name>");
2091
+ return { kind: "timer", name: rest[0] };
2092
+ case "/defer":
2093
+ if (rest[0] !== "run" || rest.length !== 1) throw new CommandSyntaxError("Usage: /defer run");
2094
+ return { kind: "defer-run" };
2095
+ case "/scenario": {
2096
+ const { words, flags, bools } = splitFlags(rest, [...LOAD_FLAGS], ["random"]);
2097
+ const load = loadFlags(flags, bools);
2098
+ const [name, ...opts] = words;
2099
+ if (!name && !load.random) throw new CommandSyntaxError("Usage: /scenario <name> [key=value \u2026] | /scenario --random");
2100
+ return withLoad({ kind: "scenario", name: name ?? "", options: assignments(opts, "scenario options") }, load);
2101
+ }
2102
+ case "/storage": {
2103
+ if (rest.length === 0) return { kind: "storage" };
2104
+ if (rest.length > 2) throw new CommandSyntaxError("Usage: /storage [key] ['<json>']");
2105
+ if (rest.length === 1) return { kind: "storage", key: rest[0] };
2106
+ let value;
2107
+ try {
2108
+ value = JSON.parse(rest[1]);
2109
+ } catch {
2110
+ throw new CommandSyntaxError(`Storage values are JSON: ${rest[1]}`);
2111
+ }
2112
+ return { kind: "storage", key: rest[0], value };
2113
+ }
2114
+ case "/db": {
2115
+ const reset = rest.includes("--reset");
2116
+ const words = rest.filter((w) => w !== "--reset");
2117
+ if (words.length > 1) throw new CommandSyntaxError("Usage: /db [table] [--reset]");
2118
+ return { kind: "db", table: words[0], reset };
2119
+ }
2120
+ case "/config":
2121
+ return rest.length ? { kind: "config", settings: assignments(rest, "settings") } : { kind: "config" };
2122
+ case "/theme":
2123
+ if (rest[0] !== "light" && rest[0] !== "dark") throw new CommandSyntaxError("Usage: /theme light|dark");
2124
+ return { kind: "theme", mode: rest[0] };
2125
+ case "/view":
2126
+ if (rest[0] !== "editor" && rest[0] !== "layer") throw new CommandSyntaxError("Usage: /view editor|layer");
2127
+ return { kind: "view", target: rest[0] };
2128
+ default:
2129
+ throw new CommandSyntaxError(`Unknown command ${head}. Type /help.`);
2130
+ }
2131
+ }
2132
+ function parseChat(tokens) {
2133
+ const { words, flags } = splitFlags(tokens, ["platform", "user", "count", "rate"]);
2134
+ let user = userFlag(flags.user);
2135
+ if (words[0]?.startsWith("@") && words.length > 1) {
2136
+ user = parseUserPrefix(words[0]);
2137
+ words.shift();
2138
+ }
2139
+ const text = words.join(" ");
2140
+ if (text === "") throw new CommandSyntaxError("Usage: /chat [@role:name] <text>");
2141
+ return withLoad({ kind: "chat", text, user, platform: flags.platform }, { count: flags.count, rate: flags.rate });
2142
+ }
2143
+ var KINDS = /* @__PURE__ */ new Set([
2144
+ "event",
2145
+ "chat",
2146
+ "reward",
2147
+ "reward-status",
2148
+ "music",
2149
+ "timer",
2150
+ "defer-run",
2151
+ "scenario",
2152
+ "storage",
2153
+ "db",
2154
+ "config",
2155
+ "theme",
2156
+ "view",
2157
+ "callback",
2158
+ "server-override",
2159
+ "repeat",
2160
+ "stop",
2161
+ "status",
2162
+ "help"
2163
+ ]);
2164
+ function commandFromJson(body) {
2165
+ if (typeof body !== "object" || body === null || Array.isArray(body)) {
2166
+ throw new CommandSyntaxError('Send { "line": "/event twitch:follower" } or a command object { "kind": \u2026 }');
2167
+ }
2168
+ const b = body;
2169
+ if (typeof b.line === "string") return parseReplLine(b.line);
2170
+ if (typeof b.kind !== "string" || !KINDS.has(b.kind)) {
2171
+ throw new CommandSyntaxError(`Unknown command kind ${JSON.stringify(b.kind)} (one of: ${[...KINDS].join(", ")})`);
2172
+ }
2173
+ return b;
2174
+ }
2175
+ function parseDuration(raw) {
2176
+ const m = /^(?:(\d+):)?(\d+(?:\.\d+)?)(s|ms)?$/.exec(raw);
2177
+ if (!m) throw new CommandSyntaxError(`Not a time: ${raw} (use 1:30, 90 or 90s)`);
2178
+ if (m[3] === "ms") return Number(m[2]) + Number(m[1] ?? 0) * 6e4;
2179
+ return Math.round((Number(m[1] ?? 0) * 60 + Number(m[2])) * 1e3);
2180
+ }
2181
+
2182
+ // src/dev/repl.ts
2183
+ var PRINT_OUTPUT = /* @__PURE__ */ new Set([
2184
+ "help",
2185
+ "status",
2186
+ "storage",
2187
+ "db",
2188
+ "config",
2189
+ "music",
2190
+ "scenario",
2191
+ "timer",
2192
+ "defer-run",
2193
+ "view",
2194
+ "reward",
2195
+ "reward-status",
2196
+ "server-override",
2197
+ "theme",
2198
+ "stop"
2199
+ ]);
2200
+ var KIND_STYLE = {
2201
+ input: chalk2.dim,
2202
+ event: chalk2.cyan,
2203
+ chat: chalk2.white,
2204
+ bot: chalk2.green,
2205
+ action: chalk2.magenta,
2206
+ server: chalk2.blue,
2207
+ storage: chalk2.blue,
2208
+ handler: chalk2.green,
2209
+ error: chalk2.red,
2210
+ warning: chalk2.yellow,
2211
+ build: chalk2.dim,
2212
+ info: chalk2.dim
2213
+ };
2214
+ function formatLogEntry(entry) {
2215
+ const style = KIND_STYLE[entry.kind];
2216
+ const text = entry.kind === "event" ? `\u2190 ${entry.text}` : entry.text;
2217
+ const payload = entry.payload === void 0 ? "" : ` ${chalk2.dim(JSON.stringify(entry.payload))}`;
2218
+ return `${style(text)}${payload}`;
2219
+ }
2220
+ function startRepl(session, onQuit) {
2221
+ const rl = readline2.createInterface({ input: process.stdin, output: process.stdout, prompt: chalk2.cyan("lumio> ") });
2222
+ let closed = false;
2223
+ const above = (text) => {
2224
+ if (closed) {
2225
+ console.log(text);
2226
+ return;
2227
+ }
2228
+ readline2.clearLine(process.stdout, 0);
2229
+ readline2.cursorTo(process.stdout, 0);
2230
+ console.log(text);
2231
+ rl.prompt(true);
2232
+ };
2233
+ rl.on("line", (line) => {
2234
+ const trimmed = line.trim();
2235
+ if (trimmed === "") {
2236
+ rl.prompt();
2237
+ return;
2238
+ }
2239
+ let cmd;
2240
+ try {
2241
+ cmd = parseReplLine(trimmed);
2242
+ } catch (e) {
2243
+ above(chalk2.red(e instanceof CommandSyntaxError ? e.message : String(e)));
2244
+ return;
2245
+ }
2246
+ if (cmd === null) {
2247
+ rl.close();
2248
+ return;
2249
+ }
2250
+ const kind = cmd.kind;
2251
+ void session.run(cmd, "repl").then((result) => {
2252
+ if (result.ok && PRINT_OUTPUT.has(kind)) for (const l of result.output) above(l);
2253
+ else rl.prompt(true);
2254
+ });
2255
+ });
2256
+ rl.on("close", () => {
2257
+ closed = true;
2258
+ onQuit();
2259
+ });
2260
+ rl.prompt();
2261
+ return {
2262
+ print(entry) {
2263
+ if (entry.kind === "input" && entry.text.startsWith("repl: ")) return;
2264
+ above(formatLogEntry(entry));
2265
+ },
2266
+ close() {
2267
+ if (!closed) rl.close();
2268
+ }
2269
+ };
2270
+ }
2271
+
2272
+ // src/dev/session.ts
2273
+ var LOG_LIMIT = 500;
2274
+ var CALL_LIMIT = 100;
2275
+ var DEFAULT_USER = "dev_user";
2276
+ var ACCOUNT_ROLES = ["owner", "administrator", "moderator"];
2277
+ var THEMES = {
2278
+ dark: {
2279
+ colors: {
2280
+ primary: "#6366f1",
2281
+ secondary: "#8b5cf6",
2282
+ accent: "#06b6d4",
2283
+ background: "transparent",
2284
+ surface: "rgba(15, 23, 42, 0.8)",
2285
+ text: "#f8fafc",
2286
+ muted: "#94a3b8",
2287
+ border: "rgba(148, 163, 184, 0.2)"
2288
+ }
2289
+ },
2290
+ light: {
2291
+ colors: {
2292
+ primary: "#4f46e5",
2293
+ secondary: "#7c3aed",
2294
+ accent: "#0891b2",
2295
+ background: "transparent",
2296
+ surface: "rgba(255, 255, 255, 0.9)",
2297
+ text: "#0f172a",
2298
+ muted: "#475569",
2299
+ border: "rgba(15, 23, 42, 0.15)"
2300
+ }
2301
+ }
2302
+ };
2303
+ var MUSIC_OPS = ["play", "pause", "skip", "seek", "volume", "shuffle", "repeat", "queue", "device", "advance"];
2304
+ var isObject = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
2305
+ function compact(value) {
2306
+ try {
2307
+ return JSON.stringify(value) ?? "undefined";
2308
+ } catch {
2309
+ return String(value);
2310
+ }
2311
+ }
2312
+ function message(e) {
2313
+ return e instanceof Error ? e.message : String(e);
2314
+ }
2315
+ var DevSession = class {
2316
+ host;
2317
+ runtime = null;
2318
+ opts;
2319
+ rawConfig;
2320
+ bundles = /* @__PURE__ */ new Map();
2321
+ server = { bundle: null, functions: null, schema: null };
2322
+ overrides = /* @__PURE__ */ new Map();
2323
+ viewTarget;
2324
+ themeMode = "dark";
2325
+ mountError = null;
2326
+ buildStatus = { ok: true, errors: [], at: null };
2327
+ cssVersion = 0;
2328
+ logEntries = [];
2329
+ nextLogId = 1;
2330
+ serverCalls = [];
2331
+ seen = { events: 0, bot: 0, calls: 0, errors: 0, warnings: 0, invocations: 0, runtimeWarnings: 0 };
2332
+ lastTree = null;
2333
+ pumpTimer = null;
2334
+ queue = Promise.resolve();
2335
+ closed = false;
2336
+ /** Running `--count` / `--rate` loads; `/stop` and `close()` abort them. */
2337
+ loads = /* @__PURE__ */ new Set();
2338
+ constructor(options) {
2339
+ this.opts = options;
2340
+ this.rawConfig = options.rawConfig;
2341
+ this.viewTarget = options.view && options.targets.includes(options.view) ? options.view : options.targets[0] ?? null;
2342
+ }
2343
+ // ── Lifecycle ───────────────────────────────────────────────────────
2344
+ /** Creates the host (and runtime) from the current bundles and mounts the view. */
2345
+ async start(server, bundles) {
2346
+ this.server = server;
2347
+ for (const [t, code] of Object.entries(bundles)) if (code !== void 0) this.bundles.set(t, code);
2348
+ this.createHost({});
2349
+ await this.mountView();
2350
+ this.pump();
2351
+ this.pumpTimer = setInterval(() => this.pump(), 200);
2352
+ this.pumpTimer.unref?.();
2353
+ }
2354
+ createHost(carry) {
2355
+ const { devkit, devkitNode } = this.opts.modules;
2356
+ const hasServer = this.server.bundle !== null || this.server.functions !== null;
2357
+ this.runtime = hasServer ? devkitNode.createBotRuntime({
2358
+ bundle: this.server.bundle ?? void 0,
2359
+ functions: this.server.functions ?? void 0,
2360
+ schema: this.server.schema ?? void 0,
2361
+ cooldowns: this.opts.cooldowns ?? true
2362
+ }) : null;
2363
+ const role = this.opts.role;
2364
+ const mocks = this.opts.mocks;
2365
+ this.host = devkit.createMockHost({
2366
+ seed: this.opts.seed ?? 1,
2367
+ context: {
2368
+ target: this.viewTarget ?? "editor",
2369
+ platform: this.opts.platform ?? "twitch",
2370
+ ...role && ACCOUNT_ROLES.includes(role) ? { role } : {}
2371
+ },
2372
+ config: this.rawConfig,
2373
+ mocks,
2374
+ services: this.opts.services,
2375
+ storage: carry.storage,
2376
+ theme: THEMES[this.themeMode],
2377
+ runtime: this.runtime ?? void 0
2378
+ });
2379
+ for (const [fn, answer] of this.overrides) this.host.mockServer(fn, answer);
2380
+ this.host.viewers.resolve({ name: DEFAULT_USER, role: this.defaultRole() });
2381
+ this.seen = { events: 0, bot: 0, calls: 0, errors: 0, warnings: 0, invocations: 0, runtimeWarnings: 0 };
2382
+ this.lastTree = null;
2383
+ }
2384
+ defaultRole() {
2385
+ const role = this.opts.role;
2386
+ if (role && this.opts.modules.devkit.VIEWER_ROLES.includes(role)) return role;
2387
+ return role === "owner" || role === "administrator" ? "broadcaster" : role === void 0 ? "everyone" : role;
2388
+ }
2389
+ async mountView() {
2390
+ this.mountError = null;
2391
+ if (this.host.mounted) await this.host.unmount();
2392
+ const target = this.viewTarget;
2393
+ if (!target) return;
2394
+ this.host.setTarget(target);
2395
+ const code = this.bundles.get(target);
2396
+ if (code === void 0) {
2397
+ this.mountError = `src/${target}.tsx is not built yet`;
2398
+ return;
2399
+ }
2400
+ try {
2401
+ const element = evaluateTargetBundle(code, `${target}.js`, this.opts.modules);
2402
+ await this.host.mount(element);
2403
+ } catch (e) {
2404
+ this.mountError = message(e);
2405
+ this.addLog("error", `${target}: ${this.mountError}`);
2406
+ if (this.host.mounted) await this.host.unmount().catch(() => {
2407
+ });
2408
+ }
2409
+ }
2410
+ async close() {
2411
+ if (this.closed) return;
2412
+ this.closed = true;
2413
+ this.stopLoads();
2414
+ if (this.pumpTimer) clearInterval(this.pumpTimer);
2415
+ this.host.destroy();
2416
+ await this.runtime?.close();
2417
+ }
2418
+ /** Commands run one after another, like typed input. */
2419
+ serial(fn) {
2420
+ const next = this.queue.then(fn, fn);
2421
+ this.queue = next.catch(() => {
2422
+ });
2423
+ return next;
2424
+ }
2425
+ // ── Rebuilds (SSE → re-init the surface / reload the handler bundle) ──
2426
+ /** A rebuilt target bundle: the surface re-initialises; storage, ctx.db and the player stay. */
2427
+ updateTarget(target, code) {
2428
+ return this.serial(async () => {
2429
+ this.bundles.set(target, code);
2430
+ if (target === this.viewTarget) {
2431
+ await this.mountView();
2432
+ this.addLog("build", `${target}.js rebuilt: surface re-initialised (storage, ctx.db and player kept)`);
2433
+ } else this.addLog("build", `${target}.js rebuilt`);
2434
+ this.pump();
2435
+ });
2436
+ }
2437
+ /** A rebuilt `server.js` / `functions.json` / schema. */
2438
+ updateServer(next) {
2439
+ return this.serial(async () => {
2440
+ const hadServer = this.runtime !== null;
2441
+ const hasServer = next.bundle !== null || next.functions !== null;
2442
+ if (!hadServer || !hasServer) {
2443
+ this.server = next;
2444
+ await this.restartHost("server code changed");
2445
+ return;
2446
+ }
2447
+ try {
2448
+ const { dbReset } = this.runtime.reload({
2449
+ bundle: next.bundle ?? void 0,
2450
+ functions: next.functions ?? void 0,
2451
+ schema: next.schema ?? void 0
2452
+ });
2453
+ this.server = next;
2454
+ this.addLog("build", dbReset ? "server.js reloaded; server/schema.ts changed, so ctx.db starts empty" : "server.js reloaded (ctx.db, cache and cooldowns kept)");
2455
+ } catch (e) {
2456
+ this.addLog("error", `server reload refused, previous version keeps running: ${message(e)}`);
2457
+ }
2458
+ this.pump();
2459
+ });
2460
+ }
2461
+ /** `lumio.config.json` changed: permissions and triggers are read once, so the host restarts. */
2462
+ updateConfig(rawConfig, mocks) {
2463
+ return this.serial(async () => {
2464
+ this.rawConfig = rawConfig;
2465
+ if (mocks !== void 0) this.opts.mocks = mocks;
2466
+ await this.restartHost("lumio.config.json or lumio/mocks/ changed");
2467
+ });
2468
+ }
2469
+ async restartHost(reason) {
2470
+ const storage = this.host.storage.entries();
2471
+ this.host.destroy();
2472
+ await this.runtime?.close();
2473
+ this.createHost({ storage });
2474
+ await this.mountView();
2475
+ this.addLog("build", `${reason}: session restarted (storage kept; ctx.db, player and rewards start fresh)`);
2476
+ this.pump();
2477
+ }
2478
+ /** The current build errors of every part; new ones are logged once. */
2479
+ setBuild(status) {
2480
+ const known = new Set(this.buildStatus.errors);
2481
+ this.buildStatus = { ...status, at: (/* @__PURE__ */ new Date()).toISOString() };
2482
+ for (const e of status.errors) if (!known.has(e)) this.addLog("error", e);
2483
+ this.opts.onChange?.();
2484
+ }
2485
+ /** The extension's stylesheet changed. */
2486
+ cssChanged() {
2487
+ this.cssVersion += 1;
2488
+ this.opts.onChange?.();
2489
+ }
2490
+ setTargets(targets) {
2491
+ this.opts.targets = targets;
2492
+ }
2493
+ // ── Log ─────────────────────────────────────────────────────────────
2494
+ addLog(kind, text, payload) {
2495
+ const entry = { id: this.nextLogId++, at: (/* @__PURE__ */ new Date()).toISOString(), kind, text, ...payload === void 0 ? {} : { payload } };
2496
+ this.logEntries.push(entry);
2497
+ if (this.logEntries.length > LOG_LIMIT) this.logEntries.splice(0, this.logEntries.length - LOG_LIMIT);
2498
+ this.opts.onLog?.(entry);
2499
+ this.opts.onChange?.();
2500
+ return entry;
2501
+ }
2502
+ /** Picks up everything the host and the runtime recorded since the last call. */
2503
+ pump() {
2504
+ if (this.closed || this.host.destroyed) return;
2505
+ const h = this.host;
2506
+ const events = h.events;
2507
+ for (; this.seen.events < events.length; this.seen.events++) {
2508
+ const e = events[this.seen.events];
2509
+ this.addLog("event", `${e.type} \u2192 ${e.delivered ? "extension" : "no surface mounted"}`, e.data);
2510
+ }
2511
+ const bot = h.botMessages;
2512
+ for (; this.seen.bot < bot.length; this.seen.bot++) {
2513
+ this.addLog("bot", `bot (${bot[this.seen.bot].platform}): ${bot[this.seen.bot].message}`);
2514
+ }
2515
+ const calls = h.calls;
2516
+ for (; this.seen.calls < calls.length; this.seen.calls++) {
2517
+ const call = calls[this.seen.calls];
2518
+ if (call.kind === "request" && !("result" in call) && !call.error) break;
2519
+ this.logCall(call);
2520
+ }
2521
+ for (; this.seen.errors < h.errors.length; this.seen.errors++) {
2522
+ const e = h.errors[this.seen.errors];
2523
+ this.addLog("error", `${e.code}: ${e.message}`);
2524
+ }
2525
+ for (; this.seen.warnings < h.warnings.length; this.seen.warnings++) this.addLog("warning", h.warnings[this.seen.warnings]);
2526
+ const rt = this.runtime;
2527
+ if (rt) {
2528
+ const inv = rt.invocations;
2529
+ for (; this.seen.invocations < inv.length; this.seen.invocations++) {
2530
+ const log = inv[this.seen.invocations];
2531
+ if (log.ms === 0 && !log.error && log.value === void 0) break;
2532
+ this.logInvocation(log);
2533
+ }
2534
+ }
2535
+ if (h.tree !== this.lastTree) {
2536
+ this.lastTree = h.tree;
2537
+ this.opts.onChange?.();
2538
+ }
2539
+ }
2540
+ logCall(call) {
2541
+ const p = isObject(call.params) ? call.params : {};
2542
+ const answer = call.error ? { error: call.error } : { result: call.result };
2543
+ switch (call.method) {
2544
+ case "action.execute":
2545
+ this.addLog("action", `action ${String(p.action)}: ${call.error ? `${call.error.message} (${call.error.code})` : "ok"}`, { params: p.params, ...answer });
2546
+ break;
2547
+ case "server.query":
2548
+ case "server.mutation": {
2549
+ const fn = String(p.fn);
2550
+ const entry = {
2551
+ id: this.seen.calls,
2552
+ method: call.method,
2553
+ fn,
2554
+ params: p.params ?? {},
2555
+ ...call.error ? { error: call.error } : { result: call.result },
2556
+ overridden: this.overrides.has(fn)
2557
+ };
2558
+ this.serverCalls.push(entry);
2559
+ if (this.serverCalls.length > CALL_LIMIT) this.serverCalls.shift();
2560
+ this.addLog("server", `${call.method} ${fn}${entry.overridden ? " (override)" : ""}: ${call.error ? call.error.message : compact(call.result)}`, { params: entry.params, ...answer });
2561
+ break;
2562
+ }
2563
+ case "storage.set":
2564
+ this.addLog("storage", `storage.set ${String(p.key)}`, p.value);
2565
+ break;
2566
+ case "ui.action":
2567
+ this.addLog("action", "ui.action", call.params);
2568
+ break;
2569
+ case "sound.play":
2570
+ case "sound.stop":
2571
+ case "sound.list":
2572
+ this.addLog("action", `${call.method}: ${call.error?.message ?? "ok"}`, call.params);
2573
+ break;
2574
+ default:
2575
+ break;
2576
+ }
2577
+ }
2578
+ logInvocation(log) {
2579
+ if (log.trigger === "command" || log.trigger === "keyword" || log.trigger === "pattern" || log.trigger === "moderate") {
2580
+ for (const l of log.logs) this.addLog("handler", `${log.name} console.${l.level}: ${l.text}`);
2581
+ return;
2582
+ }
2583
+ const parts = [`${log.trigger} ${log.name} (${Math.round(log.ms)} ms)`];
2584
+ if (log.error) parts.push(`failed: ${log.error.message}`);
2585
+ for (const r of log.replies) parts.push(`reply: ${r}`);
2586
+ for (const a of log.actions) parts.push(`action ${a.action}: ${a.success ? "ok" : a.error}`);
2587
+ for (const r of log.realtime) parts.push(`realtime ${r.topic}`);
2588
+ this.addLog(log.error ? "error" : "handler", `handler ${parts.join(" \xB7 ")}`, log.value);
2589
+ for (const l of log.logs) this.addLog("handler", `${log.name} console.${l.level}: ${l.text}`);
2590
+ }
2591
+ // ── State ───────────────────────────────────────────────────────────
2592
+ state() {
2593
+ const h = this.host;
2594
+ const rt = this.runtime;
2595
+ const cfg = this.rawConfig;
2596
+ return {
2597
+ project: {
2598
+ name: String(cfg.name ?? ""),
2599
+ category: String(cfg.category ?? "widget"),
2600
+ version: String(cfg.version ?? ""),
2601
+ extensionId: String(cfg.extensionId ?? cfg.extension_id ?? "")
2602
+ },
2603
+ versions: this.opts.modules.versions,
2604
+ view: this.viewTarget,
2605
+ targets: this.opts.targets,
2606
+ chatFirst: this.opts.chatFirst ?? false,
2607
+ mounted: h.mounted,
2608
+ mountError: this.mountError,
2609
+ tree: h.tree,
2610
+ themeMode: this.themeMode,
2611
+ cssVersion: this.cssVersion,
2612
+ build: this.buildStatus,
2613
+ storage: h.storage.entries(),
2614
+ settings: h.settings,
2615
+ db: rt ? rt.db.tables().map((table) => ({ table, rows: rt.db.rows(table) })) : null,
2616
+ player: h.player.state,
2617
+ rewards: h.rewards.list().map((r) => ({ id: r.id, platform: r.platform, title: r.title, cost: r.cost, prompt: r.prompt, inputRequired: r.inputRequired })),
2618
+ redemptions: h.rewards.history().slice(-50),
2619
+ viewers: h.viewers.list().map((v) => ({ login: v.login, displayName: v.displayName, role: v.role })),
2620
+ live: h.live,
2621
+ timers: rt ? [...rt.timers] : [],
2622
+ pendingDeferred: rt?.pendingDeferred ?? 0,
2623
+ functions: this.functionNames(),
2624
+ serverCalls: [...this.serverCalls],
2625
+ overrides: Object.fromEntries(this.overrides),
2626
+ presets: Object.keys(this.opts.mocks?.events ?? {}),
2627
+ defaults: { platform: h.context.platform, user: DEFAULT_USER, role: this.defaultRole() },
2628
+ log: [...this.logEntries]
2629
+ };
2630
+ }
2631
+ functionNames() {
2632
+ const f = this.server.functions;
2633
+ const parsed = typeof f === "string" ? JSON.parse(f) : f;
2634
+ return isObject(parsed) && isObject(parsed.functions) ? Object.keys(parsed.functions) : [];
2635
+ }
2636
+ meta() {
2637
+ const { devkit } = this.opts.modules;
2638
+ return {
2639
+ eventTypes: devkit.EVENT_TYPES.map((type) => ({ type, ...devkit.eventDelivery(type) })).map(({ type, forwarded, reason }) => ({ type, forwarded, reason })),
2640
+ scenarios: Object.keys(devkit.SCENARIOS),
2641
+ platforms: [...devkit.CHAT_PLATFORMS],
2642
+ rewardPlatforms: [...devkit.REWARD_PLATFORMS],
2643
+ roles: [...devkit.VIEWER_ROLES],
2644
+ musicOps: MUSIC_OPS,
2645
+ rewardStatuses: { twitch: ["fulfilled", "canceled"], kick: ["pending", "accepted", "rejected"] }
2646
+ };
2647
+ }
2648
+ /** A payload for the panel's form and JSON editor (`createEvent`, not sent). */
2649
+ eventTemplate(type) {
2650
+ const { devkit } = this.opts.modules;
2651
+ if (!devkit.isLumioEventType(type)) throw new CommandSyntaxError(`Unknown event type "${type}"`);
2652
+ const payload = devkit.createEvent(type, void 0, { seed: (this.opts.seed ?? 1) + this.nextLogId });
2653
+ return { payload, schema: null };
2654
+ }
2655
+ /** The tree node that registered `handlerId` and the prop it belongs to. */
2656
+ findHandler(handlerId) {
2657
+ const stack = [...this.host.tree];
2658
+ while (stack.length) {
2659
+ const node = stack.pop();
2660
+ for (const [key, value] of Object.entries(node.props ?? {})) {
2661
+ if (key.startsWith("__handler_") && value === handlerId) return { node, prop: key.slice("__handler_".length) };
2662
+ }
2663
+ for (const child of node.children ?? []) if (typeof child === "object" && child !== null) stack.push(child);
2664
+ }
2665
+ return null;
2666
+ }
2667
+ // ── Commands ────────────────────────────────────────────────────────
2668
+ /** Runs one command; every input is echoed with what the extension received. */
2669
+ run(cmd, source = "repl") {
2670
+ if (cmd.kind === "repeat") return this.repeat(cmd, source);
2671
+ if (cmd.kind === "stop") {
2672
+ const n = this.stopLoads();
2673
+ this.addLog("input", `${source}: /stop`);
2674
+ return Promise.resolve({ ok: true, output: [n ? `stopped ${n} running load(s)` : "no load is running"] });
2675
+ }
2676
+ return this.serial(async () => {
2677
+ if (cmd.kind !== "help" && cmd.kind !== "status" && cmd.kind !== "callback") {
2678
+ this.addLog("input", `${source}: ${describe(cmd)}`);
2679
+ }
2680
+ const from = this.nextLogId;
2681
+ const since = () => this.logEntries.filter((e) => e.id >= from);
2682
+ try {
2683
+ const result = await this.execute(cmd);
2684
+ await this.host.settle();
2685
+ this.pump();
2686
+ return { ...result, log: since() };
2687
+ } catch (e) {
2688
+ this.pump();
2689
+ const error = message(e);
2690
+ this.addLog("error", error);
2691
+ return { ok: false, output: [`error: ${error}`], error, log: since() };
2692
+ }
2693
+ });
2694
+ }
2695
+ stopLoads() {
2696
+ const n = this.loads.size;
2697
+ for (const ac of this.loads) ac.abort();
2698
+ this.loads.clear();
2699
+ return n;
2700
+ }
2701
+ /** `--count` / `--rate` / `--random`: the inner command, once per run, paced; the first failure stops it. */
2702
+ async repeat(cmd, source) {
2703
+ const fail = (error) => {
2704
+ this.addLog("error", error);
2705
+ return { ok: false, output: [`error: ${error}`], error };
2706
+ };
2707
+ let spec;
2708
+ let variant;
2709
+ try {
2710
+ spec = repeatSpec(cmd, (this.opts.seed ?? 1) + this.nextLogId);
2711
+ variant = repeatVariants(cmd, spec, { eventTypes: this.meta().eventTypes, scenarios: Object.keys(this.opts.modules.devkit.SCENARIOS) });
2712
+ } catch (e) {
2713
+ return fail(message(e));
2714
+ }
2715
+ this.addLog("input", `${source}: ${describe(cmd)}`);
2716
+ const ac = new AbortController();
2717
+ this.loads.add(ac);
2718
+ const from = this.nextLogId;
2719
+ let result;
2720
+ try {
2721
+ result = await runLoad(spec, () => this.closed ? Promise.resolve({ ok: false, output: [], error: "session closed" }) : this.run(variant(), source), { signal: ac.signal });
2722
+ } finally {
2723
+ this.loads.delete(ac);
2724
+ }
2725
+ const summary = summarizeLoad(spec, result);
2726
+ this.addLog(result.failed ? "error" : "info", summary);
2727
+ return {
2728
+ ok: result.failed === null,
2729
+ output: [summary, ...result.failed?.error ? [`error: ${result.failed.error}`] : []],
2730
+ payload: result.results.map((r) => r.payload),
2731
+ ...result.failed?.error ? { error: result.failed.error } : {},
2732
+ log: this.logEntries.filter((e) => e.id >= from)
2733
+ };
2734
+ }
2735
+ user(u) {
2736
+ const name = u?.name?.replace(/^@/, "") || DEFAULT_USER;
2737
+ return u?.role ? { name, role: u.role } : { name };
2738
+ }
2739
+ async execute(cmd) {
2740
+ const h = this.host;
2741
+ switch (cmd.kind) {
2742
+ case "help":
2743
+ return { ok: true, output: REPL_HELP };
2744
+ case "status": {
2745
+ const s = this.state();
2746
+ return {
2747
+ ok: true,
2748
+ output: [
2749
+ `${s.project.name} (${s.project.category}) \xB7 view ${s.view ?? "none"}${s.mountError ? ` (${s.mountError})` : ""}`,
2750
+ `surfaces: ${s.targets.join(", ") || "none"} \xB7 bot runtime: ${this.runtime ? "yes" : "no"} \xB7 functions: ${s.functions.join(", ") || "none"}`,
2751
+ `timers: ${s.timers.map((t) => t.name).join(", ") || "none"} \xB7 presets: ${s.presets.join(", ") || "none"} \xB7 deferred jobs: ${s.pendingDeferred}`,
2752
+ `devkit ${s.versions.devkit} \xB7 sdk ${s.versions.sdk} \xB7 protocol ${s.versions.protocol}`
2753
+ ]
2754
+ };
2755
+ }
2756
+ case "event": {
2757
+ let data;
2758
+ if (cmd.preset !== void 0) {
2759
+ const preset = h.preset(cmd.preset);
2760
+ if (cmd.user) preset.user = this.user(cmd.user);
2761
+ data = h.emit(preset);
2762
+ return { ok: true, output: [`${preset.type} ${compact(data)}`], payload: data };
2763
+ }
2764
+ if (!cmd.type) throw new CommandSyntaxError("An event needs a type or a preset");
2765
+ const type = cmd.type;
2766
+ const overrides = this.coerce(type, cmd.data ?? {});
2767
+ data = h.emit(type, overrides, cmd.user ? { user: this.user(cmd.user) } : void 0);
2768
+ return { ok: true, output: [`${type} ${compact(data)}`], payload: data };
2769
+ }
2770
+ case "chat": {
2771
+ const platform = cmd.platform ?? h.context.platform;
2772
+ const r = await h.chat(cmd.text, { user: this.user(cmd.user), platform });
2773
+ const out = [`${platform}:chat ${r.viewer.login} (${r.viewer.role}): ${r.message.message}`];
2774
+ const bot = r;
2775
+ if (bot.log) out.push(...bot.log.map((l) => ` ${l}`));
2776
+ else if (this.runtime === null) out.push(" (no bot module in this project: delivered as chat events only)");
2777
+ this.addLog("chat", out.join("\n"), r.message);
2778
+ return { ok: true, output: out, payload: r.message };
2779
+ }
2780
+ case "reward": {
2781
+ const platform = cmd.platform ?? (h.context.platform === "kick" ? "kick" : "twitch");
2782
+ const r = h.redeem({ reward: cmd.reward, platform, user: this.user(cmd.user), input: cmd.input });
2783
+ const ev = h.events.at(-1);
2784
+ return { ok: true, output: [`redeemed ${r.rewardId} as ${r.viewer} (redemption ${r.id}, ${r.status})`, `${ev?.type} ${compact(ev?.data)}`], payload: ev?.data };
2785
+ }
2786
+ case "reward-status": {
2787
+ const r = h.rewards.setStatus(cmd.id, cmd.status);
2788
+ const ev = h.events.at(-1);
2789
+ return { ok: true, output: [`redemption ${r.id} \u2192 ${r.status}`], payload: ev?.data };
2790
+ }
2791
+ case "music":
2792
+ return this.music(cmd.op, cmd.args ?? []);
2793
+ case "timer": {
2794
+ if (!this.runtime) throw new CommandSyntaxError("This project has no bot module (server/handler.ts), so it has no timers");
2795
+ const log = await this.runtime.fireTimer(cmd.name);
2796
+ return { ok: true, output: [log ? `timer ${cmd.name} ran (${Math.round(log.ms)} ms)` : `timer ${cmd.name} skipped (see warnings)`] };
2797
+ }
2798
+ case "defer-run": {
2799
+ if (!this.runtime) throw new CommandSyntaxError("This project has no server code, so ctx.defer has nothing queued");
2800
+ const pending = this.runtime.pendingDeferred;
2801
+ const errors = await this.runtime.runDeferred();
2802
+ return { ok: errors.length === 0, output: [`ran ${pending} deferred job batch(es)${errors.length ? `; errors: ${errors.join("; ")}` : ""}`] };
2803
+ }
2804
+ case "scenario":
2805
+ return this.scenario(cmd.name, cmd.options ?? {});
2806
+ case "storage": {
2807
+ if (cmd.key === void 0) return { ok: true, output: [compact(h.storage.entries())], payload: h.storage.entries() };
2808
+ if (!("value" in cmd) || cmd.value === void 0) return { ok: true, output: [`${cmd.key} = ${compact(h.storage.get(cmd.key))}`], payload: h.storage.get(cmd.key) };
2809
+ h.storage.set(cmd.key, cmd.value);
2810
+ return { ok: true, output: [`storage.update ${cmd.key} = ${compact(cmd.value)}`], payload: cmd.value };
2811
+ }
2812
+ case "db": {
2813
+ const rt = this.runtime;
2814
+ if (!rt) throw new CommandSyntaxError("This project has no server code, so it has no ctx.db");
2815
+ if (cmd.reset) {
2816
+ rt.db.reset();
2817
+ return { ok: true, output: ["ctx.db cleared"] };
2818
+ }
2819
+ const tables = cmd.table ? [cmd.table] : rt.db.tables();
2820
+ if (cmd.table && !rt.db.tables().includes(cmd.table)) {
2821
+ throw new CommandSyntaxError(`server/schema.ts declares no table "${cmd.table}" (tables: ${rt.db.tables().join(", ") || "none"})`);
2822
+ }
2823
+ const out = tables.length ? tables.map((t) => `${t} (${rt.db.rows(t).length}): ${compact(rt.db.rows(t))}`) : ["server/schema.ts declares no tables"];
2824
+ return { ok: true, output: out, payload: Object.fromEntries(tables.map((t) => [t, rt.db.rows(t)])) };
2825
+ }
2826
+ case "config": {
2827
+ if (!cmd.settings) return { ok: true, output: [compact(h.settings)], payload: h.settings };
2828
+ if (!isObject(cmd.settings)) throw new CommandSyntaxError("config needs an object of settings");
2829
+ const settings = { ...h.settings, ...cmd.settings };
2830
+ h.setConfig(settings);
2831
+ return { ok: true, output: [`config.update ${compact(settings)}`], payload: settings };
2832
+ }
2833
+ case "theme": {
2834
+ if (cmd.mode !== "light" && cmd.mode !== "dark") throw new CommandSyntaxError('theme is "light" or "dark"');
2835
+ this.themeMode = cmd.mode;
2836
+ h.setTheme(THEMES[cmd.mode]);
2837
+ return { ok: true, output: [`theme.change (${cmd.mode})`], payload: h.theme };
2838
+ }
2839
+ case "view": {
2840
+ if (cmd.target !== "editor" && cmd.target !== "layer") throw new CommandSyntaxError('view is "editor" or "layer"');
2841
+ if (!this.opts.targets.includes(cmd.target)) {
2842
+ throw new CommandSyntaxError(`This project has no ${cmd.target} surface (lumio.config.json targets + src/${cmd.target}.tsx)`);
2843
+ }
2844
+ this.viewTarget = cmd.target;
2845
+ await this.mountView();
2846
+ return { ok: this.mountError === null, output: [this.mountError ? `${cmd.target}: ${this.mountError}` : `view ${cmd.target} mounted`] };
2847
+ }
2848
+ case "callback": {
2849
+ if (typeof cmd.handlerId !== "string") throw new CommandSyntaxError("callback needs a handlerId");
2850
+ const found = this.findHandler(cmd.handlerId);
2851
+ if (!found) throw new CommandSyntaxError("That control is no longer on screen");
2852
+ await h.fire(found.node, found.prop, ...Array.isArray(cmd.args) ? cmd.args : []);
2853
+ return { ok: true, output: [`event.callback ${found.node.type}.${found.prop}`] };
2854
+ }
2855
+ case "server-override": {
2856
+ if (typeof cmd.fn !== "string" || cmd.fn === "") throw new CommandSyntaxError("server-override needs fn");
2857
+ if (cmd.answer === null) {
2858
+ this.overrides.delete(cmd.fn);
2859
+ h.clearMockServer(cmd.fn);
2860
+ return { ok: true, output: [`${cmd.fn}: override removed, the local function answers again`] };
2861
+ }
2862
+ h.mockServer(cmd.fn, cmd.answer);
2863
+ this.overrides.set(cmd.fn, cmd.answer);
2864
+ return { ok: true, output: [`${cmd.fn}: answered by override ${compact(cmd.answer)}`] };
2865
+ }
2866
+ default:
2867
+ throw new CommandSyntaxError(`Unknown command ${compact(cmd.kind)}`);
2868
+ }
2869
+ }
2870
+ /**
2871
+ * REPL fields arrive as `tier=2`: a value keeps the type of the field in the
2872
+ * factory output (a string field stays a string, a number field becomes a
2873
+ * number), so the schema sees what you meant.
2874
+ */
2875
+ coerce(type, data) {
2876
+ const { devkit } = this.opts.modules;
2877
+ if (!devkit.isLumioEventType(type)) return data;
2878
+ const base = devkit.createEvent(type, void 0, { seed: 1 });
2879
+ const walk = (value, template, key = "") => {
2880
+ if (isObject(value)) {
2881
+ const t = isObject(template) ? template : {};
2882
+ return Object.fromEntries(Object.entries(value).map(([k, v]) => [k, walk(v, t[k], k)]));
2883
+ }
2884
+ if (/tier$/.test(key) && typeof template === "string" && /^[123]000$/.test(template) && /^[123]$/.test(String(value))) return `${String(value)}000`;
2885
+ if (typeof template === "string" && (typeof value === "number" || typeof value === "boolean")) return String(value);
2886
+ if (typeof template === "number" && typeof value === "string" && value.trim() !== "" && !Number.isNaN(Number(value))) return Number(value);
2887
+ return value;
2888
+ };
2889
+ return walk(data, base);
2890
+ }
2891
+ music(op, args) {
2892
+ const p = this.host.player;
2893
+ const arg = args[0];
2894
+ const needArg = (what) => {
2895
+ if (arg === void 0) throw new CommandSyntaxError(`/music ${op} needs ${what}`);
2896
+ return arg;
2897
+ };
2898
+ switch (op) {
2899
+ case "play":
2900
+ p.play(arg);
2901
+ break;
2902
+ case "pause":
2903
+ p.pause();
2904
+ break;
2905
+ case "skip":
2906
+ p.skip(arg === void 0 ? 1 : Number(arg));
2907
+ break;
2908
+ case "seek":
2909
+ p.seek(parseDuration(needArg("a time (1:30)")));
2910
+ break;
2911
+ case "volume":
2912
+ p.setVolume(Number(needArg("a percentage")));
2913
+ break;
2914
+ case "shuffle":
2915
+ p.setShuffle(needArg("on|off") === "on");
2916
+ break;
2917
+ case "repeat":
2918
+ p.setRepeat(needArg("off|context|track"));
2919
+ break;
2920
+ case "queue":
2921
+ p.queueAdd(needArg("a track URI"));
2922
+ break;
2923
+ case "device":
2924
+ p.transfer(args.join(" ") || needArg("a device name"));
2925
+ break;
2926
+ case "advance":
2927
+ p.advance(parseDuration(needArg("a duration (30s)")));
2928
+ break;
2929
+ default:
2930
+ throw new CommandSyntaxError(`Unknown /music command "${op}" (${MUSIC_OPS.join(", ")})`);
2931
+ }
2932
+ const s = p.state;
2933
+ return {
2934
+ ok: true,
2935
+ output: [`player: ${s.track ? `${s.track.name}` : "nothing"} \xB7 ${s.is_playing ? "playing" : "paused"} \xB7 ${Math.round(s.progress_ms / 1e3)} s \xB7 volume ${s.device.volume_percent}%`],
2936
+ payload: s
2937
+ };
2938
+ }
2939
+ async scenario(name, options) {
2940
+ const { devkit } = this.opts.modules;
2941
+ const factories = devkit.SCENARIOS;
2942
+ const make = factories[name];
2943
+ if (!make) throw new CommandSyntaxError(`Unknown scenario "${name}" (scenarios: ${Object.keys(factories).join(", ")})`);
2944
+ const { instant, ...rest } = options;
2945
+ for (const key of ["users", "lines"]) {
2946
+ if (typeof rest[key] === "string") rest[key] = rest[key].split(",").map((s) => s.trim()).filter(Boolean);
2947
+ }
2948
+ const run = await this.host.play(make(rest), { instant: instant === true });
2949
+ return { ok: true, output: [`scenario ${run.name}: ${run.events.length} event(s)`], payload: run.events.map((e) => ({ type: e.type, data: e.data })) };
2950
+ }
2951
+ };
2952
+ function describe(cmd) {
2953
+ switch (cmd.kind) {
2954
+ case "event":
2955
+ return cmd.preset !== void 0 ? `/event @${cmd.preset}` : `/event ${cmd.type ?? "?"}${cmd.data && Object.keys(cmd.data).length ? ` ${compact(cmd.data)}` : ""}`;
2956
+ case "chat":
2957
+ return `/chat ${cmd.user?.role ? `@${cmd.user.role}:` : cmd.user?.name ? "@" : ""}${cmd.user?.name ?? ""}${cmd.user?.name || cmd.user?.role ? " " : ""}${cmd.text}${cmd.platform ? ` --platform ${cmd.platform}` : ""}`;
2958
+ case "reward":
2959
+ return `/reward ${JSON.stringify(cmd.reward)}${cmd.user?.name ? ` --user ${cmd.user.name}` : ""}${cmd.input ? ` --input ${JSON.stringify(cmd.input)}` : ""}${cmd.platform ? ` --platform ${cmd.platform}` : ""}`;
2960
+ case "reward-status":
2961
+ return `/reward-status ${cmd.id} ${cmd.status}`;
2962
+ case "music":
2963
+ return `/music ${cmd.op}${cmd.args?.length ? ` ${cmd.args.join(" ")}` : ""}`;
2964
+ case "timer":
2965
+ return `/timer ${cmd.name}`;
2966
+ case "defer-run":
2967
+ return "/defer run";
2968
+ case "scenario":
2969
+ return `/scenario ${cmd.name}${cmd.options && Object.keys(cmd.options).length ? ` ${compact(cmd.options)}` : ""}`;
2970
+ case "storage":
2971
+ return `/storage${cmd.key ? ` ${cmd.key}` : ""}${cmd.value !== void 0 ? ` ${compact(cmd.value)}` : ""}`;
2972
+ case "db":
2973
+ return `/db${cmd.table ? ` ${cmd.table}` : ""}${cmd.reset ? " --reset" : ""}`;
2974
+ case "config":
2975
+ return `/config${cmd.settings ? ` ${compact(cmd.settings)}` : ""}`;
2976
+ case "theme":
2977
+ return `/theme ${cmd.mode}`;
2978
+ case "view":
2979
+ return `/view ${cmd.target}`;
2980
+ case "callback":
2981
+ return `callback ${cmd.handlerId}`;
2982
+ case "server-override":
2983
+ return `server override ${cmd.fn}${cmd.answer === null ? " (removed)" : ` ${compact(cmd.answer)}`}`;
2984
+ case "repeat": {
2985
+ const inner = cmd.random ? cmd.command.kind === "scenario" ? "/scenario --random" : `/event --random${cmd.platform ? ` --platform ${cmd.platform}` : ""}` : describe(cmd.command);
2986
+ return `${inner} --count ${cmd.count ?? 1}${cmd.intervalMs ? ` --rate ${describeRate(cmd.intervalMs)}` : ""}${cmd.seed !== void 0 ? ` --seed ${cmd.seed}` : ""}`;
2987
+ }
2988
+ default:
2989
+ return `/${cmd.kind}`;
2990
+ }
2991
+ }
2992
+
2993
+ // src/dev/server.ts
2994
+ import * as fs8 from "fs";
2995
+ import * as http2 from "http";
2996
+ import * as path9 from "path";
2997
+ import { randomBytes as randomBytes2, timingSafeEqual } from "crypto";
2998
+ import { fileURLToPath } from "url";
2999
+ var DEFAULT_DEV_PORT = 3100;
3000
+ var TOKEN_HEADER = "x-lumio-dev-token";
3001
+ var DEV_SERVER_FILE = path9.join(".lumio", "dev-server.json");
3002
+ var MAX_BODY = 1024 * 1024;
3003
+ var CONTENT_TYPES = {
3004
+ ".html": "text/html; charset=utf-8",
3005
+ ".js": "text/javascript; charset=utf-8",
3006
+ ".css": "text/css; charset=utf-8",
3007
+ ".json": "application/json; charset=utf-8",
3008
+ ".map": "application/json; charset=utf-8",
3009
+ ".svg": "image/svg+xml",
3010
+ ".png": "image/png",
3011
+ ".jpg": "image/jpeg",
3012
+ ".jpeg": "image/jpeg",
3013
+ ".gif": "image/gif",
3014
+ ".webp": "image/webp",
3015
+ ".avif": "image/avif",
3016
+ ".ico": "image/x-icon",
3017
+ ".mp3": "audio/mpeg",
3018
+ ".wav": "audio/wav",
3019
+ ".ogg": "audio/ogg",
3020
+ ".mp4": "video/mp4",
3021
+ ".webm": "video/webm",
3022
+ ".woff": "font/woff",
3023
+ ".woff2": "font/woff2",
3024
+ ".ttf": "font/ttf",
3025
+ ".otf": "font/otf"
3026
+ };
3027
+ var PAGE_CSP = [
3028
+ "default-src 'self'",
3029
+ "script-src 'self'",
3030
+ "style-src 'self' 'unsafe-inline'",
3031
+ "img-src 'self' data: blob: https: http:",
3032
+ "media-src 'self' data: blob: https: http:",
3033
+ "font-src 'self' data: https:",
3034
+ "connect-src 'self'",
3035
+ "frame-ancestors 'none'",
3036
+ "base-uri 'none'",
3037
+ "form-action 'none'"
3038
+ ].join("; ");
3039
+ function defaultSandboxDir() {
3040
+ const here = path9.dirname(fileURLToPath(import.meta.url));
3041
+ const candidates = [path9.join(here, "sandbox"), path9.join(here, "..", "..", "dist", "sandbox")];
3042
+ return candidates.find((c) => fs8.existsSync(path9.join(c, "index.html"))) ?? candidates[0];
3043
+ }
3044
+ function createToken() {
3045
+ return randomBytes2(24).toString("base64url");
3046
+ }
3047
+ function writeDevServerFile(projectDir, info) {
3048
+ const file = path9.join(projectDir, DEV_SERVER_FILE);
3049
+ fs8.mkdirSync(path9.dirname(file), { recursive: true });
3050
+ fs8.writeFileSync(file, `${JSON.stringify(info, null, 2)}
3051
+ `, { mode: 384 });
3052
+ fs8.chmodSync(file, 384);
3053
+ ensureGitignored(projectDir);
3054
+ return file;
3055
+ }
3056
+ function ensureGitignored(projectDir) {
3057
+ const file = path9.join(projectDir, ".gitignore");
3058
+ const current = fs8.existsSync(file) ? fs8.readFileSync(file, "utf8") : "";
3059
+ const listed = current.split(/\r?\n/).some((l) => [".lumio", ".lumio/", "/.lumio", "/.lumio/", ".lumio/**"].includes(l.trim()));
3060
+ if (listed) return false;
3061
+ const prefix = current === "" || current.endsWith("\n") ? "" : "\n";
3062
+ fs8.writeFileSync(file, `${current}${prefix}# lumio dev (session token, dev bundles)
3063
+ .lumio/
3064
+ `);
3065
+ return true;
3066
+ }
3067
+ function removeDevServerFile(projectDir, token) {
3068
+ const file = path9.join(projectDir, DEV_SERVER_FILE);
3069
+ try {
3070
+ const info = JSON.parse(fs8.readFileSync(file, "utf8"));
3071
+ if (info.token === token) fs8.rmSync(file, { force: true });
3072
+ } catch {
3073
+ }
3074
+ }
3075
+ function safeEqual(a, b) {
3076
+ const x = Buffer.from(a);
3077
+ const y = Buffer.from(b);
3078
+ return x.length === y.length && timingSafeEqual(x, y);
3079
+ }
3080
+ function within(root, rel) {
3081
+ let decoded;
3082
+ try {
3083
+ decoded = decodeURIComponent(rel);
3084
+ } catch {
3085
+ return null;
3086
+ }
3087
+ if (decoded.includes("\0")) return null;
3088
+ const full = path9.resolve(root, `.${path9.posix.sep}${decoded.replace(/^\/+/, "")}`);
3089
+ const base = path9.resolve(root);
3090
+ return full === base || full.startsWith(base + path9.sep) ? full : null;
3091
+ }
3092
+ async function startDevServer(options) {
3093
+ const { session, outDir } = options;
3094
+ const sandboxDir = options.sandboxDir ?? defaultSandboxDir();
3095
+ const token = options.token ?? createToken();
3096
+ const requestedPort = options.port ?? DEFAULT_DEV_PORT;
3097
+ const streams = /* @__PURE__ */ new Set();
3098
+ let port = requestedPort;
3099
+ let notifyTimer = null;
3100
+ const allowedHosts = () => /* @__PURE__ */ new Set([`127.0.0.1:${port}`, `localhost:${port}`, `[::1]:${port}`]);
3101
+ const send = (res, status, body, type, extra = {}) => {
3102
+ res.writeHead(status, {
3103
+ "Content-Type": type,
3104
+ "Cache-Control": "no-store",
3105
+ "X-Content-Type-Options": "nosniff",
3106
+ "Referrer-Policy": "no-referrer",
3107
+ ...extra
3108
+ });
3109
+ res.end(body);
3110
+ };
3111
+ const json = (res, status, body) => send(res, status, JSON.stringify(body), CONTENT_TYPES[".json"]);
3112
+ const serveFile = (res, file, extra = {}) => {
3113
+ if (!file || !fs8.existsSync(file) || !fs8.statSync(file).isFile()) return json(res, 404, { error: "NOT_FOUND" });
3114
+ send(res, 200, fs8.readFileSync(file), CONTENT_TYPES[path9.extname(file).toLowerCase()] ?? "application/octet-stream", extra);
3115
+ };
3116
+ const readBody = (req) => new Promise((resolve13, reject) => {
3117
+ let size = 0;
3118
+ const chunks = [];
3119
+ req.on("data", (c) => {
3120
+ size += c.length;
3121
+ if (size > MAX_BODY) {
3122
+ reject(new Error("Body larger than 1 MiB"));
3123
+ req.destroy();
3124
+ } else chunks.push(c);
3125
+ });
3126
+ req.on("end", () => resolve13(Buffer.concat(chunks).toString("utf8")));
3127
+ req.on("error", reject);
3128
+ });
3129
+ const broadcast = () => {
3130
+ notifyTimer = null;
3131
+ if (streams.size === 0) return;
3132
+ const data = `event: state
3133
+ data: ${JSON.stringify(session.state())}
3134
+
3135
+ `;
3136
+ for (const res of streams) res.write(data);
3137
+ };
3138
+ const handler = async (req, res) => {
3139
+ if (!allowedHosts().has(req.headers.host ?? "")) return json(res, 421, { error: "WRONG_HOST", message: "Use the URL lumio dev printed" });
3140
+ const url = new URL(req.url ?? "/", `http://127.0.0.1:${port}`);
3141
+ const p = url.pathname;
3142
+ if (p.startsWith("/__lumio/")) {
3143
+ const given = req.headers[TOKEN_HEADER] ?? (p === "/__lumio/stream" ? url.searchParams.get("token") ?? void 0 : void 0);
3144
+ if (!given || !safeEqual(given, token)) {
3145
+ return json(res, 401, { error: "UNAUTHORIZED", message: `Send the token from ${DEV_SERVER_FILE} in the ${TOKEN_HEADER} header` });
3146
+ }
3147
+ if (req.method === "GET" && p === "/__lumio/state") return json(res, 200, session.state());
3148
+ if (req.method === "GET" && p === "/__lumio/meta") return json(res, 200, session.meta());
3149
+ if (req.method === "GET" && p === "/__lumio/event-template") {
3150
+ try {
3151
+ return json(res, 200, session.eventTemplate(url.searchParams.get("type") ?? ""));
3152
+ } catch (e) {
3153
+ return json(res, 400, { error: "BAD_REQUEST", message: e.message });
3154
+ }
3155
+ }
3156
+ if (req.method === "GET" && p === "/__lumio/stream") {
3157
+ res.writeHead(200, {
3158
+ "Content-Type": "text/event-stream",
3159
+ "Cache-Control": "no-store",
3160
+ Connection: "keep-alive",
3161
+ "X-Content-Type-Options": "nosniff"
3162
+ });
3163
+ res.write(`retry: 1000
3164
+ event: state
3165
+ data: ${JSON.stringify(session.state())}
3166
+
3167
+ `);
3168
+ streams.add(res);
3169
+ req.on("close", () => streams.delete(res));
3170
+ return;
3171
+ }
3172
+ if (req.method === "POST" && p === "/__lumio/events") {
3173
+ if (!(req.headers["content-type"] ?? "").startsWith("application/json")) {
3174
+ return json(res, 415, { error: "UNSUPPORTED_MEDIA_TYPE", message: "Send application/json" });
3175
+ }
3176
+ let cmd;
3177
+ try {
3178
+ cmd = commandFromJson(JSON.parse(await readBody(req)));
3179
+ } catch (e) {
3180
+ const status = e instanceof CommandSyntaxError || e instanceof SyntaxError ? 400 : 413;
3181
+ return json(res, status, { ok: false, error: e.message });
3182
+ }
3183
+ if (cmd === null) return json(res, 400, { ok: false, error: "Stop the session in its terminal (Ctrl+C or /quit)" });
3184
+ const header = req.headers["x-lumio-source"];
3185
+ const source = header === "panel" ? "panel" : header === "trigger" ? "trigger" : "http";
3186
+ const result = await session.run(cmd, source);
3187
+ return json(res, result.ok ? 200 : 422, result);
3188
+ }
3189
+ return json(res, 404, { error: "NOT_FOUND" });
3190
+ }
3191
+ if (req.method !== "GET" && req.method !== "HEAD") return json(res, 405, { error: "METHOD_NOT_ALLOWED" });
3192
+ if (p === "/" || p === "/index.html") {
3193
+ const file = path9.join(sandboxDir, "index.html");
3194
+ if (!fs8.existsSync(file)) {
3195
+ return send(res, 500, `The sandbox is missing from this CLI install (${file}). Reinstall @zaflun/lumio-cli.`, "text/plain; charset=utf-8");
3196
+ }
3197
+ const html = fs8.readFileSync(file, "utf8").replace("%LUMIO_DEV_TOKEN%", token);
3198
+ return send(res, 200, html, CONTENT_TYPES[".html"], { "Content-Security-Policy": PAGE_CSP });
3199
+ }
3200
+ if (p.startsWith("/sandbox/")) return serveFile(res, within(sandboxDir, p.slice("/sandbox/".length)));
3201
+ if (p.startsWith("/bundles/")) return serveFile(res, within(outDir, p.slice("/bundles/".length)));
3202
+ if (p.startsWith("/assets/")) return serveFile(res, within(path9.join(outDir, "assets"), p.slice("/assets/".length)));
3203
+ return json(res, 404, { error: "NOT_FOUND" });
3204
+ };
3205
+ const server = http2.createServer((req, res) => {
3206
+ handler(req, res).catch((e) => {
3207
+ if (!res.headersSent) json(res, 500, { error: "INTERNAL", message: e instanceof Error ? e.message : String(e) });
3208
+ else res.end();
3209
+ });
3210
+ });
3211
+ await new Promise((resolve13, reject) => {
3212
+ server.once("error", (e) => {
3213
+ reject(e.code === "EADDRINUSE" ? new Error(`Port ${requestedPort} is in use. Stop the other server or pass --port <port>.`) : e);
3214
+ });
3215
+ server.listen(requestedPort, "127.0.0.1", () => resolve13());
3216
+ });
3217
+ const address = server.address();
3218
+ if (address && typeof address === "object") port = address.port;
3219
+ const keepAlive = setInterval(() => {
3220
+ for (const res of streams) res.write(": ping\n\n");
3221
+ }, 15e3);
3222
+ keepAlive.unref();
3223
+ return {
3224
+ url: `http://127.0.0.1:${port}/`,
3225
+ port,
3226
+ token,
3227
+ notify() {
3228
+ if (!notifyTimer) notifyTimer = setTimeout(broadcast, 40);
3229
+ },
3230
+ async close() {
3231
+ clearInterval(keepAlive);
3232
+ if (notifyTimer) clearTimeout(notifyTimer);
3233
+ for (const res of streams) res.end();
3234
+ streams.clear();
3235
+ await new Promise((resolve13) => server.close(() => resolve13()));
3236
+ }
3237
+ };
3238
+ }
3239
+
3240
+ // src/commands/dev.ts
3241
+ var PLATFORMS = ["twitch", "youtube", "kick"];
3242
+ var ROLES = ["everyone", "follower", "subscriber", "vip", "moderator", "broadcaster", "owner", "administrator"];
3243
+ function intOption(name, min, max) {
3244
+ return (value) => {
3245
+ const n = Number(value);
3246
+ if (!Number.isInteger(n) || n < min || n > max) throw new InvalidArgumentError(`${name} must be an integer between ${min} and ${max}.`);
3247
+ return value;
3248
+ };
3249
+ }
3250
+ function registerDev(program2) {
3251
+ program2.command("dev").description("Run the extension locally: sandbox page, REPL and bot-module runtime in one session").option("-p, --port <port>", "Dev server port", intOption("--port", 0, 65535), String(DEFAULT_DEV_PORT)).option("--bot-module", "Open the sandbox with the chat first (the session is the same)").option("--automation-node", "Start the automation node dev simulator").option("--platform <platform>", `Default chat platform (${PLATFORMS.join(", ")})`, "twitch").option("--role <role>", `Role of the default chat user (${ROLES.slice(0, 6).join(", ")})`).option("--seed <n>", "Seed of every random event field", intOption("--seed", 0, 2 ** 31 - 1), "1").option("--no-cooldown", "Do not enforce command cooldowns").option("--view <target>", "Surface to open first (editor, layer)").option("--open", "Open the sandbox in the browser").action(async (options) => {
3252
+ const configPath = path10.resolve("lumio.config.json");
3253
+ if (!fs9.existsSync(configPath)) {
3254
+ console.error("lumio.config.json not found. Run `lumio init` first.");
3255
+ process.exit(1);
3256
+ }
3257
+ const config = parseConfigFile(fs9.readFileSync(configPath, "utf-8"));
3258
+ if (options.automationNode || config.category === "automation_node") {
3259
+ await runAutomationNodeDevServer(config);
3260
+ return;
3261
+ }
3262
+ try {
3263
+ await runDevSession(process.cwd(), config, options);
3264
+ } catch (e) {
3265
+ console.error(chalk3.red(e instanceof Error ? e.message : String(e)));
3266
+ process.exit(1);
3267
+ }
3268
+ });
3269
+ }
3270
+ function readRawConfig(projectDir) {
3271
+ return JSON.parse(fs9.readFileSync(path10.join(projectDir, "lumio.config.json"), "utf8"));
3272
+ }
3273
+ async function runDevSession(projectDir, config, options) {
3274
+ if (!PLATFORMS.includes(options.platform)) throw new Error(`--platform must be one of ${PLATFORMS.join(", ")}`);
3275
+ if (options.role !== void 0 && !ROLES.includes(options.role)) throw new Error(`--role must be one of ${ROLES.join(", ")}`);
3276
+ if (options.view !== void 0 && options.view !== "editor" && options.view !== "layer") throw new Error("--view must be editor or layer");
3277
+ let modules;
3278
+ try {
3279
+ modules = await loadProjectModules(projectDir);
3280
+ } catch (e) {
3281
+ if (e instanceof MissingPackageError) throw new Error(`${e.message}
3282
+ lumio dev runs your extension with the devkit's mock host.`);
3283
+ throw e;
3284
+ }
3285
+ const mocks = await modules.devkitNode.loadMockFiles(projectDir);
3286
+ const { targets, skipped } = devTargets(projectDir, config);
3287
+ if (targets.length === 0 && !config.server) {
3288
+ throw new Error('Nothing to run: lumio.config.json has no editor/layer target with a src/<target>.tsx and no "server": true.');
3289
+ }
3290
+ console.log(chalk3.bold(`
3291
+ Lumio dev \xB7 ${config.name}`) + chalk3.dim(` (${config.category})`));
3292
+ for (const w of modules.warnings) console.log(chalk3.yellow(`warning: ${w}`));
3293
+ for (const s of skipped) console.log(chalk3.dim(`note: ${s}`));
3294
+ let repl = null;
3295
+ let server = null;
3296
+ const pending = [];
3297
+ const session = new DevSession({
3298
+ projectDir,
3299
+ rawConfig: readRawConfig(projectDir),
3300
+ modules,
3301
+ targets,
3302
+ seed: Number(options.seed),
3303
+ platform: options.platform,
3304
+ role: options.role,
3305
+ cooldowns: options.cooldown,
3306
+ view: options.view ?? void 0,
3307
+ chatFirst: options.botModule ?? false,
3308
+ mocks,
3309
+ onLog: (entry) => repl ? repl.print(entry) : pending.push(entry),
3310
+ onChange: () => server?.notify()
3311
+ });
3312
+ const buildErrors = /* @__PURE__ */ new Map();
3313
+ const builder = new DevBuilder(projectDir, config, targets, {
3314
+ onTarget: (target, code) => void session.updateTarget(target, code),
3315
+ onServer: (next) => void session.updateServer(next),
3316
+ onCss: () => session.cssChanged(),
3317
+ onStatus: (key, errors) => {
3318
+ const before = JSON.stringify([...buildErrors]);
3319
+ if (errors) buildErrors.set(key, errors);
3320
+ else buildErrors.delete(key);
3321
+ if (JSON.stringify([...buildErrors]) === before) return;
3322
+ const all = [...buildErrors.values()].flat();
3323
+ session.setBuild({ ok: all.length === 0, errors: all });
3324
+ if (!errors && all.length === 0) session.addLog("build", "build ok");
3325
+ }
3326
+ });
3327
+ const first = await builder.start();
3328
+ await session.start(first.server, first.bundles);
3329
+ server = await startDevServer({ session, outDir: builder.outDir, port: Number(options.port) });
3330
+ writeDevServerFile(projectDir, {
3331
+ url: server.url,
3332
+ port: server.port,
3333
+ token: server.token,
3334
+ pid: process.pid,
3335
+ startedAt: (/* @__PURE__ */ new Date()).toISOString()
3336
+ });
3337
+ const state = session.state();
3338
+ console.log(`Sandbox: ${chalk3.cyan(server.url)}`);
3339
+ console.log(chalk3.dim(`Surfaces: ${state.targets.join(", ") || "none"} \xB7 bot runtime: ${config.server ? "on" : "off"} \xB7 seed ${options.seed} \xB7 platform ${options.platform}`));
3340
+ console.log(chalk3.dim("Type /help for commands. Plain text is a chat message.\n"));
3341
+ if (options.open) {
3342
+ const { default: open } = await import("open");
3343
+ await open(server.url).catch(() => {
3344
+ });
3345
+ }
3346
+ let configTimer = null;
3347
+ const reloadConfig = () => {
3348
+ if (configTimer) clearTimeout(configTimer);
3349
+ configTimer = setTimeout(() => {
3350
+ void (async () => {
3351
+ try {
3352
+ const raw = readRawConfig(projectDir);
3353
+ const nextMocks = await modules.devkitNode.loadMockFiles(projectDir);
3354
+ await session.updateConfig(raw, nextMocks);
3355
+ } catch (e) {
3356
+ session.addLog("error", `lumio.config.json / lumio/mocks/ not reloaded: ${e instanceof Error ? e.message : String(e)}`);
3357
+ }
3358
+ })();
3359
+ }, 150);
3360
+ };
3361
+ const watchers = [fs9.watch(path10.join(projectDir, "lumio.config.json"), reloadConfig)];
3362
+ const mocksDir = path10.join(projectDir, "lumio", "mocks");
3363
+ if (fs9.existsSync(mocksDir)) {
3364
+ try {
3365
+ watchers.push(fs9.watch(mocksDir, { recursive: true }, reloadConfig));
3366
+ } catch {
3367
+ }
3368
+ }
3369
+ await new Promise((resolve13) => {
3370
+ let stopping = false;
3371
+ const stop = () => {
3372
+ if (stopping) return;
3373
+ stopping = true;
3374
+ void (async () => {
3375
+ for (const w of watchers) w.close();
3376
+ if (configTimer) clearTimeout(configTimer);
3377
+ removeDevServerFile(projectDir, server?.token ?? "");
3378
+ await server?.close();
3379
+ await builder.stop();
3380
+ await session.close();
3381
+ repl?.close();
3382
+ console.log(chalk3.dim("\nlumio dev stopped."));
3383
+ resolve13();
3384
+ })();
3385
+ };
3386
+ process.once("SIGINT", stop);
3387
+ process.once("SIGTERM", stop);
3388
+ repl = startRepl(session, stop);
3389
+ for (const entry of pending.splice(0)) repl.print(entry);
3390
+ });
3391
+ }
3392
+
3393
+ // src/commands/trigger.ts
3394
+ import chalk4 from "chalk";
3395
+
3396
+ // src/dev/trigger.ts
3397
+ import * as fs10 from "fs";
3398
+ import * as path11 from "path";
3399
+ var VALUE_FLAGS = ["field", "data", "json", "file", "user", "role", "platform", "input", "timer", "storage", "count", "rate", "seed"];
3400
+ var REPEATABLE = /* @__PURE__ */ new Set(["field", "storage"]);
3401
+ var BOOLEAN_FLAGS = ["random", "quiet"];
3402
+ var TRIGGER_USAGE = [
3403
+ "lumio trigger <type> [field=value \u2026] [--field a.b=value] [--<field> value] [--data '{\u2026}'] [--file payload.json] [--user name] [--role role]",
3404
+ "lumio trigger @<preset> [--user name]",
3405
+ 'lumio trigger chat "<text>" [--user name] [--role moderator] [--platform kick]',
3406
+ 'lumio trigger reward "<title or id>" [--user name] [--input "text"] [--platform kick]',
3407
+ "lumio trigger reward-status <redemption-id> <status>",
3408
+ "lumio trigger music play|pause|skip|seek 1:30|volume 50|\u2026",
3409
+ "lumio trigger scenario <name> [key=value \u2026]",
3410
+ "lumio trigger --timer <name> lumio trigger --storage key='<json>'",
3411
+ "load: --count N --rate 5/s --random [--platform twitch] --seed n --quiet"
3412
+ ];
3413
+ function isObject2(v) {
3414
+ return typeof v === "object" && v !== null && !Array.isArray(v);
3415
+ }
3416
+ function deepMerge(target, source) {
3417
+ for (const [k, v] of Object.entries(source)) {
3418
+ if (k === "__proto__" || k === "constructor" || k === "prototype") throw new CommandSyntaxError(`Invalid field "${k}"`);
3419
+ target[k] = isObject2(v) && isObject2(target[k]) ? deepMerge(target[k], v) : v;
3420
+ }
3421
+ return target;
3422
+ }
3423
+ function jsonObject(raw, what) {
3424
+ let value;
3425
+ try {
3426
+ value = JSON.parse(raw);
3427
+ } catch (e) {
3428
+ throw new CommandSyntaxError(`${what} is not valid JSON: ${e.message}`);
3429
+ }
3430
+ if (!isObject2(value)) throw new CommandSyntaxError(`${what} must be a JSON object`);
3431
+ return value;
3432
+ }
3433
+ function roleOf(raw) {
3434
+ const role = ROLE_ALIASES[raw.toLowerCase()];
3435
+ if (!role) throw new CommandSyntaxError(`Unknown role "${raw}" (use ${Object.keys(ROLE_ALIASES).join(", ")})`);
3436
+ return role;
3437
+ }
3438
+ function storageCommand(raw) {
3439
+ const eq = raw.indexOf("=");
3440
+ if (eq <= 0) throw new CommandSyntaxError(`--storage needs key=value, got "${raw}"`);
3441
+ const key = raw.slice(0, eq);
3442
+ const text = raw.slice(eq + 1);
3443
+ let value;
3444
+ try {
3445
+ value = JSON.parse(text);
3446
+ } catch {
3447
+ if (/^\s*[[{"]/.test(text)) throw new CommandSyntaxError(`--storage ${key}: not valid JSON: ${text}`);
3448
+ value = text;
3449
+ }
3450
+ return { kind: "storage", key, value };
3451
+ }
3452
+ function parseTriggerArgs(argv, cwd = process.cwd()) {
3453
+ const words = [];
3454
+ const flags = /* @__PURE__ */ new Map();
3455
+ const bools = /* @__PURE__ */ new Set();
3456
+ const shortcuts = [];
3457
+ for (let i = 0; i < argv.length; i++) {
3458
+ const t = argv[i];
3459
+ if (!t.startsWith("--") || t === "--") {
3460
+ if (t !== "--") words.push(t);
3461
+ continue;
3462
+ }
3463
+ const eq = t.indexOf("=");
3464
+ const name = eq === -1 ? t.slice(2) : t.slice(2, eq);
3465
+ if (name === "") throw new CommandSyntaxError(`Invalid option ${t}`);
3466
+ if (BOOLEAN_FLAGS.includes(name)) {
3467
+ if (eq !== -1) throw new CommandSyntaxError(`--${name} takes no value`);
3468
+ bools.add(name);
3469
+ continue;
3470
+ }
3471
+ let value;
3472
+ if (eq !== -1) value = t.slice(eq + 1);
3473
+ else {
3474
+ if (i + 1 >= argv.length) throw new CommandSyntaxError(`--${name} needs a value`);
3475
+ value = argv[++i];
3476
+ }
3477
+ if (VALUE_FLAGS.includes(name)) {
3478
+ const list = flags.get(name) ?? [];
3479
+ if (list.length && !REPEATABLE.has(name)) throw new CommandSyntaxError(`--${name} is given twice`);
3480
+ flags.set(name, [...list, value]);
3481
+ } else shortcuts.push([name, value]);
3482
+ }
3483
+ const one = (name) => flags.get(name)?.[0];
3484
+ if (one("json") !== void 0 && one("data") !== void 0) throw new CommandSyntaxError("--json is --data: give one of them");
3485
+ const setup = [...(flags.get("storage") ?? []).map(storageCommand)];
3486
+ const timer = one("timer");
3487
+ if (timer !== void 0) setup.push({ kind: "timer", name: timer });
3488
+ let user;
3489
+ const userFlag2 = one("user");
3490
+ const role = one("role");
3491
+ if (userFlag2 !== void 0) user = userFlag2.startsWith("@") ? parseUserPrefix(userFlag2) : { name: userFlag2 };
3492
+ if (role !== void 0) user = { ...user, role: roleOf(role) };
3493
+ const load = { count: one("count"), rate: one("rate"), seed: one("seed"), random: bools.has("random") };
3494
+ const platform = one("platform");
3495
+ const only = (allowed, what) => {
3496
+ for (const name of ["field", "data", "json", "file", "input"]) {
3497
+ if (flags.has(name) && !allowed.includes(name)) throw new CommandSyntaxError(`--${name} does not apply to ${what}`);
3498
+ }
3499
+ if (shortcuts.length && !allowed.includes("field")) throw new CommandSyntaxError(`Unknown option --${shortcuts[0][0]} (${what} takes no fields)`);
3500
+ };
3501
+ const [head, ...rest] = words;
3502
+ let main;
3503
+ if (head === void 0) {
3504
+ if (load.random) {
3505
+ only([], "--random");
3506
+ main = withLoad({ kind: "event", user }, { ...load, platform });
3507
+ } else {
3508
+ if (setup.length === 0) throw new CommandSyntaxError(`Nothing to trigger. Usage:
3509
+ ${TRIGGER_USAGE.join("\n ")}`);
3510
+ only([], "--timer / --storage");
3511
+ if (user || platform || load.count || load.rate || load.seed) throw new CommandSyntaxError("--user, --role, --platform and load flags need an event, chat message or scenario");
3512
+ main = null;
3513
+ }
3514
+ } else if (head === "chat") {
3515
+ only([], "a chat message");
3516
+ const text = rest.join(" ");
3517
+ if (text === "") throw new CommandSyntaxError('Usage: lumio trigger chat "<text>" [--user name] [--role role] [--platform kick]');
3518
+ main = withLoad({ kind: "chat", text, user, platform }, load);
3519
+ } else if (head === "reward") {
3520
+ only(["input"], "a reward");
3521
+ if (rest.length !== 1) throw new CommandSyntaxError('Usage: lumio trigger reward "<title or id>" [--user name] [--input "text"] [--platform kick]');
3522
+ main = withLoad({ kind: "reward", reward: rest[0], user, input: one("input"), platform }, load);
3523
+ } else if (head === "reward-status") {
3524
+ only([], "a reward status");
3525
+ if (rest.length !== 2) throw new CommandSyntaxError("Usage: lumio trigger reward-status <redemption-id> <status>");
3526
+ main = withLoad({ kind: "reward-status", id: rest[0], status: rest[1] }, load);
3527
+ } else if (head === "music") {
3528
+ only([], "the music player");
3529
+ const [op, ...args] = rest;
3530
+ if (!op) throw new CommandSyntaxError("Usage: lumio trigger music play|pause|skip [n]|seek 1:30|volume 50|shuffle on|repeat track|queue <uri>|device <name>|advance 30s");
3531
+ main = withLoad({ kind: "music", op, args }, load);
3532
+ } else if (head === "scenario") {
3533
+ only([], "a scenario (options are key=value words)");
3534
+ const [name, ...opts] = rest;
3535
+ if (!name && !load.random) throw new CommandSyntaxError("Usage: lumio trigger scenario <name> [key=value \u2026] | lumio trigger scenario --random");
3536
+ main = withLoad({ kind: "scenario", name: name ?? "", options: assignments(opts, "scenario options") }, load);
3537
+ } else if (head.startsWith("@")) {
3538
+ only([], "a preset");
3539
+ if (rest.length) throw new CommandSyntaxError("A preset takes no fields");
3540
+ main = withLoad({ kind: "event", preset: head.slice(1), user }, load);
3541
+ } else if (head.includes(":")) {
3542
+ if (load.random) throw new CommandSyntaxError("--random picks the event type itself: drop the type (every event gets fresh random fields anyway)");
3543
+ const data = {};
3544
+ const file = one("file");
3545
+ if (file !== void 0) {
3546
+ const full = path11.resolve(cwd, file);
3547
+ if (!fs10.existsSync(full)) throw new CommandSyntaxError(`--file ${file}: no such file`);
3548
+ deepMerge(data, jsonObject(fs10.readFileSync(full, "utf8"), `--file ${file}`));
3549
+ }
3550
+ const raw = one("data") ?? one("json");
3551
+ if (raw !== void 0) deepMerge(data, jsonObject(raw, "--data"));
3552
+ deepMerge(data, assignments(rest, "event fields"));
3553
+ for (const f of flags.get("field") ?? []) deepMerge(data, assignments([f], "--field"));
3554
+ for (const [name, value] of shortcuts) setPath(data, name, parseValue(value));
3555
+ main = withLoad({ kind: "event", type: head, data, user }, { ...load, platform });
3556
+ } else {
3557
+ throw new CommandSyntaxError(`Unknown trigger "${head}": use an event type (twitch:follower), @preset, chat, reward, music or scenario`);
3558
+ }
3559
+ return { setup, main, quiet: bools.has("quiet") };
3560
+ }
3561
+ var DevServerError = class extends Error {
3562
+ code;
3563
+ constructor(code, message2) {
3564
+ super(message2);
3565
+ this.name = "DevServerError";
3566
+ this.code = code;
3567
+ }
3568
+ };
3569
+ function findProjectDir(start) {
3570
+ let dir = path11.resolve(start);
3571
+ for (; ; ) {
3572
+ if (fs10.existsSync(path11.join(dir, DEV_SERVER_FILE)) || fs10.existsSync(path11.join(dir, "lumio.config.json"))) return dir;
3573
+ const parent = path11.dirname(dir);
3574
+ if (parent === dir) return null;
3575
+ dir = parent;
3576
+ }
3577
+ }
3578
+ function readDevServerFile(projectDir) {
3579
+ const file = path11.join(projectDir, DEV_SERVER_FILE);
3580
+ if (!fs10.existsSync(file)) {
3581
+ throw new DevServerError("NO_SESSION", `No lumio dev session is running in ${projectDir} (no ${DEV_SERVER_FILE}). Start \`lumio dev\` in another terminal first.`);
3582
+ }
3583
+ let info;
3584
+ try {
3585
+ info = JSON.parse(fs10.readFileSync(file, "utf8"));
3586
+ } catch {
3587
+ throw new DevServerError("BAD_FILE", `${DEV_SERVER_FILE} is not valid JSON. Restart \`lumio dev\` to rewrite it.`);
3588
+ }
3589
+ let url = null;
3590
+ try {
3591
+ url = typeof info.url === "string" ? new URL(info.url) : null;
3592
+ } catch {
3593
+ url = null;
3594
+ }
3595
+ if (!url || url.protocol !== "http:" || url.hostname !== "127.0.0.1" || typeof info.token !== "string" || info.token === "" || typeof info.port !== "number") {
3596
+ throw new DevServerError("BAD_FILE", `${DEV_SERVER_FILE} does not describe a local lumio dev session (http://127.0.0.1:<port>/ and a token). Restart \`lumio dev\` to rewrite it.`);
3597
+ }
3598
+ return info;
3599
+ }
3600
+ function connectDevServer(projectDir, fetchImpl = fetch) {
3601
+ const info = readDevServerFile(projectDir);
3602
+ const base = new URL(info.url);
3603
+ const request = async (route, init = {}) => {
3604
+ let res;
3605
+ try {
3606
+ res = await fetchImpl(new URL(route, base), { ...init, headers: { ...init.headers, [TOKEN_HEADER]: info.token } });
3607
+ } catch {
3608
+ throw new DevServerError(
3609
+ "NOT_RUNNING",
3610
+ `The lumio dev session from ${DEV_SERVER_FILE} (port ${info.port}, pid ${info.pid}) is not answering \u2014 it was stopped or crashed. Start \`lumio dev\` again.`
3611
+ );
3612
+ }
3613
+ if (res.status === 401 || res.status === 421) {
3614
+ throw new DevServerError(
3615
+ "BAD_TOKEN",
3616
+ `The server on port ${info.port} refused the token from ${DEV_SERVER_FILE}: it is another lumio dev session (another project?) or the file is stale. Restart \`lumio dev\` in this project.`
3617
+ );
3618
+ }
3619
+ return res;
3620
+ };
3621
+ const body = async (res) => {
3622
+ try {
3623
+ return await res.json();
3624
+ } catch {
3625
+ throw new DevServerError("BAD_RESPONSE", `The server on port ${info.port} did not answer like lumio dev (HTTP ${res.status}). Is something else using the port?`);
3626
+ }
3627
+ };
3628
+ return {
3629
+ url: info.url,
3630
+ async meta() {
3631
+ const res = await request("/__lumio/meta");
3632
+ if (!res.ok) throw new DevServerError("BAD_RESPONSE", `GET /__lumio/meta answered HTTP ${res.status}`);
3633
+ return body(res);
3634
+ },
3635
+ async send(cmd) {
3636
+ const res = await request("/__lumio/events", {
3637
+ method: "POST",
3638
+ headers: { "Content-Type": "application/json", "X-Lumio-Source": "trigger" },
3639
+ body: JSON.stringify(cmd)
3640
+ });
3641
+ const r = await body(res);
3642
+ if (res.ok || res.status === 422) return { ok: r.ok === true, output: r.output ?? [], log: r.log, payload: r.payload, error: r.error };
3643
+ const error = r.error && r.message ? `${r.error}: ${r.message}` : r.error ?? r.message ?? `HTTP ${res.status}`;
3644
+ return { ok: false, output: [], error };
3645
+ }
3646
+ };
3647
+ }
3648
+ async function runTrigger(client, plan, hooks = {}) {
3649
+ const results = [];
3650
+ const send = async (cmd) => {
3651
+ const r = await client.send(cmd);
3652
+ results.push(r);
3653
+ hooks.onResult?.(r, cmd);
3654
+ return r;
3655
+ };
3656
+ for (const cmd of plan.setup) {
3657
+ if (!(await send(cmd)).ok) return { results, ok: false };
3658
+ }
3659
+ if (!plan.main) return { results, ok: true };
3660
+ if (plan.main.kind !== "repeat") return { results, ok: (await send(plan.main)).ok };
3661
+ const repeat = plan.main;
3662
+ const spec = repeatSpec(repeat, repeat.seed ?? (hooks.seed ?? freshSeed)());
3663
+ const meta = spec.random ? await client.meta() : { eventTypes: [], scenarios: [] };
3664
+ const variant = repeatVariants(repeat, spec, meta);
3665
+ const result = await runLoad(spec, () => send(variant()), { signal: hooks.signal });
3666
+ return { results, load: { spec, result, summary: summarizeLoad(spec, result) }, ok: result.failed === null };
3667
+ }
3668
+
3669
+ // src/commands/trigger.ts
3670
+ function formatResult(r) {
3671
+ const log = (r.log ?? []).filter((e) => e.kind !== "input");
3672
+ const lines = log.length ? log.map(formatLogEntry) : r.output.map((l) => r.ok ? l : chalk4.red(l));
3673
+ if (!r.ok && r.error && !log.some((e) => e.kind === "error")) lines.push(chalk4.red(`error: ${r.error}`));
3674
+ return lines;
3675
+ }
3676
+ function registerTrigger(program2) {
3677
+ program2.command("trigger").description("Send an event, chat message, reward, music command, timer or scenario to the running `lumio dev`").argument("[args...]", "what to trigger (see below)").allowUnknownOption().allowExcessArguments().helpOption("-h, --help", "Show help").addHelpText(
3678
+ "after",
3679
+ [
3680
+ "",
3681
+ "Usage:",
3682
+ ...TRIGGER_USAGE.map((l) => ` ${l}`),
3683
+ "",
3684
+ "Examples:",
3685
+ " lumio trigger twitch:follower",
3686
+ " lumio trigger twitch:subscribe --tier 2 --field user_name=Alice",
3687
+ " lumio trigger kick:reward --field reward.title=Hydrate",
3688
+ ' lumio trigger chat "!points 100" --user alice --role moderator',
3689
+ " lumio trigger twitch:follower --count 50 --rate 5/s",
3690
+ " lumio trigger --random --count 20 --rate 2/s --platform kick",
3691
+ " lumio trigger music skip",
3692
+ " lumio trigger --timer reminder",
3693
+ ` lumio trigger --storage counter='{"value":3}'`,
3694
+ "",
3695
+ "Talks to the lumio dev session of this project through .lumio/dev-server.json."
3696
+ ].join("\n")
3697
+ ).action(async (args) => {
3698
+ let plan;
3699
+ try {
3700
+ plan = parseTriggerArgs(args);
3701
+ } catch (e) {
3702
+ console.error(chalk4.red(e instanceof Error ? e.message : String(e)));
3703
+ process.exit(2);
3704
+ }
3705
+ const projectDir = findProjectDir(process.cwd()) ?? process.cwd();
3706
+ const abort = new AbortController();
3707
+ const onSigint = () => {
3708
+ if (abort.signal.aborted) process.exit(130);
3709
+ abort.abort();
3710
+ };
3711
+ process.on("SIGINT", onSigint);
3712
+ try {
3713
+ const client = connectDevServer(projectDir);
3714
+ const quiet = plan.quiet;
3715
+ const run = await runTrigger(client, plan, {
3716
+ signal: abort.signal,
3717
+ onResult: (r) => {
3718
+ if (quiet && r.ok) return;
3719
+ for (const line of formatResult(r)) console.log(line);
3720
+ }
3721
+ });
3722
+ if (run.load) console.log((run.ok ? chalk4.dim : chalk4.red)(run.load.summary));
3723
+ process.exitCode = run.ok ? 0 : 1;
3724
+ } catch (e) {
3725
+ const known = e instanceof DevServerError || e instanceof CommandSyntaxError;
3726
+ console.error(chalk4.red(known ? e.message : `lumio trigger failed: ${e instanceof Error ? e.message : String(e)}`));
3727
+ process.exitCode = 1;
3728
+ } finally {
3729
+ process.off("SIGINT", onSigint);
3730
+ }
3731
+ });
3732
+ }
3733
+
3734
+ // src/commands/build.ts
3735
+ import * as fs11 from "fs";
3736
+ import * as path12 from "path";
3737
+ function registerBuild(program2) {
3738
+ program2.command("build").description("Bundle extension as standalone HTML + source archive").action(async () => {
3739
+ const configPath = path12.resolve("lumio.config.json");
3740
+ if (!fs11.existsSync(configPath)) {
3741
+ console.error("lumio.config.json not found. Run `lumio init` first.");
3742
+ process.exit(1);
3743
+ }
3744
+ const configJson = fs11.readFileSync(configPath, "utf-8");
3745
+ const config = parseConfigFile(configJson);
3746
+ validateConfig(config);
3747
+ const changelogPath = path12.resolve("CHANGELOG.md");
3748
+ if (!fs11.existsSync(changelogPath)) {
3749
+ console.warn("Warning: No CHANGELOG.md found. It is recommended to maintain a changelog.");
3750
+ }
3751
+ console.log(`Building ${config.name} v${config.version}...`);
3752
+ const distDir = path12.resolve("dist");
3753
+ fs11.mkdirSync(distDir, { recursive: true });
3754
+ const { build } = await import("esbuild");
3755
+ const projectDir = process.cwd();
3756
+ if (config.category === "bot_module" && config.targets.length === 0) {
3757
+ console.log(" Bot module \u2014 no UI targets to bundle.");
3758
+ }
3759
+ const manifestPath = path12.join(distDir, FUNCTIONS_MANIFEST_FILE);
3760
+ fs11.rmSync(manifestPath, { force: true });
3761
+ const functionsFile = config.category !== "automation_node" ? findFunctionsFile(projectDir) : null;
3762
+ if (functionsFile && !config.server) {
3763
+ console.warn(` Warning: ${functionsFile} found but lumio.config.json has no "server": true \u2014 it is not built.`);
3764
+ }
3765
+ if (config.server) {
3766
+ let plan;
3767
+ try {
3768
+ plan = await planServerBuild(projectDir, config, path12.join(distDir, "server.js"));
3769
+ } catch (err) {
3770
+ console.error(` Error: ${err instanceof Error ? err.message : String(err)}`);
3771
+ process.exit(1);
3772
+ }
3773
+ if (plan.options) {
3774
+ await build(plan.options);
3775
+ console.log(" Built: dist/server.js");
3776
+ } else {
3777
+ console.warn(" Warning: server: true but no server/handler.ts, server/index.ts or server/functions.ts found.");
3778
+ }
3779
+ if (plan.manifest) {
3780
+ fs11.writeFileSync(manifestPath, `${JSON.stringify(plan.manifest, null, 2)}
3781
+ `);
3782
+ console.log(` Built: dist/${FUNCTIONS_MANIFEST_FILE} (${Object.keys(plan.manifest.functions).length} function(s))`);
3783
+ }
3784
+ }
3785
+ const cssEntryFile = findCssEntry(projectDir);
3786
+ try {
3787
+ const css = await compileCss(projectDir, distDir);
3788
+ if (css?.tailwind) console.log(" Tailwind CSS v4 detected \u2014 compiled dist/styles.css");
3789
+ else if (css) console.log(` Copied: ${cssEntryFile} \u2192 dist/styles.css`);
3790
+ } catch (err) {
3791
+ console.error(` Error: Tailwind CSS compilation failed. ${err instanceof CssBuildError ? err.hint : ""}`);
3792
+ console.error(` ${err instanceof Error ? err.message : String(err)}`);
3793
+ process.exit(1);
3794
+ }
3795
+ for (const target of config.targets) {
3796
+ const entryPoint = targetEntry(projectDir, target);
3797
+ if (!entryPoint) {
3798
+ console.warn(`Warning: Entry point ${path12.resolve("src", `${target}.tsx`)} not found, skipping target "${target}".`);
3799
+ continue;
3800
+ }
3801
+ await build(targetBuildOptions(entryPoint, distDir, target));
3802
+ console.log(` Built: dist/${target}.js`);
3803
+ }
3804
+ const { TarArchive } = await import("archiver");
3805
+ const sourceArchivePath = path12.join(distDir, "source.tar.gz");
3806
+ const output = fs11.createWriteStream(sourceArchivePath);
3807
+ const archive = new TarArchive({ gzip: true });
3808
+ archive.pipe(output);
3809
+ archive.glob("**/*", {
3810
+ cwd: path12.resolve("."),
3811
+ ignore: [
3812
+ "node_modules/**",
3813
+ "dist/**",
3814
+ ".git/**",
3815
+ ".git",
3816
+ ".next/**",
3817
+ ".lumio-dist/**",
3818
+ "*.tgz",
3819
+ "*.tar.gz",
3820
+ // Secrets & credentials must never be uploaded to the store: the
3821
+ // archive is visible to reviewers. Exclude env files (except the
3822
+ // example), npm/yarn auth, and local CLI state.
3823
+ // archiver 8's readdir-glob@3 dropped the `!pattern` un-ignore
3824
+ // feature (each ignore entry is now an independent minimatch, so a
3825
+ // leading `!` inverts that one matcher and ignores everything else).
3826
+ // Keep `.env.example` via an extglob instead of a negated entry.
3827
+ "**/.env",
3828
+ "**/.env.!(example)",
3829
+ "**/.npmrc",
3830
+ "**/.yarnrc",
3831
+ "**/.yarnrc.yml",
3832
+ "**/.lumio/**"
3833
+ ],
3834
+ dot: true
3835
+ });
3836
+ await archive.finalize();
3837
+ console.log(" Archive: dist/source.tar.gz");
3838
+ console.log("Build complete.");
3839
+ });
3840
+ }
3841
+
3842
+ // src/commands/deploy.ts
3843
+ import * as fs12 from "fs";
3844
+ import * as path13 from "path";
3845
+
3846
+ // src/changelog/parser.ts
3847
+ var VERSION_HEADER_REGEX = /^## \[([^\]]+)\](?:\s*-\s*(\d{4}-\d{2}-\d{2}))?/;
3848
+ function parseChangelog(markdown) {
3849
+ const lines = markdown.split("\n");
3850
+ const entries = [];
3851
+ let currentEntry = null;
3852
+ const contentLines = [];
3853
+ for (const line of lines) {
3854
+ const match = line.match(VERSION_HEADER_REGEX);
3855
+ if (match) {
3856
+ if (currentEntry) {
3857
+ currentEntry.content = contentLines.join("\n").trim();
3858
+ entries.push(currentEntry);
3859
+ contentLines.length = 0;
3860
+ }
3861
+ currentEntry = {
3862
+ version: match[1],
3863
+ date: match[2] ?? null,
3864
+ content: ""
3865
+ };
3866
+ } else if (currentEntry) {
3867
+ contentLines.push(line);
3868
+ }
3869
+ }
3870
+ if (currentEntry) {
3871
+ currentEntry.content = contentLines.join("\n").trim();
3872
+ entries.push(currentEntry);
3873
+ }
3874
+ return entries;
3875
+ }
3876
+ function getVersionEntry(changelog, version) {
3877
+ const entries = parseChangelog(changelog);
3878
+ return entries.find((e) => e.version === version) ?? null;
3879
+ }
3880
+
3881
+ // src/commands/deploy.ts
3882
+ function collectFiles(dir, base = dir) {
3883
+ const entries = fs12.readdirSync(dir, { withFileTypes: true });
3884
+ const files = [];
3885
+ for (const entry of entries) {
3886
+ const fullPath = path13.join(dir, entry.name);
3887
+ if (entry.isDirectory()) {
3888
+ files.push(...collectFiles(fullPath, base));
3889
+ } else if (entry.isFile()) {
3890
+ files.push(path13.relative(base, fullPath));
3891
+ }
3892
+ }
3893
+ return files;
3894
+ }
3895
+ var ALLOWED_EXTENSIONS = /* @__PURE__ */ new Set([
3896
+ "js",
3897
+ "css",
3898
+ "html",
3899
+ "map",
3900
+ "png",
3901
+ "jpg",
3902
+ "jpeg",
3903
+ "gif",
3904
+ "svg",
3905
+ "webp",
1263
3906
  "avif",
1264
3907
  "ico",
1265
3908
  "mp4",
@@ -1291,15 +3934,15 @@ function registerDeploy(program2) {
1291
3934
  console.error("Not logged in. Run `lumio login` or provide --token.");
1292
3935
  process.exit(1);
1293
3936
  }
1294
- const configPath = path5.resolve("lumio.config.json");
1295
- if (!fs5.existsSync(configPath)) {
3937
+ const configPath = path13.resolve("lumio.config.json");
3938
+ if (!fs12.existsSync(configPath)) {
1296
3939
  console.error("lumio.config.json not found. Run `lumio init` first.");
1297
3940
  process.exit(1);
1298
3941
  }
1299
- const configJson = fs5.readFileSync(configPath, "utf-8");
3942
+ const configJson = fs12.readFileSync(configPath, "utf-8");
1300
3943
  const config = parseConfigFile(configJson);
1301
3944
  validateConfig(config);
1302
- const distDir = path5.resolve("dist");
3945
+ const distDir = path13.resolve("dist");
1303
3946
  if (config.sounds && config.sounds.length > 0) {
1304
3947
  for (const sound of config.sounds) {
1305
3948
  const soundFile = sound.file;
@@ -1310,15 +3953,15 @@ function registerDeploy(program2) {
1310
3953
  );
1311
3954
  process.exit(1);
1312
3955
  }
1313
- const soundAbsPath = path5.join(distDir, "sounds", soundFile);
1314
- if (fs5.existsSync(distDir) && !fs5.existsSync(soundAbsPath)) {
3956
+ const soundAbsPath = path13.join(distDir, "sounds", soundFile);
3957
+ if (fs12.existsSync(distDir) && !fs12.existsSync(soundAbsPath)) {
1315
3958
  console.error(
1316
3959
  `Error: Sound file "${soundFile}" declared in lumio.config.json was not found at dist/sounds/${soundFile}. Run \`lumio build\` first.`
1317
3960
  );
1318
3961
  process.exit(1);
1319
3962
  }
1320
- if (fs5.existsSync(soundAbsPath)) {
1321
- const stat = fs5.statSync(soundAbsPath);
3963
+ if (fs12.existsSync(soundAbsPath)) {
3964
+ const stat = fs12.statSync(soundAbsPath);
1322
3965
  if (stat.size > LARGE_SOUND_THRESHOLD) {
1323
3966
  console.warn(
1324
3967
  ` Warning: Sound file "${soundFile}" is ${(stat.size / 1024 / 1024).toFixed(1)} MB. Consider compressing large audio files to reduce load times.`
@@ -1344,22 +3987,22 @@ function registerDeploy(program2) {
1344
3987
  `Error: Extension declares ${config.sounds.length} sounds, but your limit is ${maxSounds}.`
1345
3988
  );
1346
3989
  console.error(
1347
- "Request a limit increase at /account/developer/limits or contact support."
3990
+ "Request a limit increase at /dashboard/settings/developer/limits or contact support."
1348
3991
  );
1349
3992
  process.exit(1);
1350
3993
  }
1351
3994
  let totalSize2 = 0;
1352
3995
  for (const sound of config.sounds) {
1353
- const soundPath = path5.resolve("dist", "sounds", sound.file);
1354
- if (fs5.existsSync(soundPath)) {
1355
- const size = fs5.statSync(soundPath).size;
3996
+ const soundPath = path13.resolve("dist", "sounds", sound.file);
3997
+ if (fs12.existsSync(soundPath)) {
3998
+ const size = fs12.statSync(soundPath).size;
1356
3999
  totalSize2 += size;
1357
4000
  if (size > maxFileSize) {
1358
4001
  console.error(
1359
4002
  `Error: Sound file "${sound.file}" is ${(size / 1024 / 1024).toFixed(1)} MB but your per-file limit is ${(maxFileSize / 1024 / 1024).toFixed(1)} MB.`
1360
4003
  );
1361
4004
  console.error(
1362
- "Request a limit increase at /account/developer/limits or contact support."
4005
+ "Request a limit increase at /dashboard/settings/developer/limits or contact support."
1363
4006
  );
1364
4007
  process.exit(1);
1365
4008
  }
@@ -1370,7 +4013,7 @@ function registerDeploy(program2) {
1370
4013
  `Error: Total sound size is ${(totalSize2 / 1024 / 1024).toFixed(1)} MB but your storage limit is ${(maxStorage / 1024 / 1024).toFixed(1)} MB.`
1371
4014
  );
1372
4015
  console.error(
1373
- "Request a limit increase at /account/developer/limits or contact support."
4016
+ "Request a limit increase at /dashboard/settings/developer/limits or contact support."
1374
4017
  );
1375
4018
  process.exit(1);
1376
4019
  }
@@ -1388,9 +4031,9 @@ function registerDeploy(program2) {
1388
4031
  const version = options.version ?? config.version;
1389
4032
  let releaseNotes = options.description ?? "";
1390
4033
  if (!releaseNotes) {
1391
- const changelogPath = path5.resolve("CHANGELOG.md");
1392
- if (fs5.existsSync(changelogPath)) {
1393
- const changelog = fs5.readFileSync(changelogPath, "utf-8");
4034
+ const changelogPath = path13.resolve("CHANGELOG.md");
4035
+ if (fs12.existsSync(changelogPath)) {
4036
+ const changelog = fs12.readFileSync(changelogPath, "utf-8");
1394
4037
  const entry = getVersionEntry(changelog, version);
1395
4038
  if (entry) {
1396
4039
  releaseNotes = entry.content;
@@ -1401,10 +4044,23 @@ function registerDeploy(program2) {
1401
4044
  }
1402
4045
  }
1403
4046
  }
1404
- if (!fs5.existsSync(distDir)) {
4047
+ let dataSchema;
4048
+ try {
4049
+ dataSchema = await loadDataSchema(process.cwd());
4050
+ } catch (err) {
4051
+ console.error(`Error: Could not load server/schema.ts: ${err.message ?? String(err)}`);
4052
+ process.exit(1);
4053
+ }
4054
+ if (!fs12.existsSync(distDir)) {
1405
4055
  console.error("No dist/ directory found. Run `lumio build` first.");
1406
4056
  process.exit(1);
1407
4057
  }
4058
+ const exposesFunctions = config.server && config.category !== "automation_node";
4059
+ const functionsFile = exposesFunctions ? findFunctionsFile(process.cwd()) : null;
4060
+ if (functionsFile && !fs12.existsSync(path13.join(distDir, FUNCTIONS_MANIFEST_FILE))) {
4061
+ console.error(`Error: ${functionsFile} exists but dist/${FUNCTIONS_MANIFEST_FILE} is missing. Run \`lumio build\` first.`);
4062
+ process.exit(1);
4063
+ }
1408
4064
  console.log(`Deploying ${config.name} v${version}...`);
1409
4065
  const apiUrl = process.env.LUMIO_API_URL ?? store.getApiUrl();
1410
4066
  const client = new LumioApiClient(apiUrl, token);
@@ -1424,8 +4080,8 @@ function registerDeploy(program2) {
1424
4080
  console.error(`Too many files (${bundleFiles.length}). Maximum is ${MAX_FILES}.`);
1425
4081
  process.exit(1);
1426
4082
  }
1427
- const sourceArchive = path5.join(distDir, sourceFile);
1428
- if (!fs5.existsSync(sourceArchive)) {
4083
+ const sourceArchive = path13.join(distDir, sourceFile);
4084
+ if (!fs12.existsSync(sourceArchive)) {
1429
4085
  console.error("Error: source.tar.gz not found in dist/. Run `lumio build` first.");
1430
4086
  process.exit(1);
1431
4087
  }
@@ -1436,7 +4092,11 @@ function registerDeploy(program2) {
1436
4092
  formData.append("manifest", JSON.stringify(manifest));
1437
4093
  formData.append("release_notes", releaseNotes);
1438
4094
  formData.append("is_breaking", String(!!options.breaking));
1439
- const sourceBuffer = fs5.readFileSync(sourceArchive);
4095
+ if (dataSchema) {
4096
+ formData.append("data_schema", JSON.stringify(dataSchema));
4097
+ console.log(` Data schema: ${Object.keys(dataSchema.tables).length} table(s)`);
4098
+ }
4099
+ const sourceBuffer = fs12.readFileSync(sourceArchive);
1440
4100
  if (sourceBuffer.length === 0) {
1441
4101
  console.error("Error: source.tar.gz is empty. Run `lumio build` to regenerate.");
1442
4102
  process.exit(1);
@@ -1445,8 +4105,8 @@ function registerDeploy(program2) {
1445
4105
  console.log(` Source: source.tar.gz (${(sourceBuffer.length / 1024).toFixed(1)} KB)`);
1446
4106
  let totalSize = 0;
1447
4107
  for (const relPath of bundleFiles) {
1448
- const absPath = path5.join(distDir, relPath);
1449
- const buffer = fs5.readFileSync(absPath);
4108
+ const absPath = path13.join(distDir, relPath);
4109
+ const buffer = fs12.readFileSync(absPath);
1450
4110
  if (buffer.length === 0) {
1451
4111
  console.warn(` Warning: Skipping empty file: ${relPath}`);
1452
4112
  continue;
@@ -1475,9 +4135,16 @@ function registerDeploy(program2) {
1475
4135
  }
1476
4136
  } catch (err) {
1477
4137
  const status = err.status;
1478
- const message = err.message ?? String(err);
4138
+ const message2 = err.message ?? String(err);
4139
+ const schemaLines = err instanceof ApiError && status === 422 ? formatDataSchemaError(err.body) : null;
4140
+ if (schemaLines) {
4141
+ console.error(`
4142
+ Deploy failed (HTTP ${status}):`);
4143
+ for (const line of schemaLines) console.error(` ${line}`);
4144
+ process.exit(1);
4145
+ }
1479
4146
  console.error(`
1480
- Deploy failed${status ? ` (HTTP ${status})` : ""}: ${message}`);
4147
+ Deploy failed${status ? ` (HTTP ${status})` : ""}: ${message2}`);
1481
4148
  if (status === 409) {
1482
4149
  console.error(" This version already exists. Bump the version in lumio.config.json.");
1483
4150
  } else if (status === 413) {
@@ -1491,8 +4158,8 @@ function registerDeploy(program2) {
1491
4158
  }
1492
4159
 
1493
4160
  // src/commands/status.ts
1494
- import * as fs6 from "fs";
1495
- import * as path6 from "path";
4161
+ import * as fs13 from "fs";
4162
+ import * as path14 from "path";
1496
4163
  function registerStatus(program2) {
1497
4164
  program2.command("status").description("Show review status of the latest version").action(async () => {
1498
4165
  const store = new CredentialStore();
@@ -1501,12 +4168,12 @@ function registerStatus(program2) {
1501
4168
  console.error("Not logged in. Run `lumio login` first.");
1502
4169
  process.exit(1);
1503
4170
  }
1504
- const configPath = path6.resolve("lumio.config.json");
1505
- if (!fs6.existsSync(configPath)) {
4171
+ const configPath = path14.resolve("lumio.config.json");
4172
+ if (!fs13.existsSync(configPath)) {
1506
4173
  console.error("lumio.config.json not found.");
1507
4174
  process.exit(1);
1508
4175
  }
1509
- const config = parseConfigFile(fs6.readFileSync(configPath, "utf-8"));
4176
+ const config = parseConfigFile(fs13.readFileSync(configPath, "utf-8"));
1510
4177
  const client = new LumioApiClient(store.getApiUrl(), token);
1511
4178
  try {
1512
4179
  const versions = await client.get(
@@ -1535,8 +4202,8 @@ function registerStatus(program2) {
1535
4202
  }
1536
4203
 
1537
4204
  // src/commands/logs.ts
1538
- import * as fs7 from "fs";
1539
- import * as path7 from "path";
4205
+ import * as fs14 from "fs";
4206
+ import * as path15 from "path";
1540
4207
  function registerLogs(program2) {
1541
4208
  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) => {
1542
4209
  const store = new CredentialStore();
@@ -1545,12 +4212,12 @@ function registerLogs(program2) {
1545
4212
  console.error("Not logged in. Run `lumio login` first.");
1546
4213
  process.exit(1);
1547
4214
  }
1548
- const configPath = path7.resolve("lumio.config.json");
1549
- if (!fs7.existsSync(configPath)) {
4215
+ const configPath = path15.resolve("lumio.config.json");
4216
+ if (!fs14.existsSync(configPath)) {
1550
4217
  console.error("lumio.config.json not found.");
1551
4218
  process.exit(1);
1552
4219
  }
1553
- const config = parseConfigFile(fs7.readFileSync(configPath, "utf-8"));
4220
+ const config = parseConfigFile(fs14.readFileSync(configPath, "utf-8"));
1554
4221
  const apiUrl = store.getApiUrl();
1555
4222
  const logsUrl = `${apiUrl}/v1/developer/extensions/${config.extensionId}/logs?lines=${options.lines}`;
1556
4223
  console.log(`Streaming logs for ${config.name}...`);
@@ -1587,22 +4254,31 @@ function registerLogs(program2) {
1587
4254
  }
1588
4255
 
1589
4256
  // src/commands/env.ts
1590
- import * as fs8 from "fs";
1591
- import * as path8 from "path";
4257
+ import * as fs15 from "fs";
4258
+ import * as path16 from "path";
4259
+ var MAX_SECRET_KEY_LEN = 64;
4260
+ var MAX_SECRET_VALUE_BYTES = 4096;
1592
4261
  function validateSecretKey(key) {
1593
- return key.length > 0 && /^[A-Z0-9_]+$/.test(key);
4262
+ return /^[A-Z0-9_]{1,64}$/.test(key);
4263
+ }
4264
+ function validateSecretValue(value) {
4265
+ const bytes = Buffer.byteLength(value, "utf8");
4266
+ return bytes > 0 && bytes <= MAX_SECRET_VALUE_BYTES;
4267
+ }
4268
+ function secretEndpoint(extensionId, key) {
4269
+ return `/v1/developer/extensions/${extensionId}/secrets/${encodeURIComponent(key)}`;
1594
4270
  }
1595
4271
  function formatSecretList(secrets) {
1596
4272
  if (secrets.length === 0) return "No secrets configured.";
1597
4273
  return secrets.map((s) => ` ${s.key} (created ${s.created_at})`).join("\n");
1598
4274
  }
1599
4275
  function loadConfig() {
1600
- const configPath = path8.resolve("lumio.config.json");
1601
- if (!fs8.existsSync(configPath)) {
4276
+ const configPath = path16.resolve("lumio.config.json");
4277
+ if (!fs15.existsSync(configPath)) {
1602
4278
  console.error("lumio.config.json not found. Run `lumio init` first.");
1603
4279
  process.exit(1);
1604
4280
  }
1605
- return parseConfigFile(fs8.readFileSync(configPath, "utf-8"));
4281
+ return parseConfigFile(fs15.readFileSync(configPath, "utf-8"));
1606
4282
  }
1607
4283
  function requireClient() {
1608
4284
  const store = new CredentialStore();
@@ -1615,10 +4291,10 @@ function requireClient() {
1615
4291
  }
1616
4292
  function registerEnv(program2) {
1617
4293
  const envCmd = program2.command("env").description("Manage server-side secrets for the extension");
1618
- envCmd.command("set <key> [value]").description("Set a secret (POST /v1/developer/extensions/{id}/secrets). Omit <value> to be prompted (hidden) or read it from the LUMIO_SECRET env var \u2014 avoids leaking the secret into shell history / the process list.").action(async (key, value) => {
4294
+ envCmd.command("set <key> [value]").description("Set a secret (PUT /v1/developer/extensions/{id}/secrets/{key}). Omit <value> to be prompted (hidden) or read it from the LUMIO_SECRET env var \u2014 avoids leaking the secret into shell history / the process list.").action(async (key, value) => {
1619
4295
  if (!validateSecretKey(key)) {
1620
4296
  console.error(
1621
- "Invalid key format. Use uppercase letters, numbers, and underscores only (e.g. API_KEY)."
4297
+ `Secret key must match ^[A-Z0-9_]{1,${MAX_SECRET_KEY_LEN}}$ (uppercase letters, digits, underscores; e.g. API_KEY).`
1622
4298
  );
1623
4299
  process.exit(1);
1624
4300
  }
@@ -1633,9 +4309,13 @@ function registerEnv(program2) {
1633
4309
  }]);
1634
4310
  secretValue = answer.value;
1635
4311
  }
4312
+ if (!validateSecretValue(secretValue)) {
4313
+ console.error(`Secret value must be between 1 and ${MAX_SECRET_VALUE_BYTES} bytes.`);
4314
+ process.exit(1);
4315
+ }
1636
4316
  const config = loadConfig();
1637
4317
  const client = requireClient();
1638
- await client.post(`/v1/developer/extensions/${config.extensionId}/secrets`, { key, value: secretValue });
4318
+ await client.put(secretEndpoint(config.extensionId, key), { value: secretValue });
1639
4319
  console.log(`Secret ${key} set.`);
1640
4320
  });
1641
4321
  envCmd.command("list").description("List all secrets (keys only, no values)").action(async () => {
@@ -1649,14 +4329,14 @@ function registerEnv(program2) {
1649
4329
  envCmd.command("delete <key>").description("Delete a secret").action(async (key) => {
1650
4330
  const config = loadConfig();
1651
4331
  const client = requireClient();
1652
- await client.delete(`/v1/developer/extensions/${config.extensionId}/secrets/${encodeURIComponent(key)}`);
4332
+ await client.delete(secretEndpoint(config.extensionId, key));
1653
4333
  console.log(`Secret ${key} deleted.`);
1654
4334
  });
1655
4335
  }
1656
4336
 
1657
4337
  // src/commands/run.ts
1658
- import * as fs9 from "fs";
1659
- import * as path9 from "path";
4338
+ import * as fs16 from "fs";
4339
+ import * as path17 from "path";
1660
4340
  var VALID_FUNCTION_TYPES = [
1661
4341
  "query",
1662
4342
  "mutation",
@@ -1693,12 +4373,12 @@ function parseFunctionRef(ref) {
1693
4373
  function registerRun(program2) {
1694
4374
  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) => {
1695
4375
  const fnRef = parseFunctionRef(typeAndName);
1696
- const configPath = path9.resolve("lumio.config.json");
1697
- if (!fs9.existsSync(configPath)) {
4376
+ const configPath = path17.resolve("lumio.config.json");
4377
+ if (!fs16.existsSync(configPath)) {
1698
4378
  console.error("lumio.config.json not found. Run `lumio init` first.");
1699
4379
  process.exit(1);
1700
4380
  }
1701
- const config = parseConfigFile(fs9.readFileSync(configPath, "utf-8"));
4381
+ const config = parseConfigFile(fs16.readFileSync(configPath, "utf-8"));
1702
4382
  const store = new CredentialStore();
1703
4383
  const token = store.getToken();
1704
4384
  if (!token) {
@@ -1747,16 +4427,16 @@ function registerRun(program2) {
1747
4427
  }
1748
4428
 
1749
4429
  // src/commands/dashboard.ts
1750
- import * as fs10 from "fs";
1751
- import * as path10 from "path";
4430
+ import * as fs17 from "fs";
4431
+ import * as path18 from "path";
1752
4432
  function registerDashboard(program2) {
1753
4433
  program2.command("dashboard").description("Open the extension dashboard in browser").action(async () => {
1754
- const configPath = path10.resolve("lumio.config.json");
1755
- if (!fs10.existsSync(configPath)) {
4434
+ const configPath = path18.resolve("lumio.config.json");
4435
+ if (!fs17.existsSync(configPath)) {
1756
4436
  console.error("lumio.config.json not found. Run `lumio init` first.");
1757
4437
  process.exit(1);
1758
4438
  }
1759
- const config = parseConfigFile(fs10.readFileSync(configPath, "utf-8"));
4439
+ const config = parseConfigFile(fs17.readFileSync(configPath, "utf-8"));
1760
4440
  const baseUrl = process.env.LUMIO_WEB_URL ?? "https://lumio.vision";
1761
4441
  const url = `${baseUrl}/dashboard/extensions/${config.extensionId}`;
1762
4442
  console.log(`Opening ${url}...`);
@@ -1767,10 +4447,10 @@ function registerDashboard(program2) {
1767
4447
 
1768
4448
  // src/program.ts
1769
4449
  function createProgram() {
1770
- const program2 = new Command();
4450
+ const program2 = new Command2();
1771
4451
  program2.name("lumio");
1772
4452
  program2.description("Lumio Extension CLI -- Build, test, and deploy Lumio extensions");
1773
- const require2 = createRequire2(import.meta.url);
4453
+ const require2 = createRequire3(import.meta.url);
1774
4454
  const { version } = require2("../package.json");
1775
4455
  program2.version(version);
1776
4456
  registerLogin(program2);
@@ -1778,6 +4458,7 @@ function createProgram() {
1778
4458
  registerWhoami(program2);
1779
4459
  registerInit(program2);
1780
4460
  registerDev(program2);
4461
+ registerTrigger(program2);
1781
4462
  registerBuild(program2);
1782
4463
  registerDeploy(program2);
1783
4464
  registerStatus(program2);