@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.
- package/dist/index.d.ts +13 -0
- package/dist/index.js +248 -58
- 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
|
|
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
|
-
|
|
70
|
-
|
|
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 =
|
|
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://
|
|
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
|
-
|
|
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/
|
|
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.
|
|
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
|
-
|
|
117
|
-
reject(e);
|
|
159
|
+
fail(e instanceof Error ? e : new Error(String(e)));
|
|
118
160
|
return;
|
|
119
161
|
}
|
|
120
|
-
|
|
121
|
-
resolve10();
|
|
162
|
+
finish();
|
|
122
163
|
});
|
|
123
|
-
server.listen(port, async () => {
|
|
124
|
-
const
|
|
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(
|
|
168
|
+
await open(loginUrl);
|
|
128
169
|
console.log("Waiting for authentication callback...");
|
|
129
|
-
console.log("If the browser did not open, visit:",
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 = {
|
|
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", "
|
|
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(
|
|
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(
|
|
786
|
+
throw new ConfigError(`nodeType is required for automation_node extensions (${NODE_TYPES.join(", ")})`);
|
|
726
787
|
}
|
|
727
|
-
if (!
|
|
728
|
-
throw new ConfigError(
|
|
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(
|
|
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
|
-
|
|
999
|
-
|
|
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.
|
|
1009
|
-
|
|
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 {
|
|
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 =
|
|
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>
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
4
|
-
"description": "Lumio Extension CLI
|
|
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": "^
|
|
18
|
-
"chalk": "^
|
|
19
|
-
"commander": "^
|
|
20
|
-
"esbuild": "^0.
|
|
21
|
-
"eventsource": "^
|
|
22
|
-
"inquirer": "^
|
|
23
|
-
"open": "^
|
|
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.
|
|
25
|
+
"@zaflun/lumio-extension-types": "0.3.3"
|
|
26
26
|
},
|
|
27
27
|
"devDependencies": {
|
|
28
|
-
"@types/archiver": "^
|
|
28
|
+
"@types/archiver": "^8.0.0",
|
|
29
29
|
"@types/eventsource": "^1.1.15",
|
|
30
|
-
"typescript": "^
|
|
31
|
-
"vitest": "^
|
|
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",
|