@zaflun/lumio-cli 0.5.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,5 +1,6 @@
1
1
  // src/program.ts
2
- import { Command } from "commander";
2
+ import { Command as Command2 } from "commander";
3
+ import { createRequire as createRequire3 } from "module";
3
4
 
4
5
  // src/commands/login.ts
5
6
  import * as http from "http";
@@ -28,7 +29,7 @@ var CredentialStore = class {
28
29
  }
29
30
  saveToken(token, apiUrl) {
30
31
  const dir = path.dirname(this.credentialsPath);
31
- fs.mkdirSync(dir, { recursive: true });
32
+ fs.mkdirSync(dir, { recursive: true, mode: 448 });
32
33
  const credentials = {
33
34
  token,
34
35
  api_url: apiUrl ?? DEFAULT_API_URL
@@ -39,6 +40,26 @@ var CredentialStore = class {
39
40
  { mode: 384 }
40
41
  );
41
42
  }
43
+ /**
44
+ * Persist a full session from the browser PKCE flow — the short-lived JWT plus
45
+ * the long-lived refresh token and the JWT's expiry — so a later refresh can
46
+ * mint a fresh JWT without re-opening the browser.
47
+ */
48
+ saveSession(session) {
49
+ const dir = path.dirname(this.credentialsPath);
50
+ fs.mkdirSync(dir, { recursive: true, mode: 448 });
51
+ const credentials = {
52
+ token: session.token,
53
+ refresh_token: session.refreshToken,
54
+ expires_at: session.expiresAt,
55
+ api_url: session.apiUrl ?? DEFAULT_API_URL
56
+ };
57
+ fs.writeFileSync(
58
+ this.credentialsPath,
59
+ JSON.stringify(credentials, null, 2),
60
+ { mode: 384 }
61
+ );
62
+ }
42
63
  clear() {
43
64
  if (fs.existsSync(this.credentialsPath)) {
44
65
  fs.unlinkSync(this.credentialsPath);
@@ -58,48 +79,65 @@ var CredentialStore = class {
58
79
  };
59
80
 
60
81
  // src/commands/login.ts
82
+ var DEFAULT_API_URL2 = "https://api.lumio.vision";
83
+ var DEFAULT_ID_URL = "https://id.lumio.vision";
84
+ var DEFAULT_CALLBACK_PORT = 9876;
61
85
  function generatePkceChallenge() {
62
86
  const verifier = crypto.randomBytes(32).toString("base64url");
63
87
  const challenge = crypto.createHash("sha256").update(verifier).digest("base64url");
64
88
  return { verifier, challenge };
65
89
  }
66
90
  function registerLogin(program2) {
67
- program2.command("login").description("Authenticate with Lumio via browser (PKCE) or token").option("--token <token>", "Authenticate with an API token (for CI/CD)").option("--api-url <url>", "Custom API URL", "https://api.lumio.vision").action(async (options) => {
91
+ program2.command("login").description("Authenticate with Lumio via browser (PKCE) or an API token").option("--token <token>", "Authenticate with a Lumio API key (for CI/CD). For extension publishing, mint a developer key (lm_dev_*) under Account \u2192 Developer. Prefer the LUMIO_TOKEN env var to avoid leaking the token into shell history / the process list.").option("--api-url <url>", "Custom API URL", DEFAULT_API_URL2).option("--id-url <url>", "Custom ID (login) app URL", DEFAULT_ID_URL).option("--port <port>", "Local callback port for the browser PKCE flow", String(DEFAULT_CALLBACK_PORT)).action(async (options) => {
68
92
  const store = new CredentialStore();
69
- if (options.token) {
70
- store.saveToken(options.token, options.apiUrl);
93
+ const token = options.token ?? process.env.LUMIO_TOKEN;
94
+ if (token) {
95
+ store.saveToken(token, options.apiUrl);
71
96
  console.log("Authenticated with token.");
72
97
  return;
73
98
  }
74
99
  const { verifier, challenge } = generatePkceChallenge();
75
100
  const state = crypto.randomBytes(16).toString("hex");
76
- const port = 9876;
101
+ const port = Number.parseInt(options.port, 10);
102
+ if (!Number.isInteger(port) || port <= 0 || port > 65535) {
103
+ console.error(`Invalid --port: ${options.port}`);
104
+ process.exitCode = 1;
105
+ return;
106
+ }
77
107
  const redirectUri = `http://localhost:${port}/callback`;
78
- await new Promise((resolve10, reject) => {
108
+ await new Promise((resolve13, reject) => {
109
+ let timeout;
79
110
  const server = http.createServer(async (req, res) => {
111
+ const finish = () => {
112
+ clearTimeout(timeout);
113
+ server.close();
114
+ resolve13();
115
+ };
116
+ const fail = (err) => {
117
+ clearTimeout(timeout);
118
+ server.close();
119
+ reject(err);
120
+ };
80
121
  if (!req.url?.startsWith("/callback")) {
81
122
  res.writeHead(404);
82
123
  res.end();
83
124
  return;
84
125
  }
85
- const url = new URL(req.url, `http://localhost:${port}`);
126
+ const url = new URL(req.url, `http://127.0.0.1:${port}`);
86
127
  const code = url.searchParams.get("code");
87
128
  const returnedState = url.searchParams.get("state");
88
129
  if (returnedState !== state || !code) {
89
130
  res.writeHead(400, { "Content-Type": "text/html" });
90
131
  res.end("<html><body><h2>Authentication failed.</h2><p>Invalid state or missing code. You can close this window.</p></body></html>");
91
- server.close();
92
- reject(new Error("Authentication failed: invalid state or missing code"));
132
+ fail(new Error("Authentication failed: invalid state or missing code"));
93
133
  return;
94
134
  }
95
135
  try {
96
- const tokenRes = await fetch(`${options.apiUrl}/v1/auth/cli/token`, {
136
+ const tokenRes = await fetch(`${options.apiUrl}/v1/auth/token/exchange`, {
97
137
  method: "POST",
98
138
  headers: { "Content-Type": "application/json" },
99
139
  body: JSON.stringify({
100
- grant_type: "authorization_code",
101
140
  code,
102
- redirect_uri: redirectUri,
103
141
  code_verifier: verifier
104
142
  })
105
143
  });
@@ -107,28 +145,31 @@ function registerLogin(program2) {
107
145
  throw new Error(`Token exchange failed: ${tokenRes.status}`);
108
146
  }
109
147
  const tokenData = await tokenRes.json();
110
- store.saveToken(tokenData.access_token, options.apiUrl);
148
+ store.saveSession({
149
+ token: tokenData.data.token,
150
+ refreshToken: tokenData.data.refresh_token,
151
+ expiresAt: tokenData.data.expires_at,
152
+ apiUrl: options.apiUrl
153
+ });
111
154
  res.writeHead(200, { "Content-Type": "text/html" });
112
155
  res.end("<html><body><h2>Authenticated!</h2><p>You can close this window and return to the CLI.</p></body></html>");
113
156
  } catch (e) {
114
157
  res.writeHead(500, { "Content-Type": "text/html" });
115
158
  res.end("<html><body><h2>Authentication failed.</h2><p>Token exchange error. You can close this window.</p></body></html>");
116
- server.close();
117
- reject(e);
159
+ fail(e instanceof Error ? e : new Error(String(e)));
118
160
  return;
119
161
  }
120
- server.close();
121
- resolve10();
162
+ finish();
122
163
  });
123
- server.listen(port, async () => {
124
- const pkceUrl = `${options.apiUrl}/v1/auth/cli/authorize?code_challenge=${challenge}&code_challenge_method=S256&redirect_uri=${encodeURIComponent(redirectUri)}&state=${state}`;
164
+ server.listen(port, "127.0.0.1", async () => {
165
+ const loginUrl = `${options.idUrl}/login?client_type=cli&code_challenge=${challenge}&code_challenge_method=S256&redirect_uri=${encodeURIComponent(redirectUri)}&state=${state}`;
125
166
  console.log("Opening browser for authentication...");
126
167
  const { default: open } = await import("open");
127
- await open(pkceUrl);
168
+ await open(loginUrl);
128
169
  console.log("Waiting for authentication callback...");
129
- console.log("If the browser did not open, visit:", pkceUrl);
170
+ console.log("If the browser did not open, visit:", loginUrl);
130
171
  });
131
- setTimeout(() => {
172
+ timeout = setTimeout(() => {
132
173
  server.close();
133
174
  reject(new Error("Authentication timed out after 120 seconds"));
134
175
  }, 12e4);
@@ -151,6 +192,16 @@ function registerLogout(program2) {
151
192
  }
152
193
 
153
194
  // src/api/client.ts
195
+ var ApiError = class extends Error {
196
+ status;
197
+ body;
198
+ constructor(status, body, label = "API error") {
199
+ super(`${label} ${status}: ${body}`);
200
+ this.name = "ApiError";
201
+ this.status = status;
202
+ this.body = body;
203
+ }
204
+ };
154
205
  var LumioApiClient = class {
155
206
  baseUrl;
156
207
  token;
@@ -172,14 +223,18 @@ var LumioApiClient = class {
172
223
  "Authorization": `Bearer ${this.token}`
173
224
  };
174
225
  }
226
+ /** Throw an {@link ApiError} (with status) when the response is not ok. */
227
+ async ensureOk(response, label = "API error") {
228
+ if (!response.ok) {
229
+ throw new ApiError(response.status, await response.text(), label);
230
+ }
231
+ }
175
232
  async get(endpoint) {
176
233
  const response = await fetch(this.url(endpoint), {
177
234
  method: "GET",
178
235
  headers: this.authHeaders()
179
236
  });
180
- if (!response.ok) {
181
- throw new Error(`API error ${response.status}: ${await response.text()}`);
182
- }
237
+ await this.ensureOk(response);
183
238
  return response.json();
184
239
  }
185
240
  async post(endpoint, body) {
@@ -188,9 +243,7 @@ var LumioApiClient = class {
188
243
  headers: this.authHeaders(),
189
244
  body: body ? JSON.stringify(body) : void 0
190
245
  });
191
- if (!response.ok) {
192
- throw new Error(`API error ${response.status}: ${await response.text()}`);
193
- }
246
+ await this.ensureOk(response);
194
247
  return response.json();
195
248
  }
196
249
  async put(endpoint, body) {
@@ -199,9 +252,7 @@ var LumioApiClient = class {
199
252
  headers: this.authHeaders(),
200
253
  body: body ? JSON.stringify(body) : void 0
201
254
  });
202
- if (!response.ok) {
203
- throw new Error(`API error ${response.status}: ${await response.text()}`);
204
- }
255
+ await this.ensureOk(response);
205
256
  return response.json();
206
257
  }
207
258
  async delete(endpoint) {
@@ -209,9 +260,7 @@ var LumioApiClient = class {
209
260
  method: "DELETE",
210
261
  headers: this.authHeaders()
211
262
  });
212
- if (!response.ok) {
213
- throw new Error(`API error ${response.status}: ${await response.text()}`);
214
- }
263
+ await this.ensureOk(response);
215
264
  return response.json();
216
265
  }
217
266
  async uploadFile(endpoint, formData) {
@@ -220,9 +269,7 @@ var LumioApiClient = class {
220
269
  headers: this.uploadHeaders(),
221
270
  body: formData
222
271
  });
223
- if (!response.ok) {
224
- throw new Error(`Upload error ${response.status}: ${await response.text()}`);
225
- }
272
+ await this.ensureOk(response, "Upload error");
226
273
  return response.json();
227
274
  }
228
275
  static isCI() {
@@ -255,6 +302,22 @@ function registerWhoami(program2) {
255
302
  // src/commands/init.ts
256
303
  import * as fs2 from "fs";
257
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
+ `;
258
321
  function registerInit(program2) {
259
322
  program2.command("init").argument("[name]", "Extension project name").description("Create a new extension project with interactive wizard").action(async (name) => {
260
323
  const store = new CredentialStore();
@@ -298,13 +361,19 @@ function registerInit(program2) {
298
361
  choices: ["widget", "extension", "overlay", "bot_module", "integration", "theme", "automation_node"]
299
362
  }
300
363
  ]);
301
- const answers = { ...baseAnswers, targets: [], server: false };
364
+ const answers = {
365
+ name: baseAnswers.name,
366
+ description: baseAnswers.description,
367
+ category: baseAnswers.category,
368
+ targets: [],
369
+ server: false
370
+ };
302
371
  if (answers.category === "bot_module") {
303
372
  const { platforms } = await inquirer.prompt([{
304
373
  type: "checkbox",
305
374
  name: "platforms",
306
375
  message: "Which platforms should this bot module support?",
307
- choices: ["twitch", "youtube", "kick", "trovo", "discord"],
376
+ choices: ["twitch", "youtube", "kick", "discord"],
308
377
  default: ["twitch"],
309
378
  validate: (input) => input.length > 0 || "Select at least one platform"
310
379
  }]);
@@ -350,7 +419,7 @@ function registerInit(program2) {
350
419
  type: "checkbox",
351
420
  name: "targets",
352
421
  message: "Targets:",
353
- choices: ["editor", "layer", "interactive"],
422
+ choices: ["editor", "layer"],
354
423
  default: ["editor", "layer"]
355
424
  },
356
425
  { type: "confirm", name: "server", message: "Server functions?", default: false }
@@ -412,7 +481,7 @@ function registerInit(program2) {
412
481
  } catch (err) {
413
482
  if (err.status === 404) {
414
483
  console.error(
415
- "\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"
416
485
  );
417
486
  process.exit(1);
418
487
  }
@@ -526,6 +595,8 @@ function registerInit(program2) {
526
595
  },
527
596
  devDependencies: {
528
597
  "typescript": "^6.0.3",
598
+ // `lumio dev` runs the extension on the devkit's mock host.
599
+ ...!isAutomationNode ? { "@zaflun/lumio-devkit": "latest" } : {},
529
600
  ...needsReact ? { "@types/react": "^19.1.0" } : {},
530
601
  ...useTailwind ? { "tailwindcss": "^4.3.0" } : {}
531
602
  }
@@ -654,10 +725,8 @@ Lumio.render(<App />, { target: "${target}" });
654
725
  }
655
726
  if (answers.server) {
656
727
  fs2.mkdirSync(path2.join(projectDir, "server"), { recursive: true });
657
- const schema = 'import { defineSchema, defineTable, v } from "@zaflun/lumio-sdk/server";\n\nexport default defineSchema({\n items: defineTable({\n text: v.string(),\n completed: v.boolean(),\n }).storage("instance"),\n});\n';
658
- fs2.writeFileSync(path2.join(projectDir, "server", "schema.ts"), schema);
659
- const functions = 'import { queryRows, insertRow, patchRow, deleteRow, arg } from "@zaflun/lumio-sdk/server";\n\nexport const getItems = queryRows("items");\nexport const addItem = insertRow("items", { text: arg("text"), completed: false }, { editorOnly: true });\nexport const toggleItem = patchRow("items", arg("id"), { completed: arg("completed") }, { editorOnly: true });\nexport const removeItem = deleteRow("items", arg("id"), { editorOnly: true });\n';
660
- fs2.writeFileSync(path2.join(projectDir, "server", "functions.ts"), functions);
728
+ fs2.writeFileSync(path2.join(projectDir, "server", "schema.ts"), SERVER_SCHEMA_SCAFFOLD);
729
+ fs2.writeFileSync(path2.join(projectDir, "server", "functions.ts"), SERVER_FUNCTIONS_SCAFFOLD);
661
730
  }
662
731
  }
663
732
  console.log(`
@@ -667,16 +736,20 @@ Done! Created ${projectDir}/`);
667
736
  }
668
737
 
669
738
  // src/commands/dev.ts
670
- import * as fs3 from "fs";
671
- 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";
672
743
 
673
744
  // src/config/parser.ts
745
+ import { NODE_TYPES, TRIGGER_MODES, VALID_PLATFORMS } from "@zaflun/lumio-extension-types";
674
746
  var ConfigError = class extends Error {
675
- constructor(message) {
676
- super(message);
747
+ constructor(message2) {
748
+ super(message2);
677
749
  this.name = "ConfigError";
678
750
  }
679
751
  };
752
+ var VALID_TARGETS = ["editor", "layer", "designer"];
680
753
  function parseConfigFile(jsonString) {
681
754
  const raw = JSON.parse(jsonString);
682
755
  return {
@@ -713,25 +786,35 @@ function validateConfig(config) {
713
786
  if (!config.extensionId) {
714
787
  throw new ConfigError("extensionId is required. Run `lumio init` or create an extension at the dashboard.");
715
788
  }
789
+ if (!/^[A-Za-z0-9_-]{1,64}$/.test(config.extensionId)) {
790
+ throw new ConfigError("extensionId is invalid: only letters, digits, '-' and '_' (max 64 chars) are allowed.");
791
+ }
716
792
  if (!config.name) {
717
793
  throw new ConfigError("name is required in lumio.config.json");
718
794
  }
719
795
  if (config.category === "bot_module") {
720
796
  if (!config.platforms || config.platforms.length === 0) {
721
- throw new ConfigError("At least one platform is required for bot_module (twitch, youtube, kick, trovo, discord)");
797
+ throw new ConfigError(`At least one platform is required for bot_module (${VALID_PLATFORMS.join(", ")})`);
798
+ }
799
+ const invalid = config.platforms.filter((p) => !VALID_PLATFORMS.includes(p));
800
+ if (invalid.length > 0) {
801
+ throw new ConfigError(`Invalid platform(s): ${invalid.join(", ")}. Must be one of: ${VALID_PLATFORMS.join(", ")}`);
722
802
  }
723
803
  } else if (config.category === "automation_node") {
724
804
  if (!config.nodeType) {
725
- throw new ConfigError("nodeType is required for automation_node extensions (trigger, action, or logic)");
805
+ throw new ConfigError(`nodeType is required for automation_node extensions (${NODE_TYPES.join(", ")})`);
726
806
  }
727
- if (!["trigger", "action", "logic"].includes(config.nodeType)) {
728
- throw new ConfigError("nodeType must be one of: trigger, action, logic");
807
+ if (!NODE_TYPES.includes(config.nodeType)) {
808
+ throw new ConfigError(`nodeType must be one of: ${NODE_TYPES.join(", ")}`);
729
809
  }
730
810
  if (!config.node) {
731
811
  throw new ConfigError("node configuration is required for automation_node extensions");
732
812
  }
733
813
  if (config.nodeType === "trigger" && !config.node.triggerMode) {
734
- throw new ConfigError("node.triggerMode is required for trigger nodes (webhook, polling, or both)");
814
+ throw new ConfigError(`node.triggerMode is required for trigger nodes (${TRIGGER_MODES.join(", ")})`);
815
+ }
816
+ if (config.node.triggerMode && !TRIGGER_MODES.includes(config.node.triggerMode)) {
817
+ throw new ConfigError(`node.triggerMode must be one of: ${TRIGGER_MODES.join(", ")}`);
735
818
  }
736
819
  if (config.nodeType !== "trigger" && config.node.triggerMode) {
737
820
  throw new ConfigError("node.triggerMode is only valid for trigger nodes");
@@ -742,111 +825,45 @@ function validateConfig(config) {
742
825
  }
743
826
  }
744
827
  } else if (!config.targets || config.targets.length === 0) {
745
- 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
+ );
746
835
  }
747
836
  if (!config.version) {
748
837
  throw new ConfigError("version is required in lumio.config.json");
749
838
  }
750
839
  }
751
840
 
752
- // src/commands/dev-bot-module.ts
841
+ // src/commands/dev-automation-node.ts
753
842
  import * as readline from "readline";
754
843
  import chalk from "chalk";
755
- function parseUserPrefix(input) {
756
- const defaultUser = {
757
- id: "user-001",
758
- name: "dev_user",
759
- displayName: "Dev User",
760
- platform: "twitch",
761
- role: "everyone",
762
- isMod: false,
763
- isVip: false,
764
- isSub: false
765
- };
766
- if (!input.startsWith("@")) {
767
- return { user: defaultUser, message: input };
768
- }
769
- const spaceIdx = input.indexOf(" ");
770
- if (spaceIdx === -1) return { user: defaultUser, message: input };
771
- const prefix = input.substring(1, spaceIdx);
772
- const message = input.substring(spaceIdx + 1);
773
- const parts = prefix.split(":");
774
- const name = parts[parts.length - 1];
775
- const rolePrefix = parts.length > 1 ? parts[0] : "everyone";
776
- return {
777
- user: {
778
- id: `user-${name}`,
779
- name,
780
- displayName: name,
781
- platform: "twitch",
782
- role: rolePrefix,
783
- isMod: rolePrefix === "mod",
784
- isVip: rolePrefix === "vip",
785
- isSub: ["sub", "vip", "mod", "broadcaster"].includes(rolePrefix)
786
- },
787
- message
788
- };
789
- }
790
- async function runBotModuleDevServer(config) {
791
- console.log(chalk.bold("\n\u{1F916} Bot Module Dev Server"));
792
- console.log(`Extension: ${config.name}`);
793
- console.log(`Platforms: ${config.platforms?.join(", ") ?? "none"}`);
794
- console.log(chalk.dim("\nType chat messages. Prefix with @username or @role:username."));
795
- console.log(chalk.dim("Examples: !hello | @sub:viewer !points 100 | @mod:admin gg"));
796
- console.log(chalk.dim("Type !quit to exit.\n"));
797
- const rl = readline.createInterface({
798
- input: process.stdin,
799
- output: process.stdout,
800
- prompt: chalk.cyan("> ")
801
- });
802
- rl.prompt();
803
- rl.on("line", async (line) => {
804
- const trimmed = line.trim();
805
- if (!trimmed) {
806
- rl.prompt();
807
- return;
808
- }
809
- if (trimmed === "!quit") {
810
- rl.close();
811
- return;
812
- }
813
- const { user, message } = parseUserPrefix(trimmed);
814
- console.log(chalk.dim(` [${user.role}:${user.name}] ${message}`));
815
- console.log(chalk.yellow(" \u2192 Handler execution not yet wired (requires esbuild + V8)"));
816
- rl.prompt();
817
- });
818
- rl.on("close", () => {
819
- console.log(chalk.dim("\nBot Module Dev Server stopped."));
820
- process.exit(0);
821
- });
822
- }
823
-
824
- // src/commands/dev-automation-node.ts
825
- import * as readline2 from "readline";
826
- import chalk2 from "chalk";
827
844
  async function runAutomationNodeDevServer(config) {
828
845
  const nodeType = config.nodeType ?? "action";
829
- console.log(chalk2.bold("\n\u2699\uFE0F Automation Node Dev Server"));
846
+ console.log(chalk.bold("\n\u2699\uFE0F Automation Node Dev Server"));
830
847
  console.log(`Extension: ${config.name}`);
831
848
  console.log(`Node type: ${nodeType}`);
832
849
  if (nodeType === "trigger" && config.node?.triggerMode) {
833
850
  console.log(`Trigger mode: ${config.node.triggerMode}`);
834
851
  }
835
- console.log(chalk2.dim("\nCommands:"));
836
- 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"));
837
854
  if (nodeType === "trigger") {
838
- console.log(chalk2.dim(" /webhook [json] \u2014 Simulate a webhook POST payload"));
839
- console.log(chalk2.dim(" /poll \u2014 Trigger a poll cycle"));
855
+ console.log(chalk.dim(" /webhook [json] \u2014 Simulate a webhook POST payload"));
856
+ console.log(chalk.dim(" /poll \u2014 Trigger a poll cycle"));
840
857
  }
841
- console.log(chalk2.dim(" /config [json] \u2014 Set node instance config"));
842
- console.log(chalk2.dim(" /install-config [j] \u2014 Set extension install config"));
843
- console.log(chalk2.dim(" /quit \u2014 Exit\n"));
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"));
844
861
  let nodeConfig = {};
845
862
  let installConfig = {};
846
- const rl = readline2.createInterface({
863
+ const rl = readline.createInterface({
847
864
  input: process.stdin,
848
865
  output: process.stdout,
849
- prompt: chalk2.cyan("> ")
866
+ prompt: chalk.cyan("> ")
850
867
  });
851
868
  rl.prompt();
852
869
  rl.on("line", async (line) => {
@@ -862,9 +879,9 @@ async function runAutomationNodeDevServer(config) {
862
879
  if (trimmed.startsWith("/config ")) {
863
880
  try {
864
881
  nodeConfig = JSON.parse(trimmed.slice("/config ".length));
865
- console.log(chalk2.green(" Node config updated."));
882
+ console.log(chalk.green(" Node config updated."));
866
883
  } catch {
867
- console.log(chalk2.red(" Invalid JSON."));
884
+ console.log(chalk.red(" Invalid JSON."));
868
885
  }
869
886
  rl.prompt();
870
887
  return;
@@ -872,9 +889,9 @@ async function runAutomationNodeDevServer(config) {
872
889
  if (trimmed.startsWith("/install-config ")) {
873
890
  try {
874
891
  installConfig = JSON.parse(trimmed.slice("/install-config ".length));
875
- console.log(chalk2.green(" Install config updated."));
892
+ console.log(chalk.green(" Install config updated."));
876
893
  } catch {
877
- console.log(chalk2.red(" Invalid JSON."));
894
+ console.log(chalk.red(" Invalid JSON."));
878
895
  }
879
896
  rl.prompt();
880
897
  return;
@@ -886,15 +903,15 @@ async function runAutomationNodeDevServer(config) {
886
903
  try {
887
904
  inputData = JSON.parse(jsonStr);
888
905
  } catch {
889
- console.log(chalk2.red(" Invalid JSON."));
906
+ console.log(chalk.red(" Invalid JSON."));
890
907
  rl.prompt();
891
908
  return;
892
909
  }
893
910
  }
894
- console.log(chalk2.dim(` [${nodeType}] Executing with input: ${JSON.stringify(inputData)}`));
895
- console.log(chalk2.dim(` [${nodeType}] Node config: ${JSON.stringify(nodeConfig)}`));
896
- console.log(chalk2.dim(` [${nodeType}] Install config: ${JSON.stringify(installConfig)}`));
897
- console.log(chalk2.yellow(" \u2192 Handler execution not yet wired (requires esbuild + V8)"));
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)"));
898
915
  rl.prompt();
899
916
  return;
900
917
  }
@@ -905,242 +922,2977 @@ async function runAutomationNodeDevServer(config) {
905
922
  try {
906
923
  body = JSON.parse(jsonStr);
907
924
  } catch {
908
- console.log(chalk2.red(" Invalid JSON."));
925
+ console.log(chalk.red(" Invalid JSON."));
909
926
  rl.prompt();
910
927
  return;
911
928
  }
912
929
  }
913
- console.log(chalk2.dim(` [trigger:webhook] Received payload: ${JSON.stringify(body)}`));
914
- console.log(chalk2.yellow(" \u2192 Handler execution not yet wired (requires esbuild + V8)"));
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)"));
915
932
  rl.prompt();
916
933
  return;
917
934
  }
918
935
  if (nodeType === "trigger" && trimmed === "/poll") {
919
- console.log(chalk2.dim(" [trigger:poll] Polling..."));
920
- console.log(chalk2.yellow(" \u2192 Handler execution not yet wired (requires esbuild + V8)"));
936
+ console.log(chalk.dim(" [trigger:poll] Polling..."));
937
+ console.log(chalk.yellow(" \u2192 Handler execution not yet wired (requires esbuild + V8)"));
921
938
  rl.prompt();
922
939
  return;
923
940
  }
924
- 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."));
925
942
  rl.prompt();
926
943
  });
927
944
  rl.on("close", () => {
928
- console.log(chalk2.dim("\nAutomation Node Dev Server stopped."));
945
+ console.log(chalk.dim("\nAutomation Node Dev Server stopped."));
929
946
  process.exit(0);
930
947
  });
931
948
  }
932
949
 
933
- // src/commands/dev.ts
934
- function registerDev(program2) {
935
- program2.command("dev").description("Start local dev server with hot-reload and mock events").option("-p, --port <port>", "Dev server port", "3100").option("--bot-module", "Start interactive bot module chat simulator").option("--automation-node", "Start automation node dev simulator").action(async (options) => {
936
- const configPath = path3.resolve("lumio.config.json");
937
- if (options.botModule || options.automationNode || fs3.existsSync(configPath)) {
938
- const configExists = fs3.existsSync(configPath);
939
- if (configExists) {
940
- const config = parseConfigFile(fs3.readFileSync(configPath, "utf-8"));
941
- if (options.botModule || config.category === "bot_module") {
942
- await runBotModuleDevServer(config);
943
- return;
944
- }
945
- if (options.automationNode || config.category === "automation_node") {
946
- await runAutomationNodeDevServer(config);
947
- return;
948
- }
949
- } else if (options.botModule || options.automationNode) {
950
- console.error("lumio.config.json not found. Run `lumio init` first.");
951
- process.exit(1);
952
- }
953
- }
954
- console.log(`Starting Lumio dev server on port ${options.port}...`);
955
- console.log("Mock events enabled. Hot-reload active.");
956
- console.log("Open http://localhost:" + options.port + " in your browser.");
957
- console.log("\nDev server implementation requires @zaflun/lumio-sdk and Vite.");
958
- console.log("Full implementation coming in Phase 12 (Live-Code-Editor).");
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"
959
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;
960
1019
  }
961
1020
 
962
- // src/commands/build.ts
1021
+ // src/commands/functions-manifest.ts
963
1022
  import * as fs4 from "fs";
964
1023
  import * as path4 from "path";
965
- import { execFile } from "child_process";
966
- import { promisify } from "util";
967
- var execFileAsync = promisify(execFile);
968
- function registerBuild(program2) {
969
- program2.command("build").description("Bundle extension as standalone HTML + source archive").action(async () => {
970
- const configPath = path4.resolve("lumio.config.json");
971
- if (!fs4.existsSync(configPath)) {
972
- console.error("lumio.config.json not found. Run `lumio init` first.");
973
- 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
+ );
974
1079
  }
975
- const configJson = fs4.readFileSync(configPath, "utf-8");
976
- const config = parseConfigFile(configJson);
977
- validateConfig(config);
978
- const changelogPath = path4.resolve("CHANGELOG.md");
979
- if (!fs4.existsSync(changelogPath)) {
980
- 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
+ );
981
1084
  }
982
- console.log(`Building ${config.name} v${config.version}...`);
983
- const distDir = path4.resolve("dist");
984
- fs4.mkdirSync(distDir, { recursive: true });
985
- const { build } = await import("esbuild");
986
- if (config.category === "bot_module" && config.targets.length === 0) {
987
- console.log(" Bot module \u2014 no UI targets to bundle.");
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
+ );
988
1091
  }
989
- const cssEntryFile = ["src/globals.css", "src/style.css", "src/index.css"].find((f) => fs4.existsSync(path4.resolve(f)));
990
- let compiledCssPath = null;
991
- if (cssEntryFile) {
992
- const cssContent = fs4.readFileSync(path4.resolve(cssEntryFile), "utf-8");
993
- const usesTailwind = cssContent.includes("tailwindcss") || cssContent.includes("@tailwind");
994
- if (usesTailwind) {
995
- console.log(" Tailwind CSS v4 detected \u2014 compiling...");
996
- compiledCssPath = path4.join(distDir, "styles.css");
997
- try {
998
- await execFileAsync("npx", [
999
- "@tailwindcss/cli",
1000
- "-i",
1001
- path4.resolve(cssEntryFile),
1002
- "-o",
1003
- compiledCssPath,
1004
- "--minify"
1005
- ], { cwd: path4.resolve(".") });
1006
- console.log(" Compiled: dist/styles.css");
1007
- } catch {
1008
- console.warn(" Warning: Tailwind CSS compilation failed. Install tailwindcss v4: npm add -D tailwindcss@^4.3");
1009
- compiledCssPath = null;
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.`);
1010
1114
  }
1011
- } else {
1012
- compiledCssPath = path4.join(distDir, "styles.css");
1013
- fs4.copyFileSync(path4.resolve(cssEntryFile), compiledCssPath);
1014
- console.log(` Copied: ${cssEntryFile} \u2192 dist/styles.css`);
1115
+ entry.orderBy = def.orderBy;
1116
+ }
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".`);
1120
+ }
1121
+ entry.orderDir = def.orderDir;
1015
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;
1016
1126
  }
1017
- for (const target of config.targets) {
1018
- const entryPoint = path4.resolve("src", `${target}.tsx`);
1019
- if (!fs4.existsSync(entryPoint)) {
1020
- console.warn(`Warning: Entry point ${entryPoint} not found, skipping target "${target}".`);
1021
- 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.`);
1022
1163
  }
1023
- await build({
1024
- entryPoints: [entryPoint],
1025
- bundle: true,
1026
- minify: true,
1027
- format: "iife",
1028
- globalName: "LumioExtension",
1029
- footer: {
1030
- js: "if(typeof LumioExtension!=='undefined'&&typeof LumioExtension.default==='function'){var _L=(typeof require!=='undefined'?require('@zaflun/lumio-sdk'):{}).Lumio;if(_L)_L.render(LumioExtension.default);}"
1031
- },
1032
- outdir: distDir,
1033
- entryNames: `${target}`,
1034
- jsx: "automatic",
1035
- target: "es2022",
1036
- loader: {
1037
- ".css": "css",
1038
- // Asset file loaders — copies to dist/assets/ with content hash
1039
- ".png": "file",
1040
- ".jpg": "file",
1041
- ".jpeg": "file",
1042
- ".gif": "file",
1043
- ".svg": "file",
1044
- ".webp": "file",
1045
- ".avif": "file",
1046
- ".ico": "file",
1047
- ".mp4": "file",
1048
- ".webm": "file",
1049
- ".ogg": "file",
1050
- ".mp3": "file",
1051
- ".wav": "file",
1052
- ".woff": "file",
1053
- ".woff2": "file",
1054
- ".ttf": "file",
1055
- ".otf": "file",
1056
- ".lottie": "file"
1057
- },
1058
- assetNames: "assets/[name]-[hash]",
1059
- external: ["react", "react-dom", "react/jsx-runtime", "react/jsx-dev-runtime", "@zaflun/lumio-sdk", "@zaflun/lumio-protocol"],
1060
- define: {
1061
- "process.env.NODE_ENV": '"production"'
1062
- }
1063
- });
1064
- console.log(` Built: dist/${target}.js`);
1164
+ const flag = editorOnly(value.editorOnly, where);
1165
+ return { kind: "handler", category: type, editorOnly: flag };
1065
1166
  }
1066
- const { default: archiver } = await import("archiver");
1067
- const sourceArchivePath = path4.join(distDir, "source.tar.gz");
1068
- const output = fs4.createWriteStream(sourceArchivePath);
1069
- const archive = archiver("tar", { gzip: true });
1070
- archive.pipe(output);
1071
- archive.glob("**/*", {
1072
- cwd: path4.resolve("."),
1073
- ignore: [
1074
- "node_modules/**",
1075
- "dist/**",
1076
- ".git/**",
1077
- ".next/**",
1078
- ".lumio-dist/**",
1079
- "*.tgz",
1080
- "*.tar.gz"
1081
- ],
1082
- dot: true
1083
- });
1084
- await archive.finalize();
1085
- console.log(" Archive: dist/source.tar.gz");
1086
- 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}")`) };
1087
1231
  });
1088
1232
  }
1089
-
1090
- // src/commands/deploy.ts
1091
- import * as fs5 from "fs";
1092
- import * as path5 from "path";
1093
-
1094
- // src/changelog/parser.ts
1095
- var VERSION_HEADER_REGEX = /^## \[([^\]]+)\](?:\s*-\s*(\d{4}-\d{2}-\d{2}))?/;
1096
- function parseChangelog(markdown) {
1097
- const lines = markdown.split("\n");
1098
- const entries = [];
1099
- let currentEntry = null;
1100
- const contentLines = [];
1101
- for (const line of lines) {
1102
- const match = line.match(VERSION_HEADER_REGEX);
1103
- if (match) {
1104
- if (currentEntry) {
1105
- currentEntry.content = contentLines.join("\n").trim();
1106
- entries.push(currentEntry);
1107
- contentLines.length = 0;
1108
- }
1109
- currentEntry = {
1110
- version: match[1],
1111
- date: match[2] ?? null,
1112
- content: ""
1113
- };
1114
- } else if (currentEntry) {
1115
- 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.`);
1116
1245
  }
1246
+ return { __expr: "arg", name: value.name };
1117
1247
  }
1118
- if (currentEntry) {
1119
- currentEntry.content = contentLines.join("\n").trim();
1120
- 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
+ );
1121
1253
  }
1122
- return entries;
1254
+ throw new FunctionsManifestError(`${where}: ${JSON.stringify(expr)} expression is not allowed here.`);
1123
1255
  }
1124
- function getVersionEntry(changelog, version) {
1125
- const entries = parseChangelog(changelog);
1126
- 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;
1127
1292
  }
1128
1293
 
1129
- // src/commands/deploy.ts
1130
- function collectFiles(dir, base = dir) {
1131
- const entries = fs5.readdirSync(dir, { withFileTypes: true });
1132
- const files = [];
1133
- for (const entry of entries) {
1134
- const fullPath = path5.join(dir, entry.name);
1135
- if (entry.isDirectory()) {
1136
- files.push(...collectFiles(fullPath, base));
1137
- } else if (entry.isFile()) {
1138
- files.push(path5.relative(base, fullPath));
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"
1139
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 };
1140
1376
  }
1141
- 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 };
1142
1389
  }
1143
- var ALLOWED_EXTENSIONS = /* @__PURE__ */ new Set([
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([
1144
3896
  "js",
1145
3897
  "css",
1146
3898
  "html",
@@ -1175,22 +3927,22 @@ var MAX_FILE_SIZE = 10 * 1024 * 1024;
1175
3927
  var MAX_TOTAL_SIZE = 50 * 1024 * 1024;
1176
3928
  var MAX_FILES = 200;
1177
3929
  function registerDeploy(program2) {
1178
- program2.command("deploy").description("Upload extension to Lumio (status: draft)").option("-v, --version <version>", "Version override").option("-d, --description <text>", "Release notes override").option("--breaking", "Mark as breaking change").option("--draft", "Explicitly mark as draft").option("--token <token>", "API token (for CI, skips login)").option("--system", "Deploy as system extension (no developer profile required)").action(async (options) => {
3930
+ program2.command("deploy").description("Upload extension to Lumio (status: draft)").option("-v, --version <version>", "Version override").option("-d, --description <text>", "Release notes override").option("--breaking", "Mark as breaking change").option("--draft", "Explicitly mark as draft").option("--token <token>", "API token (for CI, skips login). Use a developer key (lm_dev_*) minted under Account \u2192 Developer for publishing.").option("--system", "Deploy as system extension (no developer profile required)").action(async (options) => {
1179
3931
  const store = new CredentialStore();
1180
3932
  const token = options.token ?? store.getToken();
1181
3933
  if (!token) {
1182
3934
  console.error("Not logged in. Run `lumio login` or provide --token.");
1183
3935
  process.exit(1);
1184
3936
  }
1185
- const configPath = path5.resolve("lumio.config.json");
1186
- if (!fs5.existsSync(configPath)) {
3937
+ const configPath = path13.resolve("lumio.config.json");
3938
+ if (!fs12.existsSync(configPath)) {
1187
3939
  console.error("lumio.config.json not found. Run `lumio init` first.");
1188
3940
  process.exit(1);
1189
3941
  }
1190
- const configJson = fs5.readFileSync(configPath, "utf-8");
3942
+ const configJson = fs12.readFileSync(configPath, "utf-8");
1191
3943
  const config = parseConfigFile(configJson);
1192
3944
  validateConfig(config);
1193
- const distDir = path5.resolve("dist");
3945
+ const distDir = path13.resolve("dist");
1194
3946
  if (config.sounds && config.sounds.length > 0) {
1195
3947
  for (const sound of config.sounds) {
1196
3948
  const soundFile = sound.file;
@@ -1201,15 +3953,15 @@ function registerDeploy(program2) {
1201
3953
  );
1202
3954
  process.exit(1);
1203
3955
  }
1204
- const soundAbsPath = path5.join(distDir, "sounds", soundFile);
1205
- if (fs5.existsSync(distDir) && !fs5.existsSync(soundAbsPath)) {
3956
+ const soundAbsPath = path13.join(distDir, "sounds", soundFile);
3957
+ if (fs12.existsSync(distDir) && !fs12.existsSync(soundAbsPath)) {
1206
3958
  console.error(
1207
3959
  `Error: Sound file "${soundFile}" declared in lumio.config.json was not found at dist/sounds/${soundFile}. Run \`lumio build\` first.`
1208
3960
  );
1209
3961
  process.exit(1);
1210
3962
  }
1211
- if (fs5.existsSync(soundAbsPath)) {
1212
- const stat = fs5.statSync(soundAbsPath);
3963
+ if (fs12.existsSync(soundAbsPath)) {
3964
+ const stat = fs12.statSync(soundAbsPath);
1213
3965
  if (stat.size > LARGE_SOUND_THRESHOLD) {
1214
3966
  console.warn(
1215
3967
  ` Warning: Sound file "${soundFile}" is ${(stat.size / 1024 / 1024).toFixed(1)} MB. Consider compressing large audio files to reduce load times.`
@@ -1218,12 +3970,70 @@ function registerDeploy(program2) {
1218
3970
  }
1219
3971
  }
1220
3972
  }
3973
+ if (config.sounds && config.sounds.length > 0) {
3974
+ const limitsApiUrl = process.env.LUMIO_API_URL ?? store.getApiUrl();
3975
+ try {
3976
+ const limitsRes = await fetch(
3977
+ `${limitsApiUrl}/v1/developer/extensions/${config.extensionId}/limits`,
3978
+ { headers: { Authorization: `Bearer ${token}` } }
3979
+ );
3980
+ if (limitsRes.ok) {
3981
+ const limitsJson = await limitsRes.json();
3982
+ const maxSounds = limitsJson.data?.max_sounds ?? 50;
3983
+ const maxFileSize = limitsJson.data?.max_sound_file_size ?? 10485760;
3984
+ const maxStorage = limitsJson.data?.max_sound_storage_bytes ?? 209715200;
3985
+ if (config.sounds.length > maxSounds) {
3986
+ console.error(
3987
+ `Error: Extension declares ${config.sounds.length} sounds, but your limit is ${maxSounds}.`
3988
+ );
3989
+ console.error(
3990
+ "Request a limit increase at /dashboard/settings/developer/limits or contact support."
3991
+ );
3992
+ process.exit(1);
3993
+ }
3994
+ let totalSize2 = 0;
3995
+ for (const sound of config.sounds) {
3996
+ const soundPath = path13.resolve("dist", "sounds", sound.file);
3997
+ if (fs12.existsSync(soundPath)) {
3998
+ const size = fs12.statSync(soundPath).size;
3999
+ totalSize2 += size;
4000
+ if (size > maxFileSize) {
4001
+ console.error(
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.`
4003
+ );
4004
+ console.error(
4005
+ "Request a limit increase at /dashboard/settings/developer/limits or contact support."
4006
+ );
4007
+ process.exit(1);
4008
+ }
4009
+ }
4010
+ }
4011
+ if (totalSize2 > maxStorage) {
4012
+ console.error(
4013
+ `Error: Total sound size is ${(totalSize2 / 1024 / 1024).toFixed(1)} MB but your storage limit is ${(maxStorage / 1024 / 1024).toFixed(1)} MB.`
4014
+ );
4015
+ console.error(
4016
+ "Request a limit increase at /dashboard/settings/developer/limits or contact support."
4017
+ );
4018
+ process.exit(1);
4019
+ }
4020
+ } else if (limitsRes.status !== 404) {
4021
+ console.warn(
4022
+ ` Warning: Could not fetch sound limits (HTTP ${limitsRes.status}), skipping pre-flight check.`
4023
+ );
4024
+ }
4025
+ } catch {
4026
+ console.warn(
4027
+ " Warning: Could not reach API for sound limit check, skipping pre-flight check."
4028
+ );
4029
+ }
4030
+ }
1221
4031
  const version = options.version ?? config.version;
1222
4032
  let releaseNotes = options.description ?? "";
1223
4033
  if (!releaseNotes) {
1224
- const changelogPath = path5.resolve("CHANGELOG.md");
1225
- if (fs5.existsSync(changelogPath)) {
1226
- 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");
1227
4037
  const entry = getVersionEntry(changelog, version);
1228
4038
  if (entry) {
1229
4039
  releaseNotes = entry.content;
@@ -1234,10 +4044,23 @@ function registerDeploy(program2) {
1234
4044
  }
1235
4045
  }
1236
4046
  }
1237
- 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)) {
1238
4055
  console.error("No dist/ directory found. Run `lumio build` first.");
1239
4056
  process.exit(1);
1240
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
+ }
1241
4064
  console.log(`Deploying ${config.name} v${version}...`);
1242
4065
  const apiUrl = process.env.LUMIO_API_URL ?? store.getApiUrl();
1243
4066
  const client = new LumioApiClient(apiUrl, token);
@@ -1257,8 +4080,8 @@ function registerDeploy(program2) {
1257
4080
  console.error(`Too many files (${bundleFiles.length}). Maximum is ${MAX_FILES}.`);
1258
4081
  process.exit(1);
1259
4082
  }
1260
- const sourceArchive = path5.join(distDir, sourceFile);
1261
- if (!fs5.existsSync(sourceArchive)) {
4083
+ const sourceArchive = path13.join(distDir, sourceFile);
4084
+ if (!fs12.existsSync(sourceArchive)) {
1262
4085
  console.error("Error: source.tar.gz not found in dist/. Run `lumio build` first.");
1263
4086
  process.exit(1);
1264
4087
  }
@@ -1269,7 +4092,11 @@ function registerDeploy(program2) {
1269
4092
  formData.append("manifest", JSON.stringify(manifest));
1270
4093
  formData.append("release_notes", releaseNotes);
1271
4094
  formData.append("is_breaking", String(!!options.breaking));
1272
- 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);
1273
4100
  if (sourceBuffer.length === 0) {
1274
4101
  console.error("Error: source.tar.gz is empty. Run `lumio build` to regenerate.");
1275
4102
  process.exit(1);
@@ -1278,8 +4105,8 @@ function registerDeploy(program2) {
1278
4105
  console.log(` Source: source.tar.gz (${(sourceBuffer.length / 1024).toFixed(1)} KB)`);
1279
4106
  let totalSize = 0;
1280
4107
  for (const relPath of bundleFiles) {
1281
- const absPath = path5.join(distDir, relPath);
1282
- const buffer = fs5.readFileSync(absPath);
4108
+ const absPath = path13.join(distDir, relPath);
4109
+ const buffer = fs12.readFileSync(absPath);
1283
4110
  if (buffer.length === 0) {
1284
4111
  console.warn(` Warning: Skipping empty file: ${relPath}`);
1285
4112
  continue;
@@ -1308,9 +4135,16 @@ function registerDeploy(program2) {
1308
4135
  }
1309
4136
  } catch (err) {
1310
4137
  const status = err.status;
1311
- 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
+ }
1312
4146
  console.error(`
1313
- Deploy failed${status ? ` (HTTP ${status})` : ""}: ${message}`);
4147
+ Deploy failed${status ? ` (HTTP ${status})` : ""}: ${message2}`);
1314
4148
  if (status === 409) {
1315
4149
  console.error(" This version already exists. Bump the version in lumio.config.json.");
1316
4150
  } else if (status === 413) {
@@ -1324,8 +4158,8 @@ function registerDeploy(program2) {
1324
4158
  }
1325
4159
 
1326
4160
  // src/commands/status.ts
1327
- import * as fs6 from "fs";
1328
- import * as path6 from "path";
4161
+ import * as fs13 from "fs";
4162
+ import * as path14 from "path";
1329
4163
  function registerStatus(program2) {
1330
4164
  program2.command("status").description("Show review status of the latest version").action(async () => {
1331
4165
  const store = new CredentialStore();
@@ -1334,12 +4168,12 @@ function registerStatus(program2) {
1334
4168
  console.error("Not logged in. Run `lumio login` first.");
1335
4169
  process.exit(1);
1336
4170
  }
1337
- const configPath = path6.resolve("lumio.config.json");
1338
- if (!fs6.existsSync(configPath)) {
4171
+ const configPath = path14.resolve("lumio.config.json");
4172
+ if (!fs13.existsSync(configPath)) {
1339
4173
  console.error("lumio.config.json not found.");
1340
4174
  process.exit(1);
1341
4175
  }
1342
- const config = parseConfigFile(fs6.readFileSync(configPath, "utf-8"));
4176
+ const config = parseConfigFile(fs13.readFileSync(configPath, "utf-8"));
1343
4177
  const client = new LumioApiClient(store.getApiUrl(), token);
1344
4178
  try {
1345
4179
  const versions = await client.get(
@@ -1368,8 +4202,8 @@ function registerStatus(program2) {
1368
4202
  }
1369
4203
 
1370
4204
  // src/commands/logs.ts
1371
- import * as fs7 from "fs";
1372
- import * as path7 from "path";
4205
+ import * as fs14 from "fs";
4206
+ import * as path15 from "path";
1373
4207
  function registerLogs(program2) {
1374
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) => {
1375
4209
  const store = new CredentialStore();
@@ -1378,12 +4212,12 @@ function registerLogs(program2) {
1378
4212
  console.error("Not logged in. Run `lumio login` first.");
1379
4213
  process.exit(1);
1380
4214
  }
1381
- const configPath = path7.resolve("lumio.config.json");
1382
- if (!fs7.existsSync(configPath)) {
4215
+ const configPath = path15.resolve("lumio.config.json");
4216
+ if (!fs14.existsSync(configPath)) {
1383
4217
  console.error("lumio.config.json not found.");
1384
4218
  process.exit(1);
1385
4219
  }
1386
- const config = parseConfigFile(fs7.readFileSync(configPath, "utf-8"));
4220
+ const config = parseConfigFile(fs14.readFileSync(configPath, "utf-8"));
1387
4221
  const apiUrl = store.getApiUrl();
1388
4222
  const logsUrl = `${apiUrl}/v1/developer/extensions/${config.extensionId}/logs?lines=${options.lines}`;
1389
4223
  console.log(`Streaming logs for ${config.name}...`);
@@ -1420,22 +4254,31 @@ function registerLogs(program2) {
1420
4254
  }
1421
4255
 
1422
4256
  // src/commands/env.ts
1423
- import * as fs8 from "fs";
1424
- 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;
1425
4261
  function validateSecretKey(key) {
1426
- 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)}`;
1427
4270
  }
1428
4271
  function formatSecretList(secrets) {
1429
4272
  if (secrets.length === 0) return "No secrets configured.";
1430
4273
  return secrets.map((s) => ` ${s.key} (created ${s.created_at})`).join("\n");
1431
4274
  }
1432
4275
  function loadConfig() {
1433
- const configPath = path8.resolve("lumio.config.json");
1434
- if (!fs8.existsSync(configPath)) {
4276
+ const configPath = path16.resolve("lumio.config.json");
4277
+ if (!fs15.existsSync(configPath)) {
1435
4278
  console.error("lumio.config.json not found. Run `lumio init` first.");
1436
4279
  process.exit(1);
1437
4280
  }
1438
- return parseConfigFile(fs8.readFileSync(configPath, "utf-8"));
4281
+ return parseConfigFile(fs15.readFileSync(configPath, "utf-8"));
1439
4282
  }
1440
4283
  function requireClient() {
1441
4284
  const store = new CredentialStore();
@@ -1448,16 +4291,31 @@ function requireClient() {
1448
4291
  }
1449
4292
  function registerEnv(program2) {
1450
4293
  const envCmd = program2.command("env").description("Manage server-side secrets for the extension");
1451
- envCmd.command("set <key> <value>").description("Set a secret (POST /v1/developer/extensions/{id}/secrets)").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) => {
1452
4295
  if (!validateSecretKey(key)) {
1453
4296
  console.error(
1454
- "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).`
1455
4298
  );
1456
4299
  process.exit(1);
1457
4300
  }
4301
+ let secretValue = value ?? process.env.LUMIO_SECRET;
4302
+ if (secretValue === void 0) {
4303
+ const { default: inquirer } = await import("inquirer");
4304
+ const answer = await inquirer.prompt([{
4305
+ type: "password",
4306
+ name: "value",
4307
+ mask: "*",
4308
+ message: `Value for ${key}:`
4309
+ }]);
4310
+ secretValue = answer.value;
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
+ }
1458
4316
  const config = loadConfig();
1459
4317
  const client = requireClient();
1460
- await client.post(`/v1/developer/extensions/${config.extensionId}/secrets`, { key, value });
4318
+ await client.put(secretEndpoint(config.extensionId, key), { value: secretValue });
1461
4319
  console.log(`Secret ${key} set.`);
1462
4320
  });
1463
4321
  envCmd.command("list").description("List all secrets (keys only, no values)").action(async () => {
@@ -1471,14 +4329,14 @@ function registerEnv(program2) {
1471
4329
  envCmd.command("delete <key>").description("Delete a secret").action(async (key) => {
1472
4330
  const config = loadConfig();
1473
4331
  const client = requireClient();
1474
- await client.delete(`/v1/developer/extensions/${config.extensionId}/secrets/${key}`);
4332
+ await client.delete(secretEndpoint(config.extensionId, key));
1475
4333
  console.log(`Secret ${key} deleted.`);
1476
4334
  });
1477
4335
  }
1478
4336
 
1479
4337
  // src/commands/run.ts
1480
- import * as fs9 from "fs";
1481
- import * as path9 from "path";
4338
+ import * as fs16 from "fs";
4339
+ import * as path17 from "path";
1482
4340
  var VALID_FUNCTION_TYPES = [
1483
4341
  "query",
1484
4342
  "mutation",
@@ -1515,12 +4373,12 @@ function parseFunctionRef(ref) {
1515
4373
  function registerRun(program2) {
1516
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) => {
1517
4375
  const fnRef = parseFunctionRef(typeAndName);
1518
- const configPath = path9.resolve("lumio.config.json");
1519
- if (!fs9.existsSync(configPath)) {
4376
+ const configPath = path17.resolve("lumio.config.json");
4377
+ if (!fs16.existsSync(configPath)) {
1520
4378
  console.error("lumio.config.json not found. Run `lumio init` first.");
1521
4379
  process.exit(1);
1522
4380
  }
1523
- const config = parseConfigFile(fs9.readFileSync(configPath, "utf-8"));
4381
+ const config = parseConfigFile(fs16.readFileSync(configPath, "utf-8"));
1524
4382
  const store = new CredentialStore();
1525
4383
  const token = store.getToken();
1526
4384
  if (!token) {
@@ -1528,7 +4386,17 @@ function registerRun(program2) {
1528
4386
  process.exit(1);
1529
4387
  }
1530
4388
  const client = new LumioApiClient(store.getApiUrl(), token);
1531
- const args = argsJson ? JSON.parse(argsJson) : {};
4389
+ let args = {};
4390
+ if (argsJson) {
4391
+ try {
4392
+ args = JSON.parse(argsJson);
4393
+ } catch (err) {
4394
+ console.error(
4395
+ `Invalid JSON for args: ${err instanceof Error ? err.message : String(err)}`
4396
+ );
4397
+ process.exit(1);
4398
+ }
4399
+ }
1532
4400
  const body = { type: fnRef.type, args };
1533
4401
  if (BOT_HANDLER_TYPES.includes(fnRef.type)) {
1534
4402
  body.user = {
@@ -1547,7 +4415,7 @@ function registerRun(program2) {
1547
4415
  }
1548
4416
  try {
1549
4417
  const result = await client.post(
1550
- `/v1/extension-installs/${config.extensionId}/execute/${fnRef.name}`,
4418
+ `/v1/extension-installs/${config.extensionId}/execute/${encodeURIComponent(fnRef.name)}`,
1551
4419
  body
1552
4420
  );
1553
4421
  console.log(JSON.stringify(result, null, 2));
@@ -1559,16 +4427,16 @@ function registerRun(program2) {
1559
4427
  }
1560
4428
 
1561
4429
  // src/commands/dashboard.ts
1562
- import * as fs10 from "fs";
1563
- import * as path10 from "path";
4430
+ import * as fs17 from "fs";
4431
+ import * as path18 from "path";
1564
4432
  function registerDashboard(program2) {
1565
4433
  program2.command("dashboard").description("Open the extension dashboard in browser").action(async () => {
1566
- const configPath = path10.resolve("lumio.config.json");
1567
- if (!fs10.existsSync(configPath)) {
4434
+ const configPath = path18.resolve("lumio.config.json");
4435
+ if (!fs17.existsSync(configPath)) {
1568
4436
  console.error("lumio.config.json not found. Run `lumio init` first.");
1569
4437
  process.exit(1);
1570
4438
  }
1571
- const config = parseConfigFile(fs10.readFileSync(configPath, "utf-8"));
4439
+ const config = parseConfigFile(fs17.readFileSync(configPath, "utf-8"));
1572
4440
  const baseUrl = process.env.LUMIO_WEB_URL ?? "https://lumio.vision";
1573
4441
  const url = `${baseUrl}/dashboard/extensions/${config.extensionId}`;
1574
4442
  console.log(`Opening ${url}...`);
@@ -1579,15 +4447,18 @@ function registerDashboard(program2) {
1579
4447
 
1580
4448
  // src/program.ts
1581
4449
  function createProgram() {
1582
- const program2 = new Command();
4450
+ const program2 = new Command2();
1583
4451
  program2.name("lumio");
1584
4452
  program2.description("Lumio Extension CLI -- Build, test, and deploy Lumio extensions");
1585
- program2.version("2026.5.1");
4453
+ const require2 = createRequire3(import.meta.url);
4454
+ const { version } = require2("../package.json");
4455
+ program2.version(version);
1586
4456
  registerLogin(program2);
1587
4457
  registerLogout(program2);
1588
4458
  registerWhoami(program2);
1589
4459
  registerInit(program2);
1590
4460
  registerDev(program2);
4461
+ registerTrigger(program2);
1591
4462
  registerBuild(program2);
1592
4463
  registerDeploy(program2);
1593
4464
  registerStatus(program2);