@zaflun/lumio-cli 0.1.0 → 0.3.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.
Files changed (2) hide show
  1. package/dist/index.js +336 -97
  2. package/package.json +2 -2
package/dist/index.js CHANGED
@@ -1,6 +1,10 @@
1
1
  // src/program.ts
2
2
  import { Command } from "commander";
3
3
 
4
+ // src/commands/login.ts
5
+ import * as http from "http";
6
+ import * as crypto from "crypto";
7
+
4
8
  // src/auth/credential-store.ts
5
9
  import * as fs from "fs";
6
10
  import * as path from "path";
@@ -54,6 +58,11 @@ var CredentialStore = class {
54
58
  };
55
59
 
56
60
  // src/commands/login.ts
61
+ function generatePkceChallenge() {
62
+ const verifier = crypto.randomBytes(32).toString("base64url");
63
+ const challenge = crypto.createHash("sha256").update(verifier).digest("base64url");
64
+ return { verifier, challenge };
65
+ }
57
66
  function registerLogin(program2) {
58
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) => {
59
68
  const store = new CredentialStore();
@@ -62,12 +71,69 @@ function registerLogin(program2) {
62
71
  console.log("Authenticated with token.");
63
72
  return;
64
73
  }
65
- console.log("Opening browser for authentication...");
66
- const { default: open } = await import("open");
67
- const pkceUrl = `${options.apiUrl}/v1/auth/cli/authorize`;
68
- await open(pkceUrl);
69
- console.log("Waiting for authentication callback...");
70
- console.log("If the browser did not open, visit:", pkceUrl);
74
+ const { verifier, challenge } = generatePkceChallenge();
75
+ const state = crypto.randomBytes(16).toString("hex");
76
+ const port = 9876;
77
+ const redirectUri = `http://localhost:${port}/callback`;
78
+ await new Promise((resolve10, reject) => {
79
+ const server = http.createServer(async (req, res) => {
80
+ if (!req.url?.startsWith("/callback")) {
81
+ res.writeHead(404);
82
+ res.end();
83
+ return;
84
+ }
85
+ const url = new URL(req.url, `http://localhost:${port}`);
86
+ const code = url.searchParams.get("code");
87
+ const returnedState = url.searchParams.get("state");
88
+ if (returnedState !== state || !code) {
89
+ res.writeHead(400, { "Content-Type": "text/html" });
90
+ 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"));
93
+ return;
94
+ }
95
+ try {
96
+ const tokenRes = await fetch(`${options.apiUrl}/v1/auth/cli/token`, {
97
+ method: "POST",
98
+ headers: { "Content-Type": "application/json" },
99
+ body: JSON.stringify({
100
+ grant_type: "authorization_code",
101
+ code,
102
+ redirect_uri: redirectUri,
103
+ code_verifier: verifier
104
+ })
105
+ });
106
+ if (!tokenRes.ok) {
107
+ throw new Error(`Token exchange failed: ${tokenRes.status}`);
108
+ }
109
+ const tokenData = await tokenRes.json();
110
+ store.saveToken(tokenData.access_token, options.apiUrl);
111
+ res.writeHead(200, { "Content-Type": "text/html" });
112
+ res.end("<html><body><h2>Authenticated!</h2><p>You can close this window and return to the CLI.</p></body></html>");
113
+ } catch (e) {
114
+ res.writeHead(500, { "Content-Type": "text/html" });
115
+ 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);
118
+ return;
119
+ }
120
+ server.close();
121
+ resolve10();
122
+ });
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}`;
125
+ console.log("Opening browser for authentication...");
126
+ const { default: open } = await import("open");
127
+ await open(pkceUrl);
128
+ console.log("Waiting for authentication callback...");
129
+ console.log("If the browser did not open, visit:", pkceUrl);
130
+ });
131
+ setTimeout(() => {
132
+ server.close();
133
+ reject(new Error("Authentication timed out after 120 seconds"));
134
+ }, 12e4);
135
+ });
136
+ console.log("Authenticated successfully.");
71
137
  });
72
138
  }
73
139
 
@@ -222,7 +288,7 @@ function registerInit(program2) {
222
288
  }
223
289
  } catch {
224
290
  }
225
- const answers = await inquirer.prompt([
291
+ const baseAnswers = await inquirer.prompt([
226
292
  { type: "input", name: "name", message: "Extension name:", default: name },
227
293
  { type: "input", name: "description", message: "Description:" },
228
294
  {
@@ -230,16 +296,35 @@ function registerInit(program2) {
230
296
  name: "category",
231
297
  message: "Category:",
232
298
  choices: ["widget", "extension", "overlay", "bot_module", "integration", "theme"]
233
- },
234
- {
235
- type: "checkbox",
236
- name: "targets",
237
- message: "Targets:",
238
- choices: ["editor", "layer", "interactive"],
239
- default: ["editor", "layer"]
240
- },
241
- { type: "confirm", name: "server", message: "Server functions?", default: false }
299
+ }
242
300
  ]);
301
+ const answers = { ...baseAnswers, targets: [], server: false };
302
+ if (answers.category === "bot_module") {
303
+ const { platforms } = await inquirer.prompt([{
304
+ type: "checkbox",
305
+ name: "platforms",
306
+ message: "Which platforms should this bot module support?",
307
+ choices: ["twitch", "youtube", "kick", "trovo", "discord"],
308
+ default: ["twitch"],
309
+ validate: (input) => input.length > 0 || "Select at least one platform"
310
+ }]);
311
+ answers.platforms = platforms;
312
+ answers.targets = [];
313
+ answers.server = true;
314
+ } else {
315
+ const targetAndServer = await inquirer.prompt([
316
+ {
317
+ type: "checkbox",
318
+ name: "targets",
319
+ message: "Targets:",
320
+ choices: ["editor", "layer", "interactive"],
321
+ default: ["editor", "layer"]
322
+ },
323
+ { type: "confirm", name: "server", message: "Server functions?", default: false }
324
+ ]);
325
+ answers.targets = targetAndServer.targets;
326
+ answers.server = targetAndServer.server;
327
+ }
243
328
  const createBody = {
244
329
  name: answers.name,
245
330
  description: answers.description,
@@ -253,7 +338,7 @@ function registerInit(program2) {
253
338
  const projectDir = path2.resolve(answers.name.toLowerCase().replace(/\s+/g, "-"));
254
339
  fs2.mkdirSync(projectDir, { recursive: true });
255
340
  const config = {
256
- $schema: "https://docs.lumio.vision/schemas/lumio.config.schema.json",
341
+ $schema: "https://lumio.vision/schemas/lumio.config.schema.json",
257
342
  extensionId: extension.id,
258
343
  name: answers.name,
259
344
  description: answers.description,
@@ -261,11 +346,21 @@ function registerInit(program2) {
261
346
  version: "0.1.0",
262
347
  targets: answers.targets,
263
348
  server: answers.server,
264
- permissions: [],
349
+ permissions: answers.category === "bot_module" ? ["chat:read", "chat:send"] : [],
265
350
  egress: { allowHosts: [], allowPorts: [443] },
266
351
  visibility: "private",
267
352
  pricing: { type: "free" }
268
353
  };
354
+ if (answers.category === "bot_module") {
355
+ config.platforms = answers.platforms;
356
+ config.triggers = {
357
+ commands: [{ name: "hello", description: "Say hello", cooldownGlobal: 5, cooldownUser: 10, minRole: "everyone" }],
358
+ keywords: [],
359
+ patterns: [],
360
+ events: [],
361
+ timers: []
362
+ };
363
+ }
269
364
  fs2.writeFileSync(
270
365
  path2.join(projectDir, "lumio.config.json"),
271
366
  JSON.stringify(config, null, 2)
@@ -298,8 +393,15 @@ function registerInit(program2) {
298
393
  `;
299
394
  fs2.writeFileSync(path2.join(projectDir, "CHANGELOG.md"), changelog);
300
395
  fs2.mkdirSync(path2.join(projectDir, "src"), { recursive: true });
301
- for (const target of answers.targets) {
302
- const content = `import { Lumio, CompactView, Text } from "@zaflun/lumio-sdk";
396
+ if (answers.category === "bot_module") {
397
+ fs2.mkdirSync(path2.join(projectDir, "server"), { recursive: true });
398
+ 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
+ fs2.writeFileSync(path2.join(projectDir, "server", "functions.ts"), serverFunctions);
400
+ const schema = 'import { defineSchema } from "@zaflun/lumio-sdk/server";\n\nexport default defineSchema({});\n';
401
+ fs2.writeFileSync(path2.join(projectDir, "server", "schema.ts"), schema);
402
+ } else {
403
+ for (const target of answers.targets) {
404
+ const content = `import { Lumio, CompactView, Text } from "@zaflun/lumio-sdk";
303
405
 
304
406
  function App() {
305
407
  return (
@@ -311,14 +413,15 @@ function App() {
311
413
 
312
414
  Lumio.render(<App />, { target: "${target}" });
313
415
  `;
314
- fs2.writeFileSync(path2.join(projectDir, "src", `${target}.tsx`), content);
315
- }
316
- if (answers.server) {
317
- fs2.mkdirSync(path2.join(projectDir, "server"), { recursive: true });
318
- 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';
319
- fs2.writeFileSync(path2.join(projectDir, "server", "schema.ts"), schema);
320
- 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';
321
- fs2.writeFileSync(path2.join(projectDir, "server", "functions.ts"), functions);
416
+ fs2.writeFileSync(path2.join(projectDir, "src", `${target}.tsx`), content);
417
+ }
418
+ if (answers.server) {
419
+ fs2.mkdirSync(path2.join(projectDir, "server"), { recursive: true });
420
+ 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';
421
+ fs2.writeFileSync(path2.join(projectDir, "server", "schema.ts"), schema);
422
+ 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';
423
+ fs2.writeFileSync(path2.join(projectDir, "server", "functions.ts"), functions);
424
+ }
322
425
  }
323
426
  console.log(`
324
427
  Done! Created ${projectDir}/`);
@@ -327,17 +430,6 @@ Done! Created ${projectDir}/`);
327
430
  }
328
431
 
329
432
  // src/commands/dev.ts
330
- function registerDev(program2) {
331
- program2.command("dev").description("Start local dev server with hot-reload and mock events").option("-p, --port <port>", "Dev server port", "3100").action(async (options) => {
332
- console.log(`Starting Lumio dev server on port ${options.port}...`);
333
- console.log("Mock events enabled. Hot-reload active.");
334
- console.log("Open http://localhost:" + options.port + " in your browser.");
335
- console.log("\nDev server implementation requires @zaflun/lumio-sdk and Vite.");
336
- console.log("Full implementation coming in Phase 12 (Live-Code-Editor).");
337
- });
338
- }
339
-
340
- // src/commands/build.ts
341
433
  import * as fs3 from "fs";
342
434
  import * as path3 from "path";
343
435
 
@@ -363,6 +455,8 @@ function parseConfigFile(jsonString) {
363
455
  allowHosts: raw.egress?.allowHosts ?? [],
364
456
  allowPorts: raw.egress?.allowPorts ?? []
365
457
  },
458
+ platforms: raw.platforms,
459
+ triggers: raw.triggers,
366
460
  fonts: raw.fonts,
367
461
  visibility: raw.visibility ?? "private",
368
462
  pricing: raw.pricing ?? { type: "free" }
@@ -375,7 +469,11 @@ function validateConfig(config) {
375
469
  if (!config.name) {
376
470
  throw new ConfigError("name is required in lumio.config.json");
377
471
  }
378
- if (!config.targets || config.targets.length === 0) {
472
+ if (config.category === "bot_module") {
473
+ 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)");
475
+ }
476
+ } else if (!config.targets || config.targets.length === 0) {
379
477
  throw new ConfigError("At least one target is required (editor, layer, or interactive)");
380
478
  }
381
479
  if (!config.version) {
@@ -383,28 +481,130 @@ function validateConfig(config) {
383
481
  }
384
482
  }
385
483
 
484
+ // src/commands/dev-bot-module.ts
485
+ import * as readline from "readline";
486
+ import chalk from "chalk";
487
+ function parseUserPrefix(input) {
488
+ const defaultUser = {
489
+ id: "user-001",
490
+ name: "dev_user",
491
+ displayName: "Dev User",
492
+ platform: "twitch",
493
+ role: "everyone",
494
+ isMod: false,
495
+ isVip: false,
496
+ isSub: false
497
+ };
498
+ if (!input.startsWith("@")) {
499
+ return { user: defaultUser, message: input };
500
+ }
501
+ const spaceIdx = input.indexOf(" ");
502
+ if (spaceIdx === -1) return { user: defaultUser, message: input };
503
+ const prefix = input.substring(1, spaceIdx);
504
+ const message = input.substring(spaceIdx + 1);
505
+ const parts = prefix.split(":");
506
+ const name = parts[parts.length - 1];
507
+ const rolePrefix = parts.length > 1 ? parts[0] : "everyone";
508
+ return {
509
+ user: {
510
+ id: `user-${name}`,
511
+ name,
512
+ displayName: name,
513
+ platform: "twitch",
514
+ role: rolePrefix,
515
+ isMod: rolePrefix === "mod",
516
+ isVip: rolePrefix === "vip",
517
+ isSub: ["sub", "vip", "mod", "broadcaster"].includes(rolePrefix)
518
+ },
519
+ message
520
+ };
521
+ }
522
+ async function runBotModuleDevServer(config) {
523
+ console.log(chalk.bold("\n\u{1F916} Bot Module Dev Server"));
524
+ console.log(`Extension: ${config.name}`);
525
+ console.log(`Platforms: ${config.platforms?.join(", ") ?? "none"}`);
526
+ console.log(chalk.dim("\nType chat messages. Prefix with @username or @role:username."));
527
+ console.log(chalk.dim("Examples: !hello | @sub:viewer !points 100 | @mod:admin gg"));
528
+ console.log(chalk.dim("Type !quit to exit.\n"));
529
+ const rl = readline.createInterface({
530
+ input: process.stdin,
531
+ output: process.stdout,
532
+ prompt: chalk.cyan("> ")
533
+ });
534
+ rl.prompt();
535
+ rl.on("line", async (line) => {
536
+ const trimmed = line.trim();
537
+ if (!trimmed) {
538
+ rl.prompt();
539
+ return;
540
+ }
541
+ if (trimmed === "!quit") {
542
+ rl.close();
543
+ return;
544
+ }
545
+ const { user, message } = parseUserPrefix(trimmed);
546
+ console.log(chalk.dim(` [${user.role}:${user.name}] ${message}`));
547
+ console.log(chalk.yellow(" \u2192 Handler execution not yet wired (requires esbuild + V8)"));
548
+ rl.prompt();
549
+ });
550
+ rl.on("close", () => {
551
+ console.log(chalk.dim("\nBot Module Dev Server stopped."));
552
+ process.exit(0);
553
+ });
554
+ }
555
+
556
+ // src/commands/dev.ts
557
+ 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) => {
559
+ const configPath = path3.resolve("lumio.config.json");
560
+ if (options.botModule || fs3.existsSync(configPath)) {
561
+ const configExists = fs3.existsSync(configPath);
562
+ if (configExists) {
563
+ const config = parseConfigFile(fs3.readFileSync(configPath, "utf-8"));
564
+ if (options.botModule || config.category === "bot_module") {
565
+ await runBotModuleDevServer(config);
566
+ return;
567
+ }
568
+ } else if (options.botModule) {
569
+ console.error("lumio.config.json not found. Run `lumio init` first.");
570
+ process.exit(1);
571
+ }
572
+ }
573
+ console.log(`Starting Lumio dev server on port ${options.port}...`);
574
+ console.log("Mock events enabled. Hot-reload active.");
575
+ console.log("Open http://localhost:" + options.port + " in your browser.");
576
+ console.log("\nDev server implementation requires @zaflun/lumio-sdk and Vite.");
577
+ console.log("Full implementation coming in Phase 12 (Live-Code-Editor).");
578
+ });
579
+ }
580
+
386
581
  // src/commands/build.ts
582
+ import * as fs4 from "fs";
583
+ import * as path4 from "path";
387
584
  function registerBuild(program2) {
388
585
  program2.command("build").description("Bundle extension as standalone HTML + source archive").action(async () => {
389
- const configPath = path3.resolve("lumio.config.json");
390
- if (!fs3.existsSync(configPath)) {
586
+ const configPath = path4.resolve("lumio.config.json");
587
+ if (!fs4.existsSync(configPath)) {
391
588
  console.error("lumio.config.json not found. Run `lumio init` first.");
392
589
  process.exit(1);
393
590
  }
394
- const configJson = fs3.readFileSync(configPath, "utf-8");
591
+ const configJson = fs4.readFileSync(configPath, "utf-8");
395
592
  const config = parseConfigFile(configJson);
396
593
  validateConfig(config);
397
- const changelogPath = path3.resolve("CHANGELOG.md");
398
- if (!fs3.existsSync(changelogPath)) {
594
+ const changelogPath = path4.resolve("CHANGELOG.md");
595
+ if (!fs4.existsSync(changelogPath)) {
399
596
  console.warn("Warning: No CHANGELOG.md found. It is recommended to maintain a changelog.");
400
597
  }
401
598
  console.log(`Building ${config.name} v${config.version}...`);
402
- const distDir = path3.resolve("dist");
403
- fs3.mkdirSync(distDir, { recursive: true });
599
+ const distDir = path4.resolve("dist");
600
+ fs4.mkdirSync(distDir, { recursive: true });
404
601
  const { build } = await import("esbuild");
602
+ if (config.category === "bot_module" && config.targets.length === 0) {
603
+ console.log(" Bot module \u2014 no UI targets to bundle.");
604
+ }
405
605
  for (const target of config.targets) {
406
- const entryPoint = path3.resolve("src", `${target}.tsx`);
407
- if (!fs3.existsSync(entryPoint)) {
606
+ const entryPoint = path4.resolve("src", `${target}.tsx`);
607
+ if (!fs4.existsSync(entryPoint)) {
408
608
  console.warn(`Warning: Entry point ${entryPoint} not found, skipping target "${target}".`);
409
609
  continue;
410
610
  }
@@ -413,7 +613,7 @@ function registerBuild(program2) {
413
613
  bundle: true,
414
614
  minify: true,
415
615
  format: "esm",
416
- outfile: path3.join(distDir, `${target}.js`),
616
+ outfile: path4.join(distDir, `${target}.js`),
417
617
  jsx: "automatic",
418
618
  target: "es2022",
419
619
  external: [],
@@ -424,17 +624,17 @@ function registerBuild(program2) {
424
624
  console.log(` Built: dist/${target}.js`);
425
625
  }
426
626
  const { default: archiver } = await import("archiver");
427
- const sourceArchivePath = path3.join(distDir, "source.tar.gz");
428
- const output = fs3.createWriteStream(sourceArchivePath);
627
+ const sourceArchivePath = path4.join(distDir, "source.tar.gz");
628
+ const output = fs4.createWriteStream(sourceArchivePath);
429
629
  const archive = archiver("tar", { gzip: true });
430
630
  archive.pipe(output);
431
631
  archive.directory("src/", "src");
432
- if (config.server && fs3.existsSync("server")) {
632
+ if ((config.server || config.category === "bot_module") && fs4.existsSync("server")) {
433
633
  archive.directory("server/", "server");
434
634
  }
435
635
  archive.file("lumio.config.json", { name: "lumio.config.json" });
436
636
  archive.file("package.json", { name: "package.json" });
437
- if (fs3.existsSync("CHANGELOG.md")) {
637
+ if (fs4.existsSync("CHANGELOG.md")) {
438
638
  archive.file("CHANGELOG.md", { name: "CHANGELOG.md" });
439
639
  }
440
640
  await archive.finalize();
@@ -444,8 +644,8 @@ function registerBuild(program2) {
444
644
  }
445
645
 
446
646
  // src/commands/deploy.ts
447
- import * as fs4 from "fs";
448
- import * as path4 from "path";
647
+ import * as fs5 from "fs";
648
+ import * as path5 from "path";
449
649
 
450
650
  // src/changelog/parser.ts
451
651
  var VERSION_HEADER_REGEX = /^## \[([^\]]+)\](?:\s*-\s*(\d{4}-\d{2}-\d{2}))?/;
@@ -491,20 +691,20 @@ function registerDeploy(program2) {
491
691
  console.error("Not logged in. Run `lumio login` first.");
492
692
  process.exit(1);
493
693
  }
494
- const configPath = path4.resolve("lumio.config.json");
495
- if (!fs4.existsSync(configPath)) {
694
+ const configPath = path5.resolve("lumio.config.json");
695
+ if (!fs5.existsSync(configPath)) {
496
696
  console.error("lumio.config.json not found. Run `lumio init` first.");
497
697
  process.exit(1);
498
698
  }
499
- const configJson = fs4.readFileSync(configPath, "utf-8");
699
+ const configJson = fs5.readFileSync(configPath, "utf-8");
500
700
  const config = parseConfigFile(configJson);
501
701
  validateConfig(config);
502
702
  const version = options.version ?? config.version;
503
703
  let releaseNotes = options.description ?? "";
504
704
  if (!releaseNotes) {
505
- const changelogPath = path4.resolve("CHANGELOG.md");
506
- if (fs4.existsSync(changelogPath)) {
507
- const changelog = fs4.readFileSync(changelogPath, "utf-8");
705
+ const changelogPath = path5.resolve("CHANGELOG.md");
706
+ if (fs5.existsSync(changelogPath)) {
707
+ const changelog = fs5.readFileSync(changelogPath, "utf-8");
508
708
  const entry = getVersionEntry(changelog, version);
509
709
  if (entry) {
510
710
  releaseNotes = entry.content;
@@ -515,21 +715,23 @@ function registerDeploy(program2) {
515
715
  }
516
716
  }
517
717
  }
518
- const distDir = path4.resolve("dist");
519
- if (!fs4.existsSync(distDir)) {
718
+ const distDir = path5.resolve("dist");
719
+ if (!fs5.existsSync(distDir)) {
520
720
  console.error("No dist/ directory found. Run `lumio build` first.");
521
721
  process.exit(1);
522
722
  }
523
723
  console.log(`Deploying ${config.name} v${version}...`);
524
724
  const client = new LumioApiClient(store.getApiUrl(), token);
525
725
  const formData = new FormData();
526
- const sourceArchive = path4.join(distDir, "source.tar.gz");
527
- if (fs4.existsSync(sourceArchive)) {
528
- const sourceBuffer = fs4.readFileSync(sourceArchive);
726
+ const sourceArchive = path5.join(distDir, "source.tar.gz");
727
+ if (fs5.existsSync(sourceArchive)) {
728
+ const sourceBuffer = fs5.readFileSync(sourceArchive);
529
729
  formData.append("source", new Blob([sourceBuffer]), "source.tar.gz");
530
730
  }
731
+ const manifest = JSON.parse(configJson);
732
+ manifest.version = version;
531
733
  formData.append("version", version);
532
- formData.append("manifest", configJson);
734
+ formData.append("manifest", JSON.stringify(manifest));
533
735
  formData.append("release_notes", releaseNotes);
534
736
  formData.append("is_breaking", String(!!options.breaking));
535
737
  try {
@@ -550,8 +752,8 @@ function registerDeploy(program2) {
550
752
  }
551
753
 
552
754
  // src/commands/status.ts
553
- import * as fs5 from "fs";
554
- import * as path5 from "path";
755
+ import * as fs6 from "fs";
756
+ import * as path6 from "path";
555
757
  function registerStatus(program2) {
556
758
  program2.command("status").description("Show review status of the latest version").action(async () => {
557
759
  const store = new CredentialStore();
@@ -560,12 +762,12 @@ function registerStatus(program2) {
560
762
  console.error("Not logged in. Run `lumio login` first.");
561
763
  process.exit(1);
562
764
  }
563
- const configPath = path5.resolve("lumio.config.json");
564
- if (!fs5.existsSync(configPath)) {
765
+ const configPath = path6.resolve("lumio.config.json");
766
+ if (!fs6.existsSync(configPath)) {
565
767
  console.error("lumio.config.json not found.");
566
768
  process.exit(1);
567
769
  }
568
- const config = parseConfigFile(fs5.readFileSync(configPath, "utf-8"));
770
+ const config = parseConfigFile(fs6.readFileSync(configPath, "utf-8"));
569
771
  const client = new LumioApiClient(store.getApiUrl(), token);
570
772
  try {
571
773
  const versions = await client.get(
@@ -594,8 +796,8 @@ function registerStatus(program2) {
594
796
  }
595
797
 
596
798
  // src/commands/logs.ts
597
- import * as fs6 from "fs";
598
- import * as path6 from "path";
799
+ import * as fs7 from "fs";
800
+ import * as path7 from "path";
599
801
  function registerLogs(program2) {
600
802
  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) => {
601
803
  const store = new CredentialStore();
@@ -604,12 +806,12 @@ function registerLogs(program2) {
604
806
  console.error("Not logged in. Run `lumio login` first.");
605
807
  process.exit(1);
606
808
  }
607
- const configPath = path6.resolve("lumio.config.json");
608
- if (!fs6.existsSync(configPath)) {
809
+ const configPath = path7.resolve("lumio.config.json");
810
+ if (!fs7.existsSync(configPath)) {
609
811
  console.error("lumio.config.json not found.");
610
812
  process.exit(1);
611
813
  }
612
- const config = parseConfigFile(fs6.readFileSync(configPath, "utf-8"));
814
+ const config = parseConfigFile(fs7.readFileSync(configPath, "utf-8"));
613
815
  const apiUrl = store.getApiUrl();
614
816
  const logsUrl = `${apiUrl}/v1/developer/extensions/${config.extensionId}/logs?lines=${options.lines}`;
615
817
  console.log(`Streaming logs for ${config.name}...`);
@@ -646,8 +848,8 @@ function registerLogs(program2) {
646
848
  }
647
849
 
648
850
  // src/commands/env.ts
649
- import * as fs7 from "fs";
650
- import * as path7 from "path";
851
+ import * as fs8 from "fs";
852
+ import * as path8 from "path";
651
853
  function validateSecretKey(key) {
652
854
  return key.length > 0 && /^[A-Z0-9_]+$/.test(key);
653
855
  }
@@ -656,12 +858,12 @@ function formatSecretList(secrets) {
656
858
  return secrets.map((s) => ` ${s.key} (created ${s.created_at})`).join("\n");
657
859
  }
658
860
  function loadConfig() {
659
- const configPath = path7.resolve("lumio.config.json");
660
- if (!fs7.existsSync(configPath)) {
861
+ const configPath = path8.resolve("lumio.config.json");
862
+ if (!fs8.existsSync(configPath)) {
661
863
  console.error("lumio.config.json not found. Run `lumio init` first.");
662
864
  process.exit(1);
663
865
  }
664
- return parseConfigFile(fs7.readFileSync(configPath, "utf-8"));
866
+ return parseConfigFile(fs8.readFileSync(configPath, "utf-8"));
665
867
  }
666
868
  function requireClient() {
667
869
  const store = new CredentialStore();
@@ -703,8 +905,27 @@ function registerEnv(program2) {
703
905
  }
704
906
 
705
907
  // src/commands/run.ts
706
- import * as fs8 from "fs";
707
- import * as path8 from "path";
908
+ import * as fs9 from "fs";
909
+ import * as path9 from "path";
910
+ var VALID_FUNCTION_TYPES = [
911
+ "query",
912
+ "mutation",
913
+ "action",
914
+ "command",
915
+ "keyword",
916
+ "pattern",
917
+ "event",
918
+ "timer",
919
+ "moderate"
920
+ ];
921
+ var BOT_HANDLER_TYPES = [
922
+ "command",
923
+ "keyword",
924
+ "pattern",
925
+ "event",
926
+ "timer",
927
+ "moderate"
928
+ ];
708
929
  function parseFunctionRef(ref) {
709
930
  const colonIndex = ref.indexOf(":");
710
931
  if (colonIndex === -1) {
@@ -712,20 +933,22 @@ function parseFunctionRef(ref) {
712
933
  }
713
934
  const type = ref.slice(0, colonIndex);
714
935
  const name = ref.slice(colonIndex + 1);
715
- if (!["query", "mutation", "action"].includes(type)) {
716
- throw new Error(`Function type must be query, mutation, or action. Got: "${type}"`);
936
+ if (!VALID_FUNCTION_TYPES.includes(type)) {
937
+ throw new Error(
938
+ `Function type must be one of: ${VALID_FUNCTION_TYPES.join(", ")}. Got: "${type}"`
939
+ );
717
940
  }
718
941
  return { type, name };
719
942
  }
720
943
  function registerRun(program2) {
721
- program2.command("run <type:name> [args]").description("Execute a server function locally (e.g. lumio run query:getPlayers)").action(async (typeAndName, argsJson) => {
944
+ 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) => {
722
945
  const fnRef = parseFunctionRef(typeAndName);
723
- const configPath = path8.resolve("lumio.config.json");
724
- if (!fs8.existsSync(configPath)) {
946
+ const configPath = path9.resolve("lumio.config.json");
947
+ if (!fs9.existsSync(configPath)) {
725
948
  console.error("lumio.config.json not found. Run `lumio init` first.");
726
949
  process.exit(1);
727
950
  }
728
- const config = parseConfigFile(fs8.readFileSync(configPath, "utf-8"));
951
+ const config = parseConfigFile(fs9.readFileSync(configPath, "utf-8"));
729
952
  const store = new CredentialStore();
730
953
  const token = store.getToken();
731
954
  if (!token) {
@@ -734,10 +957,26 @@ function registerRun(program2) {
734
957
  }
735
958
  const client = new LumioApiClient(store.getApiUrl(), token);
736
959
  const args = argsJson ? JSON.parse(argsJson) : {};
960
+ const body = { type: fnRef.type, args };
961
+ if (BOT_HANDLER_TYPES.includes(fnRef.type)) {
962
+ body.user = {
963
+ id: "test-user-001",
964
+ name: options.user,
965
+ displayName: options.user,
966
+ platform: options.platform,
967
+ role: options.role,
968
+ isMod: options.role === "moderator",
969
+ isVip: options.role === "vip",
970
+ isSub: ["subscriber", "vip", "moderator", "broadcaster"].includes(options.role)
971
+ };
972
+ if (options.message) {
973
+ body.message = { text: options.message, platformMessageId: "test-msg-001" };
974
+ }
975
+ }
737
976
  try {
738
977
  const result = await client.post(
739
978
  `/v1/extension-installs/${config.extensionId}/execute/${fnRef.name}`,
740
- { type: fnRef.type, args }
979
+ body
741
980
  );
742
981
  console.log(JSON.stringify(result, null, 2));
743
982
  } catch (err) {
@@ -748,16 +987,16 @@ function registerRun(program2) {
748
987
  }
749
988
 
750
989
  // src/commands/dashboard.ts
751
- import * as fs9 from "fs";
752
- import * as path9 from "path";
990
+ import * as fs10 from "fs";
991
+ import * as path10 from "path";
753
992
  function registerDashboard(program2) {
754
993
  program2.command("dashboard").description("Open the extension dashboard in browser").action(async () => {
755
- const configPath = path9.resolve("lumio.config.json");
756
- if (!fs9.existsSync(configPath)) {
994
+ const configPath = path10.resolve("lumio.config.json");
995
+ if (!fs10.existsSync(configPath)) {
757
996
  console.error("lumio.config.json not found. Run `lumio init` first.");
758
997
  process.exit(1);
759
998
  }
760
- const config = parseConfigFile(fs9.readFileSync(configPath, "utf-8"));
999
+ const config = parseConfigFile(fs10.readFileSync(configPath, "utf-8"));
761
1000
  const baseUrl = process.env.LUMIO_WEB_URL ?? "https://lumio.vision";
762
1001
  const url = `${baseUrl}/dashboard/extensions/${config.extensionId}`;
763
1002
  console.log(`Opening ${url}...`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zaflun/lumio-cli",
3
- "version": "0.1.0",
3
+ "version": "0.3.0",
4
4
  "description": "Lumio Extension CLI — Build, test, and deploy Lumio extensions",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -22,7 +22,7 @@
22
22
  "inquirer": "^12.6.0",
23
23
  "open": "^10.1.2",
24
24
  "ora": "^8.2.0",
25
- "@zaflun/lumio-extension-types": "0.1.0"
25
+ "@zaflun/lumio-extension-types": "0.3.0"
26
26
  },
27
27
  "devDependencies": {
28
28
  "@types/archiver": "^6.0.3",