@zaflun/lumio-cli 0.5.2 → 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 +248 -58
  3. package/package.json +16 -13
package/dist/index.d.ts CHANGED
@@ -9,6 +9,17 @@ declare class CredentialStore {
9
9
  getApiUrl(): string;
10
10
  isAuthenticated(): boolean;
11
11
  saveToken(token: string, apiUrl?: string): void;
12
+ /**
13
+ * Persist a full session from the browser PKCE flow — the short-lived JWT plus
14
+ * the long-lived refresh token and the JWT's expiry — so a later refresh can
15
+ * mint a fresh JWT without re-opening the browser.
16
+ */
17
+ saveSession(session: {
18
+ token: string;
19
+ refreshToken?: string;
20
+ expiresAt?: string;
21
+ apiUrl?: string;
22
+ }): void;
12
23
  clear(): void;
13
24
  private readCredentials;
14
25
  }
@@ -20,6 +31,8 @@ declare class LumioApiClient {
20
31
  url(endpoint: string): string;
21
32
  authHeaders(): Record<string, string>;
22
33
  uploadHeaders(): Record<string, string>;
34
+ /** Throw an {@link ApiError} (with status) when the response is not ok. */
35
+ private ensureOk;
23
36
  get<T>(endpoint: string): Promise<T>;
24
37
  post<T>(endpoint: string, body?: unknown): Promise<T>;
25
38
  put<T>(endpoint: string, body?: unknown): Promise<T>;
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() {
@@ -298,13 +345,19 @@ function registerInit(program2) {
298
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
  }]);
@@ -671,6 +724,7 @@ import * as fs3 from "fs";
671
724
  import * as path3 from "path";
672
725
 
673
726
  // src/config/parser.ts
727
+ import { NODE_TYPES, TRIGGER_MODES, VALID_PLATFORMS } from "@zaflun/lumio-extension-types";
674
728
  var ConfigError = class extends Error {
675
729
  constructor(message) {
676
730
  super(message);
@@ -713,25 +767,35 @@ function validateConfig(config) {
713
767
  if (!config.extensionId) {
714
768
  throw new ConfigError("extensionId is required. Run `lumio init` or create an extension at the dashboard.");
715
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
+ }
716
773
  if (!config.name) {
717
774
  throw new ConfigError("name is required in lumio.config.json");
718
775
  }
719
776
  if (config.category === "bot_module") {
720
777
  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)");
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(", ")}`);
722
783
  }
723
784
  } else if (config.category === "automation_node") {
724
785
  if (!config.nodeType) {
725
- throw new ConfigError("nodeType is required for automation_node extensions (trigger, action, or logic)");
786
+ throw new ConfigError(`nodeType is required for automation_node extensions (${NODE_TYPES.join(", ")})`);
726
787
  }
727
- if (!["trigger", "action", "logic"].includes(config.nodeType)) {
728
- throw new ConfigError("nodeType must be one of: trigger, action, logic");
788
+ if (!NODE_TYPES.includes(config.nodeType)) {
789
+ throw new ConfigError(`nodeType must be one of: ${NODE_TYPES.join(", ")}`);
729
790
  }
730
791
  if (!config.node) {
731
792
  throw new ConfigError("node configuration is required for automation_node extensions");
732
793
  }
733
794
  if (config.nodeType === "trigger" && !config.node.triggerMode) {
734
- throw new ConfigError("node.triggerMode is required for trigger nodes (webhook, polling, or both)");
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(", ")}`);
735
799
  }
736
800
  if (config.nodeType !== "trigger" && config.node.triggerMode) {
737
801
  throw new ConfigError("node.triggerMode is only valid for trigger nodes");
@@ -964,6 +1028,7 @@ import * as fs4 from "fs";
964
1028
  import * as path4 from "path";
965
1029
  import { execFile } from "child_process";
966
1030
  import { promisify } from "util";
1031
+ import { createRequire } from "module";
967
1032
  var execFileAsync = promisify(execFile);
968
1033
  function registerBuild(program2) {
969
1034
  program2.command("build").description("Bundle extension as standalone HTML + source archive").action(async () => {
@@ -986,6 +1051,27 @@ function registerBuild(program2) {
986
1051
  if (config.category === "bot_module" && config.targets.length === 0) {
987
1052
  console.log(" Bot module \u2014 no UI targets to bundle.");
988
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
+ }
989
1075
  const cssEntryFile = ["src/globals.css", "src/style.css", "src/index.css"].find((f) => fs4.existsSync(path4.resolve(f)));
990
1076
  let compiledCssPath = null;
991
1077
  if (cssEntryFile) {
@@ -995,8 +1081,16 @@ function registerBuild(program2) {
995
1081
  console.log(" Tailwind CSS v4 detected \u2014 compiling...");
996
1082
  compiledCssPath = path4.join(distDir, "styles.css");
997
1083
  try {
998
- await execFileAsync("npx", [
999
- "@tailwindcss/cli",
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,
1000
1094
  "-i",
1001
1095
  path4.resolve(cssEntryFile),
1002
1096
  "-o",
@@ -1004,9 +1098,10 @@ function registerBuild(program2) {
1004
1098
  "--minify"
1005
1099
  ], { cwd: path4.resolve(".") });
1006
1100
  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;
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);
1010
1105
  }
1011
1106
  } else {
1012
1107
  compiledCssPath = path4.join(distDir, "styles.css");
@@ -1063,10 +1158,10 @@ function registerBuild(program2) {
1063
1158
  });
1064
1159
  console.log(` Built: dist/${target}.js`);
1065
1160
  }
1066
- const { default: archiver } = await import("archiver");
1161
+ const { TarArchive } = await import("archiver");
1067
1162
  const sourceArchivePath = path4.join(distDir, "source.tar.gz");
1068
1163
  const output = fs4.createWriteStream(sourceArchivePath);
1069
- const archive = archiver("tar", { gzip: true });
1164
+ const archive = new TarArchive({ gzip: true });
1070
1165
  archive.pipe(output);
1071
1166
  archive.glob("**/*", {
1072
1167
  cwd: path4.resolve("."),
@@ -1074,10 +1169,24 @@ function registerBuild(program2) {
1074
1169
  "node_modules/**",
1075
1170
  "dist/**",
1076
1171
  ".git/**",
1172
+ ".git",
1077
1173
  ".next/**",
1078
1174
  ".lumio-dist/**",
1079
1175
  "*.tgz",
1080
- "*.tar.gz"
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/**"
1081
1190
  ],
1082
1191
  dot: true
1083
1192
  });
@@ -1175,7 +1284,7 @@ var MAX_FILE_SIZE = 10 * 1024 * 1024;
1175
1284
  var MAX_TOTAL_SIZE = 50 * 1024 * 1024;
1176
1285
  var MAX_FILES = 200;
1177
1286
  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) => {
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) => {
1179
1288
  const store = new CredentialStore();
1180
1289
  const token = options.token ?? store.getToken();
1181
1290
  if (!token) {
@@ -1218,6 +1327,64 @@ function registerDeploy(program2) {
1218
1327
  }
1219
1328
  }
1220
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
+ }
1221
1388
  const version = options.version ?? config.version;
1222
1389
  let releaseNotes = options.description ?? "";
1223
1390
  if (!releaseNotes) {
@@ -1448,16 +1615,27 @@ function requireClient() {
1448
1615
  }
1449
1616
  function registerEnv(program2) {
1450
1617
  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) => {
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) => {
1452
1619
  if (!validateSecretKey(key)) {
1453
1620
  console.error(
1454
1621
  "Invalid key format. Use uppercase letters, numbers, and underscores only (e.g. API_KEY)."
1455
1622
  );
1456
1623
  process.exit(1);
1457
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
+ }
1458
1636
  const config = loadConfig();
1459
1637
  const client = requireClient();
1460
- await client.post(`/v1/developer/extensions/${config.extensionId}/secrets`, { key, value });
1638
+ await client.post(`/v1/developer/extensions/${config.extensionId}/secrets`, { key, value: secretValue });
1461
1639
  console.log(`Secret ${key} set.`);
1462
1640
  });
1463
1641
  envCmd.command("list").description("List all secrets (keys only, no values)").action(async () => {
@@ -1471,7 +1649,7 @@ function registerEnv(program2) {
1471
1649
  envCmd.command("delete <key>").description("Delete a secret").action(async (key) => {
1472
1650
  const config = loadConfig();
1473
1651
  const client = requireClient();
1474
- await client.delete(`/v1/developer/extensions/${config.extensionId}/secrets/${key}`);
1652
+ await client.delete(`/v1/developer/extensions/${config.extensionId}/secrets/${encodeURIComponent(key)}`);
1475
1653
  console.log(`Secret ${key} deleted.`);
1476
1654
  });
1477
1655
  }
@@ -1528,7 +1706,17 @@ function registerRun(program2) {
1528
1706
  process.exit(1);
1529
1707
  }
1530
1708
  const client = new LumioApiClient(store.getApiUrl(), token);
1531
- 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
+ }
1532
1720
  const body = { type: fnRef.type, args };
1533
1721
  if (BOT_HANDLER_TYPES.includes(fnRef.type)) {
1534
1722
  body.user = {
@@ -1547,7 +1735,7 @@ function registerRun(program2) {
1547
1735
  }
1548
1736
  try {
1549
1737
  const result = await client.post(
1550
- `/v1/extension-installs/${config.extensionId}/execute/${fnRef.name}`,
1738
+ `/v1/extension-installs/${config.extensionId}/execute/${encodeURIComponent(fnRef.name)}`,
1551
1739
  body
1552
1740
  );
1553
1741
  console.log(JSON.stringify(result, null, 2));
@@ -1582,7 +1770,9 @@ function createProgram() {
1582
1770
  const program2 = new Command();
1583
1771
  program2.name("lumio");
1584
1772
  program2.description("Lumio Extension CLI -- Build, test, and deploy Lumio extensions");
1585
- program2.version("2026.5.1");
1773
+ const require2 = createRequire2(import.meta.url);
1774
+ const { version } = require2("../package.json");
1775
+ program2.version(version);
1586
1776
  registerLogin(program2);
1587
1777
  registerLogout(program2);
1588
1778
  registerWhoami(program2);
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@zaflun/lumio-cli",
3
- "version": "0.5.2",
4
- "description": "Lumio Extension CLI — Build, test, and deploy Lumio extensions",
3
+ "version": "0.6.2",
4
+ "description": "Lumio Extension CLI - Build, test, and deploy Lumio extensions",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
7
7
  "types": "dist/index.d.ts",
@@ -14,23 +14,26 @@
14
14
  "templates"
15
15
  ],
16
16
  "dependencies": {
17
- "archiver": "^7.0.1",
18
- "chalk": "^5.4.1",
19
- "commander": "^13.1.0",
20
- "esbuild": "^0.25.4",
21
- "eventsource": "^3.0.6",
22
- "inquirer": "^12.6.0",
23
- "open": "^10.1.2",
17
+ "archiver": "^8.0.0",
18
+ "chalk": "^6.0.0",
19
+ "commander": "^15.0.0",
20
+ "esbuild": "^0.28.2",
21
+ "eventsource": "^4.1.1",
22
+ "inquirer": "^14.0.2",
23
+ "open": "^11.0.2",
24
24
  "ora": "^8.2.0",
25
- "@zaflun/lumio-extension-types": "0.3.2"
25
+ "@zaflun/lumio-extension-types": "0.3.3"
26
26
  },
27
27
  "devDependencies": {
28
- "@types/archiver": "^6.0.3",
28
+ "@types/archiver": "^8.0.0",
29
29
  "@types/eventsource": "^1.1.15",
30
- "typescript": "^5.8.3",
31
- "vitest": "^3.1.4",
30
+ "typescript": "^6.0.3",
31
+ "vitest": "^4.1.10",
32
32
  "tsup": "^8.4.0"
33
33
  },
34
+ "engines": {
35
+ "node": ">=22.13.0"
36
+ },
34
37
  "license": "MIT",
35
38
  "repository": {
36
39
  "type": "git",