@zaflun/lumio-cli 0.3.0 → 0.6.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/dist/index.d.ts +13 -0
  2. package/dist/index.js +850 -88
  3. package/package.json +16 -13
package/dist/index.js CHANGED
@@ -1,5 +1,6 @@
1
1
  // src/program.ts
2
2
  import { Command } from "commander";
3
+ import { createRequire as createRequire2 } 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
108
  await new Promise((resolve10, reject) => {
109
+ let timeout;
79
110
  const server = http.createServer(async (req, res) => {
111
+ const finish = () => {
112
+ clearTimeout(timeout);
113
+ server.close();
114
+ resolve10();
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() {
@@ -295,22 +342,61 @@ function registerInit(program2) {
295
342
  type: "list",
296
343
  name: "category",
297
344
  message: "Category:",
298
- choices: ["widget", "extension", "overlay", "bot_module", "integration", "theme"]
345
+ choices: ["widget", "extension", "overlay", "bot_module", "integration", "theme", "automation_node"]
299
346
  }
300
347
  ]);
301
- const answers = { ...baseAnswers, targets: [], server: false };
348
+ const answers = {
349
+ name: baseAnswers.name,
350
+ description: baseAnswers.description,
351
+ category: baseAnswers.category,
352
+ targets: [],
353
+ server: false
354
+ };
302
355
  if (answers.category === "bot_module") {
303
356
  const { platforms } = await inquirer.prompt([{
304
357
  type: "checkbox",
305
358
  name: "platforms",
306
359
  message: "Which platforms should this bot module support?",
307
- choices: ["twitch", "youtube", "kick", "trovo", "discord"],
360
+ choices: ["twitch", "youtube", "kick", "discord"],
308
361
  default: ["twitch"],
309
362
  validate: (input) => input.length > 0 || "Select at least one platform"
310
363
  }]);
311
364
  answers.platforms = platforms;
312
365
  answers.targets = [];
313
366
  answers.server = true;
367
+ } else if (answers.category === "automation_node") {
368
+ const nodeAnswers = await inquirer.prompt([
369
+ {
370
+ type: "list",
371
+ name: "nodeType",
372
+ message: "Node type:",
373
+ choices: [
374
+ { name: "Action \u2014 perform work when the automation runs", value: "action" },
375
+ { name: "Trigger \u2014 start an automation from an external event", value: "trigger" },
376
+ { name: "Logic \u2014 conditional branching", value: "logic" }
377
+ ]
378
+ }
379
+ ]);
380
+ let triggerMode;
381
+ if (nodeAnswers.nodeType === "trigger") {
382
+ const triggerAnswers = await inquirer.prompt([
383
+ {
384
+ type: "list",
385
+ name: "triggerMode",
386
+ message: "How does this trigger receive events?",
387
+ choices: [
388
+ { name: "Webhook \u2014 receive HTTP POST from an external service", value: "webhook" },
389
+ { name: "Polling \u2014 periodically check an external service", value: "polling" },
390
+ { name: "Both \u2014 support webhook and polling", value: "both" }
391
+ ]
392
+ }
393
+ ]);
394
+ triggerMode = triggerAnswers.triggerMode;
395
+ }
396
+ answers.targets = ["editor"];
397
+ answers.server = true;
398
+ answers.nodeType = nodeAnswers.nodeType;
399
+ answers.triggerMode = triggerMode;
314
400
  } else {
315
401
  const targetAndServer = await inquirer.prompt([
316
402
  {
@@ -325,6 +411,66 @@ function registerInit(program2) {
325
411
  answers.targets = targetAndServer.targets;
326
412
  answers.server = targetAndServer.server;
327
413
  }
414
+ let editorApproach = "schema";
415
+ if (answers.category !== "bot_module" && answers.category !== "automation_node") {
416
+ const editorChoice = await inquirer.prompt([{
417
+ type: "list",
418
+ name: "approach",
419
+ message: "How should the editor UI be built?",
420
+ choices: [
421
+ { name: "config_schema only \u2014 auto-generated form from schema fields (recommended)", value: "schema" },
422
+ { name: "editor.tsx only \u2014 fully custom React UI in sandbox", value: "custom" },
423
+ { name: "Both \u2014 schema fields + custom editor.tsx for advanced controls", value: "both" }
424
+ ],
425
+ default: "schema"
426
+ }]);
427
+ editorApproach = editorChoice.approach;
428
+ if (editorApproach === "schema") {
429
+ answers.targets = answers.targets.filter((t) => t !== "editor");
430
+ } else if (!answers.targets.includes("editor")) {
431
+ answers.targets.push("editor");
432
+ }
433
+ }
434
+ answers.editorApproach = editorApproach;
435
+ let includeDesigner = false;
436
+ if (answers.category === "widget" || answers.category === "overlay") {
437
+ const designerAnswer = await inquirer.prompt([{
438
+ type: "confirm",
439
+ name: "includeDesigner",
440
+ message: "Include a visual designer? (fullpage design editor with live preview)",
441
+ default: false
442
+ }]);
443
+ includeDesigner = designerAnswer.includeDesigner;
444
+ }
445
+ let useTailwind = false;
446
+ if (answers.category !== "bot_module") {
447
+ const styleAnswer = await inquirer.prompt([
448
+ {
449
+ type: "list",
450
+ name: "styling",
451
+ message: "Styling approach:",
452
+ choices: [
453
+ { name: "Tailwind CSS v4 (recommended)", value: "tailwind" },
454
+ { name: "Plain CSS", value: "css" },
455
+ { name: "Inline styles only", value: "inline" }
456
+ ],
457
+ default: "tailwind"
458
+ }
459
+ ]);
460
+ useTailwind = styleAnswer.styling === "tailwind";
461
+ answers.styling = styleAnswer.styling;
462
+ }
463
+ try {
464
+ await client.get("/v1/developer/profile");
465
+ } catch (err) {
466
+ if (err.status === 404) {
467
+ console.error(
468
+ "\n You need to apply as a developer first.\n Visit https://lumio.vision/account/developer to submit your application.\n"
469
+ );
470
+ process.exit(1);
471
+ }
472
+ throw err;
473
+ }
328
474
  const createBody = {
329
475
  name: answers.name,
330
476
  description: answers.description,
@@ -351,6 +497,23 @@ function registerInit(program2) {
351
497
  visibility: "private",
352
498
  pricing: { type: "free" }
353
499
  };
500
+ if (editorApproach === "schema" || editorApproach === "both") {
501
+ config.config_schema = [
502
+ { key: "title", type: "string", label: "Title", default_value: answers.name, required: false, placeholder: "Enter a title..." },
503
+ { key: "fontSize", type: "number", label: "Font Size", default_value: 16, required: false, min: 8, max: 200, step: 1, unit: "px" },
504
+ { key: "color", type: "color", label: "Color", default_value: "#ffffff", required: false }
505
+ ];
506
+ }
507
+ if (includeDesigner) {
508
+ config.designer_schema = [
509
+ { key: "primaryColor", type: "color", label: "Primary Color", default_value: "#43F7CD", required: false },
510
+ { key: "backgroundColor", type: "color", label: "Background Color", default_value: "#1a1a2e", required: false },
511
+ { key: "fontFamily", type: "font", label: "Font Family", default_value: "Inter", required: false }
512
+ ];
513
+ if (config.config_schema && Array.isArray(config.config_schema)) {
514
+ config.config_schema.push({ key: "design", type: "designer", label: "Widget Designer", required: false, default_value: null });
515
+ }
516
+ }
354
517
  if (answers.category === "bot_module") {
355
518
  config.platforms = answers.platforms;
356
519
  config.triggers = {
@@ -361,29 +524,88 @@ function registerInit(program2) {
361
524
  timers: []
362
525
  };
363
526
  }
527
+ if (answers.category === "automation_node") {
528
+ const nodeType = answers.nodeType;
529
+ const triggerMode = answers.triggerMode;
530
+ config.node_type = nodeType;
531
+ const nodeConfig = {
532
+ input_schema: {
533
+ type: "object",
534
+ properties: {
535
+ value: { type: "string", title: "Input Value" }
536
+ },
537
+ required: ["value"]
538
+ },
539
+ output_schema: {
540
+ type: "object",
541
+ properties: {
542
+ result: { type: "string" }
543
+ }
544
+ }
545
+ };
546
+ if (nodeType === "trigger") {
547
+ nodeConfig.trigger_mode = triggerMode;
548
+ if (triggerMode === "polling" || triggerMode === "both") {
549
+ nodeConfig.poll_interval_seconds = 30;
550
+ }
551
+ }
552
+ nodeConfig.icon = "puzzle";
553
+ nodeConfig.color = "#a855f7";
554
+ config.node = nodeConfig;
555
+ }
364
556
  fs2.writeFileSync(
365
557
  path2.join(projectDir, "lumio.config.json"),
366
558
  JSON.stringify(config, null, 2)
367
559
  );
560
+ const isBotModule = answers.category === "bot_module";
561
+ const isAutomationNode = answers.category === "automation_node";
562
+ const needsReact = !isBotModule || isAutomationNode;
563
+ const lintDir = isBotModule ? "server/" : "src/";
368
564
  const pkg = {
369
565
  name: answers.name.toLowerCase().replace(/\s+/g, "-"),
370
566
  version: "0.1.0",
371
567
  private: true,
372
568
  type: "module",
569
+ scripts: {
570
+ "dev": isBotModule ? "lumio dev --bot-module" : isAutomationNode ? "lumio dev --automation-node" : "lumio dev",
571
+ "build": "lumio build",
572
+ "deploy": "lumio deploy",
573
+ "typecheck": "tsc --noEmit",
574
+ "lint": `eslint ${lintDir}`
575
+ },
373
576
  dependencies: {
374
577
  "@zaflun/lumio-sdk": "latest",
375
- "react": "^19.1.0",
376
- "react-dom": "^19.1.0"
578
+ ...needsReact ? { "react": "^19.2.5" } : {}
377
579
  },
378
580
  devDependencies: {
379
- "typescript": "^5.8.3",
380
- "@types/react": "^19.1.0"
581
+ "typescript": "^6.0.3",
582
+ ...needsReact ? { "@types/react": "^19.1.0" } : {},
583
+ ...useTailwind ? { "tailwindcss": "^4.3.0" } : {}
381
584
  }
382
585
  };
383
586
  fs2.writeFileSync(
384
587
  path2.join(projectDir, "package.json"),
385
588
  JSON.stringify(pkg, null, 2)
386
589
  );
590
+ const tsconfig = {
591
+ compilerOptions: {
592
+ target: "ES2022",
593
+ module: "ESNext",
594
+ moduleResolution: "bundler",
595
+ jsx: "react-jsx",
596
+ strict: true,
597
+ esModuleInterop: true,
598
+ skipLibCheck: true,
599
+ outDir: "dist"
600
+ },
601
+ include: answers.category === "bot_module" ? ["server/**/*"] : ["src/**/*", "server/**/*"]
602
+ };
603
+ fs2.writeFileSync(
604
+ path2.join(projectDir, "tsconfig.json"),
605
+ JSON.stringify(tsconfig, null, 2)
606
+ );
607
+ const eslintConfig = 'import eslintJs from "@eslint/js";\nimport tseslint from "typescript-eslint";\n\nexport default tseslint.config(\n eslintJs.configs.recommended,\n ...tseslint.configs.recommended,\n {\n rules: {\n "quotes": ["error", "double", { avoidEscape: true }],\n },\n },\n);\n';
608
+ fs2.writeFileSync(path2.join(projectDir, "eslint.config.mjs"), eslintConfig);
387
609
  const changelog = `# Changelog
388
610
 
389
611
  ## [0.1.0] - ${(/* @__PURE__ */ new Date()).toISOString().split("T")[0]}
@@ -393,15 +615,83 @@ function registerInit(program2) {
393
615
  `;
394
616
  fs2.writeFileSync(path2.join(projectDir, "CHANGELOG.md"), changelog);
395
617
  fs2.mkdirSync(path2.join(projectDir, "src"), { recursive: true });
618
+ if (useTailwind) {
619
+ fs2.writeFileSync(
620
+ path2.join(projectDir, "src", "globals.css"),
621
+ '@import "tailwindcss";\n'
622
+ );
623
+ } else if (needsReact && answers.styling === "css") {
624
+ fs2.writeFileSync(
625
+ path2.join(projectDir, "src", "globals.css"),
626
+ "/* Add your custom styles here */\n"
627
+ );
628
+ }
396
629
  if (answers.category === "bot_module") {
397
630
  fs2.mkdirSync(path2.join(projectDir, "server"), { recursive: true });
398
631
  const serverFunctions = 'import { command } from "@zaflun/lumio-sdk/server";\n\nexport const hello = command("hello", async (ctx, args) => {\n return { reply: `Hello ${ctx.user?.name ?? "stranger"}!` };\n});\n';
399
632
  fs2.writeFileSync(path2.join(projectDir, "server", "functions.ts"), serverFunctions);
400
633
  const schema = 'import { defineSchema } from "@zaflun/lumio-sdk/server";\n\nexport default defineSchema({});\n';
401
634
  fs2.writeFileSync(path2.join(projectDir, "server", "schema.ts"), schema);
635
+ } else if (answers.category === "automation_node") {
636
+ fs2.mkdirSync(path2.join(projectDir, "server"), { recursive: true });
637
+ const nodeType = answers.nodeType;
638
+ const triggerMode = answers.triggerMode;
639
+ let serverHandler;
640
+ if (nodeType === "action") {
641
+ serverHandler = 'import { defineAutomationAction } from "@zaflun/lumio-sdk/server";\n\ndefineAutomationAction(async (ctx) => {\n const value = ctx.inputData.value;\n\n // TODO: Implement your action logic\n // Use ctx.fetch() for external API calls\n // Use ctx.cache.get/set for temporary storage\n // Use ctx.installConfig for extension-level settings\n // Use ctx.nodeConfig for per-node-instance settings\n\n return {\n output: { result: `Processed: ${value}` },\n status: "success",\n };\n});\n';
642
+ } else if (nodeType === "trigger") {
643
+ const parts = [];
644
+ if (triggerMode === "webhook" || triggerMode === "both") {
645
+ parts.push(" onWebhook: async (ctx) => {\n const payload = ctx.webhookBody;\n\n // TODO: Validate the incoming webhook payload\n // Return { fired: false } to ignore the event\n // Return { fired: true, output: { ... } } to trigger the automation\n\n return {\n fired: true,\n output: {\n result: JSON.stringify(payload),\n },\n };\n }");
646
+ }
647
+ if (triggerMode === "polling" || triggerMode === "both") {
648
+ parts.push(' onPoll: async (ctx) => {\n const lastCheck = await ctx.cache.get("last_check_ts");\n\n // TODO: Check your external service for new events\n // const res = await ctx.fetch("https://api.example.com/events?since=" + lastCheck);\n // const events = await res.json();\n\n await ctx.cache.set("last_check_ts", Date.now().toString());\n\n // Return null or { fired: false } if no new events\n // Return { fired: true, output: { ... } } to trigger the automation\n return null;\n }');
649
+ }
650
+ serverHandler = `import { defineAutomationTrigger } from "@zaflun/lumio-sdk/server";
651
+
652
+ defineAutomationTrigger({
653
+ ${parts.join(",\n")}
654
+ });
655
+ `;
656
+ } else {
657
+ serverHandler = 'import { defineAutomationLogic } from "@zaflun/lumio-sdk/server";\n\ndefineAutomationLogic(async (ctx) => {\n const value = ctx.inputData.value;\n const threshold = ctx.nodeConfig.threshold ?? 50;\n\n // TODO: Implement your branching logic\n // Return the branch key that matches an output handle\n\n if (Number(value) > Number(threshold)) {\n return "above";\n }\n return "below";\n});\n';
658
+ }
659
+ fs2.writeFileSync(path2.join(projectDir, "server", "handler.ts"), serverHandler);
660
+ const editorComponent = `import { Lumio, CompactView, Text } from "@zaflun/lumio-sdk";
661
+ import { useLumioConfig } from "@zaflun/lumio-sdk/hooks";
662
+
663
+ function ConfigPanel() {
664
+ const [config, setConfig] = useLumioConfig();
665
+
666
+ return (
667
+ <CompactView title="${answers.name} \u2014 Settings">
668
+ <Text content="Configure your automation node here." />
669
+ </CompactView>
670
+ );
671
+ }
672
+
673
+ Lumio.render(<ConfigPanel />, { target: "editor" });
674
+ `;
675
+ fs2.writeFileSync(path2.join(projectDir, "src", "editor.tsx"), editorComponent);
402
676
  } else {
403
677
  for (const target of answers.targets) {
404
- const content = `import { Lumio, CompactView, Text } from "@zaflun/lumio-sdk";
678
+ if (target === "editor" && editorApproach === "schema") continue;
679
+ const isEditorTarget = target === "editor";
680
+ const content = isEditorTarget ? `import { Lumio, CompactView, Text } from "@zaflun/lumio-sdk";
681
+ import { useLumioConfig } from "@zaflun/lumio-sdk";
682
+
683
+ function Editor() {
684
+ const [config, setConfig] = useLumioConfig();
685
+
686
+ return (
687
+ <CompactView title="${answers.name} \u2014 Advanced">
688
+ <Text content="Custom editor controls go here." />
689
+ </CompactView>
690
+ );
691
+ }
692
+
693
+ export default Editor;
694
+ ` : `import { Lumio, CompactView, Text } from "@zaflun/lumio-sdk";
405
695
 
406
696
  function App() {
407
697
  return (
@@ -434,6 +724,7 @@ import * as fs3 from "fs";
434
724
  import * as path3 from "path";
435
725
 
436
726
  // src/config/parser.ts
727
+ import { NODE_TYPES, TRIGGER_MODES, VALID_PLATFORMS } from "@zaflun/lumio-extension-types";
437
728
  var ConfigError = class extends Error {
438
729
  constructor(message) {
439
730
  super(message);
@@ -443,7 +734,7 @@ var ConfigError = class extends Error {
443
734
  function parseConfigFile(jsonString) {
444
735
  const raw = JSON.parse(jsonString);
445
736
  return {
446
- extensionId: raw.extensionId ?? "",
737
+ extensionId: raw.extensionId ?? raw.extension_id ?? "",
447
738
  name: raw.name ?? "",
448
739
  description: raw.description,
449
740
  category: raw.category ?? "widget",
@@ -458,20 +749,61 @@ function parseConfigFile(jsonString) {
458
749
  platforms: raw.platforms,
459
750
  triggers: raw.triggers,
460
751
  fonts: raw.fonts,
752
+ sounds: raw.sounds,
461
753
  visibility: raw.visibility ?? "private",
462
- pricing: raw.pricing ?? { type: "free" }
754
+ pricing: raw.pricing ?? { type: "free" },
755
+ nodeType: raw.node_type ?? raw.nodeType,
756
+ node: raw.node ? {
757
+ inputSchema: raw.node.input_schema ?? raw.node.inputSchema ?? {},
758
+ outputSchema: raw.node.output_schema ?? raw.node.outputSchema ?? {},
759
+ triggerMode: raw.node.trigger_mode ?? raw.node.triggerMode,
760
+ pollIntervalSeconds: raw.node.poll_interval_seconds ?? raw.node.pollIntervalSeconds,
761
+ icon: raw.node.icon,
762
+ color: raw.node.color
763
+ } : void 0
463
764
  };
464
765
  }
465
766
  function validateConfig(config) {
466
767
  if (!config.extensionId) {
467
768
  throw new ConfigError("extensionId is required. Run `lumio init` or create an extension at the dashboard.");
468
769
  }
770
+ if (!/^[A-Za-z0-9_-]{1,64}$/.test(config.extensionId)) {
771
+ throw new ConfigError("extensionId is invalid: only letters, digits, '-' and '_' (max 64 chars) are allowed.");
772
+ }
469
773
  if (!config.name) {
470
774
  throw new ConfigError("name is required in lumio.config.json");
471
775
  }
472
776
  if (config.category === "bot_module") {
473
777
  if (!config.platforms || config.platforms.length === 0) {
474
- throw new ConfigError("At least one platform is required for bot_module (twitch, youtube, kick, trovo, discord)");
778
+ throw new ConfigError(`At least one platform is required for bot_module (${VALID_PLATFORMS.join(", ")})`);
779
+ }
780
+ const invalid = config.platforms.filter((p) => !VALID_PLATFORMS.includes(p));
781
+ if (invalid.length > 0) {
782
+ throw new ConfigError(`Invalid platform(s): ${invalid.join(", ")}. Must be one of: ${VALID_PLATFORMS.join(", ")}`);
783
+ }
784
+ } else if (config.category === "automation_node") {
785
+ if (!config.nodeType) {
786
+ throw new ConfigError(`nodeType is required for automation_node extensions (${NODE_TYPES.join(", ")})`);
787
+ }
788
+ if (!NODE_TYPES.includes(config.nodeType)) {
789
+ throw new ConfigError(`nodeType must be one of: ${NODE_TYPES.join(", ")}`);
790
+ }
791
+ if (!config.node) {
792
+ throw new ConfigError("node configuration is required for automation_node extensions");
793
+ }
794
+ if (config.nodeType === "trigger" && !config.node.triggerMode) {
795
+ throw new ConfigError(`node.triggerMode is required for trigger nodes (${TRIGGER_MODES.join(", ")})`);
796
+ }
797
+ if (config.node.triggerMode && !TRIGGER_MODES.includes(config.node.triggerMode)) {
798
+ throw new ConfigError(`node.triggerMode must be one of: ${TRIGGER_MODES.join(", ")}`);
799
+ }
800
+ if (config.nodeType !== "trigger" && config.node.triggerMode) {
801
+ throw new ConfigError("node.triggerMode is only valid for trigger nodes");
802
+ }
803
+ if (config.node.pollIntervalSeconds !== void 0) {
804
+ if (config.node.pollIntervalSeconds < 10 || config.node.pollIntervalSeconds > 300) {
805
+ throw new ConfigError("node.pollIntervalSeconds must be between 10 and 300");
806
+ }
475
807
  }
476
808
  } else if (!config.targets || config.targets.length === 0) {
477
809
  throw new ConfigError("At least one target is required (editor, layer, or interactive)");
@@ -553,11 +885,120 @@ async function runBotModuleDevServer(config) {
553
885
  });
554
886
  }
555
887
 
888
+ // src/commands/dev-automation-node.ts
889
+ import * as readline2 from "readline";
890
+ import chalk2 from "chalk";
891
+ async function runAutomationNodeDevServer(config) {
892
+ const nodeType = config.nodeType ?? "action";
893
+ console.log(chalk2.bold("\n\u2699\uFE0F Automation Node Dev Server"));
894
+ console.log(`Extension: ${config.name}`);
895
+ console.log(`Node type: ${nodeType}`);
896
+ if (nodeType === "trigger" && config.node?.triggerMode) {
897
+ console.log(`Trigger mode: ${config.node.triggerMode}`);
898
+ }
899
+ console.log(chalk2.dim("\nCommands:"));
900
+ console.log(chalk2.dim(" /execute [json] \u2014 Execute the node with input data"));
901
+ if (nodeType === "trigger") {
902
+ console.log(chalk2.dim(" /webhook [json] \u2014 Simulate a webhook POST payload"));
903
+ console.log(chalk2.dim(" /poll \u2014 Trigger a poll cycle"));
904
+ }
905
+ console.log(chalk2.dim(" /config [json] \u2014 Set node instance config"));
906
+ console.log(chalk2.dim(" /install-config [j] \u2014 Set extension install config"));
907
+ console.log(chalk2.dim(" /quit \u2014 Exit\n"));
908
+ let nodeConfig = {};
909
+ let installConfig = {};
910
+ const rl = readline2.createInterface({
911
+ input: process.stdin,
912
+ output: process.stdout,
913
+ prompt: chalk2.cyan("> ")
914
+ });
915
+ rl.prompt();
916
+ rl.on("line", async (line) => {
917
+ const trimmed = line.trim();
918
+ if (!trimmed) {
919
+ rl.prompt();
920
+ return;
921
+ }
922
+ if (trimmed === "/quit") {
923
+ rl.close();
924
+ return;
925
+ }
926
+ if (trimmed.startsWith("/config ")) {
927
+ try {
928
+ nodeConfig = JSON.parse(trimmed.slice("/config ".length));
929
+ console.log(chalk2.green(" Node config updated."));
930
+ } catch {
931
+ console.log(chalk2.red(" Invalid JSON."));
932
+ }
933
+ rl.prompt();
934
+ return;
935
+ }
936
+ if (trimmed.startsWith("/install-config ")) {
937
+ try {
938
+ installConfig = JSON.parse(trimmed.slice("/install-config ".length));
939
+ console.log(chalk2.green(" Install config updated."));
940
+ } catch {
941
+ console.log(chalk2.red(" Invalid JSON."));
942
+ }
943
+ rl.prompt();
944
+ return;
945
+ }
946
+ if (trimmed.startsWith("/execute")) {
947
+ const jsonStr = trimmed.slice("/execute".length).trim();
948
+ let inputData = {};
949
+ if (jsonStr) {
950
+ try {
951
+ inputData = JSON.parse(jsonStr);
952
+ } catch {
953
+ console.log(chalk2.red(" Invalid JSON."));
954
+ rl.prompt();
955
+ return;
956
+ }
957
+ }
958
+ console.log(chalk2.dim(` [${nodeType}] Executing with input: ${JSON.stringify(inputData)}`));
959
+ console.log(chalk2.dim(` [${nodeType}] Node config: ${JSON.stringify(nodeConfig)}`));
960
+ console.log(chalk2.dim(` [${nodeType}] Install config: ${JSON.stringify(installConfig)}`));
961
+ console.log(chalk2.yellow(" \u2192 Handler execution not yet wired (requires esbuild + V8)"));
962
+ rl.prompt();
963
+ return;
964
+ }
965
+ if (nodeType === "trigger" && trimmed.startsWith("/webhook")) {
966
+ const jsonStr = trimmed.slice("/webhook".length).trim();
967
+ let body = {};
968
+ if (jsonStr) {
969
+ try {
970
+ body = JSON.parse(jsonStr);
971
+ } catch {
972
+ console.log(chalk2.red(" Invalid JSON."));
973
+ rl.prompt();
974
+ return;
975
+ }
976
+ }
977
+ console.log(chalk2.dim(` [trigger:webhook] Received payload: ${JSON.stringify(body)}`));
978
+ console.log(chalk2.yellow(" \u2192 Handler execution not yet wired (requires esbuild + V8)"));
979
+ rl.prompt();
980
+ return;
981
+ }
982
+ if (nodeType === "trigger" && trimmed === "/poll") {
983
+ console.log(chalk2.dim(" [trigger:poll] Polling..."));
984
+ console.log(chalk2.yellow(" \u2192 Handler execution not yet wired (requires esbuild + V8)"));
985
+ rl.prompt();
986
+ return;
987
+ }
988
+ console.log(chalk2.dim(" Unknown command. Type /execute, /webhook, /poll, /config, or /quit."));
989
+ rl.prompt();
990
+ });
991
+ rl.on("close", () => {
992
+ console.log(chalk2.dim("\nAutomation Node Dev Server stopped."));
993
+ process.exit(0);
994
+ });
995
+ }
996
+
556
997
  // src/commands/dev.ts
557
998
  function registerDev(program2) {
558
- program2.command("dev").description("Start local dev server with hot-reload and mock events").option("-p, --port <port>", "Dev server port", "3100").option("--bot-module", "Start interactive bot module chat simulator").action(async (options) => {
999
+ program2.command("dev").description("Start local dev server with hot-reload and mock events").option("-p, --port <port>", "Dev server port", "3100").option("--bot-module", "Start interactive bot module chat simulator").option("--automation-node", "Start automation node dev simulator").action(async (options) => {
559
1000
  const configPath = path3.resolve("lumio.config.json");
560
- if (options.botModule || fs3.existsSync(configPath)) {
1001
+ if (options.botModule || options.automationNode || fs3.existsSync(configPath)) {
561
1002
  const configExists = fs3.existsSync(configPath);
562
1003
  if (configExists) {
563
1004
  const config = parseConfigFile(fs3.readFileSync(configPath, "utf-8"));
@@ -565,7 +1006,11 @@ function registerDev(program2) {
565
1006
  await runBotModuleDevServer(config);
566
1007
  return;
567
1008
  }
568
- } else if (options.botModule) {
1009
+ if (options.automationNode || config.category === "automation_node") {
1010
+ await runAutomationNodeDevServer(config);
1011
+ return;
1012
+ }
1013
+ } else if (options.botModule || options.automationNode) {
569
1014
  console.error("lumio.config.json not found. Run `lumio init` first.");
570
1015
  process.exit(1);
571
1016
  }
@@ -581,6 +1026,10 @@ function registerDev(program2) {
581
1026
  // src/commands/build.ts
582
1027
  import * as fs4 from "fs";
583
1028
  import * as path4 from "path";
1029
+ import { execFile } from "child_process";
1030
+ import { promisify } from "util";
1031
+ import { createRequire } from "module";
1032
+ var execFileAsync = promisify(execFile);
584
1033
  function registerBuild(program2) {
585
1034
  program2.command("build").description("Bundle extension as standalone HTML + source archive").action(async () => {
586
1035
  const configPath = path4.resolve("lumio.config.json");
@@ -602,6 +1051,64 @@ function registerBuild(program2) {
602
1051
  if (config.category === "bot_module" && config.targets.length === 0) {
603
1052
  console.log(" Bot module \u2014 no UI targets to bundle.");
604
1053
  }
1054
+ if (config.server) {
1055
+ const serverEntry = ["server/handler.ts", "server/handler.js", "server/index.ts", "server/index.js"].find((f) => fs4.existsSync(path4.resolve(f)));
1056
+ if (serverEntry) {
1057
+ await build({
1058
+ entryPoints: [path4.resolve(serverEntry)],
1059
+ bundle: true,
1060
+ minify: true,
1061
+ format: "esm",
1062
+ outfile: path4.join(distDir, "server.js"),
1063
+ platform: "neutral",
1064
+ target: "es2022",
1065
+ external: ["@zaflun/lumio-sdk", "@zaflun/lumio-protocol"],
1066
+ define: {
1067
+ "process.env.NODE_ENV": '"production"'
1068
+ }
1069
+ });
1070
+ console.log(" Built: dist/server.js");
1071
+ } else {
1072
+ console.warn(" Warning: server: true but no server/handler.ts or server/index.ts found.");
1073
+ }
1074
+ }
1075
+ const cssEntryFile = ["src/globals.css", "src/style.css", "src/index.css"].find((f) => fs4.existsSync(path4.resolve(f)));
1076
+ let compiledCssPath = null;
1077
+ if (cssEntryFile) {
1078
+ const cssContent = fs4.readFileSync(path4.resolve(cssEntryFile), "utf-8");
1079
+ const usesTailwind = cssContent.includes("tailwindcss") || cssContent.includes("@tailwind");
1080
+ if (usesTailwind) {
1081
+ console.log(" Tailwind CSS v4 detected \u2014 compiling...");
1082
+ compiledCssPath = path4.join(distDir, "styles.css");
1083
+ try {
1084
+ const projectRequire = createRequire(path4.join(path4.resolve("."), "package.json"));
1085
+ const cliPkgPath = projectRequire.resolve("@tailwindcss/cli/package.json");
1086
+ const cliPkg = JSON.parse(fs4.readFileSync(cliPkgPath, "utf-8"));
1087
+ const binRel = typeof cliPkg.bin === "string" ? cliPkg.bin : cliPkg.bin?.tailwindcss ?? Object.values(cliPkg.bin ?? {})[0];
1088
+ if (!binRel) {
1089
+ throw new Error("@tailwindcss/cli has no bin entry");
1090
+ }
1091
+ const binPath = path4.join(path4.dirname(cliPkgPath), binRel);
1092
+ await execFileAsync(process.execPath, [
1093
+ binPath,
1094
+ "-i",
1095
+ path4.resolve(cssEntryFile),
1096
+ "-o",
1097
+ compiledCssPath,
1098
+ "--minify"
1099
+ ], { cwd: path4.resolve(".") });
1100
+ console.log(" Compiled: dist/styles.css");
1101
+ } catch (err) {
1102
+ console.error(" Error: Tailwind CSS compilation failed. Install tailwindcss v4: npm add -D tailwindcss@^4.3 @tailwindcss/cli@^4.3");
1103
+ console.error(` ${err instanceof Error ? err.message : String(err)}`);
1104
+ process.exit(1);
1105
+ }
1106
+ } else {
1107
+ compiledCssPath = path4.join(distDir, "styles.css");
1108
+ fs4.copyFileSync(path4.resolve(cssEntryFile), compiledCssPath);
1109
+ console.log(` Copied: ${cssEntryFile} \u2192 dist/styles.css`);
1110
+ }
1111
+ }
605
1112
  for (const target of config.targets) {
606
1113
  const entryPoint = path4.resolve("src", `${target}.tsx`);
607
1114
  if (!fs4.existsSync(entryPoint)) {
@@ -612,31 +1119,77 @@ function registerBuild(program2) {
612
1119
  entryPoints: [entryPoint],
613
1120
  bundle: true,
614
1121
  minify: true,
615
- format: "esm",
616
- outfile: path4.join(distDir, `${target}.js`),
1122
+ format: "iife",
1123
+ globalName: "LumioExtension",
1124
+ footer: {
1125
+ js: "if(typeof LumioExtension!=='undefined'&&typeof LumioExtension.default==='function'){var _L=(typeof require!=='undefined'?require('@zaflun/lumio-sdk'):{}).Lumio;if(_L)_L.render(LumioExtension.default);}"
1126
+ },
1127
+ outdir: distDir,
1128
+ entryNames: `${target}`,
617
1129
  jsx: "automatic",
618
1130
  target: "es2022",
619
- external: [],
1131
+ loader: {
1132
+ ".css": "css",
1133
+ // Asset file loaders — copies to dist/assets/ with content hash
1134
+ ".png": "file",
1135
+ ".jpg": "file",
1136
+ ".jpeg": "file",
1137
+ ".gif": "file",
1138
+ ".svg": "file",
1139
+ ".webp": "file",
1140
+ ".avif": "file",
1141
+ ".ico": "file",
1142
+ ".mp4": "file",
1143
+ ".webm": "file",
1144
+ ".ogg": "file",
1145
+ ".mp3": "file",
1146
+ ".wav": "file",
1147
+ ".woff": "file",
1148
+ ".woff2": "file",
1149
+ ".ttf": "file",
1150
+ ".otf": "file",
1151
+ ".lottie": "file"
1152
+ },
1153
+ assetNames: "assets/[name]-[hash]",
1154
+ external: ["react", "react-dom", "react/jsx-runtime", "react/jsx-dev-runtime", "@zaflun/lumio-sdk", "@zaflun/lumio-protocol"],
620
1155
  define: {
621
1156
  "process.env.NODE_ENV": '"production"'
622
1157
  }
623
1158
  });
624
1159
  console.log(` Built: dist/${target}.js`);
625
1160
  }
626
- const { default: archiver } = await import("archiver");
1161
+ const { TarArchive } = await import("archiver");
627
1162
  const sourceArchivePath = path4.join(distDir, "source.tar.gz");
628
1163
  const output = fs4.createWriteStream(sourceArchivePath);
629
- const archive = archiver("tar", { gzip: true });
1164
+ const archive = new TarArchive({ gzip: true });
630
1165
  archive.pipe(output);
631
- archive.directory("src/", "src");
632
- if ((config.server || config.category === "bot_module") && fs4.existsSync("server")) {
633
- archive.directory("server/", "server");
634
- }
635
- archive.file("lumio.config.json", { name: "lumio.config.json" });
636
- archive.file("package.json", { name: "package.json" });
637
- if (fs4.existsSync("CHANGELOG.md")) {
638
- archive.file("CHANGELOG.md", { name: "CHANGELOG.md" });
639
- }
1166
+ archive.glob("**/*", {
1167
+ cwd: path4.resolve("."),
1168
+ ignore: [
1169
+ "node_modules/**",
1170
+ "dist/**",
1171
+ ".git/**",
1172
+ ".git",
1173
+ ".next/**",
1174
+ ".lumio-dist/**",
1175
+ "*.tgz",
1176
+ "*.tar.gz",
1177
+ // Secrets & credentials must never be uploaded to the store: the
1178
+ // archive is visible to reviewers. Exclude env files (except the
1179
+ // example), npm/yarn auth, and local CLI state.
1180
+ // archiver 8's readdir-glob@3 dropped the `!pattern` un-ignore
1181
+ // feature (each ignore entry is now an independent minimatch, so a
1182
+ // leading `!` inverts that one matcher and ignores everything else).
1183
+ // Keep `.env.example` via an extglob instead of a negated entry.
1184
+ "**/.env",
1185
+ "**/.env.!(example)",
1186
+ "**/.npmrc",
1187
+ "**/.yarnrc",
1188
+ "**/.yarnrc.yml",
1189
+ "**/.lumio/**"
1190
+ ],
1191
+ dot: true
1192
+ });
640
1193
  await archive.finalize();
641
1194
  console.log(" Archive: dist/source.tar.gz");
642
1195
  console.log("Build complete.");
@@ -683,12 +1236,59 @@ function getVersionEntry(changelog, version) {
683
1236
  }
684
1237
 
685
1238
  // src/commands/deploy.ts
1239
+ function collectFiles(dir, base = dir) {
1240
+ const entries = fs5.readdirSync(dir, { withFileTypes: true });
1241
+ const files = [];
1242
+ for (const entry of entries) {
1243
+ const fullPath = path5.join(dir, entry.name);
1244
+ if (entry.isDirectory()) {
1245
+ files.push(...collectFiles(fullPath, base));
1246
+ } else if (entry.isFile()) {
1247
+ files.push(path5.relative(base, fullPath));
1248
+ }
1249
+ }
1250
+ return files;
1251
+ }
1252
+ var ALLOWED_EXTENSIONS = /* @__PURE__ */ new Set([
1253
+ "js",
1254
+ "css",
1255
+ "html",
1256
+ "map",
1257
+ "png",
1258
+ "jpg",
1259
+ "jpeg",
1260
+ "gif",
1261
+ "svg",
1262
+ "webp",
1263
+ "avif",
1264
+ "ico",
1265
+ "mp4",
1266
+ "webm",
1267
+ "ogg",
1268
+ "mp3",
1269
+ "wav",
1270
+ "m4a",
1271
+ "aac",
1272
+ "flac",
1273
+ "woff",
1274
+ "woff2",
1275
+ "ttf",
1276
+ "otf",
1277
+ "eot",
1278
+ "json",
1279
+ "lottie"
1280
+ ]);
1281
+ var AUDIO_EXTENSIONS = /* @__PURE__ */ new Set(["mp3", "wav", "ogg", "webm", "m4a", "aac", "flac"]);
1282
+ var LARGE_SOUND_THRESHOLD = 5 * 1024 * 1024;
1283
+ var MAX_FILE_SIZE = 10 * 1024 * 1024;
1284
+ var MAX_TOTAL_SIZE = 50 * 1024 * 1024;
1285
+ var MAX_FILES = 200;
686
1286
  function registerDeploy(program2) {
687
- program2.command("deploy").description("Upload extension to Lumio (status: draft)").option("-v, --version <version>", "Version override").option("-d, --description <text>", "Release notes override").option("--breaking", "Mark as breaking change").option("--draft", "Explicitly mark as draft").action(async (options) => {
1287
+ 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) => {
688
1288
  const store = new CredentialStore();
689
- const token = store.getToken();
1289
+ const token = options.token ?? store.getToken();
690
1290
  if (!token) {
691
- console.error("Not logged in. Run `lumio login` first.");
1291
+ console.error("Not logged in. Run `lumio login` or provide --token.");
692
1292
  process.exit(1);
693
1293
  }
694
1294
  const configPath = path5.resolve("lumio.config.json");
@@ -699,6 +1299,92 @@ function registerDeploy(program2) {
699
1299
  const configJson = fs5.readFileSync(configPath, "utf-8");
700
1300
  const config = parseConfigFile(configJson);
701
1301
  validateConfig(config);
1302
+ const distDir = path5.resolve("dist");
1303
+ if (config.sounds && config.sounds.length > 0) {
1304
+ for (const sound of config.sounds) {
1305
+ const soundFile = sound.file;
1306
+ const ext = soundFile.split(".").pop()?.toLowerCase() ?? "";
1307
+ if (!AUDIO_EXTENSIONS.has(ext)) {
1308
+ console.error(
1309
+ `Error: Sound file "${soundFile}" has unsupported extension ".${ext}". Allowed audio formats: ${[...AUDIO_EXTENSIONS].join(", ")}.`
1310
+ );
1311
+ process.exit(1);
1312
+ }
1313
+ const soundAbsPath = path5.join(distDir, "sounds", soundFile);
1314
+ if (fs5.existsSync(distDir) && !fs5.existsSync(soundAbsPath)) {
1315
+ console.error(
1316
+ `Error: Sound file "${soundFile}" declared in lumio.config.json was not found at dist/sounds/${soundFile}. Run \`lumio build\` first.`
1317
+ );
1318
+ process.exit(1);
1319
+ }
1320
+ if (fs5.existsSync(soundAbsPath)) {
1321
+ const stat = fs5.statSync(soundAbsPath);
1322
+ if (stat.size > LARGE_SOUND_THRESHOLD) {
1323
+ console.warn(
1324
+ ` Warning: Sound file "${soundFile}" is ${(stat.size / 1024 / 1024).toFixed(1)} MB. Consider compressing large audio files to reduce load times.`
1325
+ );
1326
+ }
1327
+ }
1328
+ }
1329
+ }
1330
+ if (config.sounds && config.sounds.length > 0) {
1331
+ const limitsApiUrl = process.env.LUMIO_API_URL ?? store.getApiUrl();
1332
+ try {
1333
+ const limitsRes = await fetch(
1334
+ `${limitsApiUrl}/v1/developer/extensions/${config.extensionId}/limits`,
1335
+ { headers: { Authorization: `Bearer ${token}` } }
1336
+ );
1337
+ if (limitsRes.ok) {
1338
+ const limitsJson = await limitsRes.json();
1339
+ const maxSounds = limitsJson.data?.max_sounds ?? 50;
1340
+ const maxFileSize = limitsJson.data?.max_sound_file_size ?? 10485760;
1341
+ const maxStorage = limitsJson.data?.max_sound_storage_bytes ?? 209715200;
1342
+ if (config.sounds.length > maxSounds) {
1343
+ console.error(
1344
+ `Error: Extension declares ${config.sounds.length} sounds, but your limit is ${maxSounds}.`
1345
+ );
1346
+ console.error(
1347
+ "Request a limit increase at /account/developer/limits or contact support."
1348
+ );
1349
+ process.exit(1);
1350
+ }
1351
+ let totalSize2 = 0;
1352
+ for (const sound of config.sounds) {
1353
+ const soundPath = path5.resolve("dist", "sounds", sound.file);
1354
+ if (fs5.existsSync(soundPath)) {
1355
+ const size = fs5.statSync(soundPath).size;
1356
+ totalSize2 += size;
1357
+ if (size > maxFileSize) {
1358
+ console.error(
1359
+ `Error: Sound file "${sound.file}" is ${(size / 1024 / 1024).toFixed(1)} MB but your per-file limit is ${(maxFileSize / 1024 / 1024).toFixed(1)} MB.`
1360
+ );
1361
+ console.error(
1362
+ "Request a limit increase at /account/developer/limits or contact support."
1363
+ );
1364
+ process.exit(1);
1365
+ }
1366
+ }
1367
+ }
1368
+ if (totalSize2 > maxStorage) {
1369
+ console.error(
1370
+ `Error: Total sound size is ${(totalSize2 / 1024 / 1024).toFixed(1)} MB but your storage limit is ${(maxStorage / 1024 / 1024).toFixed(1)} MB.`
1371
+ );
1372
+ console.error(
1373
+ "Request a limit increase at /account/developer/limits or contact support."
1374
+ );
1375
+ process.exit(1);
1376
+ }
1377
+ } else if (limitsRes.status !== 404) {
1378
+ console.warn(
1379
+ ` Warning: Could not fetch sound limits (HTTP ${limitsRes.status}), skipping pre-flight check.`
1380
+ );
1381
+ }
1382
+ } catch {
1383
+ console.warn(
1384
+ " Warning: Could not reach API for sound limit check, skipping pre-flight check."
1385
+ );
1386
+ }
1387
+ }
702
1388
  const version = options.version ?? config.version;
703
1389
  let releaseNotes = options.description ?? "";
704
1390
  if (!releaseNotes) {
@@ -708,44 +1394,97 @@ function registerDeploy(program2) {
708
1394
  const entry = getVersionEntry(changelog, version);
709
1395
  if (entry) {
710
1396
  releaseNotes = entry.content;
711
- } else if (!LumioApiClient.isCI()) {
1397
+ } else if (!LumioApiClient.isCI() && !options.system) {
712
1398
  console.error(`No changelog entry found for version ${version}.`);
713
1399
  console.error("Add an entry to CHANGELOG.md or use -d to provide release notes.");
714
1400
  process.exit(1);
715
1401
  }
716
1402
  }
717
1403
  }
718
- const distDir = path5.resolve("dist");
719
1404
  if (!fs5.existsSync(distDir)) {
720
1405
  console.error("No dist/ directory found. Run `lumio build` first.");
721
1406
  process.exit(1);
722
1407
  }
723
1408
  console.log(`Deploying ${config.name} v${version}...`);
724
- const client = new LumioApiClient(store.getApiUrl(), token);
725
- const formData = new FormData();
726
- const sourceArchive = path5.join(distDir, "source.tar.gz");
727
- if (fs5.existsSync(sourceArchive)) {
728
- const sourceBuffer = fs5.readFileSync(sourceArchive);
729
- formData.append("source", new Blob([sourceBuffer]), "source.tar.gz");
1409
+ const apiUrl = process.env.LUMIO_API_URL ?? store.getApiUrl();
1410
+ const client = new LumioApiClient(apiUrl, token);
1411
+ const allFiles = collectFiles(distDir);
1412
+ const sourceFile = "source.tar.gz";
1413
+ const bundleFiles = allFiles.filter((f) => {
1414
+ if (f === sourceFile) return false;
1415
+ if (f === "bundle.zip") return false;
1416
+ const ext = f.split(".").pop()?.toLowerCase() ?? "";
1417
+ return ALLOWED_EXTENSIONS.has(ext);
1418
+ });
1419
+ if (bundleFiles.length === 0) {
1420
+ console.error("No uploadable files found in dist/. Run `lumio build` first.");
1421
+ process.exit(1);
1422
+ }
1423
+ if (bundleFiles.length > MAX_FILES) {
1424
+ console.error(`Too many files (${bundleFiles.length}). Maximum is ${MAX_FILES}.`);
1425
+ process.exit(1);
1426
+ }
1427
+ const sourceArchive = path5.join(distDir, sourceFile);
1428
+ if (!fs5.existsSync(sourceArchive)) {
1429
+ console.error("Error: source.tar.gz not found in dist/. Run `lumio build` first.");
1430
+ process.exit(1);
730
1431
  }
1432
+ const formData = new FormData();
731
1433
  const manifest = JSON.parse(configJson);
732
1434
  manifest.version = version;
733
1435
  formData.append("version", version);
734
1436
  formData.append("manifest", JSON.stringify(manifest));
735
1437
  formData.append("release_notes", releaseNotes);
736
1438
  formData.append("is_breaking", String(!!options.breaking));
1439
+ const sourceBuffer = fs5.readFileSync(sourceArchive);
1440
+ if (sourceBuffer.length === 0) {
1441
+ console.error("Error: source.tar.gz is empty. Run `lumio build` to regenerate.");
1442
+ process.exit(1);
1443
+ }
1444
+ formData.append("source", new Blob([sourceBuffer]), "source.tar.gz");
1445
+ console.log(` Source: source.tar.gz (${(sourceBuffer.length / 1024).toFixed(1)} KB)`);
1446
+ let totalSize = 0;
1447
+ for (const relPath of bundleFiles) {
1448
+ const absPath = path5.join(distDir, relPath);
1449
+ const buffer = fs5.readFileSync(absPath);
1450
+ if (buffer.length === 0) {
1451
+ console.warn(` Warning: Skipping empty file: ${relPath}`);
1452
+ continue;
1453
+ }
1454
+ if (buffer.length > MAX_FILE_SIZE) {
1455
+ console.error(`Error: File '${relPath}' exceeds ${MAX_FILE_SIZE / 1024 / 1024} MB limit.`);
1456
+ process.exit(1);
1457
+ }
1458
+ totalSize += buffer.length;
1459
+ if (totalSize > MAX_TOTAL_SIZE) {
1460
+ console.error(`Error: Total bundle size exceeds ${MAX_TOTAL_SIZE / 1024 / 1024} MB limit.`);
1461
+ process.exit(1);
1462
+ }
1463
+ formData.append("files", new Blob([buffer]), relPath);
1464
+ console.log(` File: ${relPath} (${(buffer.length / 1024).toFixed(1)} KB)`);
1465
+ }
1466
+ console.log(` Total: ${bundleFiles.length} files (${(totalSize / 1024).toFixed(1)} KB)`);
1467
+ console.log(" Uploading...");
737
1468
  try {
738
- await client.uploadFile(
739
- `/v1/developer/extensions/${config.extensionId}/versions`,
740
- formData
741
- );
742
- console.log("Upload successful. Status: draft");
743
- console.log("Submit for review via the dashboard or `lumio status`.");
1469
+ const uploadPath = options.system ? `/v1/system/extensions/${config.extensionId}/versions/upload` : `/v1/developer/extensions/${config.extensionId}/versions/upload`;
1470
+ await client.uploadFile(uploadPath, formData);
1471
+ console.log("\n Upload successful. Status: draft");
1472
+ console.log(" Submit for review via the dashboard or `lumio status`.");
744
1473
  if (LumioApiClient.isCI()) {
745
1474
  process.exit(0);
746
1475
  }
747
1476
  } catch (err) {
748
- console.error("Deploy failed:", err);
1477
+ const status = err.status;
1478
+ const message = err.message ?? String(err);
1479
+ console.error(`
1480
+ Deploy failed${status ? ` (HTTP ${status})` : ""}: ${message}`);
1481
+ if (status === 409) {
1482
+ console.error(" This version already exists. Bump the version in lumio.config.json.");
1483
+ } else if (status === 413) {
1484
+ console.error(" Bundle or source archive exceeds the maximum upload size.");
1485
+ } else if (status === 401 || status === 403) {
1486
+ console.error(" Authentication failed. Run `lumio login` to refresh your token.");
1487
+ }
749
1488
  process.exit(1);
750
1489
  }
751
1490
  });
@@ -876,16 +1615,27 @@ function requireClient() {
876
1615
  }
877
1616
  function registerEnv(program2) {
878
1617
  const envCmd = program2.command("env").description("Manage server-side secrets for the extension");
879
- envCmd.command("set <key> <value>").description("Set a secret (POST /v1/developer/extensions/{id}/secrets)").action(async (key, value) => {
1618
+ envCmd.command("set <key> [value]").description("Set a secret (POST /v1/developer/extensions/{id}/secrets). Omit <value> to be prompted (hidden) or read it from the LUMIO_SECRET env var \u2014 avoids leaking the secret into shell history / the process list.").action(async (key, value) => {
880
1619
  if (!validateSecretKey(key)) {
881
1620
  console.error(
882
1621
  "Invalid key format. Use uppercase letters, numbers, and underscores only (e.g. API_KEY)."
883
1622
  );
884
1623
  process.exit(1);
885
1624
  }
1625
+ let secretValue = value ?? process.env.LUMIO_SECRET;
1626
+ if (secretValue === void 0) {
1627
+ const { default: inquirer } = await import("inquirer");
1628
+ const answer = await inquirer.prompt([{
1629
+ type: "password",
1630
+ name: "value",
1631
+ mask: "*",
1632
+ message: `Value for ${key}:`
1633
+ }]);
1634
+ secretValue = answer.value;
1635
+ }
886
1636
  const config = loadConfig();
887
1637
  const client = requireClient();
888
- await client.post(`/v1/developer/extensions/${config.extensionId}/secrets`, { key, value });
1638
+ await client.post(`/v1/developer/extensions/${config.extensionId}/secrets`, { key, value: secretValue });
889
1639
  console.log(`Secret ${key} set.`);
890
1640
  });
891
1641
  envCmd.command("list").description("List all secrets (keys only, no values)").action(async () => {
@@ -899,7 +1649,7 @@ function registerEnv(program2) {
899
1649
  envCmd.command("delete <key>").description("Delete a secret").action(async (key) => {
900
1650
  const config = loadConfig();
901
1651
  const client = requireClient();
902
- await client.delete(`/v1/developer/extensions/${config.extensionId}/secrets/${key}`);
1652
+ await client.delete(`/v1/developer/extensions/${config.extensionId}/secrets/${encodeURIComponent(key)}`);
903
1653
  console.log(`Secret ${key} deleted.`);
904
1654
  });
905
1655
  }
@@ -956,7 +1706,17 @@ function registerRun(program2) {
956
1706
  process.exit(1);
957
1707
  }
958
1708
  const client = new LumioApiClient(store.getApiUrl(), token);
959
- const args = argsJson ? JSON.parse(argsJson) : {};
1709
+ let args = {};
1710
+ if (argsJson) {
1711
+ try {
1712
+ args = JSON.parse(argsJson);
1713
+ } catch (err) {
1714
+ console.error(
1715
+ `Invalid JSON for args: ${err instanceof Error ? err.message : String(err)}`
1716
+ );
1717
+ process.exit(1);
1718
+ }
1719
+ }
960
1720
  const body = { type: fnRef.type, args };
961
1721
  if (BOT_HANDLER_TYPES.includes(fnRef.type)) {
962
1722
  body.user = {
@@ -975,7 +1735,7 @@ function registerRun(program2) {
975
1735
  }
976
1736
  try {
977
1737
  const result = await client.post(
978
- `/v1/extension-installs/${config.extensionId}/execute/${fnRef.name}`,
1738
+ `/v1/extension-installs/${config.extensionId}/execute/${encodeURIComponent(fnRef.name)}`,
979
1739
  body
980
1740
  );
981
1741
  console.log(JSON.stringify(result, null, 2));
@@ -1010,7 +1770,9 @@ function createProgram() {
1010
1770
  const program2 = new Command();
1011
1771
  program2.name("lumio");
1012
1772
  program2.description("Lumio Extension CLI -- Build, test, and deploy Lumio extensions");
1013
- program2.version("2026.5.1");
1773
+ const require2 = createRequire2(import.meta.url);
1774
+ const { version } = require2("../package.json");
1775
+ program2.version(version);
1014
1776
  registerLogin(program2);
1015
1777
  registerLogout(program2);
1016
1778
  registerWhoami(program2);