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