@zaflun/lumio-cli 0.6.2 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -2
- package/dist/index.js +3093 -412
- package/dist/sandbox/index.html +14 -0
- package/dist/sandbox/sandbox.css +2 -0
- package/dist/sandbox/sandbox.js +11 -0
- package/package.json +17 -6
package/dist/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// src/program.ts
|
|
2
|
-
import { Command } from "commander";
|
|
3
|
-
import { createRequire as
|
|
2
|
+
import { Command as Command2 } from "commander";
|
|
3
|
+
import { createRequire as createRequire3 } from "module";
|
|
4
4
|
|
|
5
5
|
// src/commands/login.ts
|
|
6
6
|
import * as http from "http";
|
|
@@ -105,13 +105,13 @@ function registerLogin(program2) {
|
|
|
105
105
|
return;
|
|
106
106
|
}
|
|
107
107
|
const redirectUri = `http://localhost:${port}/callback`;
|
|
108
|
-
await new Promise((
|
|
108
|
+
await new Promise((resolve13, reject) => {
|
|
109
109
|
let timeout;
|
|
110
110
|
const server = http.createServer(async (req, res) => {
|
|
111
111
|
const finish = () => {
|
|
112
112
|
clearTimeout(timeout);
|
|
113
113
|
server.close();
|
|
114
|
-
|
|
114
|
+
resolve13();
|
|
115
115
|
};
|
|
116
116
|
const fail = (err) => {
|
|
117
117
|
clearTimeout(timeout);
|
|
@@ -302,6 +302,22 @@ function registerWhoami(program2) {
|
|
|
302
302
|
// src/commands/init.ts
|
|
303
303
|
import * as fs2 from "fs";
|
|
304
304
|
import * as path2 from "path";
|
|
305
|
+
var SERVER_SCHEMA_SCAFFOLD = `import { defineSchema, defineTable, v } from "@zaflun/lumio-sdk/server";
|
|
306
|
+
|
|
307
|
+
export default defineSchema({
|
|
308
|
+
items: defineTable({
|
|
309
|
+
text: v.string(),
|
|
310
|
+
completed: v.boolean(),
|
|
311
|
+
}).storage("instance"),
|
|
312
|
+
});
|
|
313
|
+
`;
|
|
314
|
+
var SERVER_FUNCTIONS_SCAFFOLD = `import { queryRows, insertRow, patchRow, deleteRow, arg } from "@zaflun/lumio-sdk/server";
|
|
315
|
+
|
|
316
|
+
export const getItems = queryRows("items", { orderBy: "created_at", orderDir: "asc" });
|
|
317
|
+
export const addItem = insertRow("items", { text: arg("text"), completed: false }, { editorOnly: true });
|
|
318
|
+
export const toggleItem = patchRow("items", arg("id"), { completed: arg("completed") }, { editorOnly: true });
|
|
319
|
+
export const removeItem = deleteRow("items", arg("id"), { editorOnly: true });
|
|
320
|
+
`;
|
|
305
321
|
function registerInit(program2) {
|
|
306
322
|
program2.command("init").argument("[name]", "Extension project name").description("Create a new extension project with interactive wizard").action(async (name) => {
|
|
307
323
|
const store = new CredentialStore();
|
|
@@ -403,7 +419,7 @@ function registerInit(program2) {
|
|
|
403
419
|
type: "checkbox",
|
|
404
420
|
name: "targets",
|
|
405
421
|
message: "Targets:",
|
|
406
|
-
choices: ["editor", "layer"
|
|
422
|
+
choices: ["editor", "layer"],
|
|
407
423
|
default: ["editor", "layer"]
|
|
408
424
|
},
|
|
409
425
|
{ type: "confirm", name: "server", message: "Server functions?", default: false }
|
|
@@ -465,7 +481,7 @@ function registerInit(program2) {
|
|
|
465
481
|
} catch (err) {
|
|
466
482
|
if (err.status === 404) {
|
|
467
483
|
console.error(
|
|
468
|
-
"\n You need to apply as a developer first.\n Visit https://lumio.vision/
|
|
484
|
+
"\n You need to apply as a developer first.\n Visit https://lumio.vision/dashboard/settings/developer to submit your application.\n"
|
|
469
485
|
);
|
|
470
486
|
process.exit(1);
|
|
471
487
|
}
|
|
@@ -579,6 +595,8 @@ function registerInit(program2) {
|
|
|
579
595
|
},
|
|
580
596
|
devDependencies: {
|
|
581
597
|
"typescript": "^6.0.3",
|
|
598
|
+
// `lumio dev` runs the extension on the devkit's mock host.
|
|
599
|
+
...!isAutomationNode ? { "@zaflun/lumio-devkit": "latest" } : {},
|
|
582
600
|
...needsReact ? { "@types/react": "^19.1.0" } : {},
|
|
583
601
|
...useTailwind ? { "tailwindcss": "^4.3.0" } : {}
|
|
584
602
|
}
|
|
@@ -707,10 +725,8 @@ Lumio.render(<App />, { target: "${target}" });
|
|
|
707
725
|
}
|
|
708
726
|
if (answers.server) {
|
|
709
727
|
fs2.mkdirSync(path2.join(projectDir, "server"), { recursive: true });
|
|
710
|
-
|
|
711
|
-
fs2.writeFileSync(path2.join(projectDir, "server", "
|
|
712
|
-
const functions = 'import { queryRows, insertRow, patchRow, deleteRow, arg } from "@zaflun/lumio-sdk/server";\n\nexport const getItems = queryRows("items");\nexport const addItem = insertRow("items", { text: arg("text"), completed: false }, { editorOnly: true });\nexport const toggleItem = patchRow("items", arg("id"), { completed: arg("completed") }, { editorOnly: true });\nexport const removeItem = deleteRow("items", arg("id"), { editorOnly: true });\n';
|
|
713
|
-
fs2.writeFileSync(path2.join(projectDir, "server", "functions.ts"), functions);
|
|
728
|
+
fs2.writeFileSync(path2.join(projectDir, "server", "schema.ts"), SERVER_SCHEMA_SCAFFOLD);
|
|
729
|
+
fs2.writeFileSync(path2.join(projectDir, "server", "functions.ts"), SERVER_FUNCTIONS_SCAFFOLD);
|
|
714
730
|
}
|
|
715
731
|
}
|
|
716
732
|
console.log(`
|
|
@@ -720,17 +736,20 @@ Done! Created ${projectDir}/`);
|
|
|
720
736
|
}
|
|
721
737
|
|
|
722
738
|
// src/commands/dev.ts
|
|
723
|
-
import * as
|
|
724
|
-
import * as
|
|
739
|
+
import * as fs9 from "fs";
|
|
740
|
+
import * as path10 from "path";
|
|
741
|
+
import { InvalidArgumentError } from "commander";
|
|
742
|
+
import chalk3 from "chalk";
|
|
725
743
|
|
|
726
744
|
// src/config/parser.ts
|
|
727
745
|
import { NODE_TYPES, TRIGGER_MODES, VALID_PLATFORMS } from "@zaflun/lumio-extension-types";
|
|
728
746
|
var ConfigError = class extends Error {
|
|
729
|
-
constructor(
|
|
730
|
-
super(
|
|
747
|
+
constructor(message2) {
|
|
748
|
+
super(message2);
|
|
731
749
|
this.name = "ConfigError";
|
|
732
750
|
}
|
|
733
751
|
};
|
|
752
|
+
var VALID_TARGETS = ["editor", "layer", "designer"];
|
|
734
753
|
function parseConfigFile(jsonString) {
|
|
735
754
|
const raw = JSON.parse(jsonString);
|
|
736
755
|
return {
|
|
@@ -806,111 +825,45 @@ function validateConfig(config) {
|
|
|
806
825
|
}
|
|
807
826
|
}
|
|
808
827
|
} else if (!config.targets || config.targets.length === 0) {
|
|
809
|
-
throw new ConfigError("At least one target is required (editor
|
|
828
|
+
throw new ConfigError("At least one target is required (editor or layer)");
|
|
829
|
+
}
|
|
830
|
+
const unknownTargets = (config.targets ?? []).filter((t) => !VALID_TARGETS.includes(t));
|
|
831
|
+
if (unknownTargets.length > 0) {
|
|
832
|
+
throw new ConfigError(
|
|
833
|
+
`Invalid target(s): ${unknownTargets.join(", ")}. Must be one of: ${VALID_TARGETS.join(", ")}. Viewer input comes through chat commands and channel point rewards handled by a bot module.`
|
|
834
|
+
);
|
|
810
835
|
}
|
|
811
836
|
if (!config.version) {
|
|
812
837
|
throw new ConfigError("version is required in lumio.config.json");
|
|
813
838
|
}
|
|
814
839
|
}
|
|
815
840
|
|
|
816
|
-
// src/commands/dev-
|
|
841
|
+
// src/commands/dev-automation-node.ts
|
|
817
842
|
import * as readline from "readline";
|
|
818
843
|
import chalk from "chalk";
|
|
819
|
-
function parseUserPrefix(input) {
|
|
820
|
-
const defaultUser = {
|
|
821
|
-
id: "user-001",
|
|
822
|
-
name: "dev_user",
|
|
823
|
-
displayName: "Dev User",
|
|
824
|
-
platform: "twitch",
|
|
825
|
-
role: "everyone",
|
|
826
|
-
isMod: false,
|
|
827
|
-
isVip: false,
|
|
828
|
-
isSub: false
|
|
829
|
-
};
|
|
830
|
-
if (!input.startsWith("@")) {
|
|
831
|
-
return { user: defaultUser, message: input };
|
|
832
|
-
}
|
|
833
|
-
const spaceIdx = input.indexOf(" ");
|
|
834
|
-
if (spaceIdx === -1) return { user: defaultUser, message: input };
|
|
835
|
-
const prefix = input.substring(1, spaceIdx);
|
|
836
|
-
const message = input.substring(spaceIdx + 1);
|
|
837
|
-
const parts = prefix.split(":");
|
|
838
|
-
const name = parts[parts.length - 1];
|
|
839
|
-
const rolePrefix = parts.length > 1 ? parts[0] : "everyone";
|
|
840
|
-
return {
|
|
841
|
-
user: {
|
|
842
|
-
id: `user-${name}`,
|
|
843
|
-
name,
|
|
844
|
-
displayName: name,
|
|
845
|
-
platform: "twitch",
|
|
846
|
-
role: rolePrefix,
|
|
847
|
-
isMod: rolePrefix === "mod",
|
|
848
|
-
isVip: rolePrefix === "vip",
|
|
849
|
-
isSub: ["sub", "vip", "mod", "broadcaster"].includes(rolePrefix)
|
|
850
|
-
},
|
|
851
|
-
message
|
|
852
|
-
};
|
|
853
|
-
}
|
|
854
|
-
async function runBotModuleDevServer(config) {
|
|
855
|
-
console.log(chalk.bold("\n\u{1F916} Bot Module Dev Server"));
|
|
856
|
-
console.log(`Extension: ${config.name}`);
|
|
857
|
-
console.log(`Platforms: ${config.platforms?.join(", ") ?? "none"}`);
|
|
858
|
-
console.log(chalk.dim("\nType chat messages. Prefix with @username or @role:username."));
|
|
859
|
-
console.log(chalk.dim("Examples: !hello | @sub:viewer !points 100 | @mod:admin gg"));
|
|
860
|
-
console.log(chalk.dim("Type !quit to exit.\n"));
|
|
861
|
-
const rl = readline.createInterface({
|
|
862
|
-
input: process.stdin,
|
|
863
|
-
output: process.stdout,
|
|
864
|
-
prompt: chalk.cyan("> ")
|
|
865
|
-
});
|
|
866
|
-
rl.prompt();
|
|
867
|
-
rl.on("line", async (line) => {
|
|
868
|
-
const trimmed = line.trim();
|
|
869
|
-
if (!trimmed) {
|
|
870
|
-
rl.prompt();
|
|
871
|
-
return;
|
|
872
|
-
}
|
|
873
|
-
if (trimmed === "!quit") {
|
|
874
|
-
rl.close();
|
|
875
|
-
return;
|
|
876
|
-
}
|
|
877
|
-
const { user, message } = parseUserPrefix(trimmed);
|
|
878
|
-
console.log(chalk.dim(` [${user.role}:${user.name}] ${message}`));
|
|
879
|
-
console.log(chalk.yellow(" \u2192 Handler execution not yet wired (requires esbuild + V8)"));
|
|
880
|
-
rl.prompt();
|
|
881
|
-
});
|
|
882
|
-
rl.on("close", () => {
|
|
883
|
-
console.log(chalk.dim("\nBot Module Dev Server stopped."));
|
|
884
|
-
process.exit(0);
|
|
885
|
-
});
|
|
886
|
-
}
|
|
887
|
-
|
|
888
|
-
// src/commands/dev-automation-node.ts
|
|
889
|
-
import * as readline2 from "readline";
|
|
890
|
-
import chalk2 from "chalk";
|
|
891
844
|
async function runAutomationNodeDevServer(config) {
|
|
892
845
|
const nodeType = config.nodeType ?? "action";
|
|
893
|
-
console.log(
|
|
846
|
+
console.log(chalk.bold("\n\u2699\uFE0F Automation Node Dev Server"));
|
|
894
847
|
console.log(`Extension: ${config.name}`);
|
|
895
848
|
console.log(`Node type: ${nodeType}`);
|
|
896
849
|
if (nodeType === "trigger" && config.node?.triggerMode) {
|
|
897
850
|
console.log(`Trigger mode: ${config.node.triggerMode}`);
|
|
898
851
|
}
|
|
899
|
-
console.log(
|
|
900
|
-
console.log(
|
|
852
|
+
console.log(chalk.dim("\nCommands:"));
|
|
853
|
+
console.log(chalk.dim(" /execute [json] \u2014 Execute the node with input data"));
|
|
901
854
|
if (nodeType === "trigger") {
|
|
902
|
-
console.log(
|
|
903
|
-
console.log(
|
|
855
|
+
console.log(chalk.dim(" /webhook [json] \u2014 Simulate a webhook POST payload"));
|
|
856
|
+
console.log(chalk.dim(" /poll \u2014 Trigger a poll cycle"));
|
|
904
857
|
}
|
|
905
|
-
console.log(
|
|
906
|
-
console.log(
|
|
907
|
-
console.log(
|
|
858
|
+
console.log(chalk.dim(" /config [json] \u2014 Set node instance config"));
|
|
859
|
+
console.log(chalk.dim(" /install-config [j] \u2014 Set extension install config"));
|
|
860
|
+
console.log(chalk.dim(" /quit \u2014 Exit\n"));
|
|
908
861
|
let nodeConfig = {};
|
|
909
862
|
let installConfig = {};
|
|
910
|
-
const rl =
|
|
863
|
+
const rl = readline.createInterface({
|
|
911
864
|
input: process.stdin,
|
|
912
865
|
output: process.stdout,
|
|
913
|
-
prompt:
|
|
866
|
+
prompt: chalk.cyan("> ")
|
|
914
867
|
});
|
|
915
868
|
rl.prompt();
|
|
916
869
|
rl.on("line", async (line) => {
|
|
@@ -926,9 +879,9 @@ async function runAutomationNodeDevServer(config) {
|
|
|
926
879
|
if (trimmed.startsWith("/config ")) {
|
|
927
880
|
try {
|
|
928
881
|
nodeConfig = JSON.parse(trimmed.slice("/config ".length));
|
|
929
|
-
console.log(
|
|
882
|
+
console.log(chalk.green(" Node config updated."));
|
|
930
883
|
} catch {
|
|
931
|
-
console.log(
|
|
884
|
+
console.log(chalk.red(" Invalid JSON."));
|
|
932
885
|
}
|
|
933
886
|
rl.prompt();
|
|
934
887
|
return;
|
|
@@ -936,9 +889,9 @@ async function runAutomationNodeDevServer(config) {
|
|
|
936
889
|
if (trimmed.startsWith("/install-config ")) {
|
|
937
890
|
try {
|
|
938
891
|
installConfig = JSON.parse(trimmed.slice("/install-config ".length));
|
|
939
|
-
console.log(
|
|
892
|
+
console.log(chalk.green(" Install config updated."));
|
|
940
893
|
} catch {
|
|
941
|
-
console.log(
|
|
894
|
+
console.log(chalk.red(" Invalid JSON."));
|
|
942
895
|
}
|
|
943
896
|
rl.prompt();
|
|
944
897
|
return;
|
|
@@ -950,15 +903,15 @@ async function runAutomationNodeDevServer(config) {
|
|
|
950
903
|
try {
|
|
951
904
|
inputData = JSON.parse(jsonStr);
|
|
952
905
|
} catch {
|
|
953
|
-
console.log(
|
|
906
|
+
console.log(chalk.red(" Invalid JSON."));
|
|
954
907
|
rl.prompt();
|
|
955
908
|
return;
|
|
956
909
|
}
|
|
957
910
|
}
|
|
958
|
-
console.log(
|
|
959
|
-
console.log(
|
|
960
|
-
console.log(
|
|
961
|
-
console.log(
|
|
911
|
+
console.log(chalk.dim(` [${nodeType}] Executing with input: ${JSON.stringify(inputData)}`));
|
|
912
|
+
console.log(chalk.dim(` [${nodeType}] Node config: ${JSON.stringify(nodeConfig)}`));
|
|
913
|
+
console.log(chalk.dim(` [${nodeType}] Install config: ${JSON.stringify(installConfig)}`));
|
|
914
|
+
console.log(chalk.yellow(" \u2192 Handler execution not yet wired (requires esbuild + V8)"));
|
|
962
915
|
rl.prompt();
|
|
963
916
|
return;
|
|
964
917
|
}
|
|
@@ -969,297 +922,2987 @@ async function runAutomationNodeDevServer(config) {
|
|
|
969
922
|
try {
|
|
970
923
|
body = JSON.parse(jsonStr);
|
|
971
924
|
} catch {
|
|
972
|
-
console.log(
|
|
925
|
+
console.log(chalk.red(" Invalid JSON."));
|
|
973
926
|
rl.prompt();
|
|
974
927
|
return;
|
|
975
928
|
}
|
|
976
929
|
}
|
|
977
|
-
console.log(
|
|
978
|
-
console.log(
|
|
930
|
+
console.log(chalk.dim(` [trigger:webhook] Received payload: ${JSON.stringify(body)}`));
|
|
931
|
+
console.log(chalk.yellow(" \u2192 Handler execution not yet wired (requires esbuild + V8)"));
|
|
979
932
|
rl.prompt();
|
|
980
933
|
return;
|
|
981
934
|
}
|
|
982
935
|
if (nodeType === "trigger" && trimmed === "/poll") {
|
|
983
|
-
console.log(
|
|
984
|
-
console.log(
|
|
936
|
+
console.log(chalk.dim(" [trigger:poll] Polling..."));
|
|
937
|
+
console.log(chalk.yellow(" \u2192 Handler execution not yet wired (requires esbuild + V8)"));
|
|
985
938
|
rl.prompt();
|
|
986
939
|
return;
|
|
987
940
|
}
|
|
988
|
-
console.log(
|
|
941
|
+
console.log(chalk.dim(" Unknown command. Type /execute, /webhook, /poll, /config, or /quit."));
|
|
989
942
|
rl.prompt();
|
|
990
943
|
});
|
|
991
944
|
rl.on("close", () => {
|
|
992
|
-
console.log(
|
|
945
|
+
console.log(chalk.dim("\nAutomation Node Dev Server stopped."));
|
|
993
946
|
process.exit(0);
|
|
994
947
|
});
|
|
995
948
|
}
|
|
996
949
|
|
|
997
|
-
// src/
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
950
|
+
// src/dev/builder.ts
|
|
951
|
+
import * as fs6 from "fs";
|
|
952
|
+
import * as path6 from "path";
|
|
953
|
+
|
|
954
|
+
// src/build/pipeline.ts
|
|
955
|
+
import * as fs5 from "fs";
|
|
956
|
+
import * as path5 from "path";
|
|
957
|
+
import { execFile } from "child_process";
|
|
958
|
+
import { promisify } from "util";
|
|
959
|
+
import { createRequire } from "module";
|
|
960
|
+
|
|
961
|
+
// src/commands/data-schema.ts
|
|
962
|
+
import * as fs3 from "fs";
|
|
963
|
+
import * as path3 from "path";
|
|
964
|
+
var SCHEMA_FILES = ["server/schema.ts", "server/schema.js", "server/schema.mjs"];
|
|
965
|
+
function findSchemaFile(projectDir) {
|
|
966
|
+
return SCHEMA_FILES.find((f) => fs3.existsSync(path3.join(projectDir, f))) ?? null;
|
|
967
|
+
}
|
|
968
|
+
async function loadDataSchema(projectDir) {
|
|
969
|
+
const schemaFile = findSchemaFile(projectDir);
|
|
970
|
+
if (!schemaFile) return null;
|
|
971
|
+
const { build } = await import("esbuild");
|
|
972
|
+
const result = await build({
|
|
973
|
+
entryPoints: [path3.join(projectDir, schemaFile)],
|
|
974
|
+
absWorkingDir: projectDir,
|
|
975
|
+
bundle: true,
|
|
976
|
+
write: false,
|
|
977
|
+
format: "esm",
|
|
978
|
+
platform: "node",
|
|
979
|
+
target: "es2022",
|
|
980
|
+
logLevel: "silent"
|
|
1023
981
|
});
|
|
982
|
+
const code = result.outputFiles[0]?.text ?? "";
|
|
983
|
+
const url = `data:text/javascript;base64,${Buffer.from(code).toString("base64")}`;
|
|
984
|
+
const mod = await import(url);
|
|
985
|
+
return toDataSchemaJson(mod.default, schemaFile);
|
|
986
|
+
}
|
|
987
|
+
function toDataSchemaJson(value, schemaFile = "server/schema.ts") {
|
|
988
|
+
const tables = value?.tables;
|
|
989
|
+
if (typeof tables !== "object" || tables === null || Array.isArray(tables)) {
|
|
990
|
+
throw new Error(
|
|
991
|
+
`${schemaFile} must \`export default defineSchema({ ... })\` from "@zaflun/lumio-sdk/server".`
|
|
992
|
+
);
|
|
993
|
+
}
|
|
994
|
+
return JSON.parse(JSON.stringify(value));
|
|
995
|
+
}
|
|
996
|
+
function formatDataSchemaError(body) {
|
|
997
|
+
let parsed;
|
|
998
|
+
try {
|
|
999
|
+
parsed = JSON.parse(body);
|
|
1000
|
+
} catch {
|
|
1001
|
+
return null;
|
|
1002
|
+
}
|
|
1003
|
+
const message2 = typeof parsed.message === "string" ? parsed.message : "";
|
|
1004
|
+
if (parsed.error === "INVALID_DATA_SCHEMA") {
|
|
1005
|
+
return [`Invalid data schema (server/schema.ts): ${message2}`];
|
|
1006
|
+
}
|
|
1007
|
+
if (parsed.error === "DATA_SCHEMA_NOT_ADDITIVE") {
|
|
1008
|
+
const violations = Array.isArray(parsed.details?.violations) ? parsed.details.violations : [];
|
|
1009
|
+
if (violations.length === 0) return [message2];
|
|
1010
|
+
return [
|
|
1011
|
+
"Data schema change is not additive. Published tables only allow new tables, new optional columns and relaxing NOT NULL:",
|
|
1012
|
+
...violations.map((v) => {
|
|
1013
|
+
const target = v.column ? `${v.table}.${v.column}` : v.table;
|
|
1014
|
+
return ` - ${target}: ${v.rule} (applied: ${v.applied}, declared: ${v.declared})`;
|
|
1015
|
+
})
|
|
1016
|
+
];
|
|
1017
|
+
}
|
|
1018
|
+
return null;
|
|
1024
1019
|
}
|
|
1025
1020
|
|
|
1026
|
-
// src/commands/
|
|
1021
|
+
// src/commands/functions-manifest.ts
|
|
1027
1022
|
import * as fs4 from "fs";
|
|
1028
1023
|
import * as path4 from "path";
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
var
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1024
|
+
var FUNCTIONS_FILES = ["server/functions.ts", "server/functions.js", "server/functions.mjs"];
|
|
1025
|
+
var FUNCTIONS_MANIFEST_VERSION = 1;
|
|
1026
|
+
var FUNCTIONS_MANIFEST_FILE = "functions.json";
|
|
1027
|
+
var FILTER_OPERATORS = /* @__PURE__ */ new Set(["=", "!=", ">", ">=", "<", "<=", "like", "in"]);
|
|
1028
|
+
var NAME_PATTERN = /^[A-Za-z_][A-Za-z0-9_]{0,63}$/;
|
|
1029
|
+
var FunctionsManifestError = class extends Error {
|
|
1030
|
+
constructor(message2) {
|
|
1031
|
+
super(message2);
|
|
1032
|
+
this.name = "FunctionsManifestError";
|
|
1033
|
+
}
|
|
1034
|
+
};
|
|
1035
|
+
function findFunctionsFile(projectDir) {
|
|
1036
|
+
return FUNCTIONS_FILES.find((f) => fs4.existsSync(path4.join(projectDir, f))) ?? null;
|
|
1037
|
+
}
|
|
1038
|
+
function combinedServerEntry(handlerEntry, functionsFile, names) {
|
|
1039
|
+
const spec = (file) => JSON.stringify(`./${file.replace(/\\/g, "/")}`);
|
|
1040
|
+
const lines = [`export * from ${spec(handlerEntry)};`];
|
|
1041
|
+
if (names.length > 0) {
|
|
1042
|
+
lines.push(`export { ${names.join(", ")} } from ${spec(functionsFile)};`);
|
|
1043
|
+
}
|
|
1044
|
+
return `${lines.join("\n")}
|
|
1045
|
+
`;
|
|
1046
|
+
}
|
|
1047
|
+
function handlerNames(manifest) {
|
|
1048
|
+
return Object.entries(manifest.functions).filter(([, entry]) => entry.kind === "handler").map(([name]) => name);
|
|
1049
|
+
}
|
|
1050
|
+
async function loadFunctionExports(projectDir) {
|
|
1051
|
+
const file = findFunctionsFile(projectDir);
|
|
1052
|
+
if (!file) return null;
|
|
1053
|
+
const { build } = await import("esbuild");
|
|
1054
|
+
const result = await build({
|
|
1055
|
+
entryPoints: [path4.join(projectDir, file)],
|
|
1056
|
+
absWorkingDir: projectDir,
|
|
1057
|
+
bundle: true,
|
|
1058
|
+
write: false,
|
|
1059
|
+
format: "esm",
|
|
1060
|
+
platform: "node",
|
|
1061
|
+
target: "es2022",
|
|
1062
|
+
logLevel: "silent"
|
|
1063
|
+
});
|
|
1064
|
+
const code = result.outputFiles[0]?.text ?? "";
|
|
1065
|
+
const url = `data:text/javascript;base64,${Buffer.from(code).toString("base64")}`;
|
|
1066
|
+
const mod = await import(url);
|
|
1067
|
+
return { file, exports: { ...mod } };
|
|
1068
|
+
}
|
|
1069
|
+
function buildFunctionsManifest(exports, schema, file = "server/functions.ts") {
|
|
1070
|
+
const functions = {};
|
|
1071
|
+
const seen = /* @__PURE__ */ new Map();
|
|
1072
|
+
for (const name of Object.keys(exports).sort()) {
|
|
1073
|
+
const value = exports[name];
|
|
1074
|
+
const where = `${file}: export "${name}"`;
|
|
1075
|
+
if (name === "default") {
|
|
1076
|
+
throw new FunctionsManifestError(
|
|
1077
|
+
`${file}: a default export is not allowed. Export each function under its name (\`export const getRules = queryRows("rules")\`).`
|
|
1078
|
+
);
|
|
1039
1079
|
}
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
if (!fs4.existsSync(changelogPath)) {
|
|
1045
|
-
console.warn("Warning: No CHANGELOG.md found. It is recommended to maintain a changelog.");
|
|
1080
|
+
if (!NAME_PATTERN.test(name)) {
|
|
1081
|
+
throw new FunctionsManifestError(
|
|
1082
|
+
`${where}: function names may only use letters, digits and "_" (at most 64 characters).`
|
|
1083
|
+
);
|
|
1046
1084
|
}
|
|
1047
|
-
|
|
1048
|
-
const
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1085
|
+
const folded = name.toLowerCase();
|
|
1086
|
+
const other = seen.get(folded);
|
|
1087
|
+
if (other !== void 0) {
|
|
1088
|
+
throw new FunctionsManifestError(
|
|
1089
|
+
`${where}: duplicate function name \u2014 it differs from "${other}" only in case.`
|
|
1090
|
+
);
|
|
1053
1091
|
}
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
}
|
|
1072
|
-
|
|
1092
|
+
seen.set(folded, name);
|
|
1093
|
+
const entry = toEntry(value, where);
|
|
1094
|
+
if (entry.table !== void 0) checkTable(entry.table, schema, where);
|
|
1095
|
+
functions[name] = entry;
|
|
1096
|
+
}
|
|
1097
|
+
return { version: FUNCTIONS_MANIFEST_VERSION, functions };
|
|
1098
|
+
}
|
|
1099
|
+
function toEntry(value, where) {
|
|
1100
|
+
const type = value?.__type;
|
|
1101
|
+
switch (type) {
|
|
1102
|
+
case "queryRows": {
|
|
1103
|
+
const def = value;
|
|
1104
|
+
const entry = {
|
|
1105
|
+
kind: "queryRows",
|
|
1106
|
+
category: "query",
|
|
1107
|
+
editorOnly: false,
|
|
1108
|
+
table: tableName(def.table, where)
|
|
1109
|
+
};
|
|
1110
|
+
if (def.filter !== void 0) entry.filter = filters(def.filter, where);
|
|
1111
|
+
if (def.orderBy !== void 0) {
|
|
1112
|
+
if (typeof def.orderBy !== "string" || def.orderBy === "") {
|
|
1113
|
+
throw new FunctionsManifestError(`${where}: orderBy must be a column name.`);
|
|
1114
|
+
}
|
|
1115
|
+
entry.orderBy = def.orderBy;
|
|
1073
1116
|
}
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
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);
|
|
1117
|
+
if (def.orderDir !== void 0) {
|
|
1118
|
+
if (def.orderDir !== "asc" && def.orderDir !== "desc") {
|
|
1119
|
+
throw new FunctionsManifestError(`${where}: orderDir must be "asc" or "desc".`);
|
|
1105
1120
|
}
|
|
1106
|
-
|
|
1107
|
-
compiledCssPath = path4.join(distDir, "styles.css");
|
|
1108
|
-
fs4.copyFileSync(path4.resolve(cssEntryFile), compiledCssPath);
|
|
1109
|
-
console.log(` Copied: ${cssEntryFile} \u2192 dist/styles.css`);
|
|
1121
|
+
entry.orderDir = def.orderDir;
|
|
1110
1122
|
}
|
|
1123
|
+
if (def.limit !== void 0) entry.limit = count(def.limit, "limit", where);
|
|
1124
|
+
if (def.offset !== void 0) entry.offset = count(def.offset, "offset", where);
|
|
1125
|
+
return entry;
|
|
1111
1126
|
}
|
|
1112
|
-
|
|
1113
|
-
const
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1127
|
+
case "insertRow": {
|
|
1128
|
+
const def = value;
|
|
1129
|
+
return {
|
|
1130
|
+
kind: "insertRow",
|
|
1131
|
+
category: "mutation",
|
|
1132
|
+
editorOnly: editorOnly(def.editorOnly, where),
|
|
1133
|
+
table: tableName(def.table, where),
|
|
1134
|
+
values: rowValues(def.values, where)
|
|
1135
|
+
};
|
|
1136
|
+
}
|
|
1137
|
+
case "patchRow": {
|
|
1138
|
+
const def = value;
|
|
1139
|
+
return {
|
|
1140
|
+
kind: "patchRow",
|
|
1141
|
+
category: "mutation",
|
|
1142
|
+
editorOnly: editorOnly(def.editorOnly, where),
|
|
1143
|
+
table: tableName(def.table, where),
|
|
1144
|
+
rowId: rowId(def.rowId, where),
|
|
1145
|
+
values: rowValues(def.values, where)
|
|
1146
|
+
};
|
|
1147
|
+
}
|
|
1148
|
+
case "deleteRow": {
|
|
1149
|
+
const def = value;
|
|
1150
|
+
return {
|
|
1151
|
+
kind: "deleteRow",
|
|
1152
|
+
category: "mutation",
|
|
1153
|
+
editorOnly: editorOnly(def.editorOnly, where),
|
|
1154
|
+
table: tableName(def.table, where),
|
|
1155
|
+
rowId: rowId(def.rowId, where)
|
|
1156
|
+
};
|
|
1157
|
+
}
|
|
1158
|
+
case "query":
|
|
1159
|
+
case "mutation":
|
|
1160
|
+
case "action": {
|
|
1161
|
+
if (typeof value.handler !== "function") {
|
|
1162
|
+
throw new FunctionsManifestError(`${where}: ${type}() needs a handler function.`);
|
|
1117
1163
|
}
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
bundle: true,
|
|
1121
|
-
minify: true,
|
|
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}`,
|
|
1129
|
-
jsx: "automatic",
|
|
1130
|
-
target: "es2022",
|
|
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"],
|
|
1155
|
-
define: {
|
|
1156
|
-
"process.env.NODE_ENV": '"production"'
|
|
1157
|
-
}
|
|
1158
|
-
});
|
|
1159
|
-
console.log(` Built: dist/${target}.js`);
|
|
1164
|
+
const flag = editorOnly(value.editorOnly, where);
|
|
1165
|
+
return { kind: "handler", category: type, editorOnly: flag };
|
|
1160
1166
|
}
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1167
|
+
case "fetchAction":
|
|
1168
|
+
throw new FunctionsManifestError(
|
|
1169
|
+
`${where}: fetchAction() is not supported in server functions. Call ctx.fetch() from an action() handler instead.`
|
|
1170
|
+
);
|
|
1171
|
+
default:
|
|
1172
|
+
throw new FunctionsManifestError(
|
|
1173
|
+
`${where}: not a server function definition. server/functions.ts may only export queryRows, insertRow, patchRow, deleteRow, query, mutation and action definitions \u2014 move other values into a separate module.`
|
|
1174
|
+
);
|
|
1175
|
+
}
|
|
1176
|
+
}
|
|
1177
|
+
function tableName(value, where) {
|
|
1178
|
+
if (typeof value !== "string" || value === "") {
|
|
1179
|
+
throw new FunctionsManifestError(`${where}: the table name must be a non-empty string.`);
|
|
1180
|
+
}
|
|
1181
|
+
return value;
|
|
1182
|
+
}
|
|
1183
|
+
function checkTable(table, schema, where) {
|
|
1184
|
+
if (!schema) {
|
|
1185
|
+
throw new FunctionsManifestError(
|
|
1186
|
+
`${where}: uses table "${table}", but the extension has no server/schema.ts. Declare the table with defineSchema().`
|
|
1187
|
+
);
|
|
1188
|
+
}
|
|
1189
|
+
if (!Object.prototype.hasOwnProperty.call(schema.tables, table)) {
|
|
1190
|
+
const known = Object.keys(schema.tables);
|
|
1191
|
+
throw new FunctionsManifestError(
|
|
1192
|
+
`${where}: unknown table "${table}" (server/schema.ts declares ${known.length > 0 ? known.map((t) => `"${t}"`).join(", ") : "no tables"}).`
|
|
1193
|
+
);
|
|
1194
|
+
}
|
|
1195
|
+
}
|
|
1196
|
+
function editorOnly(value, where) {
|
|
1197
|
+
if (value === void 0) return false;
|
|
1198
|
+
if (typeof value !== "boolean") {
|
|
1199
|
+
throw new FunctionsManifestError(`${where}: editorOnly must be true or false.`);
|
|
1200
|
+
}
|
|
1201
|
+
return value;
|
|
1202
|
+
}
|
|
1203
|
+
function rowId(value, where) {
|
|
1204
|
+
if (typeof value === "string" && value !== "") return value;
|
|
1205
|
+
const expr = expression(value, where);
|
|
1206
|
+
if (expr?.__expr === "arg") return expr;
|
|
1207
|
+
throw new FunctionsManifestError(`${where}: the row id must be a string or arg("\u2026").`);
|
|
1208
|
+
}
|
|
1209
|
+
function count(value, field, where) {
|
|
1210
|
+
if (typeof value === "number" && Number.isSafeInteger(value) && value >= 0) return value;
|
|
1211
|
+
const expr = expression(value, where);
|
|
1212
|
+
if (expr?.__expr === "arg") return expr;
|
|
1213
|
+
throw new FunctionsManifestError(`${where}: ${field} must be a non-negative integer or arg("\u2026").`);
|
|
1214
|
+
}
|
|
1215
|
+
function filters(value, where) {
|
|
1216
|
+
const list = Array.isArray(value) ? value : [value];
|
|
1217
|
+
return list.map((item) => {
|
|
1218
|
+
const f = item;
|
|
1219
|
+
if (typeof f !== "object" || f === null || f.__expr !== "filter") {
|
|
1220
|
+
throw new FunctionsManifestError(`${where}: filter must be filter(field, op, value) or a list of them.`);
|
|
1221
|
+
}
|
|
1222
|
+
if (typeof f.field !== "string" || f.field === "") {
|
|
1223
|
+
throw new FunctionsManifestError(`${where}: filter field must be a column name.`);
|
|
1224
|
+
}
|
|
1225
|
+
if (typeof f.op !== "string" || !FILTER_OPERATORS.has(f.op)) {
|
|
1226
|
+
throw new FunctionsManifestError(
|
|
1227
|
+
`${where}: unsupported filter operator ${JSON.stringify(f.op)} (use ${[...FILTER_OPERATORS].map((o) => `"${o}"`).join(", ")}).`
|
|
1228
|
+
);
|
|
1229
|
+
}
|
|
1230
|
+
return { field: f.field, op: f.op, value: jsonValue(f.value, `${where} (filter "${f.field}")`) };
|
|
1196
1231
|
});
|
|
1197
1232
|
}
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
const
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
for (const line of lines) {
|
|
1211
|
-
const match = line.match(VERSION_HEADER_REGEX);
|
|
1212
|
-
if (match) {
|
|
1213
|
-
if (currentEntry) {
|
|
1214
|
-
currentEntry.content = contentLines.join("\n").trim();
|
|
1215
|
-
entries.push(currentEntry);
|
|
1216
|
-
contentLines.length = 0;
|
|
1217
|
-
}
|
|
1218
|
-
currentEntry = {
|
|
1219
|
-
version: match[1],
|
|
1220
|
-
date: match[2] ?? null,
|
|
1221
|
-
content: ""
|
|
1222
|
-
};
|
|
1223
|
-
} else if (currentEntry) {
|
|
1224
|
-
contentLines.push(line);
|
|
1233
|
+
function rowValues(value, where) {
|
|
1234
|
+
if (!isPlainObject(value)) {
|
|
1235
|
+
throw new FunctionsManifestError(`${where}: row values must be an object of column \u2192 value.`);
|
|
1236
|
+
}
|
|
1237
|
+
return jsonValue(value, where);
|
|
1238
|
+
}
|
|
1239
|
+
function expression(value, where) {
|
|
1240
|
+
if (!isPlainObject(value) || !("__expr" in value)) return null;
|
|
1241
|
+
const expr = value.__expr;
|
|
1242
|
+
if (expr === "arg") {
|
|
1243
|
+
if (typeof value.name !== "string" || value.name === "") {
|
|
1244
|
+
throw new FunctionsManifestError(`${where}: arg() needs a parameter name.`);
|
|
1225
1245
|
}
|
|
1246
|
+
return { __expr: "arg", name: value.name };
|
|
1226
1247
|
}
|
|
1227
|
-
if (
|
|
1228
|
-
|
|
1229
|
-
|
|
1248
|
+
if (expr === "identity") return { __expr: "identity" };
|
|
1249
|
+
if (expr === "secret") {
|
|
1250
|
+
throw new FunctionsManifestError(
|
|
1251
|
+
`${where}: secret() is only available in action() handlers, not in declarative functions.`
|
|
1252
|
+
);
|
|
1230
1253
|
}
|
|
1231
|
-
|
|
1254
|
+
throw new FunctionsManifestError(`${where}: ${JSON.stringify(expr)} expression is not allowed here.`);
|
|
1232
1255
|
}
|
|
1233
|
-
function
|
|
1234
|
-
|
|
1235
|
-
|
|
1256
|
+
function jsonValue(value, where, stack = []) {
|
|
1257
|
+
if (value === null || typeof value === "string" || typeof value === "boolean") return value;
|
|
1258
|
+
if (typeof value === "number") {
|
|
1259
|
+
if (!Number.isFinite(value)) {
|
|
1260
|
+
throw new FunctionsManifestError(`${where}: ${value} is not a JSON number.`);
|
|
1261
|
+
}
|
|
1262
|
+
return value;
|
|
1263
|
+
}
|
|
1264
|
+
if (value === void 0) {
|
|
1265
|
+
throw new FunctionsManifestError(`${where}: undefined is not a JSON value; use null.`);
|
|
1266
|
+
}
|
|
1267
|
+
if (typeof value !== "object") {
|
|
1268
|
+
throw new FunctionsManifestError(`${where}: a ${typeof value} value cannot be serialised.`);
|
|
1269
|
+
}
|
|
1270
|
+
if (stack.includes(value)) {
|
|
1271
|
+
throw new FunctionsManifestError(`${where}: circular value cannot be serialised.`);
|
|
1272
|
+
}
|
|
1273
|
+
if (Array.isArray(value)) {
|
|
1274
|
+
return value.map((item) => jsonValue(item, where, [...stack, value]));
|
|
1275
|
+
}
|
|
1276
|
+
if (!isPlainObject(value)) {
|
|
1277
|
+
const ctor = value.constructor?.name ?? "object";
|
|
1278
|
+
throw new FunctionsManifestError(`${where}: a ${ctor} value cannot be serialised; use plain JSON.`);
|
|
1279
|
+
}
|
|
1280
|
+
const expr = expression(value, where);
|
|
1281
|
+
if (expr) return expr;
|
|
1282
|
+
const out = {};
|
|
1283
|
+
for (const [key, item] of Object.entries(value)) {
|
|
1284
|
+
out[key] = jsonValue(item, where, [...stack, value]);
|
|
1285
|
+
}
|
|
1286
|
+
return out;
|
|
1287
|
+
}
|
|
1288
|
+
function isPlainObject(value) {
|
|
1289
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
|
|
1290
|
+
const proto = Object.getPrototypeOf(value);
|
|
1291
|
+
return proto === Object.prototype || proto === null;
|
|
1236
1292
|
}
|
|
1237
1293
|
|
|
1238
|
-
// src/
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1294
|
+
// src/build/pipeline.ts
|
|
1295
|
+
var execFileAsync = promisify(execFile);
|
|
1296
|
+
var HANDLER_FILES = ["server/handler.ts", "server/handler.js", "server/index.ts", "server/index.js"];
|
|
1297
|
+
var CSS_ENTRY_FILES = ["src/globals.css", "src/style.css", "src/index.css"];
|
|
1298
|
+
var TARGET_FOOTER = "if(typeof LumioExtension!=='undefined'&&typeof LumioExtension.default==='function'){var _L=(typeof require!=='undefined'?require('@zaflun/lumio-sdk'):{}).Lumio;if(_L)_L.render(LumioExtension.default);}";
|
|
1299
|
+
var ASSET_LOADERS = Object.fromEntries(
|
|
1300
|
+
[".png", ".jpg", ".jpeg", ".gif", ".svg", ".webp", ".avif", ".ico", ".mp4", ".webm", ".ogg", ".mp3", ".wav", ".woff", ".woff2", ".ttf", ".otf", ".lottie"].map((ext) => [ext, "file"])
|
|
1301
|
+
);
|
|
1302
|
+
function define(mode) {
|
|
1303
|
+
return { "process.env.NODE_ENV": JSON.stringify(mode.dev ? "development" : "production") };
|
|
1304
|
+
}
|
|
1305
|
+
function targetEntry(projectDir, target) {
|
|
1306
|
+
const entry = path5.join(projectDir, "src", `${target}.tsx`);
|
|
1307
|
+
return fs5.existsSync(entry) ? entry : null;
|
|
1308
|
+
}
|
|
1309
|
+
function targetBuildOptions(entryPoint, outdir, target, mode = {}) {
|
|
1310
|
+
return {
|
|
1311
|
+
entryPoints: [entryPoint],
|
|
1312
|
+
bundle: true,
|
|
1313
|
+
minify: !mode.dev,
|
|
1314
|
+
format: "iife",
|
|
1315
|
+
globalName: "LumioExtension",
|
|
1316
|
+
footer: { js: TARGET_FOOTER },
|
|
1317
|
+
// outdir instead of outfile so esbuild can emit asset files alongside JS;
|
|
1318
|
+
// entryNames keeps the target name.
|
|
1319
|
+
outdir,
|
|
1320
|
+
entryNames: target,
|
|
1321
|
+
jsx: "automatic",
|
|
1322
|
+
target: "es2022",
|
|
1323
|
+
loader: { ".css": "css", ...ASSET_LOADERS },
|
|
1324
|
+
assetNames: "assets/[name]-[hash]",
|
|
1325
|
+
external: ["react", "react-dom", "react/jsx-runtime", "react/jsx-dev-runtime", "@zaflun/lumio-sdk", "@zaflun/lumio-protocol"],
|
|
1326
|
+
define: define(mode)
|
|
1327
|
+
};
|
|
1328
|
+
}
|
|
1329
|
+
async function planServerBuild(projectDir, config, outfile, mode = {}) {
|
|
1330
|
+
const functionsFile = config.category !== "automation_node" ? findFunctionsFile(projectDir) : null;
|
|
1331
|
+
const schema = await loadDataSchema(projectDir);
|
|
1332
|
+
let manifest = null;
|
|
1333
|
+
if (functionsFile) {
|
|
1334
|
+
const loaded = await loadFunctionExports(projectDir);
|
|
1335
|
+
manifest = buildFunctionsManifest(loaded?.exports ?? {}, schema, functionsFile);
|
|
1336
|
+
}
|
|
1337
|
+
const handlerEntry = HANDLER_FILES.find((f) => fs5.existsSync(path5.join(projectDir, f))) ?? null;
|
|
1338
|
+
const sfHandlers = manifest ? handlerNames(manifest) : [];
|
|
1339
|
+
const entry = handlerEntry && functionsFile && sfHandlers.length > 0 ? {
|
|
1340
|
+
stdin: {
|
|
1341
|
+
contents: combinedServerEntry(handlerEntry, functionsFile, sfHandlers),
|
|
1342
|
+
resolveDir: projectDir,
|
|
1343
|
+
sourcefile: "lumio-server-entry.ts",
|
|
1344
|
+
loader: "ts"
|
|
1248
1345
|
}
|
|
1346
|
+
} : handlerEntry || functionsFile ? { entryPoints: [path5.join(projectDir, handlerEntry ?? functionsFile)] } : null;
|
|
1347
|
+
const options = entry ? {
|
|
1348
|
+
...entry,
|
|
1349
|
+
absWorkingDir: projectDir,
|
|
1350
|
+
bundle: true,
|
|
1351
|
+
// Minified in dev too: the isolate's ESM → script transform (backend
|
|
1352
|
+
// and devkit replica alike) reads esbuild's minified import/export form.
|
|
1353
|
+
minify: true,
|
|
1354
|
+
format: "esm",
|
|
1355
|
+
outfile,
|
|
1356
|
+
platform: "neutral",
|
|
1357
|
+
target: "es2022",
|
|
1358
|
+
external: ["@zaflun/lumio-sdk", "@zaflun/lumio-protocol"],
|
|
1359
|
+
define: define(mode)
|
|
1360
|
+
} : null;
|
|
1361
|
+
return { options, manifest, schema, handlerEntry, functionsFile };
|
|
1362
|
+
}
|
|
1363
|
+
function findCssEntry(projectDir) {
|
|
1364
|
+
return CSS_ENTRY_FILES.find((f) => fs5.existsSync(path5.join(projectDir, f))) ?? null;
|
|
1365
|
+
}
|
|
1366
|
+
async function compileCss(projectDir, outDir, mode = {}) {
|
|
1367
|
+
const cssEntryFile = findCssEntry(projectDir);
|
|
1368
|
+
if (!cssEntryFile) return null;
|
|
1369
|
+
const input = path5.join(projectDir, cssEntryFile);
|
|
1370
|
+
const output = path5.join(outDir, "styles.css");
|
|
1371
|
+
const cssContent = fs5.readFileSync(input, "utf-8");
|
|
1372
|
+
const usesTailwind = cssContent.includes("tailwindcss") || cssContent.includes("@tailwind");
|
|
1373
|
+
if (!usesTailwind) {
|
|
1374
|
+
fs5.copyFileSync(input, output);
|
|
1375
|
+
return { file: output, tailwind: false };
|
|
1249
1376
|
}
|
|
1250
|
-
|
|
1377
|
+
try {
|
|
1378
|
+
const projectRequire = createRequire(path5.join(projectDir, "package.json"));
|
|
1379
|
+
const cliPkgPath = projectRequire.resolve("@tailwindcss/cli/package.json");
|
|
1380
|
+
const cliPkg = JSON.parse(fs5.readFileSync(cliPkgPath, "utf-8"));
|
|
1381
|
+
const binRel = typeof cliPkg.bin === "string" ? cliPkg.bin : cliPkg.bin?.tailwindcss ?? Object.values(cliPkg.bin ?? {})[0];
|
|
1382
|
+
if (!binRel) throw new Error("@tailwindcss/cli has no bin entry");
|
|
1383
|
+
const binPath = path5.join(path5.dirname(cliPkgPath), binRel);
|
|
1384
|
+
await execFileAsync(process.execPath, [binPath, "-i", input, "-o", output, ...mode.dev ? [] : ["--minify"]], { cwd: projectDir });
|
|
1385
|
+
} catch (err) {
|
|
1386
|
+
throw new CssBuildError(err instanceof Error ? err.message : String(err));
|
|
1387
|
+
}
|
|
1388
|
+
return { file: output, tailwind: true };
|
|
1251
1389
|
}
|
|
1252
|
-
var
|
|
1253
|
-
"
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1390
|
+
var CssBuildError = class extends Error {
|
|
1391
|
+
hint = "Install tailwindcss v4: npm add -D tailwindcss@^4.3 @tailwindcss/cli@^4.3";
|
|
1392
|
+
constructor(message2) {
|
|
1393
|
+
super(message2);
|
|
1394
|
+
this.name = "CssBuildError";
|
|
1395
|
+
}
|
|
1396
|
+
};
|
|
1397
|
+
|
|
1398
|
+
// src/dev/builder.ts
|
|
1399
|
+
var DEV_OUT_DIR = path6.join(".lumio", "dev");
|
|
1400
|
+
var DEV_TARGETS = ["editor", "layer"];
|
|
1401
|
+
function devTargets(projectDir, config) {
|
|
1402
|
+
const targets = [];
|
|
1403
|
+
const skipped = [];
|
|
1404
|
+
for (const t of config.targets) {
|
|
1405
|
+
if (!DEV_TARGETS.includes(t)) {
|
|
1406
|
+
skipped.push(`target "${t}" is not simulated by lumio dev`);
|
|
1407
|
+
} else if (!targetEntry(projectDir, t)) {
|
|
1408
|
+
skipped.push(`target "${t}": src/${t}.tsx not found`);
|
|
1409
|
+
} else targets.push(t);
|
|
1410
|
+
}
|
|
1411
|
+
return { targets, skipped };
|
|
1412
|
+
}
|
|
1413
|
+
function formatMessages(messages) {
|
|
1414
|
+
return messages.map((m) => m.location ? `${m.location.file}:${m.location.line}:${m.location.column}: ${m.text}` : m.text);
|
|
1415
|
+
}
|
|
1416
|
+
var DevBuilder = class {
|
|
1417
|
+
constructor(projectDir, config, targets, handlers) {
|
|
1418
|
+
this.projectDir = projectDir;
|
|
1419
|
+
this.config = config;
|
|
1420
|
+
this.targets = targets;
|
|
1421
|
+
this.handlers = handlers;
|
|
1422
|
+
this.outDir = path6.join(projectDir, DEV_OUT_DIR);
|
|
1423
|
+
}
|
|
1424
|
+
projectDir;
|
|
1425
|
+
config;
|
|
1426
|
+
targets;
|
|
1427
|
+
handlers;
|
|
1428
|
+
contexts = [];
|
|
1429
|
+
serverContext = null;
|
|
1430
|
+
serverEntryKey = "";
|
|
1431
|
+
watchers = [];
|
|
1432
|
+
serverTimer = null;
|
|
1433
|
+
serverBusy = false;
|
|
1434
|
+
lastTarget = /* @__PURE__ */ new Map();
|
|
1435
|
+
lastServer = "";
|
|
1436
|
+
cssRunning = null;
|
|
1437
|
+
stopped = false;
|
|
1438
|
+
outDir;
|
|
1439
|
+
/** Builds everything once and starts watching. Resolves to the first bundles. */
|
|
1440
|
+
async start() {
|
|
1441
|
+
fs6.mkdirSync(this.outDir, { recursive: true });
|
|
1442
|
+
const { context } = await import("esbuild");
|
|
1443
|
+
const bundles = {};
|
|
1444
|
+
let first = true;
|
|
1445
|
+
for (const target of this.targets) {
|
|
1446
|
+
const entry = targetEntry(this.projectDir, target);
|
|
1447
|
+
const outFile = path6.join(this.outDir, `${target}.js`);
|
|
1448
|
+
const ctx = await context({
|
|
1449
|
+
...targetBuildOptions(entry, this.outDir, target, { dev: true }),
|
|
1450
|
+
absWorkingDir: this.projectDir,
|
|
1451
|
+
logLevel: "silent",
|
|
1452
|
+
plugins: [
|
|
1453
|
+
this.onEnd(`${target}.js`, (ok) => {
|
|
1454
|
+
if (!ok) return;
|
|
1455
|
+
const code = fs6.readFileSync(outFile, "utf8");
|
|
1456
|
+
if (this.lastTarget.get(target) === code) return;
|
|
1457
|
+
this.lastTarget.set(target, code);
|
|
1458
|
+
if (first) bundles[target] = code;
|
|
1459
|
+
else this.handlers.onTarget(target, code);
|
|
1460
|
+
void this.css();
|
|
1461
|
+
})
|
|
1462
|
+
]
|
|
1463
|
+
});
|
|
1464
|
+
this.contexts.push(ctx);
|
|
1465
|
+
await ctx.rebuild().catch(() => {
|
|
1466
|
+
});
|
|
1467
|
+
}
|
|
1468
|
+
const server = await this.buildServer(true);
|
|
1469
|
+
await this.css();
|
|
1470
|
+
first = false;
|
|
1471
|
+
for (const ctx of this.contexts) await ctx.watch();
|
|
1472
|
+
if (this.serverContext) await this.serverContext.watch();
|
|
1473
|
+
this.watch(path6.join(this.projectDir, "server"), () => this.scheduleServer());
|
|
1474
|
+
return { bundles, server };
|
|
1475
|
+
}
|
|
1476
|
+
onEnd(key, done) {
|
|
1477
|
+
return {
|
|
1478
|
+
name: "lumio-dev",
|
|
1479
|
+
setup: (build) => {
|
|
1480
|
+
build.onEnd(async (result) => {
|
|
1481
|
+
if (this.stopped) return;
|
|
1482
|
+
const ok = result.errors.length === 0;
|
|
1483
|
+
this.handlers.onStatus(key, ok ? null : formatMessages(result.errors).map((m) => `${key}: ${m}`));
|
|
1484
|
+
await done(ok, result);
|
|
1485
|
+
});
|
|
1486
|
+
}
|
|
1487
|
+
};
|
|
1488
|
+
}
|
|
1489
|
+
watch(dir, onChange) {
|
|
1490
|
+
if (!fs6.existsSync(dir)) return;
|
|
1491
|
+
try {
|
|
1492
|
+
this.watchers.push(fs6.watch(dir, { recursive: true }, () => onChange()));
|
|
1493
|
+
} catch {
|
|
1494
|
+
}
|
|
1495
|
+
}
|
|
1496
|
+
scheduleServer() {
|
|
1497
|
+
if (this.stopped) return;
|
|
1498
|
+
if (this.serverTimer) clearTimeout(this.serverTimer);
|
|
1499
|
+
this.serverTimer = setTimeout(() => {
|
|
1500
|
+
this.serverTimer = null;
|
|
1501
|
+
void this.buildServer(false);
|
|
1502
|
+
}, 120);
|
|
1503
|
+
}
|
|
1504
|
+
/** Plans the server build (functions.json + schema), (re)creates its context when the entry changed, and builds. */
|
|
1505
|
+
async buildServer(first) {
|
|
1506
|
+
const empty = { bundle: null, functions: null, schema: null };
|
|
1507
|
+
if (!this.config.server) return empty;
|
|
1508
|
+
this.serverBusy = true;
|
|
1509
|
+
try {
|
|
1510
|
+
const outfile = path6.join(this.outDir, "server.js");
|
|
1511
|
+
let plan;
|
|
1512
|
+
try {
|
|
1513
|
+
plan = await planServerBuild(this.projectDir, this.config, outfile, { dev: true });
|
|
1514
|
+
this.handlers.onStatus("server", null);
|
|
1515
|
+
} catch (e) {
|
|
1516
|
+
this.handlers.onStatus("server", [`server: ${e instanceof Error ? e.message : String(e)}`]);
|
|
1517
|
+
return empty;
|
|
1518
|
+
}
|
|
1519
|
+
const result = { bundle: null, functions: plan.manifest ? JSON.stringify(plan.manifest) : null, schema: plan.schema };
|
|
1520
|
+
if (!plan.options) {
|
|
1521
|
+
await this.serverContext?.dispose();
|
|
1522
|
+
this.serverContext = null;
|
|
1523
|
+
} else {
|
|
1524
|
+
const key = JSON.stringify({ e: plan.options.entryPoints, s: plan.options.stdin });
|
|
1525
|
+
if (!this.serverContext || key !== this.serverEntryKey) {
|
|
1526
|
+
await this.serverContext?.dispose();
|
|
1527
|
+
const { context } = await import("esbuild");
|
|
1528
|
+
this.serverContext = await context({
|
|
1529
|
+
...plan.options,
|
|
1530
|
+
logLevel: "silent",
|
|
1531
|
+
// A change inside the bundle graph re-plans: functions.ts may be part of it.
|
|
1532
|
+
plugins: [this.onEnd("server.js", () => {
|
|
1533
|
+
if (!this.serverBusy) this.scheduleServer();
|
|
1534
|
+
})]
|
|
1535
|
+
});
|
|
1536
|
+
this.serverEntryKey = key;
|
|
1537
|
+
if (!first) await this.serverContext.watch();
|
|
1538
|
+
}
|
|
1539
|
+
try {
|
|
1540
|
+
await this.serverContext.rebuild();
|
|
1541
|
+
} catch {
|
|
1542
|
+
if (!first) return result;
|
|
1543
|
+
}
|
|
1544
|
+
result.bundle = this.readServer(outfile);
|
|
1545
|
+
}
|
|
1546
|
+
const fingerprint = JSON.stringify(result);
|
|
1547
|
+
if (!first && fingerprint !== this.lastServer) this.handlers.onServer(result);
|
|
1548
|
+
this.lastServer = fingerprint;
|
|
1549
|
+
return result;
|
|
1550
|
+
} finally {
|
|
1551
|
+
this.serverBusy = false;
|
|
1552
|
+
}
|
|
1553
|
+
}
|
|
1554
|
+
readServer(outfile) {
|
|
1555
|
+
return fs6.existsSync(outfile) ? fs6.readFileSync(outfile, "utf8") : null;
|
|
1556
|
+
}
|
|
1557
|
+
/** Tailwind scans the sources, so the stylesheet follows every target build. */
|
|
1558
|
+
css() {
|
|
1559
|
+
if (this.cssRunning) return this.cssRunning;
|
|
1560
|
+
this.cssRunning = (async () => {
|
|
1561
|
+
try {
|
|
1562
|
+
const css = await compileCss(this.projectDir, this.outDir, { dev: true });
|
|
1563
|
+
this.handlers.onStatus("styles.css", null);
|
|
1564
|
+
if (css) this.handlers.onCss();
|
|
1565
|
+
} catch (e) {
|
|
1566
|
+
const hint = e instanceof CssBuildError ? ` (${e.hint})` : "";
|
|
1567
|
+
this.handlers.onStatus("styles.css", [`styles.css: ${e instanceof Error ? e.message : String(e)}${hint}`]);
|
|
1568
|
+
} finally {
|
|
1569
|
+
this.cssRunning = null;
|
|
1570
|
+
}
|
|
1571
|
+
})();
|
|
1572
|
+
return this.cssRunning;
|
|
1573
|
+
}
|
|
1574
|
+
/** The built stylesheet, or `null`. */
|
|
1575
|
+
stylesheet() {
|
|
1576
|
+
const file = path6.join(this.outDir, "styles.css");
|
|
1577
|
+
return fs6.existsSync(file) ? fs6.readFileSync(file, "utf8") : null;
|
|
1578
|
+
}
|
|
1579
|
+
async stop() {
|
|
1580
|
+
this.stopped = true;
|
|
1581
|
+
if (this.serverTimer) clearTimeout(this.serverTimer);
|
|
1582
|
+
for (const w of this.watchers.splice(0)) w.close();
|
|
1583
|
+
for (const ctx of this.contexts.splice(0)) await ctx.dispose();
|
|
1584
|
+
await this.serverContext?.dispose();
|
|
1585
|
+
this.serverContext = null;
|
|
1586
|
+
}
|
|
1587
|
+
};
|
|
1588
|
+
|
|
1589
|
+
// src/dev/project-modules.ts
|
|
1590
|
+
import * as vm from "vm";
|
|
1591
|
+
import { createRequire as createRequire2 } from "module";
|
|
1592
|
+
import * as path8 from "path";
|
|
1593
|
+
|
|
1594
|
+
// src/dev/resolve.ts
|
|
1595
|
+
import * as fs7 from "fs";
|
|
1596
|
+
import * as path7 from "path";
|
|
1597
|
+
import { pathToFileURL } from "url";
|
|
1598
|
+
function findPackage(fromDir, name) {
|
|
1599
|
+
let dir = path7.resolve(fromDir);
|
|
1600
|
+
for (; ; ) {
|
|
1601
|
+
const candidate = path7.join(dir, "node_modules", name, "package.json");
|
|
1602
|
+
if (fs7.existsSync(candidate)) {
|
|
1603
|
+
const manifest = JSON.parse(fs7.readFileSync(candidate, "utf8"));
|
|
1604
|
+
return {
|
|
1605
|
+
name,
|
|
1606
|
+
version: typeof manifest.version === "string" ? manifest.version : "0.0.0",
|
|
1607
|
+
dir: fs7.realpathSync(path7.dirname(candidate)),
|
|
1608
|
+
manifest
|
|
1609
|
+
};
|
|
1610
|
+
}
|
|
1611
|
+
const parent = path7.dirname(dir);
|
|
1612
|
+
if (parent === dir) return null;
|
|
1613
|
+
dir = parent;
|
|
1614
|
+
}
|
|
1615
|
+
}
|
|
1616
|
+
function entryOf(pkg, subpath = ".") {
|
|
1617
|
+
const exportsField = pkg.manifest.exports;
|
|
1618
|
+
let target;
|
|
1619
|
+
if (typeof exportsField === "string") {
|
|
1620
|
+
target = subpath === "." ? exportsField : void 0;
|
|
1621
|
+
} else if (exportsField && typeof exportsField === "object") {
|
|
1622
|
+
const map = exportsField;
|
|
1623
|
+
const isSubpathMap = Object.keys(map).some((k) => k.startsWith("."));
|
|
1624
|
+
const entry = isSubpathMap ? map[subpath] : subpath === "." ? map : void 0;
|
|
1625
|
+
target = pickCondition(entry);
|
|
1626
|
+
}
|
|
1627
|
+
if (target === void 0 && subpath === ".") {
|
|
1628
|
+
target = pkg.manifest.module ?? pkg.manifest.main ?? "index.js";
|
|
1629
|
+
}
|
|
1630
|
+
if (typeof target !== "string") {
|
|
1631
|
+
throw new Error(`${pkg.name} does not export "${subpath}" (installed ${pkg.version} at ${pkg.dir})`);
|
|
1632
|
+
}
|
|
1633
|
+
return path7.join(pkg.dir, target);
|
|
1634
|
+
}
|
|
1635
|
+
function pickCondition(entry) {
|
|
1636
|
+
if (typeof entry === "string" || entry === void 0 || entry === null) return entry ?? void 0;
|
|
1637
|
+
if (typeof entry !== "object") return void 0;
|
|
1638
|
+
const conditions = entry;
|
|
1639
|
+
for (const key of ["import", "node", "default"]) {
|
|
1640
|
+
if (key in conditions) return pickCondition(conditions[key]);
|
|
1641
|
+
}
|
|
1642
|
+
return void 0;
|
|
1643
|
+
}
|
|
1644
|
+
async function importFile(file) {
|
|
1645
|
+
return await import(pathToFileURL(file).href);
|
|
1646
|
+
}
|
|
1647
|
+
function compareVersions(a, b) {
|
|
1648
|
+
const parse = (v) => v.split("-")[0].split(".").map((n) => Number.parseInt(n, 10) || 0);
|
|
1649
|
+
const [x, y] = [parse(a), parse(b)];
|
|
1650
|
+
for (let i = 0; i < 3; i++) {
|
|
1651
|
+
const d = (x[i] ?? 0) - (y[i] ?? 0);
|
|
1652
|
+
if (d !== 0) return d < 0 ? -1 : 1;
|
|
1653
|
+
}
|
|
1654
|
+
return 0;
|
|
1655
|
+
}
|
|
1656
|
+
|
|
1657
|
+
// src/dev/version.ts
|
|
1658
|
+
var CLI_PROTOCOL_VERSION = "0.9.0";
|
|
1659
|
+
|
|
1660
|
+
// src/dev/project-modules.ts
|
|
1661
|
+
var MissingPackageError = class extends Error {
|
|
1662
|
+
constructor(packageName) {
|
|
1663
|
+
super(`${packageName} is not installed in this project. Run: npm install --save-dev ${packageName}`);
|
|
1664
|
+
this.packageName = packageName;
|
|
1665
|
+
this.name = "MissingPackageError";
|
|
1666
|
+
}
|
|
1667
|
+
packageName;
|
|
1668
|
+
};
|
|
1669
|
+
function need(fromDir, name) {
|
|
1670
|
+
const pkg = findPackage(fromDir, name);
|
|
1671
|
+
if (!pkg) throw new MissingPackageError(name);
|
|
1672
|
+
return pkg;
|
|
1673
|
+
}
|
|
1674
|
+
async function loadProjectModules(projectDir) {
|
|
1675
|
+
const devkitPkg = need(projectDir, "@zaflun/lumio-devkit");
|
|
1676
|
+
const sdkPkg = findPackage(devkitPkg.dir, "@zaflun/lumio-sdk") ?? need(projectDir, "@zaflun/lumio-sdk");
|
|
1677
|
+
const protocolPkg = findPackage(sdkPkg.dir, "@zaflun/lumio-protocol") ?? need(projectDir, "@zaflun/lumio-protocol");
|
|
1678
|
+
const devkit = await importFile(entryOf(devkitPkg));
|
|
1679
|
+
const devkitNode = await importFile(entryOf(devkitPkg, "./node"));
|
|
1680
|
+
const sdk = await importFile(entryOf(sdkPkg));
|
|
1681
|
+
const sdkServer = await importFile(entryOf(sdkPkg, "./server"));
|
|
1682
|
+
const protocol = await importFile(entryOf(protocolPkg));
|
|
1683
|
+
const sdkRequire = createRequire2(path8.join(sdkPkg.dir, "package.json"));
|
|
1684
|
+
const react = sdkRequire("react");
|
|
1685
|
+
const jsxRuntime = sdkRequire("react/jsx-runtime");
|
|
1686
|
+
const warnings = [];
|
|
1687
|
+
if (compareVersions(protocolPkg.version, CLI_PROTOCOL_VERSION) < 0) {
|
|
1688
|
+
warnings.push(
|
|
1689
|
+
`@zaflun/lumio-sdk ${sdkPkg.version} of this project speaks protocol ${protocolPkg.version}, older than ${CLI_PROTOCOL_VERSION} of this CLI. Update: npm install @zaflun/lumio-sdk@latest && npm install --save-dev @zaflun/lumio-devkit@latest`
|
|
1690
|
+
);
|
|
1691
|
+
}
|
|
1692
|
+
const devkitProtocol = findPackage(devkitPkg.dir, "@zaflun/lumio-protocol");
|
|
1693
|
+
if (devkitProtocol && compareVersions(devkitProtocol.version, CLI_PROTOCOL_VERSION) < 0) {
|
|
1694
|
+
warnings.push(
|
|
1695
|
+
`@zaflun/lumio-devkit ${devkitPkg.version} mocks protocol ${devkitProtocol.version}, older than ${CLI_PROTOCOL_VERSION} of this CLI: events and actions added since are missing. Update: npm install --save-dev @zaflun/lumio-devkit@latest`
|
|
1696
|
+
);
|
|
1697
|
+
}
|
|
1698
|
+
return {
|
|
1699
|
+
devkit,
|
|
1700
|
+
devkitNode,
|
|
1701
|
+
react,
|
|
1702
|
+
registry: {
|
|
1703
|
+
react,
|
|
1704
|
+
"react/jsx-runtime": jsxRuntime,
|
|
1705
|
+
"@zaflun/lumio-sdk": sdk,
|
|
1706
|
+
"@zaflun/lumio-sdk/server": sdkServer,
|
|
1707
|
+
"@zaflun/lumio-protocol": protocol
|
|
1708
|
+
},
|
|
1709
|
+
versions: { devkit: devkitPkg.version, sdk: sdkPkg.version, protocol: protocolPkg.version },
|
|
1710
|
+
warnings
|
|
1711
|
+
};
|
|
1712
|
+
}
|
|
1713
|
+
var BundleError = class extends Error {
|
|
1714
|
+
constructor(message2) {
|
|
1715
|
+
super(message2);
|
|
1716
|
+
this.name = "BundleError";
|
|
1717
|
+
}
|
|
1718
|
+
};
|
|
1719
|
+
function evaluateTargetBundle(code, file, modules) {
|
|
1720
|
+
const react = modules.react;
|
|
1721
|
+
let rendered = null;
|
|
1722
|
+
const lumio = {
|
|
1723
|
+
render(input) {
|
|
1724
|
+
if (rendered) throw new Error("Lumio.render() can only be called once");
|
|
1725
|
+
rendered = typeof input === "function" ? react.createElement(input) : input;
|
|
1726
|
+
}
|
|
1727
|
+
};
|
|
1728
|
+
const registry = {
|
|
1729
|
+
...modules.registry,
|
|
1730
|
+
"@zaflun/lumio-sdk": { ...modules.registry["@zaflun/lumio-sdk"], Lumio: lumio }
|
|
1731
|
+
};
|
|
1732
|
+
const lumioRequire = (id) => {
|
|
1733
|
+
if (!(id in registry)) throw new Error(`[Lumio Runtime] Module "${id}" is not available in the extension sandbox`);
|
|
1734
|
+
return registry[id];
|
|
1735
|
+
};
|
|
1736
|
+
try {
|
|
1737
|
+
const fn = vm.runInThisContext(`(function (require, self) {
|
|
1738
|
+
${code}
|
|
1739
|
+
})`, { filename: file });
|
|
1740
|
+
fn(lumioRequire, globalThis);
|
|
1741
|
+
} catch (e) {
|
|
1742
|
+
throw new BundleError(`${path8.basename(file)} failed to evaluate: ${e instanceof Error ? e.message : String(e)}`);
|
|
1743
|
+
}
|
|
1744
|
+
if (!rendered) {
|
|
1745
|
+
throw new BundleError(`${path8.basename(file)} rendered nothing: export a default component or call Lumio.render(<App />)`);
|
|
1746
|
+
}
|
|
1747
|
+
return rendered;
|
|
1748
|
+
}
|
|
1749
|
+
|
|
1750
|
+
// src/dev/repl.ts
|
|
1751
|
+
import * as readline2 from "readline";
|
|
1752
|
+
import chalk2 from "chalk";
|
|
1753
|
+
|
|
1754
|
+
// src/dev/errors.ts
|
|
1755
|
+
var CommandSyntaxError = class extends Error {
|
|
1756
|
+
constructor(message2) {
|
|
1757
|
+
super(message2);
|
|
1758
|
+
this.name = "CommandSyntaxError";
|
|
1759
|
+
}
|
|
1760
|
+
};
|
|
1761
|
+
|
|
1762
|
+
// src/dev/load.ts
|
|
1763
|
+
var MAX_COUNT = 1e4;
|
|
1764
|
+
var MAX_RATE_PER_SECOND = 1e3;
|
|
1765
|
+
function parseCount(raw) {
|
|
1766
|
+
const n = Number(raw);
|
|
1767
|
+
if (!Number.isInteger(n) || n < 1 || n > MAX_COUNT) throw new CommandSyntaxError(`--count must be an integer between 1 and ${MAX_COUNT}, got "${raw}"`);
|
|
1768
|
+
return n;
|
|
1769
|
+
}
|
|
1770
|
+
function parseRate(raw) {
|
|
1771
|
+
const m = /^(\d+(?:\.\d+)?)(?:\/(s|sec|m|min|h))?$/.exec(raw.trim());
|
|
1772
|
+
if (!m) throw new CommandSyntaxError(`--rate is a count per time, e.g. 5/s, 120/m or 600/h, got "${raw}"`);
|
|
1773
|
+
const per = { s: 1, sec: 1, m: 60, min: 60, h: 3600 }[m[2] ?? "s"];
|
|
1774
|
+
const perSecond = Number(m[1]) / per;
|
|
1775
|
+
if (!(perSecond > 0) || perSecond > MAX_RATE_PER_SECOND) {
|
|
1776
|
+
throw new CommandSyntaxError(`--rate must be above 0 and at most ${MAX_RATE_PER_SECOND}/s, got "${raw}"`);
|
|
1777
|
+
}
|
|
1778
|
+
return 1e3 / perSecond;
|
|
1779
|
+
}
|
|
1780
|
+
function describeRate(intervalMs) {
|
|
1781
|
+
if (intervalMs <= 0) return "back to back";
|
|
1782
|
+
const perSecond = 1e3 / intervalMs;
|
|
1783
|
+
return perSecond >= 1 ? `${round(perSecond)}/s` : `${round(perSecond * 60)}/m`;
|
|
1784
|
+
}
|
|
1785
|
+
function round(n) {
|
|
1786
|
+
return Math.round(n * 100) / 100;
|
|
1787
|
+
}
|
|
1788
|
+
function seededPicker(seed) {
|
|
1789
|
+
let state = seed >>> 0;
|
|
1790
|
+
return (items) => {
|
|
1791
|
+
if (items.length === 0) throw new CommandSyntaxError("Nothing to pick from");
|
|
1792
|
+
state = state + 1831565813 >>> 0;
|
|
1793
|
+
let t = state;
|
|
1794
|
+
t = Math.imul(t ^ t >>> 15, t | 1);
|
|
1795
|
+
t ^= t + Math.imul(t ^ t >>> 7, t | 61);
|
|
1796
|
+
const r = ((t ^ t >>> 14) >>> 0) / 4294967296;
|
|
1797
|
+
return items[Math.floor(r * items.length)];
|
|
1798
|
+
};
|
|
1799
|
+
}
|
|
1800
|
+
function freshSeed() {
|
|
1801
|
+
return Math.floor(Math.random() * 2 ** 31);
|
|
1802
|
+
}
|
|
1803
|
+
function randomEventPool(eventTypes, platform) {
|
|
1804
|
+
const pool = eventTypes.filter((e) => e.forwarded && (platform === void 0 || e.type.startsWith(`${platform}:`))).map((e) => e.type);
|
|
1805
|
+
if (pool.length === 0) {
|
|
1806
|
+
throw new CommandSyntaxError(platform ? `No ${platform}:* event type is forwarded to extensions` : "No event type is forwarded to extensions");
|
|
1807
|
+
}
|
|
1808
|
+
return pool;
|
|
1809
|
+
}
|
|
1810
|
+
function repeatSpec(cmd, defaultSeed) {
|
|
1811
|
+
const spec = { count: cmd.count ?? 1, intervalMs: cmd.intervalMs ?? 0, random: cmd.random === true, seed: cmd.seed ?? defaultSeed };
|
|
1812
|
+
if (!Number.isInteger(spec.count) || spec.count < 1 || spec.count > MAX_COUNT) throw new CommandSyntaxError(`count must be an integer between 1 and ${MAX_COUNT}`);
|
|
1813
|
+
if (typeof spec.intervalMs !== "number" || !(spec.intervalMs >= 0)) throw new CommandSyntaxError("intervalMs must be 0 or more");
|
|
1814
|
+
const inner = cmd.command;
|
|
1815
|
+
if (typeof inner !== "object" || inner === null || inner.kind !== "event" && inner.kind !== "scenario" && inner.kind !== "chat") {
|
|
1816
|
+
throw new CommandSyntaxError("repeat runs an event, scenario or chat command");
|
|
1817
|
+
}
|
|
1818
|
+
return spec;
|
|
1819
|
+
}
|
|
1820
|
+
function repeatVariants(cmd, spec, pools) {
|
|
1821
|
+
const inner = cmd.command;
|
|
1822
|
+
if (!spec.random) return () => inner;
|
|
1823
|
+
const pick = seededPicker(spec.seed);
|
|
1824
|
+
if (inner.kind === "scenario") {
|
|
1825
|
+
if (pools.scenarios.length === 0) throw new CommandSyntaxError("This devkit has no scenarios");
|
|
1826
|
+
return () => ({ ...inner, name: pick(pools.scenarios) });
|
|
1827
|
+
}
|
|
1828
|
+
if (inner.kind !== "event") throw new CommandSyntaxError("--random picks an event type or a scenario");
|
|
1829
|
+
const pool = randomEventPool(pools.eventTypes, cmd.platform);
|
|
1830
|
+
return () => ({ ...inner, type: pick(pool) });
|
|
1831
|
+
}
|
|
1832
|
+
function defaultSleep(ms, signal) {
|
|
1833
|
+
return new Promise((resolve13) => {
|
|
1834
|
+
if (signal?.aborted) return resolve13();
|
|
1835
|
+
const timer = setTimeout(done, ms);
|
|
1836
|
+
function done() {
|
|
1837
|
+
clearTimeout(timer);
|
|
1838
|
+
signal?.removeEventListener("abort", done);
|
|
1839
|
+
resolve13();
|
|
1840
|
+
}
|
|
1841
|
+
signal?.addEventListener("abort", done, { once: true });
|
|
1842
|
+
});
|
|
1843
|
+
}
|
|
1844
|
+
async function runLoad(spec, run, hooks = {}) {
|
|
1845
|
+
const now = hooks.now ?? Date.now;
|
|
1846
|
+
const sleep = hooks.sleep ?? defaultSleep;
|
|
1847
|
+
const results = [];
|
|
1848
|
+
const start = now();
|
|
1849
|
+
for (let i = 0; i < spec.count; i++) {
|
|
1850
|
+
if (hooks.signal?.aborted) return { results, failed: null, stopped: true };
|
|
1851
|
+
if (i > 0 && spec.intervalMs > 0) {
|
|
1852
|
+
const wait = start + i * spec.intervalMs - now();
|
|
1853
|
+
if (wait > 0) await sleep(wait, hooks.signal);
|
|
1854
|
+
if (hooks.signal?.aborted) return { results, failed: null, stopped: true };
|
|
1855
|
+
}
|
|
1856
|
+
const r = await run(i);
|
|
1857
|
+
results.push(r);
|
|
1858
|
+
if (!r.ok) return { results, failed: r, stopped: false };
|
|
1859
|
+
}
|
|
1860
|
+
return { results, failed: null, stopped: false };
|
|
1861
|
+
}
|
|
1862
|
+
function summarizeLoad(spec, result) {
|
|
1863
|
+
const ok = result.results.filter((r) => r.ok).length;
|
|
1864
|
+
const pace = spec.count > 1 ? ` (${describeRate(spec.intervalMs)})` : "";
|
|
1865
|
+
const random = spec.random ? ` \xB7 random picks, seed ${spec.seed}` : "";
|
|
1866
|
+
if (result.failed) return `failed at ${result.results.length}/${spec.count}, ${ok} sent${pace}${random}`;
|
|
1867
|
+
if (result.stopped) return `stopped after ${ok}/${spec.count}${pace}${random}`;
|
|
1868
|
+
return `sent ${ok}/${spec.count}${pace}${random}`;
|
|
1869
|
+
}
|
|
1870
|
+
|
|
1871
|
+
// src/dev/commands.ts
|
|
1872
|
+
var REPL_HELP = [
|
|
1873
|
+
"Commands (plain text without a / is a chat message):",
|
|
1874
|
+
" /event <type> [field=value \u2026] [--json '{\u2026}'] [--user name] send an event (fields are dotted paths)",
|
|
1875
|
+
" /event @<preset> send a preset from lumio/mocks/events/",
|
|
1876
|
+
" /chat [@role:name] <text> [--platform kick] a chat message (commands, keywords, moderation)",
|
|
1877
|
+
' /reward "<title>" [--user name] [--input "text"] [--platform kick] redeem a channel-point reward',
|
|
1878
|
+
" /reward-status <redemption-id> <status> fulfilled|canceled (Twitch), accepted|rejected (Kick)",
|
|
1879
|
+
" /music play|pause|skip [n]|seek 1:30|volume 50|shuffle on|repeat track|queue <uri>|device <name>|advance 30s",
|
|
1880
|
+
" /timer <name> fire a declared timer /defer run run queued ctx.defer jobs",
|
|
1881
|
+
" /scenario <name> [key=value \u2026] e.g. /scenario raid viewers=500",
|
|
1882
|
+
" load flags on /event, /scenario and chat: --count N --rate 5/s --random [--platform kick] --seed n",
|
|
1883
|
+
" e.g. /event twitch:follower --count 50 --rate 5/s \xB7 /event --random --count 20 \xB7 /scenario --random",
|
|
1884
|
+
" /storage [key] [json] show or set storage /db [table] [--reset] show ctx.db rows",
|
|
1885
|
+
" /config [key=value \u2026] show or change settings /theme light|dark",
|
|
1886
|
+
" /view editor|layer switch the sandbox view /stop stop running loads /status /help /quit"
|
|
1887
|
+
];
|
|
1888
|
+
function tokenize(line) {
|
|
1889
|
+
const out = [];
|
|
1890
|
+
let current = "";
|
|
1891
|
+
let quote = null;
|
|
1892
|
+
let inToken = false;
|
|
1893
|
+
for (let i = 0; i < line.length; i++) {
|
|
1894
|
+
const c = line[i];
|
|
1895
|
+
if (quote) {
|
|
1896
|
+
if (c === quote) quote = null;
|
|
1897
|
+
else if (c === "\\" && quote === '"' && i + 1 < line.length) current += line[++i];
|
|
1898
|
+
else current += c;
|
|
1899
|
+
continue;
|
|
1900
|
+
}
|
|
1901
|
+
if (c === '"' || c === "'") {
|
|
1902
|
+
quote = c;
|
|
1903
|
+
inToken = true;
|
|
1904
|
+
} else if (c === "\\" && i + 1 < line.length) {
|
|
1905
|
+
current += line[++i];
|
|
1906
|
+
inToken = true;
|
|
1907
|
+
} else if (/\s/.test(c)) {
|
|
1908
|
+
if (inToken) out.push(current);
|
|
1909
|
+
current = "";
|
|
1910
|
+
inToken = false;
|
|
1911
|
+
} else {
|
|
1912
|
+
current += c;
|
|
1913
|
+
inToken = true;
|
|
1914
|
+
}
|
|
1915
|
+
}
|
|
1916
|
+
if (quote) throw new CommandSyntaxError(`Unclosed ${quote} quote`);
|
|
1917
|
+
if (inToken) out.push(current);
|
|
1918
|
+
return out;
|
|
1919
|
+
}
|
|
1920
|
+
var LOAD_FLAGS = ["count", "rate", "seed"];
|
|
1921
|
+
function splitFlags(tokens, flags, booleans = []) {
|
|
1922
|
+
const words = [];
|
|
1923
|
+
const found = {};
|
|
1924
|
+
const bools = /* @__PURE__ */ new Set();
|
|
1925
|
+
const all = [...flags, ...booleans];
|
|
1926
|
+
for (let i = 0; i < tokens.length; i++) {
|
|
1927
|
+
const t = tokens[i];
|
|
1928
|
+
if (t.startsWith("--")) {
|
|
1929
|
+
const name = t.slice(2);
|
|
1930
|
+
if (!all.includes(name)) throw new CommandSyntaxError(`Unknown option ${t} (options: ${all.map((f) => `--${f}`).join(", ") || "none"})`);
|
|
1931
|
+
if (booleans.includes(name)) {
|
|
1932
|
+
bools.add(name);
|
|
1933
|
+
continue;
|
|
1934
|
+
}
|
|
1935
|
+
if (i + 1 >= tokens.length) throw new CommandSyntaxError(`${t} needs a value`);
|
|
1936
|
+
found[name] = tokens[++i];
|
|
1937
|
+
} else words.push(t);
|
|
1938
|
+
}
|
|
1939
|
+
return { words, flags: found, bools };
|
|
1940
|
+
}
|
|
1941
|
+
function loadFlags(flags, bools) {
|
|
1942
|
+
return { count: flags.count, rate: flags.rate, seed: flags.seed, random: bools.has("random") };
|
|
1943
|
+
}
|
|
1944
|
+
function withLoad(inner, load) {
|
|
1945
|
+
const random = load.random === true;
|
|
1946
|
+
if (load.platform !== void 0 && !random) throw new CommandSyntaxError("--platform filters --random; the event type already names its platform");
|
|
1947
|
+
if (load.count === void 0 && load.rate === void 0 && load.seed === void 0 && !random) return inner;
|
|
1948
|
+
if (load.rate !== void 0 && load.count === void 0) throw new CommandSyntaxError("--rate paces --count runs: add --count N");
|
|
1949
|
+
if (load.seed !== void 0 && !random) throw new CommandSyntaxError("--seed sets the --random picks; event fields follow the session seed (lumio dev --seed)");
|
|
1950
|
+
if (inner.kind !== "event" && inner.kind !== "scenario" && inner.kind !== "chat") {
|
|
1951
|
+
throw new CommandSyntaxError("--count, --rate and --random apply to events, scenarios and chat messages");
|
|
1952
|
+
}
|
|
1953
|
+
if (random) {
|
|
1954
|
+
if (inner.kind === "chat") throw new CommandSyntaxError("--random picks an event type or a scenario, not a chat message");
|
|
1955
|
+
if (inner.kind === "event" && (inner.type !== void 0 || inner.preset !== void 0 || Object.keys(inner.data ?? {}).length > 0)) {
|
|
1956
|
+
throw new CommandSyntaxError("--random picks the event type itself: drop the type, preset and fields (every event gets fresh random fields anyway)");
|
|
1957
|
+
}
|
|
1958
|
+
if (inner.kind === "scenario" && (inner.name !== "" || Object.keys(inner.options ?? {}).length > 0)) {
|
|
1959
|
+
throw new CommandSyntaxError("--random picks the scenario itself: drop the name and options");
|
|
1960
|
+
}
|
|
1961
|
+
}
|
|
1962
|
+
if (random && load.platform !== void 0 && inner.kind !== "event") throw new CommandSyntaxError("--platform filters random event types only");
|
|
1963
|
+
let seed;
|
|
1964
|
+
if (load.seed !== void 0) {
|
|
1965
|
+
seed = Number(load.seed);
|
|
1966
|
+
if (!Number.isInteger(seed) || seed < 0 || seed > 2 ** 31 - 1) throw new CommandSyntaxError(`--seed must be an integer between 0 and ${2 ** 31 - 1}`);
|
|
1967
|
+
}
|
|
1968
|
+
return {
|
|
1969
|
+
kind: "repeat",
|
|
1970
|
+
command: inner,
|
|
1971
|
+
count: load.count === void 0 ? 1 : parseCount(load.count),
|
|
1972
|
+
intervalMs: load.rate === void 0 ? 0 : parseRate(load.rate),
|
|
1973
|
+
...random ? { random: true } : {},
|
|
1974
|
+
...load.platform !== void 0 ? { platform: load.platform } : {},
|
|
1975
|
+
...seed !== void 0 ? { seed } : {}
|
|
1976
|
+
};
|
|
1977
|
+
}
|
|
1978
|
+
function parseValue(raw) {
|
|
1979
|
+
if (raw === "true") return true;
|
|
1980
|
+
if (raw === "false") return false;
|
|
1981
|
+
if (raw === "null") return null;
|
|
1982
|
+
if (/^-?\d+(\.\d+)?$/.test(raw)) return Number(raw);
|
|
1983
|
+
if (/^[[{]/.test(raw)) {
|
|
1984
|
+
try {
|
|
1985
|
+
return JSON.parse(raw);
|
|
1986
|
+
} catch {
|
|
1987
|
+
throw new CommandSyntaxError(`Not valid JSON: ${raw}`);
|
|
1988
|
+
}
|
|
1989
|
+
}
|
|
1990
|
+
return raw;
|
|
1991
|
+
}
|
|
1992
|
+
function setPath(target, dotted, value) {
|
|
1993
|
+
const parts = dotted.split(".");
|
|
1994
|
+
if (parts.some((p) => p === "" || p === "__proto__" || p === "constructor" || p === "prototype")) {
|
|
1995
|
+
throw new CommandSyntaxError(`Invalid field path "${dotted}"`);
|
|
1996
|
+
}
|
|
1997
|
+
let node = target;
|
|
1998
|
+
for (const part of parts.slice(0, -1)) {
|
|
1999
|
+
const next = node[part];
|
|
2000
|
+
if (typeof next !== "object" || next === null || Array.isArray(next)) node[part] = {};
|
|
2001
|
+
node = node[part];
|
|
2002
|
+
}
|
|
2003
|
+
node[parts[parts.length - 1]] = value;
|
|
2004
|
+
}
|
|
2005
|
+
function assignments(words, what) {
|
|
2006
|
+
const out = {};
|
|
2007
|
+
for (const w of words) {
|
|
2008
|
+
const eq = w.indexOf("=");
|
|
2009
|
+
if (eq <= 0) throw new CommandSyntaxError(`Expected ${what} as key=value, got "${w}"`);
|
|
2010
|
+
setPath(out, w.slice(0, eq), parseValue(w.slice(eq + 1)));
|
|
2011
|
+
}
|
|
2012
|
+
return out;
|
|
2013
|
+
}
|
|
2014
|
+
var ROLE_ALIASES = {
|
|
2015
|
+
mod: "moderator",
|
|
2016
|
+
moderator: "moderator",
|
|
2017
|
+
sub: "subscriber",
|
|
2018
|
+
subscriber: "subscriber",
|
|
2019
|
+
vip: "vip",
|
|
2020
|
+
broadcaster: "broadcaster",
|
|
2021
|
+
everyone: "everyone",
|
|
2022
|
+
viewer: "everyone",
|
|
2023
|
+
follower: "follower"
|
|
2024
|
+
};
|
|
2025
|
+
function parseUserPrefix(word) {
|
|
2026
|
+
const body = word.slice(1);
|
|
2027
|
+
const colon = body.indexOf(":");
|
|
2028
|
+
if (colon === -1) return { name: body };
|
|
2029
|
+
const role = ROLE_ALIASES[body.slice(0, colon).toLowerCase()];
|
|
2030
|
+
if (!role) throw new CommandSyntaxError(`Unknown role "${body.slice(0, colon)}" (use ${Object.keys(ROLE_ALIASES).join(", ")})`);
|
|
2031
|
+
return { role, name: body.slice(colon + 1) };
|
|
2032
|
+
}
|
|
2033
|
+
function userFlag(value) {
|
|
2034
|
+
if (value === void 0) return void 0;
|
|
2035
|
+
return value.startsWith("@") ? parseUserPrefix(value) : { name: value };
|
|
2036
|
+
}
|
|
2037
|
+
function parseReplLine(line) {
|
|
2038
|
+
const trimmed = line.trim();
|
|
2039
|
+
if (trimmed === "") throw new CommandSyntaxError("Empty input");
|
|
2040
|
+
if (!trimmed.startsWith("/")) return parseChat(tokenize(trimmed));
|
|
2041
|
+
const [head, ...rest] = tokenize(trimmed);
|
|
2042
|
+
switch (head) {
|
|
2043
|
+
case "/quit":
|
|
2044
|
+
case "/exit":
|
|
2045
|
+
return null;
|
|
2046
|
+
case "/help":
|
|
2047
|
+
return { kind: "help" };
|
|
2048
|
+
case "/status":
|
|
2049
|
+
return { kind: "status" };
|
|
2050
|
+
case "/stop":
|
|
2051
|
+
return { kind: "stop" };
|
|
2052
|
+
case "/event": {
|
|
2053
|
+
const { words, flags, bools } = splitFlags(rest, ["json", "user", "platform", ...LOAD_FLAGS], ["random"]);
|
|
2054
|
+
const load = { ...loadFlags(flags, bools), platform: flags.platform };
|
|
2055
|
+
const [type, ...fields] = words;
|
|
2056
|
+
if (!type && load.random) {
|
|
2057
|
+
if (flags.json !== void 0) throw new CommandSyntaxError("--random picks the event type itself: drop --json");
|
|
2058
|
+
return withLoad({ kind: "event", user: userFlag(flags.user) }, load);
|
|
2059
|
+
}
|
|
2060
|
+
if (!type) throw new CommandSyntaxError("Usage: /event <type> [field=value \u2026] | /event @<preset> | /event --random");
|
|
2061
|
+
if (type.startsWith("@")) {
|
|
2062
|
+
if (fields.length || flags.json) throw new CommandSyntaxError("A preset takes no fields");
|
|
2063
|
+
return withLoad({ kind: "event", preset: type.slice(1), user: userFlag(flags.user) }, load);
|
|
2064
|
+
}
|
|
2065
|
+
const data = assignments(fields, "event fields");
|
|
2066
|
+
if (flags.json !== void 0) {
|
|
2067
|
+
const json = parseValue(flags.json);
|
|
2068
|
+
if (typeof json !== "object" || json === null || Array.isArray(json)) throw new CommandSyntaxError("--json needs a JSON object");
|
|
2069
|
+
Object.assign(data, json);
|
|
2070
|
+
}
|
|
2071
|
+
return withLoad({ kind: "event", type, data, user: userFlag(flags.user) }, load);
|
|
2072
|
+
}
|
|
2073
|
+
case "/chat":
|
|
2074
|
+
return parseChat(rest);
|
|
2075
|
+
case "/reward": {
|
|
2076
|
+
const { words, flags } = splitFlags(rest, ["user", "input", "platform"]);
|
|
2077
|
+
if (words.length !== 1) throw new CommandSyntaxError('Usage: /reward "<title or id>" [--user name] [--input "text"] [--platform twitch|kick]');
|
|
2078
|
+
return { kind: "reward", reward: words[0], user: userFlag(flags.user), input: flags.input, platform: flags.platform };
|
|
2079
|
+
}
|
|
2080
|
+
case "/reward-status": {
|
|
2081
|
+
if (rest.length !== 2) throw new CommandSyntaxError("Usage: /reward-status <redemption-id> <status>");
|
|
2082
|
+
return { kind: "reward-status", id: rest[0], status: rest[1] };
|
|
2083
|
+
}
|
|
2084
|
+
case "/music": {
|
|
2085
|
+
const [op, ...args] = rest;
|
|
2086
|
+
if (!op) throw new CommandSyntaxError("Usage: /music play|pause|skip|seek|volume|shuffle|repeat|queue|device|advance \u2026");
|
|
2087
|
+
return { kind: "music", op, args };
|
|
2088
|
+
}
|
|
2089
|
+
case "/timer":
|
|
2090
|
+
if (rest.length !== 1) throw new CommandSyntaxError("Usage: /timer <name>");
|
|
2091
|
+
return { kind: "timer", name: rest[0] };
|
|
2092
|
+
case "/defer":
|
|
2093
|
+
if (rest[0] !== "run" || rest.length !== 1) throw new CommandSyntaxError("Usage: /defer run");
|
|
2094
|
+
return { kind: "defer-run" };
|
|
2095
|
+
case "/scenario": {
|
|
2096
|
+
const { words, flags, bools } = splitFlags(rest, [...LOAD_FLAGS], ["random"]);
|
|
2097
|
+
const load = loadFlags(flags, bools);
|
|
2098
|
+
const [name, ...opts] = words;
|
|
2099
|
+
if (!name && !load.random) throw new CommandSyntaxError("Usage: /scenario <name> [key=value \u2026] | /scenario --random");
|
|
2100
|
+
return withLoad({ kind: "scenario", name: name ?? "", options: assignments(opts, "scenario options") }, load);
|
|
2101
|
+
}
|
|
2102
|
+
case "/storage": {
|
|
2103
|
+
if (rest.length === 0) return { kind: "storage" };
|
|
2104
|
+
if (rest.length > 2) throw new CommandSyntaxError("Usage: /storage [key] ['<json>']");
|
|
2105
|
+
if (rest.length === 1) return { kind: "storage", key: rest[0] };
|
|
2106
|
+
let value;
|
|
2107
|
+
try {
|
|
2108
|
+
value = JSON.parse(rest[1]);
|
|
2109
|
+
} catch {
|
|
2110
|
+
throw new CommandSyntaxError(`Storage values are JSON: ${rest[1]}`);
|
|
2111
|
+
}
|
|
2112
|
+
return { kind: "storage", key: rest[0], value };
|
|
2113
|
+
}
|
|
2114
|
+
case "/db": {
|
|
2115
|
+
const reset = rest.includes("--reset");
|
|
2116
|
+
const words = rest.filter((w) => w !== "--reset");
|
|
2117
|
+
if (words.length > 1) throw new CommandSyntaxError("Usage: /db [table] [--reset]");
|
|
2118
|
+
return { kind: "db", table: words[0], reset };
|
|
2119
|
+
}
|
|
2120
|
+
case "/config":
|
|
2121
|
+
return rest.length ? { kind: "config", settings: assignments(rest, "settings") } : { kind: "config" };
|
|
2122
|
+
case "/theme":
|
|
2123
|
+
if (rest[0] !== "light" && rest[0] !== "dark") throw new CommandSyntaxError("Usage: /theme light|dark");
|
|
2124
|
+
return { kind: "theme", mode: rest[0] };
|
|
2125
|
+
case "/view":
|
|
2126
|
+
if (rest[0] !== "editor" && rest[0] !== "layer") throw new CommandSyntaxError("Usage: /view editor|layer");
|
|
2127
|
+
return { kind: "view", target: rest[0] };
|
|
2128
|
+
default:
|
|
2129
|
+
throw new CommandSyntaxError(`Unknown command ${head}. Type /help.`);
|
|
2130
|
+
}
|
|
2131
|
+
}
|
|
2132
|
+
function parseChat(tokens) {
|
|
2133
|
+
const { words, flags } = splitFlags(tokens, ["platform", "user", "count", "rate"]);
|
|
2134
|
+
let user = userFlag(flags.user);
|
|
2135
|
+
if (words[0]?.startsWith("@") && words.length > 1) {
|
|
2136
|
+
user = parseUserPrefix(words[0]);
|
|
2137
|
+
words.shift();
|
|
2138
|
+
}
|
|
2139
|
+
const text = words.join(" ");
|
|
2140
|
+
if (text === "") throw new CommandSyntaxError("Usage: /chat [@role:name] <text>");
|
|
2141
|
+
return withLoad({ kind: "chat", text, user, platform: flags.platform }, { count: flags.count, rate: flags.rate });
|
|
2142
|
+
}
|
|
2143
|
+
var KINDS = /* @__PURE__ */ new Set([
|
|
2144
|
+
"event",
|
|
2145
|
+
"chat",
|
|
2146
|
+
"reward",
|
|
2147
|
+
"reward-status",
|
|
2148
|
+
"music",
|
|
2149
|
+
"timer",
|
|
2150
|
+
"defer-run",
|
|
2151
|
+
"scenario",
|
|
2152
|
+
"storage",
|
|
2153
|
+
"db",
|
|
2154
|
+
"config",
|
|
2155
|
+
"theme",
|
|
2156
|
+
"view",
|
|
2157
|
+
"callback",
|
|
2158
|
+
"server-override",
|
|
2159
|
+
"repeat",
|
|
2160
|
+
"stop",
|
|
2161
|
+
"status",
|
|
2162
|
+
"help"
|
|
2163
|
+
]);
|
|
2164
|
+
function commandFromJson(body) {
|
|
2165
|
+
if (typeof body !== "object" || body === null || Array.isArray(body)) {
|
|
2166
|
+
throw new CommandSyntaxError('Send { "line": "/event twitch:follower" } or a command object { "kind": \u2026 }');
|
|
2167
|
+
}
|
|
2168
|
+
const b = body;
|
|
2169
|
+
if (typeof b.line === "string") return parseReplLine(b.line);
|
|
2170
|
+
if (typeof b.kind !== "string" || !KINDS.has(b.kind)) {
|
|
2171
|
+
throw new CommandSyntaxError(`Unknown command kind ${JSON.stringify(b.kind)} (one of: ${[...KINDS].join(", ")})`);
|
|
2172
|
+
}
|
|
2173
|
+
return b;
|
|
2174
|
+
}
|
|
2175
|
+
function parseDuration(raw) {
|
|
2176
|
+
const m = /^(?:(\d+):)?(\d+(?:\.\d+)?)(s|ms)?$/.exec(raw);
|
|
2177
|
+
if (!m) throw new CommandSyntaxError(`Not a time: ${raw} (use 1:30, 90 or 90s)`);
|
|
2178
|
+
if (m[3] === "ms") return Number(m[2]) + Number(m[1] ?? 0) * 6e4;
|
|
2179
|
+
return Math.round((Number(m[1] ?? 0) * 60 + Number(m[2])) * 1e3);
|
|
2180
|
+
}
|
|
2181
|
+
|
|
2182
|
+
// src/dev/repl.ts
|
|
2183
|
+
var PRINT_OUTPUT = /* @__PURE__ */ new Set([
|
|
2184
|
+
"help",
|
|
2185
|
+
"status",
|
|
2186
|
+
"storage",
|
|
2187
|
+
"db",
|
|
2188
|
+
"config",
|
|
2189
|
+
"music",
|
|
2190
|
+
"scenario",
|
|
2191
|
+
"timer",
|
|
2192
|
+
"defer-run",
|
|
2193
|
+
"view",
|
|
2194
|
+
"reward",
|
|
2195
|
+
"reward-status",
|
|
2196
|
+
"server-override",
|
|
2197
|
+
"theme",
|
|
2198
|
+
"stop"
|
|
2199
|
+
]);
|
|
2200
|
+
var KIND_STYLE = {
|
|
2201
|
+
input: chalk2.dim,
|
|
2202
|
+
event: chalk2.cyan,
|
|
2203
|
+
chat: chalk2.white,
|
|
2204
|
+
bot: chalk2.green,
|
|
2205
|
+
action: chalk2.magenta,
|
|
2206
|
+
server: chalk2.blue,
|
|
2207
|
+
storage: chalk2.blue,
|
|
2208
|
+
handler: chalk2.green,
|
|
2209
|
+
error: chalk2.red,
|
|
2210
|
+
warning: chalk2.yellow,
|
|
2211
|
+
build: chalk2.dim,
|
|
2212
|
+
info: chalk2.dim
|
|
2213
|
+
};
|
|
2214
|
+
function formatLogEntry(entry) {
|
|
2215
|
+
const style = KIND_STYLE[entry.kind];
|
|
2216
|
+
const text = entry.kind === "event" ? `\u2190 ${entry.text}` : entry.text;
|
|
2217
|
+
const payload = entry.payload === void 0 ? "" : ` ${chalk2.dim(JSON.stringify(entry.payload))}`;
|
|
2218
|
+
return `${style(text)}${payload}`;
|
|
2219
|
+
}
|
|
2220
|
+
function startRepl(session, onQuit) {
|
|
2221
|
+
const rl = readline2.createInterface({ input: process.stdin, output: process.stdout, prompt: chalk2.cyan("lumio> ") });
|
|
2222
|
+
let closed = false;
|
|
2223
|
+
const above = (text) => {
|
|
2224
|
+
if (closed) {
|
|
2225
|
+
console.log(text);
|
|
2226
|
+
return;
|
|
2227
|
+
}
|
|
2228
|
+
readline2.clearLine(process.stdout, 0);
|
|
2229
|
+
readline2.cursorTo(process.stdout, 0);
|
|
2230
|
+
console.log(text);
|
|
2231
|
+
rl.prompt(true);
|
|
2232
|
+
};
|
|
2233
|
+
rl.on("line", (line) => {
|
|
2234
|
+
const trimmed = line.trim();
|
|
2235
|
+
if (trimmed === "") {
|
|
2236
|
+
rl.prompt();
|
|
2237
|
+
return;
|
|
2238
|
+
}
|
|
2239
|
+
let cmd;
|
|
2240
|
+
try {
|
|
2241
|
+
cmd = parseReplLine(trimmed);
|
|
2242
|
+
} catch (e) {
|
|
2243
|
+
above(chalk2.red(e instanceof CommandSyntaxError ? e.message : String(e)));
|
|
2244
|
+
return;
|
|
2245
|
+
}
|
|
2246
|
+
if (cmd === null) {
|
|
2247
|
+
rl.close();
|
|
2248
|
+
return;
|
|
2249
|
+
}
|
|
2250
|
+
const kind = cmd.kind;
|
|
2251
|
+
void session.run(cmd, "repl").then((result) => {
|
|
2252
|
+
if (result.ok && PRINT_OUTPUT.has(kind)) for (const l of result.output) above(l);
|
|
2253
|
+
else rl.prompt(true);
|
|
2254
|
+
});
|
|
2255
|
+
});
|
|
2256
|
+
rl.on("close", () => {
|
|
2257
|
+
closed = true;
|
|
2258
|
+
onQuit();
|
|
2259
|
+
});
|
|
2260
|
+
rl.prompt();
|
|
2261
|
+
return {
|
|
2262
|
+
print(entry) {
|
|
2263
|
+
if (entry.kind === "input" && entry.text.startsWith("repl: ")) return;
|
|
2264
|
+
above(formatLogEntry(entry));
|
|
2265
|
+
},
|
|
2266
|
+
close() {
|
|
2267
|
+
if (!closed) rl.close();
|
|
2268
|
+
}
|
|
2269
|
+
};
|
|
2270
|
+
}
|
|
2271
|
+
|
|
2272
|
+
// src/dev/session.ts
|
|
2273
|
+
var LOG_LIMIT = 500;
|
|
2274
|
+
var CALL_LIMIT = 100;
|
|
2275
|
+
var DEFAULT_USER = "dev_user";
|
|
2276
|
+
var ACCOUNT_ROLES = ["owner", "administrator", "moderator"];
|
|
2277
|
+
var THEMES = {
|
|
2278
|
+
dark: {
|
|
2279
|
+
colors: {
|
|
2280
|
+
primary: "#6366f1",
|
|
2281
|
+
secondary: "#8b5cf6",
|
|
2282
|
+
accent: "#06b6d4",
|
|
2283
|
+
background: "transparent",
|
|
2284
|
+
surface: "rgba(15, 23, 42, 0.8)",
|
|
2285
|
+
text: "#f8fafc",
|
|
2286
|
+
muted: "#94a3b8",
|
|
2287
|
+
border: "rgba(148, 163, 184, 0.2)"
|
|
2288
|
+
}
|
|
2289
|
+
},
|
|
2290
|
+
light: {
|
|
2291
|
+
colors: {
|
|
2292
|
+
primary: "#4f46e5",
|
|
2293
|
+
secondary: "#7c3aed",
|
|
2294
|
+
accent: "#0891b2",
|
|
2295
|
+
background: "transparent",
|
|
2296
|
+
surface: "rgba(255, 255, 255, 0.9)",
|
|
2297
|
+
text: "#0f172a",
|
|
2298
|
+
muted: "#475569",
|
|
2299
|
+
border: "rgba(15, 23, 42, 0.15)"
|
|
2300
|
+
}
|
|
2301
|
+
}
|
|
2302
|
+
};
|
|
2303
|
+
var MUSIC_OPS = ["play", "pause", "skip", "seek", "volume", "shuffle", "repeat", "queue", "device", "advance"];
|
|
2304
|
+
var isObject = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
2305
|
+
function compact(value) {
|
|
2306
|
+
try {
|
|
2307
|
+
return JSON.stringify(value) ?? "undefined";
|
|
2308
|
+
} catch {
|
|
2309
|
+
return String(value);
|
|
2310
|
+
}
|
|
2311
|
+
}
|
|
2312
|
+
function message(e) {
|
|
2313
|
+
return e instanceof Error ? e.message : String(e);
|
|
2314
|
+
}
|
|
2315
|
+
var DevSession = class {
|
|
2316
|
+
host;
|
|
2317
|
+
runtime = null;
|
|
2318
|
+
opts;
|
|
2319
|
+
rawConfig;
|
|
2320
|
+
bundles = /* @__PURE__ */ new Map();
|
|
2321
|
+
server = { bundle: null, functions: null, schema: null };
|
|
2322
|
+
overrides = /* @__PURE__ */ new Map();
|
|
2323
|
+
viewTarget;
|
|
2324
|
+
themeMode = "dark";
|
|
2325
|
+
mountError = null;
|
|
2326
|
+
buildStatus = { ok: true, errors: [], at: null };
|
|
2327
|
+
cssVersion = 0;
|
|
2328
|
+
logEntries = [];
|
|
2329
|
+
nextLogId = 1;
|
|
2330
|
+
serverCalls = [];
|
|
2331
|
+
seen = { events: 0, bot: 0, calls: 0, errors: 0, warnings: 0, invocations: 0, runtimeWarnings: 0 };
|
|
2332
|
+
lastTree = null;
|
|
2333
|
+
pumpTimer = null;
|
|
2334
|
+
queue = Promise.resolve();
|
|
2335
|
+
closed = false;
|
|
2336
|
+
/** Running `--count` / `--rate` loads; `/stop` and `close()` abort them. */
|
|
2337
|
+
loads = /* @__PURE__ */ new Set();
|
|
2338
|
+
constructor(options) {
|
|
2339
|
+
this.opts = options;
|
|
2340
|
+
this.rawConfig = options.rawConfig;
|
|
2341
|
+
this.viewTarget = options.view && options.targets.includes(options.view) ? options.view : options.targets[0] ?? null;
|
|
2342
|
+
}
|
|
2343
|
+
// ── Lifecycle ───────────────────────────────────────────────────────
|
|
2344
|
+
/** Creates the host (and runtime) from the current bundles and mounts the view. */
|
|
2345
|
+
async start(server, bundles) {
|
|
2346
|
+
this.server = server;
|
|
2347
|
+
for (const [t, code] of Object.entries(bundles)) if (code !== void 0) this.bundles.set(t, code);
|
|
2348
|
+
this.createHost({});
|
|
2349
|
+
await this.mountView();
|
|
2350
|
+
this.pump();
|
|
2351
|
+
this.pumpTimer = setInterval(() => this.pump(), 200);
|
|
2352
|
+
this.pumpTimer.unref?.();
|
|
2353
|
+
}
|
|
2354
|
+
createHost(carry) {
|
|
2355
|
+
const { devkit, devkitNode } = this.opts.modules;
|
|
2356
|
+
const hasServer = this.server.bundle !== null || this.server.functions !== null;
|
|
2357
|
+
this.runtime = hasServer ? devkitNode.createBotRuntime({
|
|
2358
|
+
bundle: this.server.bundle ?? void 0,
|
|
2359
|
+
functions: this.server.functions ?? void 0,
|
|
2360
|
+
schema: this.server.schema ?? void 0,
|
|
2361
|
+
cooldowns: this.opts.cooldowns ?? true
|
|
2362
|
+
}) : null;
|
|
2363
|
+
const role = this.opts.role;
|
|
2364
|
+
const mocks = this.opts.mocks;
|
|
2365
|
+
this.host = devkit.createMockHost({
|
|
2366
|
+
seed: this.opts.seed ?? 1,
|
|
2367
|
+
context: {
|
|
2368
|
+
target: this.viewTarget ?? "editor",
|
|
2369
|
+
platform: this.opts.platform ?? "twitch",
|
|
2370
|
+
...role && ACCOUNT_ROLES.includes(role) ? { role } : {}
|
|
2371
|
+
},
|
|
2372
|
+
config: this.rawConfig,
|
|
2373
|
+
mocks,
|
|
2374
|
+
services: this.opts.services,
|
|
2375
|
+
storage: carry.storage,
|
|
2376
|
+
theme: THEMES[this.themeMode],
|
|
2377
|
+
runtime: this.runtime ?? void 0
|
|
2378
|
+
});
|
|
2379
|
+
for (const [fn, answer] of this.overrides) this.host.mockServer(fn, answer);
|
|
2380
|
+
this.host.viewers.resolve({ name: DEFAULT_USER, role: this.defaultRole() });
|
|
2381
|
+
this.seen = { events: 0, bot: 0, calls: 0, errors: 0, warnings: 0, invocations: 0, runtimeWarnings: 0 };
|
|
2382
|
+
this.lastTree = null;
|
|
2383
|
+
}
|
|
2384
|
+
defaultRole() {
|
|
2385
|
+
const role = this.opts.role;
|
|
2386
|
+
if (role && this.opts.modules.devkit.VIEWER_ROLES.includes(role)) return role;
|
|
2387
|
+
return role === "owner" || role === "administrator" ? "broadcaster" : role === void 0 ? "everyone" : role;
|
|
2388
|
+
}
|
|
2389
|
+
async mountView() {
|
|
2390
|
+
this.mountError = null;
|
|
2391
|
+
if (this.host.mounted) await this.host.unmount();
|
|
2392
|
+
const target = this.viewTarget;
|
|
2393
|
+
if (!target) return;
|
|
2394
|
+
this.host.setTarget(target);
|
|
2395
|
+
const code = this.bundles.get(target);
|
|
2396
|
+
if (code === void 0) {
|
|
2397
|
+
this.mountError = `src/${target}.tsx is not built yet`;
|
|
2398
|
+
return;
|
|
2399
|
+
}
|
|
2400
|
+
try {
|
|
2401
|
+
const element = evaluateTargetBundle(code, `${target}.js`, this.opts.modules);
|
|
2402
|
+
await this.host.mount(element);
|
|
2403
|
+
} catch (e) {
|
|
2404
|
+
this.mountError = message(e);
|
|
2405
|
+
this.addLog("error", `${target}: ${this.mountError}`);
|
|
2406
|
+
if (this.host.mounted) await this.host.unmount().catch(() => {
|
|
2407
|
+
});
|
|
2408
|
+
}
|
|
2409
|
+
}
|
|
2410
|
+
async close() {
|
|
2411
|
+
if (this.closed) return;
|
|
2412
|
+
this.closed = true;
|
|
2413
|
+
this.stopLoads();
|
|
2414
|
+
if (this.pumpTimer) clearInterval(this.pumpTimer);
|
|
2415
|
+
this.host.destroy();
|
|
2416
|
+
await this.runtime?.close();
|
|
2417
|
+
}
|
|
2418
|
+
/** Commands run one after another, like typed input. */
|
|
2419
|
+
serial(fn) {
|
|
2420
|
+
const next = this.queue.then(fn, fn);
|
|
2421
|
+
this.queue = next.catch(() => {
|
|
2422
|
+
});
|
|
2423
|
+
return next;
|
|
2424
|
+
}
|
|
2425
|
+
// ── Rebuilds (SSE → re-init the surface / reload the handler bundle) ──
|
|
2426
|
+
/** A rebuilt target bundle: the surface re-initialises; storage, ctx.db and the player stay. */
|
|
2427
|
+
updateTarget(target, code) {
|
|
2428
|
+
return this.serial(async () => {
|
|
2429
|
+
this.bundles.set(target, code);
|
|
2430
|
+
if (target === this.viewTarget) {
|
|
2431
|
+
await this.mountView();
|
|
2432
|
+
this.addLog("build", `${target}.js rebuilt: surface re-initialised (storage, ctx.db and player kept)`);
|
|
2433
|
+
} else this.addLog("build", `${target}.js rebuilt`);
|
|
2434
|
+
this.pump();
|
|
2435
|
+
});
|
|
2436
|
+
}
|
|
2437
|
+
/** A rebuilt `server.js` / `functions.json` / schema. */
|
|
2438
|
+
updateServer(next) {
|
|
2439
|
+
return this.serial(async () => {
|
|
2440
|
+
const hadServer = this.runtime !== null;
|
|
2441
|
+
const hasServer = next.bundle !== null || next.functions !== null;
|
|
2442
|
+
if (!hadServer || !hasServer) {
|
|
2443
|
+
this.server = next;
|
|
2444
|
+
await this.restartHost("server code changed");
|
|
2445
|
+
return;
|
|
2446
|
+
}
|
|
2447
|
+
try {
|
|
2448
|
+
const { dbReset } = this.runtime.reload({
|
|
2449
|
+
bundle: next.bundle ?? void 0,
|
|
2450
|
+
functions: next.functions ?? void 0,
|
|
2451
|
+
schema: next.schema ?? void 0
|
|
2452
|
+
});
|
|
2453
|
+
this.server = next;
|
|
2454
|
+
this.addLog("build", dbReset ? "server.js reloaded; server/schema.ts changed, so ctx.db starts empty" : "server.js reloaded (ctx.db, cache and cooldowns kept)");
|
|
2455
|
+
} catch (e) {
|
|
2456
|
+
this.addLog("error", `server reload refused, previous version keeps running: ${message(e)}`);
|
|
2457
|
+
}
|
|
2458
|
+
this.pump();
|
|
2459
|
+
});
|
|
2460
|
+
}
|
|
2461
|
+
/** `lumio.config.json` changed: permissions and triggers are read once, so the host restarts. */
|
|
2462
|
+
updateConfig(rawConfig, mocks) {
|
|
2463
|
+
return this.serial(async () => {
|
|
2464
|
+
this.rawConfig = rawConfig;
|
|
2465
|
+
if (mocks !== void 0) this.opts.mocks = mocks;
|
|
2466
|
+
await this.restartHost("lumio.config.json or lumio/mocks/ changed");
|
|
2467
|
+
});
|
|
2468
|
+
}
|
|
2469
|
+
async restartHost(reason) {
|
|
2470
|
+
const storage = this.host.storage.entries();
|
|
2471
|
+
this.host.destroy();
|
|
2472
|
+
await this.runtime?.close();
|
|
2473
|
+
this.createHost({ storage });
|
|
2474
|
+
await this.mountView();
|
|
2475
|
+
this.addLog("build", `${reason}: session restarted (storage kept; ctx.db, player and rewards start fresh)`);
|
|
2476
|
+
this.pump();
|
|
2477
|
+
}
|
|
2478
|
+
/** The current build errors of every part; new ones are logged once. */
|
|
2479
|
+
setBuild(status) {
|
|
2480
|
+
const known = new Set(this.buildStatus.errors);
|
|
2481
|
+
this.buildStatus = { ...status, at: (/* @__PURE__ */ new Date()).toISOString() };
|
|
2482
|
+
for (const e of status.errors) if (!known.has(e)) this.addLog("error", e);
|
|
2483
|
+
this.opts.onChange?.();
|
|
2484
|
+
}
|
|
2485
|
+
/** The extension's stylesheet changed. */
|
|
2486
|
+
cssChanged() {
|
|
2487
|
+
this.cssVersion += 1;
|
|
2488
|
+
this.opts.onChange?.();
|
|
2489
|
+
}
|
|
2490
|
+
setTargets(targets) {
|
|
2491
|
+
this.opts.targets = targets;
|
|
2492
|
+
}
|
|
2493
|
+
// ── Log ─────────────────────────────────────────────────────────────
|
|
2494
|
+
addLog(kind, text, payload) {
|
|
2495
|
+
const entry = { id: this.nextLogId++, at: (/* @__PURE__ */ new Date()).toISOString(), kind, text, ...payload === void 0 ? {} : { payload } };
|
|
2496
|
+
this.logEntries.push(entry);
|
|
2497
|
+
if (this.logEntries.length > LOG_LIMIT) this.logEntries.splice(0, this.logEntries.length - LOG_LIMIT);
|
|
2498
|
+
this.opts.onLog?.(entry);
|
|
2499
|
+
this.opts.onChange?.();
|
|
2500
|
+
return entry;
|
|
2501
|
+
}
|
|
2502
|
+
/** Picks up everything the host and the runtime recorded since the last call. */
|
|
2503
|
+
pump() {
|
|
2504
|
+
if (this.closed || this.host.destroyed) return;
|
|
2505
|
+
const h = this.host;
|
|
2506
|
+
const events = h.events;
|
|
2507
|
+
for (; this.seen.events < events.length; this.seen.events++) {
|
|
2508
|
+
const e = events[this.seen.events];
|
|
2509
|
+
this.addLog("event", `${e.type} \u2192 ${e.delivered ? "extension" : "no surface mounted"}`, e.data);
|
|
2510
|
+
}
|
|
2511
|
+
const bot = h.botMessages;
|
|
2512
|
+
for (; this.seen.bot < bot.length; this.seen.bot++) {
|
|
2513
|
+
this.addLog("bot", `bot (${bot[this.seen.bot].platform}): ${bot[this.seen.bot].message}`);
|
|
2514
|
+
}
|
|
2515
|
+
const calls = h.calls;
|
|
2516
|
+
for (; this.seen.calls < calls.length; this.seen.calls++) {
|
|
2517
|
+
const call = calls[this.seen.calls];
|
|
2518
|
+
if (call.kind === "request" && !("result" in call) && !call.error) break;
|
|
2519
|
+
this.logCall(call);
|
|
2520
|
+
}
|
|
2521
|
+
for (; this.seen.errors < h.errors.length; this.seen.errors++) {
|
|
2522
|
+
const e = h.errors[this.seen.errors];
|
|
2523
|
+
this.addLog("error", `${e.code}: ${e.message}`);
|
|
2524
|
+
}
|
|
2525
|
+
for (; this.seen.warnings < h.warnings.length; this.seen.warnings++) this.addLog("warning", h.warnings[this.seen.warnings]);
|
|
2526
|
+
const rt = this.runtime;
|
|
2527
|
+
if (rt) {
|
|
2528
|
+
const inv = rt.invocations;
|
|
2529
|
+
for (; this.seen.invocations < inv.length; this.seen.invocations++) {
|
|
2530
|
+
const log = inv[this.seen.invocations];
|
|
2531
|
+
if (log.ms === 0 && !log.error && log.value === void 0) break;
|
|
2532
|
+
this.logInvocation(log);
|
|
2533
|
+
}
|
|
2534
|
+
}
|
|
2535
|
+
if (h.tree !== this.lastTree) {
|
|
2536
|
+
this.lastTree = h.tree;
|
|
2537
|
+
this.opts.onChange?.();
|
|
2538
|
+
}
|
|
2539
|
+
}
|
|
2540
|
+
logCall(call) {
|
|
2541
|
+
const p = isObject(call.params) ? call.params : {};
|
|
2542
|
+
const answer = call.error ? { error: call.error } : { result: call.result };
|
|
2543
|
+
switch (call.method) {
|
|
2544
|
+
case "action.execute":
|
|
2545
|
+
this.addLog("action", `action ${String(p.action)}: ${call.error ? `${call.error.message} (${call.error.code})` : "ok"}`, { params: p.params, ...answer });
|
|
2546
|
+
break;
|
|
2547
|
+
case "server.query":
|
|
2548
|
+
case "server.mutation": {
|
|
2549
|
+
const fn = String(p.fn);
|
|
2550
|
+
const entry = {
|
|
2551
|
+
id: this.seen.calls,
|
|
2552
|
+
method: call.method,
|
|
2553
|
+
fn,
|
|
2554
|
+
params: p.params ?? {},
|
|
2555
|
+
...call.error ? { error: call.error } : { result: call.result },
|
|
2556
|
+
overridden: this.overrides.has(fn)
|
|
2557
|
+
};
|
|
2558
|
+
this.serverCalls.push(entry);
|
|
2559
|
+
if (this.serverCalls.length > CALL_LIMIT) this.serverCalls.shift();
|
|
2560
|
+
this.addLog("server", `${call.method} ${fn}${entry.overridden ? " (override)" : ""}: ${call.error ? call.error.message : compact(call.result)}`, { params: entry.params, ...answer });
|
|
2561
|
+
break;
|
|
2562
|
+
}
|
|
2563
|
+
case "storage.set":
|
|
2564
|
+
this.addLog("storage", `storage.set ${String(p.key)}`, p.value);
|
|
2565
|
+
break;
|
|
2566
|
+
case "ui.action":
|
|
2567
|
+
this.addLog("action", "ui.action", call.params);
|
|
2568
|
+
break;
|
|
2569
|
+
case "sound.play":
|
|
2570
|
+
case "sound.stop":
|
|
2571
|
+
case "sound.list":
|
|
2572
|
+
this.addLog("action", `${call.method}: ${call.error?.message ?? "ok"}`, call.params);
|
|
2573
|
+
break;
|
|
2574
|
+
default:
|
|
2575
|
+
break;
|
|
2576
|
+
}
|
|
2577
|
+
}
|
|
2578
|
+
logInvocation(log) {
|
|
2579
|
+
if (log.trigger === "command" || log.trigger === "keyword" || log.trigger === "pattern" || log.trigger === "moderate") {
|
|
2580
|
+
for (const l of log.logs) this.addLog("handler", `${log.name} console.${l.level}: ${l.text}`);
|
|
2581
|
+
return;
|
|
2582
|
+
}
|
|
2583
|
+
const parts = [`${log.trigger} ${log.name} (${Math.round(log.ms)} ms)`];
|
|
2584
|
+
if (log.error) parts.push(`failed: ${log.error.message}`);
|
|
2585
|
+
for (const r of log.replies) parts.push(`reply: ${r}`);
|
|
2586
|
+
for (const a of log.actions) parts.push(`action ${a.action}: ${a.success ? "ok" : a.error}`);
|
|
2587
|
+
for (const r of log.realtime) parts.push(`realtime ${r.topic}`);
|
|
2588
|
+
this.addLog(log.error ? "error" : "handler", `handler ${parts.join(" \xB7 ")}`, log.value);
|
|
2589
|
+
for (const l of log.logs) this.addLog("handler", `${log.name} console.${l.level}: ${l.text}`);
|
|
2590
|
+
}
|
|
2591
|
+
// ── State ───────────────────────────────────────────────────────────
|
|
2592
|
+
state() {
|
|
2593
|
+
const h = this.host;
|
|
2594
|
+
const rt = this.runtime;
|
|
2595
|
+
const cfg = this.rawConfig;
|
|
2596
|
+
return {
|
|
2597
|
+
project: {
|
|
2598
|
+
name: String(cfg.name ?? ""),
|
|
2599
|
+
category: String(cfg.category ?? "widget"),
|
|
2600
|
+
version: String(cfg.version ?? ""),
|
|
2601
|
+
extensionId: String(cfg.extensionId ?? cfg.extension_id ?? "")
|
|
2602
|
+
},
|
|
2603
|
+
versions: this.opts.modules.versions,
|
|
2604
|
+
view: this.viewTarget,
|
|
2605
|
+
targets: this.opts.targets,
|
|
2606
|
+
chatFirst: this.opts.chatFirst ?? false,
|
|
2607
|
+
mounted: h.mounted,
|
|
2608
|
+
mountError: this.mountError,
|
|
2609
|
+
tree: h.tree,
|
|
2610
|
+
themeMode: this.themeMode,
|
|
2611
|
+
cssVersion: this.cssVersion,
|
|
2612
|
+
build: this.buildStatus,
|
|
2613
|
+
storage: h.storage.entries(),
|
|
2614
|
+
settings: h.settings,
|
|
2615
|
+
db: rt ? rt.db.tables().map((table) => ({ table, rows: rt.db.rows(table) })) : null,
|
|
2616
|
+
player: h.player.state,
|
|
2617
|
+
rewards: h.rewards.list().map((r) => ({ id: r.id, platform: r.platform, title: r.title, cost: r.cost, prompt: r.prompt, inputRequired: r.inputRequired })),
|
|
2618
|
+
redemptions: h.rewards.history().slice(-50),
|
|
2619
|
+
viewers: h.viewers.list().map((v) => ({ login: v.login, displayName: v.displayName, role: v.role })),
|
|
2620
|
+
live: h.live,
|
|
2621
|
+
timers: rt ? [...rt.timers] : [],
|
|
2622
|
+
pendingDeferred: rt?.pendingDeferred ?? 0,
|
|
2623
|
+
functions: this.functionNames(),
|
|
2624
|
+
serverCalls: [...this.serverCalls],
|
|
2625
|
+
overrides: Object.fromEntries(this.overrides),
|
|
2626
|
+
presets: Object.keys(this.opts.mocks?.events ?? {}),
|
|
2627
|
+
defaults: { platform: h.context.platform, user: DEFAULT_USER, role: this.defaultRole() },
|
|
2628
|
+
log: [...this.logEntries]
|
|
2629
|
+
};
|
|
2630
|
+
}
|
|
2631
|
+
functionNames() {
|
|
2632
|
+
const f = this.server.functions;
|
|
2633
|
+
const parsed = typeof f === "string" ? JSON.parse(f) : f;
|
|
2634
|
+
return isObject(parsed) && isObject(parsed.functions) ? Object.keys(parsed.functions) : [];
|
|
2635
|
+
}
|
|
2636
|
+
meta() {
|
|
2637
|
+
const { devkit } = this.opts.modules;
|
|
2638
|
+
return {
|
|
2639
|
+
eventTypes: devkit.EVENT_TYPES.map((type) => ({ type, ...devkit.eventDelivery(type) })).map(({ type, forwarded, reason }) => ({ type, forwarded, reason })),
|
|
2640
|
+
scenarios: Object.keys(devkit.SCENARIOS),
|
|
2641
|
+
platforms: [...devkit.CHAT_PLATFORMS],
|
|
2642
|
+
rewardPlatforms: [...devkit.REWARD_PLATFORMS],
|
|
2643
|
+
roles: [...devkit.VIEWER_ROLES],
|
|
2644
|
+
musicOps: MUSIC_OPS,
|
|
2645
|
+
rewardStatuses: { twitch: ["fulfilled", "canceled"], kick: ["pending", "accepted", "rejected"] }
|
|
2646
|
+
};
|
|
2647
|
+
}
|
|
2648
|
+
/** A payload for the panel's form and JSON editor (`createEvent`, not sent). */
|
|
2649
|
+
eventTemplate(type) {
|
|
2650
|
+
const { devkit } = this.opts.modules;
|
|
2651
|
+
if (!devkit.isLumioEventType(type)) throw new CommandSyntaxError(`Unknown event type "${type}"`);
|
|
2652
|
+
const payload = devkit.createEvent(type, void 0, { seed: (this.opts.seed ?? 1) + this.nextLogId });
|
|
2653
|
+
return { payload, schema: null };
|
|
2654
|
+
}
|
|
2655
|
+
/** The tree node that registered `handlerId` and the prop it belongs to. */
|
|
2656
|
+
findHandler(handlerId) {
|
|
2657
|
+
const stack = [...this.host.tree];
|
|
2658
|
+
while (stack.length) {
|
|
2659
|
+
const node = stack.pop();
|
|
2660
|
+
for (const [key, value] of Object.entries(node.props ?? {})) {
|
|
2661
|
+
if (key.startsWith("__handler_") && value === handlerId) return { node, prop: key.slice("__handler_".length) };
|
|
2662
|
+
}
|
|
2663
|
+
for (const child of node.children ?? []) if (typeof child === "object" && child !== null) stack.push(child);
|
|
2664
|
+
}
|
|
2665
|
+
return null;
|
|
2666
|
+
}
|
|
2667
|
+
// ── Commands ────────────────────────────────────────────────────────
|
|
2668
|
+
/** Runs one command; every input is echoed with what the extension received. */
|
|
2669
|
+
run(cmd, source = "repl") {
|
|
2670
|
+
if (cmd.kind === "repeat") return this.repeat(cmd, source);
|
|
2671
|
+
if (cmd.kind === "stop") {
|
|
2672
|
+
const n = this.stopLoads();
|
|
2673
|
+
this.addLog("input", `${source}: /stop`);
|
|
2674
|
+
return Promise.resolve({ ok: true, output: [n ? `stopped ${n} running load(s)` : "no load is running"] });
|
|
2675
|
+
}
|
|
2676
|
+
return this.serial(async () => {
|
|
2677
|
+
if (cmd.kind !== "help" && cmd.kind !== "status" && cmd.kind !== "callback") {
|
|
2678
|
+
this.addLog("input", `${source}: ${describe(cmd)}`);
|
|
2679
|
+
}
|
|
2680
|
+
const from = this.nextLogId;
|
|
2681
|
+
const since = () => this.logEntries.filter((e) => e.id >= from);
|
|
2682
|
+
try {
|
|
2683
|
+
const result = await this.execute(cmd);
|
|
2684
|
+
await this.host.settle();
|
|
2685
|
+
this.pump();
|
|
2686
|
+
return { ...result, log: since() };
|
|
2687
|
+
} catch (e) {
|
|
2688
|
+
this.pump();
|
|
2689
|
+
const error = message(e);
|
|
2690
|
+
this.addLog("error", error);
|
|
2691
|
+
return { ok: false, output: [`error: ${error}`], error, log: since() };
|
|
2692
|
+
}
|
|
2693
|
+
});
|
|
2694
|
+
}
|
|
2695
|
+
stopLoads() {
|
|
2696
|
+
const n = this.loads.size;
|
|
2697
|
+
for (const ac of this.loads) ac.abort();
|
|
2698
|
+
this.loads.clear();
|
|
2699
|
+
return n;
|
|
2700
|
+
}
|
|
2701
|
+
/** `--count` / `--rate` / `--random`: the inner command, once per run, paced; the first failure stops it. */
|
|
2702
|
+
async repeat(cmd, source) {
|
|
2703
|
+
const fail = (error) => {
|
|
2704
|
+
this.addLog("error", error);
|
|
2705
|
+
return { ok: false, output: [`error: ${error}`], error };
|
|
2706
|
+
};
|
|
2707
|
+
let spec;
|
|
2708
|
+
let variant;
|
|
2709
|
+
try {
|
|
2710
|
+
spec = repeatSpec(cmd, (this.opts.seed ?? 1) + this.nextLogId);
|
|
2711
|
+
variant = repeatVariants(cmd, spec, { eventTypes: this.meta().eventTypes, scenarios: Object.keys(this.opts.modules.devkit.SCENARIOS) });
|
|
2712
|
+
} catch (e) {
|
|
2713
|
+
return fail(message(e));
|
|
2714
|
+
}
|
|
2715
|
+
this.addLog("input", `${source}: ${describe(cmd)}`);
|
|
2716
|
+
const ac = new AbortController();
|
|
2717
|
+
this.loads.add(ac);
|
|
2718
|
+
const from = this.nextLogId;
|
|
2719
|
+
let result;
|
|
2720
|
+
try {
|
|
2721
|
+
result = await runLoad(spec, () => this.closed ? Promise.resolve({ ok: false, output: [], error: "session closed" }) : this.run(variant(), source), { signal: ac.signal });
|
|
2722
|
+
} finally {
|
|
2723
|
+
this.loads.delete(ac);
|
|
2724
|
+
}
|
|
2725
|
+
const summary = summarizeLoad(spec, result);
|
|
2726
|
+
this.addLog(result.failed ? "error" : "info", summary);
|
|
2727
|
+
return {
|
|
2728
|
+
ok: result.failed === null,
|
|
2729
|
+
output: [summary, ...result.failed?.error ? [`error: ${result.failed.error}`] : []],
|
|
2730
|
+
payload: result.results.map((r) => r.payload),
|
|
2731
|
+
...result.failed?.error ? { error: result.failed.error } : {},
|
|
2732
|
+
log: this.logEntries.filter((e) => e.id >= from)
|
|
2733
|
+
};
|
|
2734
|
+
}
|
|
2735
|
+
user(u) {
|
|
2736
|
+
const name = u?.name?.replace(/^@/, "") || DEFAULT_USER;
|
|
2737
|
+
return u?.role ? { name, role: u.role } : { name };
|
|
2738
|
+
}
|
|
2739
|
+
async execute(cmd) {
|
|
2740
|
+
const h = this.host;
|
|
2741
|
+
switch (cmd.kind) {
|
|
2742
|
+
case "help":
|
|
2743
|
+
return { ok: true, output: REPL_HELP };
|
|
2744
|
+
case "status": {
|
|
2745
|
+
const s = this.state();
|
|
2746
|
+
return {
|
|
2747
|
+
ok: true,
|
|
2748
|
+
output: [
|
|
2749
|
+
`${s.project.name} (${s.project.category}) \xB7 view ${s.view ?? "none"}${s.mountError ? ` (${s.mountError})` : ""}`,
|
|
2750
|
+
`surfaces: ${s.targets.join(", ") || "none"} \xB7 bot runtime: ${this.runtime ? "yes" : "no"} \xB7 functions: ${s.functions.join(", ") || "none"}`,
|
|
2751
|
+
`timers: ${s.timers.map((t) => t.name).join(", ") || "none"} \xB7 presets: ${s.presets.join(", ") || "none"} \xB7 deferred jobs: ${s.pendingDeferred}`,
|
|
2752
|
+
`devkit ${s.versions.devkit} \xB7 sdk ${s.versions.sdk} \xB7 protocol ${s.versions.protocol}`
|
|
2753
|
+
]
|
|
2754
|
+
};
|
|
2755
|
+
}
|
|
2756
|
+
case "event": {
|
|
2757
|
+
let data;
|
|
2758
|
+
if (cmd.preset !== void 0) {
|
|
2759
|
+
const preset = h.preset(cmd.preset);
|
|
2760
|
+
if (cmd.user) preset.user = this.user(cmd.user);
|
|
2761
|
+
data = h.emit(preset);
|
|
2762
|
+
return { ok: true, output: [`${preset.type} ${compact(data)}`], payload: data };
|
|
2763
|
+
}
|
|
2764
|
+
if (!cmd.type) throw new CommandSyntaxError("An event needs a type or a preset");
|
|
2765
|
+
const type = cmd.type;
|
|
2766
|
+
const overrides = this.coerce(type, cmd.data ?? {});
|
|
2767
|
+
data = h.emit(type, overrides, cmd.user ? { user: this.user(cmd.user) } : void 0);
|
|
2768
|
+
return { ok: true, output: [`${type} ${compact(data)}`], payload: data };
|
|
2769
|
+
}
|
|
2770
|
+
case "chat": {
|
|
2771
|
+
const platform = cmd.platform ?? h.context.platform;
|
|
2772
|
+
const r = await h.chat(cmd.text, { user: this.user(cmd.user), platform });
|
|
2773
|
+
const out = [`${platform}:chat ${r.viewer.login} (${r.viewer.role}): ${r.message.message}`];
|
|
2774
|
+
const bot = r;
|
|
2775
|
+
if (bot.log) out.push(...bot.log.map((l) => ` ${l}`));
|
|
2776
|
+
else if (this.runtime === null) out.push(" (no bot module in this project: delivered as chat events only)");
|
|
2777
|
+
this.addLog("chat", out.join("\n"), r.message);
|
|
2778
|
+
return { ok: true, output: out, payload: r.message };
|
|
2779
|
+
}
|
|
2780
|
+
case "reward": {
|
|
2781
|
+
const platform = cmd.platform ?? (h.context.platform === "kick" ? "kick" : "twitch");
|
|
2782
|
+
const r = h.redeem({ reward: cmd.reward, platform, user: this.user(cmd.user), input: cmd.input });
|
|
2783
|
+
const ev = h.events.at(-1);
|
|
2784
|
+
return { ok: true, output: [`redeemed ${r.rewardId} as ${r.viewer} (redemption ${r.id}, ${r.status})`, `${ev?.type} ${compact(ev?.data)}`], payload: ev?.data };
|
|
2785
|
+
}
|
|
2786
|
+
case "reward-status": {
|
|
2787
|
+
const r = h.rewards.setStatus(cmd.id, cmd.status);
|
|
2788
|
+
const ev = h.events.at(-1);
|
|
2789
|
+
return { ok: true, output: [`redemption ${r.id} \u2192 ${r.status}`], payload: ev?.data };
|
|
2790
|
+
}
|
|
2791
|
+
case "music":
|
|
2792
|
+
return this.music(cmd.op, cmd.args ?? []);
|
|
2793
|
+
case "timer": {
|
|
2794
|
+
if (!this.runtime) throw new CommandSyntaxError("This project has no bot module (server/handler.ts), so it has no timers");
|
|
2795
|
+
const log = await this.runtime.fireTimer(cmd.name);
|
|
2796
|
+
return { ok: true, output: [log ? `timer ${cmd.name} ran (${Math.round(log.ms)} ms)` : `timer ${cmd.name} skipped (see warnings)`] };
|
|
2797
|
+
}
|
|
2798
|
+
case "defer-run": {
|
|
2799
|
+
if (!this.runtime) throw new CommandSyntaxError("This project has no server code, so ctx.defer has nothing queued");
|
|
2800
|
+
const pending = this.runtime.pendingDeferred;
|
|
2801
|
+
const errors = await this.runtime.runDeferred();
|
|
2802
|
+
return { ok: errors.length === 0, output: [`ran ${pending} deferred job batch(es)${errors.length ? `; errors: ${errors.join("; ")}` : ""}`] };
|
|
2803
|
+
}
|
|
2804
|
+
case "scenario":
|
|
2805
|
+
return this.scenario(cmd.name, cmd.options ?? {});
|
|
2806
|
+
case "storage": {
|
|
2807
|
+
if (cmd.key === void 0) return { ok: true, output: [compact(h.storage.entries())], payload: h.storage.entries() };
|
|
2808
|
+
if (!("value" in cmd) || cmd.value === void 0) return { ok: true, output: [`${cmd.key} = ${compact(h.storage.get(cmd.key))}`], payload: h.storage.get(cmd.key) };
|
|
2809
|
+
h.storage.set(cmd.key, cmd.value);
|
|
2810
|
+
return { ok: true, output: [`storage.update ${cmd.key} = ${compact(cmd.value)}`], payload: cmd.value };
|
|
2811
|
+
}
|
|
2812
|
+
case "db": {
|
|
2813
|
+
const rt = this.runtime;
|
|
2814
|
+
if (!rt) throw new CommandSyntaxError("This project has no server code, so it has no ctx.db");
|
|
2815
|
+
if (cmd.reset) {
|
|
2816
|
+
rt.db.reset();
|
|
2817
|
+
return { ok: true, output: ["ctx.db cleared"] };
|
|
2818
|
+
}
|
|
2819
|
+
const tables = cmd.table ? [cmd.table] : rt.db.tables();
|
|
2820
|
+
if (cmd.table && !rt.db.tables().includes(cmd.table)) {
|
|
2821
|
+
throw new CommandSyntaxError(`server/schema.ts declares no table "${cmd.table}" (tables: ${rt.db.tables().join(", ") || "none"})`);
|
|
2822
|
+
}
|
|
2823
|
+
const out = tables.length ? tables.map((t) => `${t} (${rt.db.rows(t).length}): ${compact(rt.db.rows(t))}`) : ["server/schema.ts declares no tables"];
|
|
2824
|
+
return { ok: true, output: out, payload: Object.fromEntries(tables.map((t) => [t, rt.db.rows(t)])) };
|
|
2825
|
+
}
|
|
2826
|
+
case "config": {
|
|
2827
|
+
if (!cmd.settings) return { ok: true, output: [compact(h.settings)], payload: h.settings };
|
|
2828
|
+
if (!isObject(cmd.settings)) throw new CommandSyntaxError("config needs an object of settings");
|
|
2829
|
+
const settings = { ...h.settings, ...cmd.settings };
|
|
2830
|
+
h.setConfig(settings);
|
|
2831
|
+
return { ok: true, output: [`config.update ${compact(settings)}`], payload: settings };
|
|
2832
|
+
}
|
|
2833
|
+
case "theme": {
|
|
2834
|
+
if (cmd.mode !== "light" && cmd.mode !== "dark") throw new CommandSyntaxError('theme is "light" or "dark"');
|
|
2835
|
+
this.themeMode = cmd.mode;
|
|
2836
|
+
h.setTheme(THEMES[cmd.mode]);
|
|
2837
|
+
return { ok: true, output: [`theme.change (${cmd.mode})`], payload: h.theme };
|
|
2838
|
+
}
|
|
2839
|
+
case "view": {
|
|
2840
|
+
if (cmd.target !== "editor" && cmd.target !== "layer") throw new CommandSyntaxError('view is "editor" or "layer"');
|
|
2841
|
+
if (!this.opts.targets.includes(cmd.target)) {
|
|
2842
|
+
throw new CommandSyntaxError(`This project has no ${cmd.target} surface (lumio.config.json targets + src/${cmd.target}.tsx)`);
|
|
2843
|
+
}
|
|
2844
|
+
this.viewTarget = cmd.target;
|
|
2845
|
+
await this.mountView();
|
|
2846
|
+
return { ok: this.mountError === null, output: [this.mountError ? `${cmd.target}: ${this.mountError}` : `view ${cmd.target} mounted`] };
|
|
2847
|
+
}
|
|
2848
|
+
case "callback": {
|
|
2849
|
+
if (typeof cmd.handlerId !== "string") throw new CommandSyntaxError("callback needs a handlerId");
|
|
2850
|
+
const found = this.findHandler(cmd.handlerId);
|
|
2851
|
+
if (!found) throw new CommandSyntaxError("That control is no longer on screen");
|
|
2852
|
+
await h.fire(found.node, found.prop, ...Array.isArray(cmd.args) ? cmd.args : []);
|
|
2853
|
+
return { ok: true, output: [`event.callback ${found.node.type}.${found.prop}`] };
|
|
2854
|
+
}
|
|
2855
|
+
case "server-override": {
|
|
2856
|
+
if (typeof cmd.fn !== "string" || cmd.fn === "") throw new CommandSyntaxError("server-override needs fn");
|
|
2857
|
+
if (cmd.answer === null) {
|
|
2858
|
+
this.overrides.delete(cmd.fn);
|
|
2859
|
+
h.clearMockServer(cmd.fn);
|
|
2860
|
+
return { ok: true, output: [`${cmd.fn}: override removed, the local function answers again`] };
|
|
2861
|
+
}
|
|
2862
|
+
h.mockServer(cmd.fn, cmd.answer);
|
|
2863
|
+
this.overrides.set(cmd.fn, cmd.answer);
|
|
2864
|
+
return { ok: true, output: [`${cmd.fn}: answered by override ${compact(cmd.answer)}`] };
|
|
2865
|
+
}
|
|
2866
|
+
default:
|
|
2867
|
+
throw new CommandSyntaxError(`Unknown command ${compact(cmd.kind)}`);
|
|
2868
|
+
}
|
|
2869
|
+
}
|
|
2870
|
+
/**
|
|
2871
|
+
* REPL fields arrive as `tier=2`: a value keeps the type of the field in the
|
|
2872
|
+
* factory output (a string field stays a string, a number field becomes a
|
|
2873
|
+
* number), so the schema sees what you meant.
|
|
2874
|
+
*/
|
|
2875
|
+
coerce(type, data) {
|
|
2876
|
+
const { devkit } = this.opts.modules;
|
|
2877
|
+
if (!devkit.isLumioEventType(type)) return data;
|
|
2878
|
+
const base = devkit.createEvent(type, void 0, { seed: 1 });
|
|
2879
|
+
const walk = (value, template, key = "") => {
|
|
2880
|
+
if (isObject(value)) {
|
|
2881
|
+
const t = isObject(template) ? template : {};
|
|
2882
|
+
return Object.fromEntries(Object.entries(value).map(([k, v]) => [k, walk(v, t[k], k)]));
|
|
2883
|
+
}
|
|
2884
|
+
if (/tier$/.test(key) && typeof template === "string" && /^[123]000$/.test(template) && /^[123]$/.test(String(value))) return `${String(value)}000`;
|
|
2885
|
+
if (typeof template === "string" && (typeof value === "number" || typeof value === "boolean")) return String(value);
|
|
2886
|
+
if (typeof template === "number" && typeof value === "string" && value.trim() !== "" && !Number.isNaN(Number(value))) return Number(value);
|
|
2887
|
+
return value;
|
|
2888
|
+
};
|
|
2889
|
+
return walk(data, base);
|
|
2890
|
+
}
|
|
2891
|
+
music(op, args) {
|
|
2892
|
+
const p = this.host.player;
|
|
2893
|
+
const arg = args[0];
|
|
2894
|
+
const needArg = (what) => {
|
|
2895
|
+
if (arg === void 0) throw new CommandSyntaxError(`/music ${op} needs ${what}`);
|
|
2896
|
+
return arg;
|
|
2897
|
+
};
|
|
2898
|
+
switch (op) {
|
|
2899
|
+
case "play":
|
|
2900
|
+
p.play(arg);
|
|
2901
|
+
break;
|
|
2902
|
+
case "pause":
|
|
2903
|
+
p.pause();
|
|
2904
|
+
break;
|
|
2905
|
+
case "skip":
|
|
2906
|
+
p.skip(arg === void 0 ? 1 : Number(arg));
|
|
2907
|
+
break;
|
|
2908
|
+
case "seek":
|
|
2909
|
+
p.seek(parseDuration(needArg("a time (1:30)")));
|
|
2910
|
+
break;
|
|
2911
|
+
case "volume":
|
|
2912
|
+
p.setVolume(Number(needArg("a percentage")));
|
|
2913
|
+
break;
|
|
2914
|
+
case "shuffle":
|
|
2915
|
+
p.setShuffle(needArg("on|off") === "on");
|
|
2916
|
+
break;
|
|
2917
|
+
case "repeat":
|
|
2918
|
+
p.setRepeat(needArg("off|context|track"));
|
|
2919
|
+
break;
|
|
2920
|
+
case "queue":
|
|
2921
|
+
p.queueAdd(needArg("a track URI"));
|
|
2922
|
+
break;
|
|
2923
|
+
case "device":
|
|
2924
|
+
p.transfer(args.join(" ") || needArg("a device name"));
|
|
2925
|
+
break;
|
|
2926
|
+
case "advance":
|
|
2927
|
+
p.advance(parseDuration(needArg("a duration (30s)")));
|
|
2928
|
+
break;
|
|
2929
|
+
default:
|
|
2930
|
+
throw new CommandSyntaxError(`Unknown /music command "${op}" (${MUSIC_OPS.join(", ")})`);
|
|
2931
|
+
}
|
|
2932
|
+
const s = p.state;
|
|
2933
|
+
return {
|
|
2934
|
+
ok: true,
|
|
2935
|
+
output: [`player: ${s.track ? `${s.track.name}` : "nothing"} \xB7 ${s.is_playing ? "playing" : "paused"} \xB7 ${Math.round(s.progress_ms / 1e3)} s \xB7 volume ${s.device.volume_percent}%`],
|
|
2936
|
+
payload: s
|
|
2937
|
+
};
|
|
2938
|
+
}
|
|
2939
|
+
async scenario(name, options) {
|
|
2940
|
+
const { devkit } = this.opts.modules;
|
|
2941
|
+
const factories = devkit.SCENARIOS;
|
|
2942
|
+
const make = factories[name];
|
|
2943
|
+
if (!make) throw new CommandSyntaxError(`Unknown scenario "${name}" (scenarios: ${Object.keys(factories).join(", ")})`);
|
|
2944
|
+
const { instant, ...rest } = options;
|
|
2945
|
+
for (const key of ["users", "lines"]) {
|
|
2946
|
+
if (typeof rest[key] === "string") rest[key] = rest[key].split(",").map((s) => s.trim()).filter(Boolean);
|
|
2947
|
+
}
|
|
2948
|
+
const run = await this.host.play(make(rest), { instant: instant === true });
|
|
2949
|
+
return { ok: true, output: [`scenario ${run.name}: ${run.events.length} event(s)`], payload: run.events.map((e) => ({ type: e.type, data: e.data })) };
|
|
2950
|
+
}
|
|
2951
|
+
};
|
|
2952
|
+
function describe(cmd) {
|
|
2953
|
+
switch (cmd.kind) {
|
|
2954
|
+
case "event":
|
|
2955
|
+
return cmd.preset !== void 0 ? `/event @${cmd.preset}` : `/event ${cmd.type ?? "?"}${cmd.data && Object.keys(cmd.data).length ? ` ${compact(cmd.data)}` : ""}`;
|
|
2956
|
+
case "chat":
|
|
2957
|
+
return `/chat ${cmd.user?.role ? `@${cmd.user.role}:` : cmd.user?.name ? "@" : ""}${cmd.user?.name ?? ""}${cmd.user?.name || cmd.user?.role ? " " : ""}${cmd.text}${cmd.platform ? ` --platform ${cmd.platform}` : ""}`;
|
|
2958
|
+
case "reward":
|
|
2959
|
+
return `/reward ${JSON.stringify(cmd.reward)}${cmd.user?.name ? ` --user ${cmd.user.name}` : ""}${cmd.input ? ` --input ${JSON.stringify(cmd.input)}` : ""}${cmd.platform ? ` --platform ${cmd.platform}` : ""}`;
|
|
2960
|
+
case "reward-status":
|
|
2961
|
+
return `/reward-status ${cmd.id} ${cmd.status}`;
|
|
2962
|
+
case "music":
|
|
2963
|
+
return `/music ${cmd.op}${cmd.args?.length ? ` ${cmd.args.join(" ")}` : ""}`;
|
|
2964
|
+
case "timer":
|
|
2965
|
+
return `/timer ${cmd.name}`;
|
|
2966
|
+
case "defer-run":
|
|
2967
|
+
return "/defer run";
|
|
2968
|
+
case "scenario":
|
|
2969
|
+
return `/scenario ${cmd.name}${cmd.options && Object.keys(cmd.options).length ? ` ${compact(cmd.options)}` : ""}`;
|
|
2970
|
+
case "storage":
|
|
2971
|
+
return `/storage${cmd.key ? ` ${cmd.key}` : ""}${cmd.value !== void 0 ? ` ${compact(cmd.value)}` : ""}`;
|
|
2972
|
+
case "db":
|
|
2973
|
+
return `/db${cmd.table ? ` ${cmd.table}` : ""}${cmd.reset ? " --reset" : ""}`;
|
|
2974
|
+
case "config":
|
|
2975
|
+
return `/config${cmd.settings ? ` ${compact(cmd.settings)}` : ""}`;
|
|
2976
|
+
case "theme":
|
|
2977
|
+
return `/theme ${cmd.mode}`;
|
|
2978
|
+
case "view":
|
|
2979
|
+
return `/view ${cmd.target}`;
|
|
2980
|
+
case "callback":
|
|
2981
|
+
return `callback ${cmd.handlerId}`;
|
|
2982
|
+
case "server-override":
|
|
2983
|
+
return `server override ${cmd.fn}${cmd.answer === null ? " (removed)" : ` ${compact(cmd.answer)}`}`;
|
|
2984
|
+
case "repeat": {
|
|
2985
|
+
const inner = cmd.random ? cmd.command.kind === "scenario" ? "/scenario --random" : `/event --random${cmd.platform ? ` --platform ${cmd.platform}` : ""}` : describe(cmd.command);
|
|
2986
|
+
return `${inner} --count ${cmd.count ?? 1}${cmd.intervalMs ? ` --rate ${describeRate(cmd.intervalMs)}` : ""}${cmd.seed !== void 0 ? ` --seed ${cmd.seed}` : ""}`;
|
|
2987
|
+
}
|
|
2988
|
+
default:
|
|
2989
|
+
return `/${cmd.kind}`;
|
|
2990
|
+
}
|
|
2991
|
+
}
|
|
2992
|
+
|
|
2993
|
+
// src/dev/server.ts
|
|
2994
|
+
import * as fs8 from "fs";
|
|
2995
|
+
import * as http2 from "http";
|
|
2996
|
+
import * as path9 from "path";
|
|
2997
|
+
import { randomBytes as randomBytes2, timingSafeEqual } from "crypto";
|
|
2998
|
+
import { fileURLToPath } from "url";
|
|
2999
|
+
var DEFAULT_DEV_PORT = 3100;
|
|
3000
|
+
var TOKEN_HEADER = "x-lumio-dev-token";
|
|
3001
|
+
var DEV_SERVER_FILE = path9.join(".lumio", "dev-server.json");
|
|
3002
|
+
var MAX_BODY = 1024 * 1024;
|
|
3003
|
+
var CONTENT_TYPES = {
|
|
3004
|
+
".html": "text/html; charset=utf-8",
|
|
3005
|
+
".js": "text/javascript; charset=utf-8",
|
|
3006
|
+
".css": "text/css; charset=utf-8",
|
|
3007
|
+
".json": "application/json; charset=utf-8",
|
|
3008
|
+
".map": "application/json; charset=utf-8",
|
|
3009
|
+
".svg": "image/svg+xml",
|
|
3010
|
+
".png": "image/png",
|
|
3011
|
+
".jpg": "image/jpeg",
|
|
3012
|
+
".jpeg": "image/jpeg",
|
|
3013
|
+
".gif": "image/gif",
|
|
3014
|
+
".webp": "image/webp",
|
|
3015
|
+
".avif": "image/avif",
|
|
3016
|
+
".ico": "image/x-icon",
|
|
3017
|
+
".mp3": "audio/mpeg",
|
|
3018
|
+
".wav": "audio/wav",
|
|
3019
|
+
".ogg": "audio/ogg",
|
|
3020
|
+
".mp4": "video/mp4",
|
|
3021
|
+
".webm": "video/webm",
|
|
3022
|
+
".woff": "font/woff",
|
|
3023
|
+
".woff2": "font/woff2",
|
|
3024
|
+
".ttf": "font/ttf",
|
|
3025
|
+
".otf": "font/otf"
|
|
3026
|
+
};
|
|
3027
|
+
var PAGE_CSP = [
|
|
3028
|
+
"default-src 'self'",
|
|
3029
|
+
"script-src 'self'",
|
|
3030
|
+
"style-src 'self' 'unsafe-inline'",
|
|
3031
|
+
"img-src 'self' data: blob: https: http:",
|
|
3032
|
+
"media-src 'self' data: blob: https: http:",
|
|
3033
|
+
"font-src 'self' data: https:",
|
|
3034
|
+
"connect-src 'self'",
|
|
3035
|
+
"frame-ancestors 'none'",
|
|
3036
|
+
"base-uri 'none'",
|
|
3037
|
+
"form-action 'none'"
|
|
3038
|
+
].join("; ");
|
|
3039
|
+
function defaultSandboxDir() {
|
|
3040
|
+
const here = path9.dirname(fileURLToPath(import.meta.url));
|
|
3041
|
+
const candidates = [path9.join(here, "sandbox"), path9.join(here, "..", "..", "dist", "sandbox")];
|
|
3042
|
+
return candidates.find((c) => fs8.existsSync(path9.join(c, "index.html"))) ?? candidates[0];
|
|
3043
|
+
}
|
|
3044
|
+
function createToken() {
|
|
3045
|
+
return randomBytes2(24).toString("base64url");
|
|
3046
|
+
}
|
|
3047
|
+
function writeDevServerFile(projectDir, info) {
|
|
3048
|
+
const file = path9.join(projectDir, DEV_SERVER_FILE);
|
|
3049
|
+
fs8.mkdirSync(path9.dirname(file), { recursive: true });
|
|
3050
|
+
fs8.writeFileSync(file, `${JSON.stringify(info, null, 2)}
|
|
3051
|
+
`, { mode: 384 });
|
|
3052
|
+
fs8.chmodSync(file, 384);
|
|
3053
|
+
ensureGitignored(projectDir);
|
|
3054
|
+
return file;
|
|
3055
|
+
}
|
|
3056
|
+
function ensureGitignored(projectDir) {
|
|
3057
|
+
const file = path9.join(projectDir, ".gitignore");
|
|
3058
|
+
const current = fs8.existsSync(file) ? fs8.readFileSync(file, "utf8") : "";
|
|
3059
|
+
const listed = current.split(/\r?\n/).some((l) => [".lumio", ".lumio/", "/.lumio", "/.lumio/", ".lumio/**"].includes(l.trim()));
|
|
3060
|
+
if (listed) return false;
|
|
3061
|
+
const prefix = current === "" || current.endsWith("\n") ? "" : "\n";
|
|
3062
|
+
fs8.writeFileSync(file, `${current}${prefix}# lumio dev (session token, dev bundles)
|
|
3063
|
+
.lumio/
|
|
3064
|
+
`);
|
|
3065
|
+
return true;
|
|
3066
|
+
}
|
|
3067
|
+
function removeDevServerFile(projectDir, token) {
|
|
3068
|
+
const file = path9.join(projectDir, DEV_SERVER_FILE);
|
|
3069
|
+
try {
|
|
3070
|
+
const info = JSON.parse(fs8.readFileSync(file, "utf8"));
|
|
3071
|
+
if (info.token === token) fs8.rmSync(file, { force: true });
|
|
3072
|
+
} catch {
|
|
3073
|
+
}
|
|
3074
|
+
}
|
|
3075
|
+
function safeEqual(a, b) {
|
|
3076
|
+
const x = Buffer.from(a);
|
|
3077
|
+
const y = Buffer.from(b);
|
|
3078
|
+
return x.length === y.length && timingSafeEqual(x, y);
|
|
3079
|
+
}
|
|
3080
|
+
function within(root, rel) {
|
|
3081
|
+
let decoded;
|
|
3082
|
+
try {
|
|
3083
|
+
decoded = decodeURIComponent(rel);
|
|
3084
|
+
} catch {
|
|
3085
|
+
return null;
|
|
3086
|
+
}
|
|
3087
|
+
if (decoded.includes("\0")) return null;
|
|
3088
|
+
const full = path9.resolve(root, `.${path9.posix.sep}${decoded.replace(/^\/+/, "")}`);
|
|
3089
|
+
const base = path9.resolve(root);
|
|
3090
|
+
return full === base || full.startsWith(base + path9.sep) ? full : null;
|
|
3091
|
+
}
|
|
3092
|
+
async function startDevServer(options) {
|
|
3093
|
+
const { session, outDir } = options;
|
|
3094
|
+
const sandboxDir = options.sandboxDir ?? defaultSandboxDir();
|
|
3095
|
+
const token = options.token ?? createToken();
|
|
3096
|
+
const requestedPort = options.port ?? DEFAULT_DEV_PORT;
|
|
3097
|
+
const streams = /* @__PURE__ */ new Set();
|
|
3098
|
+
let port = requestedPort;
|
|
3099
|
+
let notifyTimer = null;
|
|
3100
|
+
const allowedHosts = () => /* @__PURE__ */ new Set([`127.0.0.1:${port}`, `localhost:${port}`, `[::1]:${port}`]);
|
|
3101
|
+
const send = (res, status, body, type, extra = {}) => {
|
|
3102
|
+
res.writeHead(status, {
|
|
3103
|
+
"Content-Type": type,
|
|
3104
|
+
"Cache-Control": "no-store",
|
|
3105
|
+
"X-Content-Type-Options": "nosniff",
|
|
3106
|
+
"Referrer-Policy": "no-referrer",
|
|
3107
|
+
...extra
|
|
3108
|
+
});
|
|
3109
|
+
res.end(body);
|
|
3110
|
+
};
|
|
3111
|
+
const json = (res, status, body) => send(res, status, JSON.stringify(body), CONTENT_TYPES[".json"]);
|
|
3112
|
+
const serveFile = (res, file, extra = {}) => {
|
|
3113
|
+
if (!file || !fs8.existsSync(file) || !fs8.statSync(file).isFile()) return json(res, 404, { error: "NOT_FOUND" });
|
|
3114
|
+
send(res, 200, fs8.readFileSync(file), CONTENT_TYPES[path9.extname(file).toLowerCase()] ?? "application/octet-stream", extra);
|
|
3115
|
+
};
|
|
3116
|
+
const readBody = (req) => new Promise((resolve13, reject) => {
|
|
3117
|
+
let size = 0;
|
|
3118
|
+
const chunks = [];
|
|
3119
|
+
req.on("data", (c) => {
|
|
3120
|
+
size += c.length;
|
|
3121
|
+
if (size > MAX_BODY) {
|
|
3122
|
+
reject(new Error("Body larger than 1 MiB"));
|
|
3123
|
+
req.destroy();
|
|
3124
|
+
} else chunks.push(c);
|
|
3125
|
+
});
|
|
3126
|
+
req.on("end", () => resolve13(Buffer.concat(chunks).toString("utf8")));
|
|
3127
|
+
req.on("error", reject);
|
|
3128
|
+
});
|
|
3129
|
+
const broadcast = () => {
|
|
3130
|
+
notifyTimer = null;
|
|
3131
|
+
if (streams.size === 0) return;
|
|
3132
|
+
const data = `event: state
|
|
3133
|
+
data: ${JSON.stringify(session.state())}
|
|
3134
|
+
|
|
3135
|
+
`;
|
|
3136
|
+
for (const res of streams) res.write(data);
|
|
3137
|
+
};
|
|
3138
|
+
const handler = async (req, res) => {
|
|
3139
|
+
if (!allowedHosts().has(req.headers.host ?? "")) return json(res, 421, { error: "WRONG_HOST", message: "Use the URL lumio dev printed" });
|
|
3140
|
+
const url = new URL(req.url ?? "/", `http://127.0.0.1:${port}`);
|
|
3141
|
+
const p = url.pathname;
|
|
3142
|
+
if (p.startsWith("/__lumio/")) {
|
|
3143
|
+
const given = req.headers[TOKEN_HEADER] ?? (p === "/__lumio/stream" ? url.searchParams.get("token") ?? void 0 : void 0);
|
|
3144
|
+
if (!given || !safeEqual(given, token)) {
|
|
3145
|
+
return json(res, 401, { error: "UNAUTHORIZED", message: `Send the token from ${DEV_SERVER_FILE} in the ${TOKEN_HEADER} header` });
|
|
3146
|
+
}
|
|
3147
|
+
if (req.method === "GET" && p === "/__lumio/state") return json(res, 200, session.state());
|
|
3148
|
+
if (req.method === "GET" && p === "/__lumio/meta") return json(res, 200, session.meta());
|
|
3149
|
+
if (req.method === "GET" && p === "/__lumio/event-template") {
|
|
3150
|
+
try {
|
|
3151
|
+
return json(res, 200, session.eventTemplate(url.searchParams.get("type") ?? ""));
|
|
3152
|
+
} catch (e) {
|
|
3153
|
+
return json(res, 400, { error: "BAD_REQUEST", message: e.message });
|
|
3154
|
+
}
|
|
3155
|
+
}
|
|
3156
|
+
if (req.method === "GET" && p === "/__lumio/stream") {
|
|
3157
|
+
res.writeHead(200, {
|
|
3158
|
+
"Content-Type": "text/event-stream",
|
|
3159
|
+
"Cache-Control": "no-store",
|
|
3160
|
+
Connection: "keep-alive",
|
|
3161
|
+
"X-Content-Type-Options": "nosniff"
|
|
3162
|
+
});
|
|
3163
|
+
res.write(`retry: 1000
|
|
3164
|
+
event: state
|
|
3165
|
+
data: ${JSON.stringify(session.state())}
|
|
3166
|
+
|
|
3167
|
+
`);
|
|
3168
|
+
streams.add(res);
|
|
3169
|
+
req.on("close", () => streams.delete(res));
|
|
3170
|
+
return;
|
|
3171
|
+
}
|
|
3172
|
+
if (req.method === "POST" && p === "/__lumio/events") {
|
|
3173
|
+
if (!(req.headers["content-type"] ?? "").startsWith("application/json")) {
|
|
3174
|
+
return json(res, 415, { error: "UNSUPPORTED_MEDIA_TYPE", message: "Send application/json" });
|
|
3175
|
+
}
|
|
3176
|
+
let cmd;
|
|
3177
|
+
try {
|
|
3178
|
+
cmd = commandFromJson(JSON.parse(await readBody(req)));
|
|
3179
|
+
} catch (e) {
|
|
3180
|
+
const status = e instanceof CommandSyntaxError || e instanceof SyntaxError ? 400 : 413;
|
|
3181
|
+
return json(res, status, { ok: false, error: e.message });
|
|
3182
|
+
}
|
|
3183
|
+
if (cmd === null) return json(res, 400, { ok: false, error: "Stop the session in its terminal (Ctrl+C or /quit)" });
|
|
3184
|
+
const header = req.headers["x-lumio-source"];
|
|
3185
|
+
const source = header === "panel" ? "panel" : header === "trigger" ? "trigger" : "http";
|
|
3186
|
+
const result = await session.run(cmd, source);
|
|
3187
|
+
return json(res, result.ok ? 200 : 422, result);
|
|
3188
|
+
}
|
|
3189
|
+
return json(res, 404, { error: "NOT_FOUND" });
|
|
3190
|
+
}
|
|
3191
|
+
if (req.method !== "GET" && req.method !== "HEAD") return json(res, 405, { error: "METHOD_NOT_ALLOWED" });
|
|
3192
|
+
if (p === "/" || p === "/index.html") {
|
|
3193
|
+
const file = path9.join(sandboxDir, "index.html");
|
|
3194
|
+
if (!fs8.existsSync(file)) {
|
|
3195
|
+
return send(res, 500, `The sandbox is missing from this CLI install (${file}). Reinstall @zaflun/lumio-cli.`, "text/plain; charset=utf-8");
|
|
3196
|
+
}
|
|
3197
|
+
const html = fs8.readFileSync(file, "utf8").replace("%LUMIO_DEV_TOKEN%", token);
|
|
3198
|
+
return send(res, 200, html, CONTENT_TYPES[".html"], { "Content-Security-Policy": PAGE_CSP });
|
|
3199
|
+
}
|
|
3200
|
+
if (p.startsWith("/sandbox/")) return serveFile(res, within(sandboxDir, p.slice("/sandbox/".length)));
|
|
3201
|
+
if (p.startsWith("/bundles/")) return serveFile(res, within(outDir, p.slice("/bundles/".length)));
|
|
3202
|
+
if (p.startsWith("/assets/")) return serveFile(res, within(path9.join(outDir, "assets"), p.slice("/assets/".length)));
|
|
3203
|
+
return json(res, 404, { error: "NOT_FOUND" });
|
|
3204
|
+
};
|
|
3205
|
+
const server = http2.createServer((req, res) => {
|
|
3206
|
+
handler(req, res).catch((e) => {
|
|
3207
|
+
if (!res.headersSent) json(res, 500, { error: "INTERNAL", message: e instanceof Error ? e.message : String(e) });
|
|
3208
|
+
else res.end();
|
|
3209
|
+
});
|
|
3210
|
+
});
|
|
3211
|
+
await new Promise((resolve13, reject) => {
|
|
3212
|
+
server.once("error", (e) => {
|
|
3213
|
+
reject(e.code === "EADDRINUSE" ? new Error(`Port ${requestedPort} is in use. Stop the other server or pass --port <port>.`) : e);
|
|
3214
|
+
});
|
|
3215
|
+
server.listen(requestedPort, "127.0.0.1", () => resolve13());
|
|
3216
|
+
});
|
|
3217
|
+
const address = server.address();
|
|
3218
|
+
if (address && typeof address === "object") port = address.port;
|
|
3219
|
+
const keepAlive = setInterval(() => {
|
|
3220
|
+
for (const res of streams) res.write(": ping\n\n");
|
|
3221
|
+
}, 15e3);
|
|
3222
|
+
keepAlive.unref();
|
|
3223
|
+
return {
|
|
3224
|
+
url: `http://127.0.0.1:${port}/`,
|
|
3225
|
+
port,
|
|
3226
|
+
token,
|
|
3227
|
+
notify() {
|
|
3228
|
+
if (!notifyTimer) notifyTimer = setTimeout(broadcast, 40);
|
|
3229
|
+
},
|
|
3230
|
+
async close() {
|
|
3231
|
+
clearInterval(keepAlive);
|
|
3232
|
+
if (notifyTimer) clearTimeout(notifyTimer);
|
|
3233
|
+
for (const res of streams) res.end();
|
|
3234
|
+
streams.clear();
|
|
3235
|
+
await new Promise((resolve13) => server.close(() => resolve13()));
|
|
3236
|
+
}
|
|
3237
|
+
};
|
|
3238
|
+
}
|
|
3239
|
+
|
|
3240
|
+
// src/commands/dev.ts
|
|
3241
|
+
var PLATFORMS = ["twitch", "youtube", "kick"];
|
|
3242
|
+
var ROLES = ["everyone", "follower", "subscriber", "vip", "moderator", "broadcaster", "owner", "administrator"];
|
|
3243
|
+
function intOption(name, min, max) {
|
|
3244
|
+
return (value) => {
|
|
3245
|
+
const n = Number(value);
|
|
3246
|
+
if (!Number.isInteger(n) || n < min || n > max) throw new InvalidArgumentError(`${name} must be an integer between ${min} and ${max}.`);
|
|
3247
|
+
return value;
|
|
3248
|
+
};
|
|
3249
|
+
}
|
|
3250
|
+
function registerDev(program2) {
|
|
3251
|
+
program2.command("dev").description("Run the extension locally: sandbox page, REPL and bot-module runtime in one session").option("-p, --port <port>", "Dev server port", intOption("--port", 0, 65535), String(DEFAULT_DEV_PORT)).option("--bot-module", "Open the sandbox with the chat first (the session is the same)").option("--automation-node", "Start the automation node dev simulator").option("--platform <platform>", `Default chat platform (${PLATFORMS.join(", ")})`, "twitch").option("--role <role>", `Role of the default chat user (${ROLES.slice(0, 6).join(", ")})`).option("--seed <n>", "Seed of every random event field", intOption("--seed", 0, 2 ** 31 - 1), "1").option("--no-cooldown", "Do not enforce command cooldowns").option("--view <target>", "Surface to open first (editor, layer)").option("--open", "Open the sandbox in the browser").action(async (options) => {
|
|
3252
|
+
const configPath = path10.resolve("lumio.config.json");
|
|
3253
|
+
if (!fs9.existsSync(configPath)) {
|
|
3254
|
+
console.error("lumio.config.json not found. Run `lumio init` first.");
|
|
3255
|
+
process.exit(1);
|
|
3256
|
+
}
|
|
3257
|
+
const config = parseConfigFile(fs9.readFileSync(configPath, "utf-8"));
|
|
3258
|
+
if (options.automationNode || config.category === "automation_node") {
|
|
3259
|
+
await runAutomationNodeDevServer(config);
|
|
3260
|
+
return;
|
|
3261
|
+
}
|
|
3262
|
+
try {
|
|
3263
|
+
await runDevSession(process.cwd(), config, options);
|
|
3264
|
+
} catch (e) {
|
|
3265
|
+
console.error(chalk3.red(e instanceof Error ? e.message : String(e)));
|
|
3266
|
+
process.exit(1);
|
|
3267
|
+
}
|
|
3268
|
+
});
|
|
3269
|
+
}
|
|
3270
|
+
function readRawConfig(projectDir) {
|
|
3271
|
+
return JSON.parse(fs9.readFileSync(path10.join(projectDir, "lumio.config.json"), "utf8"));
|
|
3272
|
+
}
|
|
3273
|
+
async function runDevSession(projectDir, config, options) {
|
|
3274
|
+
if (!PLATFORMS.includes(options.platform)) throw new Error(`--platform must be one of ${PLATFORMS.join(", ")}`);
|
|
3275
|
+
if (options.role !== void 0 && !ROLES.includes(options.role)) throw new Error(`--role must be one of ${ROLES.join(", ")}`);
|
|
3276
|
+
if (options.view !== void 0 && options.view !== "editor" && options.view !== "layer") throw new Error("--view must be editor or layer");
|
|
3277
|
+
let modules;
|
|
3278
|
+
try {
|
|
3279
|
+
modules = await loadProjectModules(projectDir);
|
|
3280
|
+
} catch (e) {
|
|
3281
|
+
if (e instanceof MissingPackageError) throw new Error(`${e.message}
|
|
3282
|
+
lumio dev runs your extension with the devkit's mock host.`);
|
|
3283
|
+
throw e;
|
|
3284
|
+
}
|
|
3285
|
+
const mocks = await modules.devkitNode.loadMockFiles(projectDir);
|
|
3286
|
+
const { targets, skipped } = devTargets(projectDir, config);
|
|
3287
|
+
if (targets.length === 0 && !config.server) {
|
|
3288
|
+
throw new Error('Nothing to run: lumio.config.json has no editor/layer target with a src/<target>.tsx and no "server": true.');
|
|
3289
|
+
}
|
|
3290
|
+
console.log(chalk3.bold(`
|
|
3291
|
+
Lumio dev \xB7 ${config.name}`) + chalk3.dim(` (${config.category})`));
|
|
3292
|
+
for (const w of modules.warnings) console.log(chalk3.yellow(`warning: ${w}`));
|
|
3293
|
+
for (const s of skipped) console.log(chalk3.dim(`note: ${s}`));
|
|
3294
|
+
let repl = null;
|
|
3295
|
+
let server = null;
|
|
3296
|
+
const pending = [];
|
|
3297
|
+
const session = new DevSession({
|
|
3298
|
+
projectDir,
|
|
3299
|
+
rawConfig: readRawConfig(projectDir),
|
|
3300
|
+
modules,
|
|
3301
|
+
targets,
|
|
3302
|
+
seed: Number(options.seed),
|
|
3303
|
+
platform: options.platform,
|
|
3304
|
+
role: options.role,
|
|
3305
|
+
cooldowns: options.cooldown,
|
|
3306
|
+
view: options.view ?? void 0,
|
|
3307
|
+
chatFirst: options.botModule ?? false,
|
|
3308
|
+
mocks,
|
|
3309
|
+
onLog: (entry) => repl ? repl.print(entry) : pending.push(entry),
|
|
3310
|
+
onChange: () => server?.notify()
|
|
3311
|
+
});
|
|
3312
|
+
const buildErrors = /* @__PURE__ */ new Map();
|
|
3313
|
+
const builder = new DevBuilder(projectDir, config, targets, {
|
|
3314
|
+
onTarget: (target, code) => void session.updateTarget(target, code),
|
|
3315
|
+
onServer: (next) => void session.updateServer(next),
|
|
3316
|
+
onCss: () => session.cssChanged(),
|
|
3317
|
+
onStatus: (key, errors) => {
|
|
3318
|
+
const before = JSON.stringify([...buildErrors]);
|
|
3319
|
+
if (errors) buildErrors.set(key, errors);
|
|
3320
|
+
else buildErrors.delete(key);
|
|
3321
|
+
if (JSON.stringify([...buildErrors]) === before) return;
|
|
3322
|
+
const all = [...buildErrors.values()].flat();
|
|
3323
|
+
session.setBuild({ ok: all.length === 0, errors: all });
|
|
3324
|
+
if (!errors && all.length === 0) session.addLog("build", "build ok");
|
|
3325
|
+
}
|
|
3326
|
+
});
|
|
3327
|
+
const first = await builder.start();
|
|
3328
|
+
await session.start(first.server, first.bundles);
|
|
3329
|
+
server = await startDevServer({ session, outDir: builder.outDir, port: Number(options.port) });
|
|
3330
|
+
writeDevServerFile(projectDir, {
|
|
3331
|
+
url: server.url,
|
|
3332
|
+
port: server.port,
|
|
3333
|
+
token: server.token,
|
|
3334
|
+
pid: process.pid,
|
|
3335
|
+
startedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
3336
|
+
});
|
|
3337
|
+
const state = session.state();
|
|
3338
|
+
console.log(`Sandbox: ${chalk3.cyan(server.url)}`);
|
|
3339
|
+
console.log(chalk3.dim(`Surfaces: ${state.targets.join(", ") || "none"} \xB7 bot runtime: ${config.server ? "on" : "off"} \xB7 seed ${options.seed} \xB7 platform ${options.platform}`));
|
|
3340
|
+
console.log(chalk3.dim("Type /help for commands. Plain text is a chat message.\n"));
|
|
3341
|
+
if (options.open) {
|
|
3342
|
+
const { default: open } = await import("open");
|
|
3343
|
+
await open(server.url).catch(() => {
|
|
3344
|
+
});
|
|
3345
|
+
}
|
|
3346
|
+
let configTimer = null;
|
|
3347
|
+
const reloadConfig = () => {
|
|
3348
|
+
if (configTimer) clearTimeout(configTimer);
|
|
3349
|
+
configTimer = setTimeout(() => {
|
|
3350
|
+
void (async () => {
|
|
3351
|
+
try {
|
|
3352
|
+
const raw = readRawConfig(projectDir);
|
|
3353
|
+
const nextMocks = await modules.devkitNode.loadMockFiles(projectDir);
|
|
3354
|
+
await session.updateConfig(raw, nextMocks);
|
|
3355
|
+
} catch (e) {
|
|
3356
|
+
session.addLog("error", `lumio.config.json / lumio/mocks/ not reloaded: ${e instanceof Error ? e.message : String(e)}`);
|
|
3357
|
+
}
|
|
3358
|
+
})();
|
|
3359
|
+
}, 150);
|
|
3360
|
+
};
|
|
3361
|
+
const watchers = [fs9.watch(path10.join(projectDir, "lumio.config.json"), reloadConfig)];
|
|
3362
|
+
const mocksDir = path10.join(projectDir, "lumio", "mocks");
|
|
3363
|
+
if (fs9.existsSync(mocksDir)) {
|
|
3364
|
+
try {
|
|
3365
|
+
watchers.push(fs9.watch(mocksDir, { recursive: true }, reloadConfig));
|
|
3366
|
+
} catch {
|
|
3367
|
+
}
|
|
3368
|
+
}
|
|
3369
|
+
await new Promise((resolve13) => {
|
|
3370
|
+
let stopping = false;
|
|
3371
|
+
const stop = () => {
|
|
3372
|
+
if (stopping) return;
|
|
3373
|
+
stopping = true;
|
|
3374
|
+
void (async () => {
|
|
3375
|
+
for (const w of watchers) w.close();
|
|
3376
|
+
if (configTimer) clearTimeout(configTimer);
|
|
3377
|
+
removeDevServerFile(projectDir, server?.token ?? "");
|
|
3378
|
+
await server?.close();
|
|
3379
|
+
await builder.stop();
|
|
3380
|
+
await session.close();
|
|
3381
|
+
repl?.close();
|
|
3382
|
+
console.log(chalk3.dim("\nlumio dev stopped."));
|
|
3383
|
+
resolve13();
|
|
3384
|
+
})();
|
|
3385
|
+
};
|
|
3386
|
+
process.once("SIGINT", stop);
|
|
3387
|
+
process.once("SIGTERM", stop);
|
|
3388
|
+
repl = startRepl(session, stop);
|
|
3389
|
+
for (const entry of pending.splice(0)) repl.print(entry);
|
|
3390
|
+
});
|
|
3391
|
+
}
|
|
3392
|
+
|
|
3393
|
+
// src/commands/trigger.ts
|
|
3394
|
+
import chalk4 from "chalk";
|
|
3395
|
+
|
|
3396
|
+
// src/dev/trigger.ts
|
|
3397
|
+
import * as fs10 from "fs";
|
|
3398
|
+
import * as path11 from "path";
|
|
3399
|
+
var VALUE_FLAGS = ["field", "data", "json", "file", "user", "role", "platform", "input", "timer", "storage", "count", "rate", "seed"];
|
|
3400
|
+
var REPEATABLE = /* @__PURE__ */ new Set(["field", "storage"]);
|
|
3401
|
+
var BOOLEAN_FLAGS = ["random", "quiet"];
|
|
3402
|
+
var TRIGGER_USAGE = [
|
|
3403
|
+
"lumio trigger <type> [field=value \u2026] [--field a.b=value] [--<field> value] [--data '{\u2026}'] [--file payload.json] [--user name] [--role role]",
|
|
3404
|
+
"lumio trigger @<preset> [--user name]",
|
|
3405
|
+
'lumio trigger chat "<text>" [--user name] [--role moderator] [--platform kick]',
|
|
3406
|
+
'lumio trigger reward "<title or id>" [--user name] [--input "text"] [--platform kick]',
|
|
3407
|
+
"lumio trigger reward-status <redemption-id> <status>",
|
|
3408
|
+
"lumio trigger music play|pause|skip|seek 1:30|volume 50|\u2026",
|
|
3409
|
+
"lumio trigger scenario <name> [key=value \u2026]",
|
|
3410
|
+
"lumio trigger --timer <name> lumio trigger --storage key='<json>'",
|
|
3411
|
+
"load: --count N --rate 5/s --random [--platform twitch] --seed n --quiet"
|
|
3412
|
+
];
|
|
3413
|
+
function isObject2(v) {
|
|
3414
|
+
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
3415
|
+
}
|
|
3416
|
+
function deepMerge(target, source) {
|
|
3417
|
+
for (const [k, v] of Object.entries(source)) {
|
|
3418
|
+
if (k === "__proto__" || k === "constructor" || k === "prototype") throw new CommandSyntaxError(`Invalid field "${k}"`);
|
|
3419
|
+
target[k] = isObject2(v) && isObject2(target[k]) ? deepMerge(target[k], v) : v;
|
|
3420
|
+
}
|
|
3421
|
+
return target;
|
|
3422
|
+
}
|
|
3423
|
+
function jsonObject(raw, what) {
|
|
3424
|
+
let value;
|
|
3425
|
+
try {
|
|
3426
|
+
value = JSON.parse(raw);
|
|
3427
|
+
} catch (e) {
|
|
3428
|
+
throw new CommandSyntaxError(`${what} is not valid JSON: ${e.message}`);
|
|
3429
|
+
}
|
|
3430
|
+
if (!isObject2(value)) throw new CommandSyntaxError(`${what} must be a JSON object`);
|
|
3431
|
+
return value;
|
|
3432
|
+
}
|
|
3433
|
+
function roleOf(raw) {
|
|
3434
|
+
const role = ROLE_ALIASES[raw.toLowerCase()];
|
|
3435
|
+
if (!role) throw new CommandSyntaxError(`Unknown role "${raw}" (use ${Object.keys(ROLE_ALIASES).join(", ")})`);
|
|
3436
|
+
return role;
|
|
3437
|
+
}
|
|
3438
|
+
function storageCommand(raw) {
|
|
3439
|
+
const eq = raw.indexOf("=");
|
|
3440
|
+
if (eq <= 0) throw new CommandSyntaxError(`--storage needs key=value, got "${raw}"`);
|
|
3441
|
+
const key = raw.slice(0, eq);
|
|
3442
|
+
const text = raw.slice(eq + 1);
|
|
3443
|
+
let value;
|
|
3444
|
+
try {
|
|
3445
|
+
value = JSON.parse(text);
|
|
3446
|
+
} catch {
|
|
3447
|
+
if (/^\s*[[{"]/.test(text)) throw new CommandSyntaxError(`--storage ${key}: not valid JSON: ${text}`);
|
|
3448
|
+
value = text;
|
|
3449
|
+
}
|
|
3450
|
+
return { kind: "storage", key, value };
|
|
3451
|
+
}
|
|
3452
|
+
function parseTriggerArgs(argv, cwd = process.cwd()) {
|
|
3453
|
+
const words = [];
|
|
3454
|
+
const flags = /* @__PURE__ */ new Map();
|
|
3455
|
+
const bools = /* @__PURE__ */ new Set();
|
|
3456
|
+
const shortcuts = [];
|
|
3457
|
+
for (let i = 0; i < argv.length; i++) {
|
|
3458
|
+
const t = argv[i];
|
|
3459
|
+
if (!t.startsWith("--") || t === "--") {
|
|
3460
|
+
if (t !== "--") words.push(t);
|
|
3461
|
+
continue;
|
|
3462
|
+
}
|
|
3463
|
+
const eq = t.indexOf("=");
|
|
3464
|
+
const name = eq === -1 ? t.slice(2) : t.slice(2, eq);
|
|
3465
|
+
if (name === "") throw new CommandSyntaxError(`Invalid option ${t}`);
|
|
3466
|
+
if (BOOLEAN_FLAGS.includes(name)) {
|
|
3467
|
+
if (eq !== -1) throw new CommandSyntaxError(`--${name} takes no value`);
|
|
3468
|
+
bools.add(name);
|
|
3469
|
+
continue;
|
|
3470
|
+
}
|
|
3471
|
+
let value;
|
|
3472
|
+
if (eq !== -1) value = t.slice(eq + 1);
|
|
3473
|
+
else {
|
|
3474
|
+
if (i + 1 >= argv.length) throw new CommandSyntaxError(`--${name} needs a value`);
|
|
3475
|
+
value = argv[++i];
|
|
3476
|
+
}
|
|
3477
|
+
if (VALUE_FLAGS.includes(name)) {
|
|
3478
|
+
const list = flags.get(name) ?? [];
|
|
3479
|
+
if (list.length && !REPEATABLE.has(name)) throw new CommandSyntaxError(`--${name} is given twice`);
|
|
3480
|
+
flags.set(name, [...list, value]);
|
|
3481
|
+
} else shortcuts.push([name, value]);
|
|
3482
|
+
}
|
|
3483
|
+
const one = (name) => flags.get(name)?.[0];
|
|
3484
|
+
if (one("json") !== void 0 && one("data") !== void 0) throw new CommandSyntaxError("--json is --data: give one of them");
|
|
3485
|
+
const setup = [...(flags.get("storage") ?? []).map(storageCommand)];
|
|
3486
|
+
const timer = one("timer");
|
|
3487
|
+
if (timer !== void 0) setup.push({ kind: "timer", name: timer });
|
|
3488
|
+
let user;
|
|
3489
|
+
const userFlag2 = one("user");
|
|
3490
|
+
const role = one("role");
|
|
3491
|
+
if (userFlag2 !== void 0) user = userFlag2.startsWith("@") ? parseUserPrefix(userFlag2) : { name: userFlag2 };
|
|
3492
|
+
if (role !== void 0) user = { ...user, role: roleOf(role) };
|
|
3493
|
+
const load = { count: one("count"), rate: one("rate"), seed: one("seed"), random: bools.has("random") };
|
|
3494
|
+
const platform = one("platform");
|
|
3495
|
+
const only = (allowed, what) => {
|
|
3496
|
+
for (const name of ["field", "data", "json", "file", "input"]) {
|
|
3497
|
+
if (flags.has(name) && !allowed.includes(name)) throw new CommandSyntaxError(`--${name} does not apply to ${what}`);
|
|
3498
|
+
}
|
|
3499
|
+
if (shortcuts.length && !allowed.includes("field")) throw new CommandSyntaxError(`Unknown option --${shortcuts[0][0]} (${what} takes no fields)`);
|
|
3500
|
+
};
|
|
3501
|
+
const [head, ...rest] = words;
|
|
3502
|
+
let main;
|
|
3503
|
+
if (head === void 0) {
|
|
3504
|
+
if (load.random) {
|
|
3505
|
+
only([], "--random");
|
|
3506
|
+
main = withLoad({ kind: "event", user }, { ...load, platform });
|
|
3507
|
+
} else {
|
|
3508
|
+
if (setup.length === 0) throw new CommandSyntaxError(`Nothing to trigger. Usage:
|
|
3509
|
+
${TRIGGER_USAGE.join("\n ")}`);
|
|
3510
|
+
only([], "--timer / --storage");
|
|
3511
|
+
if (user || platform || load.count || load.rate || load.seed) throw new CommandSyntaxError("--user, --role, --platform and load flags need an event, chat message or scenario");
|
|
3512
|
+
main = null;
|
|
3513
|
+
}
|
|
3514
|
+
} else if (head === "chat") {
|
|
3515
|
+
only([], "a chat message");
|
|
3516
|
+
const text = rest.join(" ");
|
|
3517
|
+
if (text === "") throw new CommandSyntaxError('Usage: lumio trigger chat "<text>" [--user name] [--role role] [--platform kick]');
|
|
3518
|
+
main = withLoad({ kind: "chat", text, user, platform }, load);
|
|
3519
|
+
} else if (head === "reward") {
|
|
3520
|
+
only(["input"], "a reward");
|
|
3521
|
+
if (rest.length !== 1) throw new CommandSyntaxError('Usage: lumio trigger reward "<title or id>" [--user name] [--input "text"] [--platform kick]');
|
|
3522
|
+
main = withLoad({ kind: "reward", reward: rest[0], user, input: one("input"), platform }, load);
|
|
3523
|
+
} else if (head === "reward-status") {
|
|
3524
|
+
only([], "a reward status");
|
|
3525
|
+
if (rest.length !== 2) throw new CommandSyntaxError("Usage: lumio trigger reward-status <redemption-id> <status>");
|
|
3526
|
+
main = withLoad({ kind: "reward-status", id: rest[0], status: rest[1] }, load);
|
|
3527
|
+
} else if (head === "music") {
|
|
3528
|
+
only([], "the music player");
|
|
3529
|
+
const [op, ...args] = rest;
|
|
3530
|
+
if (!op) throw new CommandSyntaxError("Usage: lumio trigger music play|pause|skip [n]|seek 1:30|volume 50|shuffle on|repeat track|queue <uri>|device <name>|advance 30s");
|
|
3531
|
+
main = withLoad({ kind: "music", op, args }, load);
|
|
3532
|
+
} else if (head === "scenario") {
|
|
3533
|
+
only([], "a scenario (options are key=value words)");
|
|
3534
|
+
const [name, ...opts] = rest;
|
|
3535
|
+
if (!name && !load.random) throw new CommandSyntaxError("Usage: lumio trigger scenario <name> [key=value \u2026] | lumio trigger scenario --random");
|
|
3536
|
+
main = withLoad({ kind: "scenario", name: name ?? "", options: assignments(opts, "scenario options") }, load);
|
|
3537
|
+
} else if (head.startsWith("@")) {
|
|
3538
|
+
only([], "a preset");
|
|
3539
|
+
if (rest.length) throw new CommandSyntaxError("A preset takes no fields");
|
|
3540
|
+
main = withLoad({ kind: "event", preset: head.slice(1), user }, load);
|
|
3541
|
+
} else if (head.includes(":")) {
|
|
3542
|
+
if (load.random) throw new CommandSyntaxError("--random picks the event type itself: drop the type (every event gets fresh random fields anyway)");
|
|
3543
|
+
const data = {};
|
|
3544
|
+
const file = one("file");
|
|
3545
|
+
if (file !== void 0) {
|
|
3546
|
+
const full = path11.resolve(cwd, file);
|
|
3547
|
+
if (!fs10.existsSync(full)) throw new CommandSyntaxError(`--file ${file}: no such file`);
|
|
3548
|
+
deepMerge(data, jsonObject(fs10.readFileSync(full, "utf8"), `--file ${file}`));
|
|
3549
|
+
}
|
|
3550
|
+
const raw = one("data") ?? one("json");
|
|
3551
|
+
if (raw !== void 0) deepMerge(data, jsonObject(raw, "--data"));
|
|
3552
|
+
deepMerge(data, assignments(rest, "event fields"));
|
|
3553
|
+
for (const f of flags.get("field") ?? []) deepMerge(data, assignments([f], "--field"));
|
|
3554
|
+
for (const [name, value] of shortcuts) setPath(data, name, parseValue(value));
|
|
3555
|
+
main = withLoad({ kind: "event", type: head, data, user }, { ...load, platform });
|
|
3556
|
+
} else {
|
|
3557
|
+
throw new CommandSyntaxError(`Unknown trigger "${head}": use an event type (twitch:follower), @preset, chat, reward, music or scenario`);
|
|
3558
|
+
}
|
|
3559
|
+
return { setup, main, quiet: bools.has("quiet") };
|
|
3560
|
+
}
|
|
3561
|
+
var DevServerError = class extends Error {
|
|
3562
|
+
code;
|
|
3563
|
+
constructor(code, message2) {
|
|
3564
|
+
super(message2);
|
|
3565
|
+
this.name = "DevServerError";
|
|
3566
|
+
this.code = code;
|
|
3567
|
+
}
|
|
3568
|
+
};
|
|
3569
|
+
function findProjectDir(start) {
|
|
3570
|
+
let dir = path11.resolve(start);
|
|
3571
|
+
for (; ; ) {
|
|
3572
|
+
if (fs10.existsSync(path11.join(dir, DEV_SERVER_FILE)) || fs10.existsSync(path11.join(dir, "lumio.config.json"))) return dir;
|
|
3573
|
+
const parent = path11.dirname(dir);
|
|
3574
|
+
if (parent === dir) return null;
|
|
3575
|
+
dir = parent;
|
|
3576
|
+
}
|
|
3577
|
+
}
|
|
3578
|
+
function readDevServerFile(projectDir) {
|
|
3579
|
+
const file = path11.join(projectDir, DEV_SERVER_FILE);
|
|
3580
|
+
if (!fs10.existsSync(file)) {
|
|
3581
|
+
throw new DevServerError("NO_SESSION", `No lumio dev session is running in ${projectDir} (no ${DEV_SERVER_FILE}). Start \`lumio dev\` in another terminal first.`);
|
|
3582
|
+
}
|
|
3583
|
+
let info;
|
|
3584
|
+
try {
|
|
3585
|
+
info = JSON.parse(fs10.readFileSync(file, "utf8"));
|
|
3586
|
+
} catch {
|
|
3587
|
+
throw new DevServerError("BAD_FILE", `${DEV_SERVER_FILE} is not valid JSON. Restart \`lumio dev\` to rewrite it.`);
|
|
3588
|
+
}
|
|
3589
|
+
let url = null;
|
|
3590
|
+
try {
|
|
3591
|
+
url = typeof info.url === "string" ? new URL(info.url) : null;
|
|
3592
|
+
} catch {
|
|
3593
|
+
url = null;
|
|
3594
|
+
}
|
|
3595
|
+
if (!url || url.protocol !== "http:" || url.hostname !== "127.0.0.1" || typeof info.token !== "string" || info.token === "" || typeof info.port !== "number") {
|
|
3596
|
+
throw new DevServerError("BAD_FILE", `${DEV_SERVER_FILE} does not describe a local lumio dev session (http://127.0.0.1:<port>/ and a token). Restart \`lumio dev\` to rewrite it.`);
|
|
3597
|
+
}
|
|
3598
|
+
return info;
|
|
3599
|
+
}
|
|
3600
|
+
function connectDevServer(projectDir, fetchImpl = fetch) {
|
|
3601
|
+
const info = readDevServerFile(projectDir);
|
|
3602
|
+
const base = new URL(info.url);
|
|
3603
|
+
const request = async (route, init = {}) => {
|
|
3604
|
+
let res;
|
|
3605
|
+
try {
|
|
3606
|
+
res = await fetchImpl(new URL(route, base), { ...init, headers: { ...init.headers, [TOKEN_HEADER]: info.token } });
|
|
3607
|
+
} catch {
|
|
3608
|
+
throw new DevServerError(
|
|
3609
|
+
"NOT_RUNNING",
|
|
3610
|
+
`The lumio dev session from ${DEV_SERVER_FILE} (port ${info.port}, pid ${info.pid}) is not answering \u2014 it was stopped or crashed. Start \`lumio dev\` again.`
|
|
3611
|
+
);
|
|
3612
|
+
}
|
|
3613
|
+
if (res.status === 401 || res.status === 421) {
|
|
3614
|
+
throw new DevServerError(
|
|
3615
|
+
"BAD_TOKEN",
|
|
3616
|
+
`The server on port ${info.port} refused the token from ${DEV_SERVER_FILE}: it is another lumio dev session (another project?) or the file is stale. Restart \`lumio dev\` in this project.`
|
|
3617
|
+
);
|
|
3618
|
+
}
|
|
3619
|
+
return res;
|
|
3620
|
+
};
|
|
3621
|
+
const body = async (res) => {
|
|
3622
|
+
try {
|
|
3623
|
+
return await res.json();
|
|
3624
|
+
} catch {
|
|
3625
|
+
throw new DevServerError("BAD_RESPONSE", `The server on port ${info.port} did not answer like lumio dev (HTTP ${res.status}). Is something else using the port?`);
|
|
3626
|
+
}
|
|
3627
|
+
};
|
|
3628
|
+
return {
|
|
3629
|
+
url: info.url,
|
|
3630
|
+
async meta() {
|
|
3631
|
+
const res = await request("/__lumio/meta");
|
|
3632
|
+
if (!res.ok) throw new DevServerError("BAD_RESPONSE", `GET /__lumio/meta answered HTTP ${res.status}`);
|
|
3633
|
+
return body(res);
|
|
3634
|
+
},
|
|
3635
|
+
async send(cmd) {
|
|
3636
|
+
const res = await request("/__lumio/events", {
|
|
3637
|
+
method: "POST",
|
|
3638
|
+
headers: { "Content-Type": "application/json", "X-Lumio-Source": "trigger" },
|
|
3639
|
+
body: JSON.stringify(cmd)
|
|
3640
|
+
});
|
|
3641
|
+
const r = await body(res);
|
|
3642
|
+
if (res.ok || res.status === 422) return { ok: r.ok === true, output: r.output ?? [], log: r.log, payload: r.payload, error: r.error };
|
|
3643
|
+
const error = r.error && r.message ? `${r.error}: ${r.message}` : r.error ?? r.message ?? `HTTP ${res.status}`;
|
|
3644
|
+
return { ok: false, output: [], error };
|
|
3645
|
+
}
|
|
3646
|
+
};
|
|
3647
|
+
}
|
|
3648
|
+
async function runTrigger(client, plan, hooks = {}) {
|
|
3649
|
+
const results = [];
|
|
3650
|
+
const send = async (cmd) => {
|
|
3651
|
+
const r = await client.send(cmd);
|
|
3652
|
+
results.push(r);
|
|
3653
|
+
hooks.onResult?.(r, cmd);
|
|
3654
|
+
return r;
|
|
3655
|
+
};
|
|
3656
|
+
for (const cmd of plan.setup) {
|
|
3657
|
+
if (!(await send(cmd)).ok) return { results, ok: false };
|
|
3658
|
+
}
|
|
3659
|
+
if (!plan.main) return { results, ok: true };
|
|
3660
|
+
if (plan.main.kind !== "repeat") return { results, ok: (await send(plan.main)).ok };
|
|
3661
|
+
const repeat = plan.main;
|
|
3662
|
+
const spec = repeatSpec(repeat, repeat.seed ?? (hooks.seed ?? freshSeed)());
|
|
3663
|
+
const meta = spec.random ? await client.meta() : { eventTypes: [], scenarios: [] };
|
|
3664
|
+
const variant = repeatVariants(repeat, spec, meta);
|
|
3665
|
+
const result = await runLoad(spec, () => send(variant()), { signal: hooks.signal });
|
|
3666
|
+
return { results, load: { spec, result, summary: summarizeLoad(spec, result) }, ok: result.failed === null };
|
|
3667
|
+
}
|
|
3668
|
+
|
|
3669
|
+
// src/commands/trigger.ts
|
|
3670
|
+
function formatResult(r) {
|
|
3671
|
+
const log = (r.log ?? []).filter((e) => e.kind !== "input");
|
|
3672
|
+
const lines = log.length ? log.map(formatLogEntry) : r.output.map((l) => r.ok ? l : chalk4.red(l));
|
|
3673
|
+
if (!r.ok && r.error && !log.some((e) => e.kind === "error")) lines.push(chalk4.red(`error: ${r.error}`));
|
|
3674
|
+
return lines;
|
|
3675
|
+
}
|
|
3676
|
+
function registerTrigger(program2) {
|
|
3677
|
+
program2.command("trigger").description("Send an event, chat message, reward, music command, timer or scenario to the running `lumio dev`").argument("[args...]", "what to trigger (see below)").allowUnknownOption().allowExcessArguments().helpOption("-h, --help", "Show help").addHelpText(
|
|
3678
|
+
"after",
|
|
3679
|
+
[
|
|
3680
|
+
"",
|
|
3681
|
+
"Usage:",
|
|
3682
|
+
...TRIGGER_USAGE.map((l) => ` ${l}`),
|
|
3683
|
+
"",
|
|
3684
|
+
"Examples:",
|
|
3685
|
+
" lumio trigger twitch:follower",
|
|
3686
|
+
" lumio trigger twitch:subscribe --tier 2 --field user_name=Alice",
|
|
3687
|
+
" lumio trigger kick:reward --field reward.title=Hydrate",
|
|
3688
|
+
' lumio trigger chat "!points 100" --user alice --role moderator',
|
|
3689
|
+
" lumio trigger twitch:follower --count 50 --rate 5/s",
|
|
3690
|
+
" lumio trigger --random --count 20 --rate 2/s --platform kick",
|
|
3691
|
+
" lumio trigger music skip",
|
|
3692
|
+
" lumio trigger --timer reminder",
|
|
3693
|
+
` lumio trigger --storage counter='{"value":3}'`,
|
|
3694
|
+
"",
|
|
3695
|
+
"Talks to the lumio dev session of this project through .lumio/dev-server.json."
|
|
3696
|
+
].join("\n")
|
|
3697
|
+
).action(async (args) => {
|
|
3698
|
+
let plan;
|
|
3699
|
+
try {
|
|
3700
|
+
plan = parseTriggerArgs(args);
|
|
3701
|
+
} catch (e) {
|
|
3702
|
+
console.error(chalk4.red(e instanceof Error ? e.message : String(e)));
|
|
3703
|
+
process.exit(2);
|
|
3704
|
+
}
|
|
3705
|
+
const projectDir = findProjectDir(process.cwd()) ?? process.cwd();
|
|
3706
|
+
const abort = new AbortController();
|
|
3707
|
+
const onSigint = () => {
|
|
3708
|
+
if (abort.signal.aborted) process.exit(130);
|
|
3709
|
+
abort.abort();
|
|
3710
|
+
};
|
|
3711
|
+
process.on("SIGINT", onSigint);
|
|
3712
|
+
try {
|
|
3713
|
+
const client = connectDevServer(projectDir);
|
|
3714
|
+
const quiet = plan.quiet;
|
|
3715
|
+
const run = await runTrigger(client, plan, {
|
|
3716
|
+
signal: abort.signal,
|
|
3717
|
+
onResult: (r) => {
|
|
3718
|
+
if (quiet && r.ok) return;
|
|
3719
|
+
for (const line of formatResult(r)) console.log(line);
|
|
3720
|
+
}
|
|
3721
|
+
});
|
|
3722
|
+
if (run.load) console.log((run.ok ? chalk4.dim : chalk4.red)(run.load.summary));
|
|
3723
|
+
process.exitCode = run.ok ? 0 : 1;
|
|
3724
|
+
} catch (e) {
|
|
3725
|
+
const known = e instanceof DevServerError || e instanceof CommandSyntaxError;
|
|
3726
|
+
console.error(chalk4.red(known ? e.message : `lumio trigger failed: ${e instanceof Error ? e.message : String(e)}`));
|
|
3727
|
+
process.exitCode = 1;
|
|
3728
|
+
} finally {
|
|
3729
|
+
process.off("SIGINT", onSigint);
|
|
3730
|
+
}
|
|
3731
|
+
});
|
|
3732
|
+
}
|
|
3733
|
+
|
|
3734
|
+
// src/commands/build.ts
|
|
3735
|
+
import * as fs11 from "fs";
|
|
3736
|
+
import * as path12 from "path";
|
|
3737
|
+
function registerBuild(program2) {
|
|
3738
|
+
program2.command("build").description("Bundle extension as standalone HTML + source archive").action(async () => {
|
|
3739
|
+
const configPath = path12.resolve("lumio.config.json");
|
|
3740
|
+
if (!fs11.existsSync(configPath)) {
|
|
3741
|
+
console.error("lumio.config.json not found. Run `lumio init` first.");
|
|
3742
|
+
process.exit(1);
|
|
3743
|
+
}
|
|
3744
|
+
const configJson = fs11.readFileSync(configPath, "utf-8");
|
|
3745
|
+
const config = parseConfigFile(configJson);
|
|
3746
|
+
validateConfig(config);
|
|
3747
|
+
const changelogPath = path12.resolve("CHANGELOG.md");
|
|
3748
|
+
if (!fs11.existsSync(changelogPath)) {
|
|
3749
|
+
console.warn("Warning: No CHANGELOG.md found. It is recommended to maintain a changelog.");
|
|
3750
|
+
}
|
|
3751
|
+
console.log(`Building ${config.name} v${config.version}...`);
|
|
3752
|
+
const distDir = path12.resolve("dist");
|
|
3753
|
+
fs11.mkdirSync(distDir, { recursive: true });
|
|
3754
|
+
const { build } = await import("esbuild");
|
|
3755
|
+
const projectDir = process.cwd();
|
|
3756
|
+
if (config.category === "bot_module" && config.targets.length === 0) {
|
|
3757
|
+
console.log(" Bot module \u2014 no UI targets to bundle.");
|
|
3758
|
+
}
|
|
3759
|
+
const manifestPath = path12.join(distDir, FUNCTIONS_MANIFEST_FILE);
|
|
3760
|
+
fs11.rmSync(manifestPath, { force: true });
|
|
3761
|
+
const functionsFile = config.category !== "automation_node" ? findFunctionsFile(projectDir) : null;
|
|
3762
|
+
if (functionsFile && !config.server) {
|
|
3763
|
+
console.warn(` Warning: ${functionsFile} found but lumio.config.json has no "server": true \u2014 it is not built.`);
|
|
3764
|
+
}
|
|
3765
|
+
if (config.server) {
|
|
3766
|
+
let plan;
|
|
3767
|
+
try {
|
|
3768
|
+
plan = await planServerBuild(projectDir, config, path12.join(distDir, "server.js"));
|
|
3769
|
+
} catch (err) {
|
|
3770
|
+
console.error(` Error: ${err instanceof Error ? err.message : String(err)}`);
|
|
3771
|
+
process.exit(1);
|
|
3772
|
+
}
|
|
3773
|
+
if (plan.options) {
|
|
3774
|
+
await build(plan.options);
|
|
3775
|
+
console.log(" Built: dist/server.js");
|
|
3776
|
+
} else {
|
|
3777
|
+
console.warn(" Warning: server: true but no server/handler.ts, server/index.ts or server/functions.ts found.");
|
|
3778
|
+
}
|
|
3779
|
+
if (plan.manifest) {
|
|
3780
|
+
fs11.writeFileSync(manifestPath, `${JSON.stringify(plan.manifest, null, 2)}
|
|
3781
|
+
`);
|
|
3782
|
+
console.log(` Built: dist/${FUNCTIONS_MANIFEST_FILE} (${Object.keys(plan.manifest.functions).length} function(s))`);
|
|
3783
|
+
}
|
|
3784
|
+
}
|
|
3785
|
+
const cssEntryFile = findCssEntry(projectDir);
|
|
3786
|
+
try {
|
|
3787
|
+
const css = await compileCss(projectDir, distDir);
|
|
3788
|
+
if (css?.tailwind) console.log(" Tailwind CSS v4 detected \u2014 compiled dist/styles.css");
|
|
3789
|
+
else if (css) console.log(` Copied: ${cssEntryFile} \u2192 dist/styles.css`);
|
|
3790
|
+
} catch (err) {
|
|
3791
|
+
console.error(` Error: Tailwind CSS compilation failed. ${err instanceof CssBuildError ? err.hint : ""}`);
|
|
3792
|
+
console.error(` ${err instanceof Error ? err.message : String(err)}`);
|
|
3793
|
+
process.exit(1);
|
|
3794
|
+
}
|
|
3795
|
+
for (const target of config.targets) {
|
|
3796
|
+
const entryPoint = targetEntry(projectDir, target);
|
|
3797
|
+
if (!entryPoint) {
|
|
3798
|
+
console.warn(`Warning: Entry point ${path12.resolve("src", `${target}.tsx`)} not found, skipping target "${target}".`);
|
|
3799
|
+
continue;
|
|
3800
|
+
}
|
|
3801
|
+
await build(targetBuildOptions(entryPoint, distDir, target));
|
|
3802
|
+
console.log(` Built: dist/${target}.js`);
|
|
3803
|
+
}
|
|
3804
|
+
const { TarArchive } = await import("archiver");
|
|
3805
|
+
const sourceArchivePath = path12.join(distDir, "source.tar.gz");
|
|
3806
|
+
const output = fs11.createWriteStream(sourceArchivePath);
|
|
3807
|
+
const archive = new TarArchive({ gzip: true });
|
|
3808
|
+
archive.pipe(output);
|
|
3809
|
+
archive.glob("**/*", {
|
|
3810
|
+
cwd: path12.resolve("."),
|
|
3811
|
+
ignore: [
|
|
3812
|
+
"node_modules/**",
|
|
3813
|
+
"dist/**",
|
|
3814
|
+
".git/**",
|
|
3815
|
+
".git",
|
|
3816
|
+
".next/**",
|
|
3817
|
+
".lumio-dist/**",
|
|
3818
|
+
"*.tgz",
|
|
3819
|
+
"*.tar.gz",
|
|
3820
|
+
// Secrets & credentials must never be uploaded to the store: the
|
|
3821
|
+
// archive is visible to reviewers. Exclude env files (except the
|
|
3822
|
+
// example), npm/yarn auth, and local CLI state.
|
|
3823
|
+
// archiver 8's readdir-glob@3 dropped the `!pattern` un-ignore
|
|
3824
|
+
// feature (each ignore entry is now an independent minimatch, so a
|
|
3825
|
+
// leading `!` inverts that one matcher and ignores everything else).
|
|
3826
|
+
// Keep `.env.example` via an extglob instead of a negated entry.
|
|
3827
|
+
"**/.env",
|
|
3828
|
+
"**/.env.!(example)",
|
|
3829
|
+
"**/.npmrc",
|
|
3830
|
+
"**/.yarnrc",
|
|
3831
|
+
"**/.yarnrc.yml",
|
|
3832
|
+
"**/.lumio/**"
|
|
3833
|
+
],
|
|
3834
|
+
dot: true
|
|
3835
|
+
});
|
|
3836
|
+
await archive.finalize();
|
|
3837
|
+
console.log(" Archive: dist/source.tar.gz");
|
|
3838
|
+
console.log("Build complete.");
|
|
3839
|
+
});
|
|
3840
|
+
}
|
|
3841
|
+
|
|
3842
|
+
// src/commands/deploy.ts
|
|
3843
|
+
import * as fs12 from "fs";
|
|
3844
|
+
import * as path13 from "path";
|
|
3845
|
+
|
|
3846
|
+
// src/changelog/parser.ts
|
|
3847
|
+
var VERSION_HEADER_REGEX = /^## \[([^\]]+)\](?:\s*-\s*(\d{4}-\d{2}-\d{2}))?/;
|
|
3848
|
+
function parseChangelog(markdown) {
|
|
3849
|
+
const lines = markdown.split("\n");
|
|
3850
|
+
const entries = [];
|
|
3851
|
+
let currentEntry = null;
|
|
3852
|
+
const contentLines = [];
|
|
3853
|
+
for (const line of lines) {
|
|
3854
|
+
const match = line.match(VERSION_HEADER_REGEX);
|
|
3855
|
+
if (match) {
|
|
3856
|
+
if (currentEntry) {
|
|
3857
|
+
currentEntry.content = contentLines.join("\n").trim();
|
|
3858
|
+
entries.push(currentEntry);
|
|
3859
|
+
contentLines.length = 0;
|
|
3860
|
+
}
|
|
3861
|
+
currentEntry = {
|
|
3862
|
+
version: match[1],
|
|
3863
|
+
date: match[2] ?? null,
|
|
3864
|
+
content: ""
|
|
3865
|
+
};
|
|
3866
|
+
} else if (currentEntry) {
|
|
3867
|
+
contentLines.push(line);
|
|
3868
|
+
}
|
|
3869
|
+
}
|
|
3870
|
+
if (currentEntry) {
|
|
3871
|
+
currentEntry.content = contentLines.join("\n").trim();
|
|
3872
|
+
entries.push(currentEntry);
|
|
3873
|
+
}
|
|
3874
|
+
return entries;
|
|
3875
|
+
}
|
|
3876
|
+
function getVersionEntry(changelog, version) {
|
|
3877
|
+
const entries = parseChangelog(changelog);
|
|
3878
|
+
return entries.find((e) => e.version === version) ?? null;
|
|
3879
|
+
}
|
|
3880
|
+
|
|
3881
|
+
// src/commands/deploy.ts
|
|
3882
|
+
function collectFiles(dir, base = dir) {
|
|
3883
|
+
const entries = fs12.readdirSync(dir, { withFileTypes: true });
|
|
3884
|
+
const files = [];
|
|
3885
|
+
for (const entry of entries) {
|
|
3886
|
+
const fullPath = path13.join(dir, entry.name);
|
|
3887
|
+
if (entry.isDirectory()) {
|
|
3888
|
+
files.push(...collectFiles(fullPath, base));
|
|
3889
|
+
} else if (entry.isFile()) {
|
|
3890
|
+
files.push(path13.relative(base, fullPath));
|
|
3891
|
+
}
|
|
3892
|
+
}
|
|
3893
|
+
return files;
|
|
3894
|
+
}
|
|
3895
|
+
var ALLOWED_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
3896
|
+
"js",
|
|
3897
|
+
"css",
|
|
3898
|
+
"html",
|
|
3899
|
+
"map",
|
|
3900
|
+
"png",
|
|
3901
|
+
"jpg",
|
|
3902
|
+
"jpeg",
|
|
3903
|
+
"gif",
|
|
3904
|
+
"svg",
|
|
3905
|
+
"webp",
|
|
1263
3906
|
"avif",
|
|
1264
3907
|
"ico",
|
|
1265
3908
|
"mp4",
|
|
@@ -1291,15 +3934,15 @@ function registerDeploy(program2) {
|
|
|
1291
3934
|
console.error("Not logged in. Run `lumio login` or provide --token.");
|
|
1292
3935
|
process.exit(1);
|
|
1293
3936
|
}
|
|
1294
|
-
const configPath =
|
|
1295
|
-
if (!
|
|
3937
|
+
const configPath = path13.resolve("lumio.config.json");
|
|
3938
|
+
if (!fs12.existsSync(configPath)) {
|
|
1296
3939
|
console.error("lumio.config.json not found. Run `lumio init` first.");
|
|
1297
3940
|
process.exit(1);
|
|
1298
3941
|
}
|
|
1299
|
-
const configJson =
|
|
3942
|
+
const configJson = fs12.readFileSync(configPath, "utf-8");
|
|
1300
3943
|
const config = parseConfigFile(configJson);
|
|
1301
3944
|
validateConfig(config);
|
|
1302
|
-
const distDir =
|
|
3945
|
+
const distDir = path13.resolve("dist");
|
|
1303
3946
|
if (config.sounds && config.sounds.length > 0) {
|
|
1304
3947
|
for (const sound of config.sounds) {
|
|
1305
3948
|
const soundFile = sound.file;
|
|
@@ -1310,15 +3953,15 @@ function registerDeploy(program2) {
|
|
|
1310
3953
|
);
|
|
1311
3954
|
process.exit(1);
|
|
1312
3955
|
}
|
|
1313
|
-
const soundAbsPath =
|
|
1314
|
-
if (
|
|
3956
|
+
const soundAbsPath = path13.join(distDir, "sounds", soundFile);
|
|
3957
|
+
if (fs12.existsSync(distDir) && !fs12.existsSync(soundAbsPath)) {
|
|
1315
3958
|
console.error(
|
|
1316
3959
|
`Error: Sound file "${soundFile}" declared in lumio.config.json was not found at dist/sounds/${soundFile}. Run \`lumio build\` first.`
|
|
1317
3960
|
);
|
|
1318
3961
|
process.exit(1);
|
|
1319
3962
|
}
|
|
1320
|
-
if (
|
|
1321
|
-
const stat =
|
|
3963
|
+
if (fs12.existsSync(soundAbsPath)) {
|
|
3964
|
+
const stat = fs12.statSync(soundAbsPath);
|
|
1322
3965
|
if (stat.size > LARGE_SOUND_THRESHOLD) {
|
|
1323
3966
|
console.warn(
|
|
1324
3967
|
` Warning: Sound file "${soundFile}" is ${(stat.size / 1024 / 1024).toFixed(1)} MB. Consider compressing large audio files to reduce load times.`
|
|
@@ -1344,22 +3987,22 @@ function registerDeploy(program2) {
|
|
|
1344
3987
|
`Error: Extension declares ${config.sounds.length} sounds, but your limit is ${maxSounds}.`
|
|
1345
3988
|
);
|
|
1346
3989
|
console.error(
|
|
1347
|
-
"Request a limit increase at /
|
|
3990
|
+
"Request a limit increase at /dashboard/settings/developer/limits or contact support."
|
|
1348
3991
|
);
|
|
1349
3992
|
process.exit(1);
|
|
1350
3993
|
}
|
|
1351
3994
|
let totalSize2 = 0;
|
|
1352
3995
|
for (const sound of config.sounds) {
|
|
1353
|
-
const soundPath =
|
|
1354
|
-
if (
|
|
1355
|
-
const size =
|
|
3996
|
+
const soundPath = path13.resolve("dist", "sounds", sound.file);
|
|
3997
|
+
if (fs12.existsSync(soundPath)) {
|
|
3998
|
+
const size = fs12.statSync(soundPath).size;
|
|
1356
3999
|
totalSize2 += size;
|
|
1357
4000
|
if (size > maxFileSize) {
|
|
1358
4001
|
console.error(
|
|
1359
4002
|
`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
4003
|
);
|
|
1361
4004
|
console.error(
|
|
1362
|
-
"Request a limit increase at /
|
|
4005
|
+
"Request a limit increase at /dashboard/settings/developer/limits or contact support."
|
|
1363
4006
|
);
|
|
1364
4007
|
process.exit(1);
|
|
1365
4008
|
}
|
|
@@ -1370,7 +4013,7 @@ function registerDeploy(program2) {
|
|
|
1370
4013
|
`Error: Total sound size is ${(totalSize2 / 1024 / 1024).toFixed(1)} MB but your storage limit is ${(maxStorage / 1024 / 1024).toFixed(1)} MB.`
|
|
1371
4014
|
);
|
|
1372
4015
|
console.error(
|
|
1373
|
-
"Request a limit increase at /
|
|
4016
|
+
"Request a limit increase at /dashboard/settings/developer/limits or contact support."
|
|
1374
4017
|
);
|
|
1375
4018
|
process.exit(1);
|
|
1376
4019
|
}
|
|
@@ -1388,9 +4031,9 @@ function registerDeploy(program2) {
|
|
|
1388
4031
|
const version = options.version ?? config.version;
|
|
1389
4032
|
let releaseNotes = options.description ?? "";
|
|
1390
4033
|
if (!releaseNotes) {
|
|
1391
|
-
const changelogPath =
|
|
1392
|
-
if (
|
|
1393
|
-
const changelog =
|
|
4034
|
+
const changelogPath = path13.resolve("CHANGELOG.md");
|
|
4035
|
+
if (fs12.existsSync(changelogPath)) {
|
|
4036
|
+
const changelog = fs12.readFileSync(changelogPath, "utf-8");
|
|
1394
4037
|
const entry = getVersionEntry(changelog, version);
|
|
1395
4038
|
if (entry) {
|
|
1396
4039
|
releaseNotes = entry.content;
|
|
@@ -1401,10 +4044,23 @@ function registerDeploy(program2) {
|
|
|
1401
4044
|
}
|
|
1402
4045
|
}
|
|
1403
4046
|
}
|
|
1404
|
-
|
|
4047
|
+
let dataSchema;
|
|
4048
|
+
try {
|
|
4049
|
+
dataSchema = await loadDataSchema(process.cwd());
|
|
4050
|
+
} catch (err) {
|
|
4051
|
+
console.error(`Error: Could not load server/schema.ts: ${err.message ?? String(err)}`);
|
|
4052
|
+
process.exit(1);
|
|
4053
|
+
}
|
|
4054
|
+
if (!fs12.existsSync(distDir)) {
|
|
1405
4055
|
console.error("No dist/ directory found. Run `lumio build` first.");
|
|
1406
4056
|
process.exit(1);
|
|
1407
4057
|
}
|
|
4058
|
+
const exposesFunctions = config.server && config.category !== "automation_node";
|
|
4059
|
+
const functionsFile = exposesFunctions ? findFunctionsFile(process.cwd()) : null;
|
|
4060
|
+
if (functionsFile && !fs12.existsSync(path13.join(distDir, FUNCTIONS_MANIFEST_FILE))) {
|
|
4061
|
+
console.error(`Error: ${functionsFile} exists but dist/${FUNCTIONS_MANIFEST_FILE} is missing. Run \`lumio build\` first.`);
|
|
4062
|
+
process.exit(1);
|
|
4063
|
+
}
|
|
1408
4064
|
console.log(`Deploying ${config.name} v${version}...`);
|
|
1409
4065
|
const apiUrl = process.env.LUMIO_API_URL ?? store.getApiUrl();
|
|
1410
4066
|
const client = new LumioApiClient(apiUrl, token);
|
|
@@ -1424,8 +4080,8 @@ function registerDeploy(program2) {
|
|
|
1424
4080
|
console.error(`Too many files (${bundleFiles.length}). Maximum is ${MAX_FILES}.`);
|
|
1425
4081
|
process.exit(1);
|
|
1426
4082
|
}
|
|
1427
|
-
const sourceArchive =
|
|
1428
|
-
if (!
|
|
4083
|
+
const sourceArchive = path13.join(distDir, sourceFile);
|
|
4084
|
+
if (!fs12.existsSync(sourceArchive)) {
|
|
1429
4085
|
console.error("Error: source.tar.gz not found in dist/. Run `lumio build` first.");
|
|
1430
4086
|
process.exit(1);
|
|
1431
4087
|
}
|
|
@@ -1436,7 +4092,11 @@ function registerDeploy(program2) {
|
|
|
1436
4092
|
formData.append("manifest", JSON.stringify(manifest));
|
|
1437
4093
|
formData.append("release_notes", releaseNotes);
|
|
1438
4094
|
formData.append("is_breaking", String(!!options.breaking));
|
|
1439
|
-
|
|
4095
|
+
if (dataSchema) {
|
|
4096
|
+
formData.append("data_schema", JSON.stringify(dataSchema));
|
|
4097
|
+
console.log(` Data schema: ${Object.keys(dataSchema.tables).length} table(s)`);
|
|
4098
|
+
}
|
|
4099
|
+
const sourceBuffer = fs12.readFileSync(sourceArchive);
|
|
1440
4100
|
if (sourceBuffer.length === 0) {
|
|
1441
4101
|
console.error("Error: source.tar.gz is empty. Run `lumio build` to regenerate.");
|
|
1442
4102
|
process.exit(1);
|
|
@@ -1445,8 +4105,8 @@ function registerDeploy(program2) {
|
|
|
1445
4105
|
console.log(` Source: source.tar.gz (${(sourceBuffer.length / 1024).toFixed(1)} KB)`);
|
|
1446
4106
|
let totalSize = 0;
|
|
1447
4107
|
for (const relPath of bundleFiles) {
|
|
1448
|
-
const absPath =
|
|
1449
|
-
const buffer =
|
|
4108
|
+
const absPath = path13.join(distDir, relPath);
|
|
4109
|
+
const buffer = fs12.readFileSync(absPath);
|
|
1450
4110
|
if (buffer.length === 0) {
|
|
1451
4111
|
console.warn(` Warning: Skipping empty file: ${relPath}`);
|
|
1452
4112
|
continue;
|
|
@@ -1475,9 +4135,16 @@ function registerDeploy(program2) {
|
|
|
1475
4135
|
}
|
|
1476
4136
|
} catch (err) {
|
|
1477
4137
|
const status = err.status;
|
|
1478
|
-
const
|
|
4138
|
+
const message2 = err.message ?? String(err);
|
|
4139
|
+
const schemaLines = err instanceof ApiError && status === 422 ? formatDataSchemaError(err.body) : null;
|
|
4140
|
+
if (schemaLines) {
|
|
4141
|
+
console.error(`
|
|
4142
|
+
Deploy failed (HTTP ${status}):`);
|
|
4143
|
+
for (const line of schemaLines) console.error(` ${line}`);
|
|
4144
|
+
process.exit(1);
|
|
4145
|
+
}
|
|
1479
4146
|
console.error(`
|
|
1480
|
-
Deploy failed${status ? ` (HTTP ${status})` : ""}: ${
|
|
4147
|
+
Deploy failed${status ? ` (HTTP ${status})` : ""}: ${message2}`);
|
|
1481
4148
|
if (status === 409) {
|
|
1482
4149
|
console.error(" This version already exists. Bump the version in lumio.config.json.");
|
|
1483
4150
|
} else if (status === 413) {
|
|
@@ -1491,8 +4158,8 @@ function registerDeploy(program2) {
|
|
|
1491
4158
|
}
|
|
1492
4159
|
|
|
1493
4160
|
// src/commands/status.ts
|
|
1494
|
-
import * as
|
|
1495
|
-
import * as
|
|
4161
|
+
import * as fs13 from "fs";
|
|
4162
|
+
import * as path14 from "path";
|
|
1496
4163
|
function registerStatus(program2) {
|
|
1497
4164
|
program2.command("status").description("Show review status of the latest version").action(async () => {
|
|
1498
4165
|
const store = new CredentialStore();
|
|
@@ -1501,12 +4168,12 @@ function registerStatus(program2) {
|
|
|
1501
4168
|
console.error("Not logged in. Run `lumio login` first.");
|
|
1502
4169
|
process.exit(1);
|
|
1503
4170
|
}
|
|
1504
|
-
const configPath =
|
|
1505
|
-
if (!
|
|
4171
|
+
const configPath = path14.resolve("lumio.config.json");
|
|
4172
|
+
if (!fs13.existsSync(configPath)) {
|
|
1506
4173
|
console.error("lumio.config.json not found.");
|
|
1507
4174
|
process.exit(1);
|
|
1508
4175
|
}
|
|
1509
|
-
const config = parseConfigFile(
|
|
4176
|
+
const config = parseConfigFile(fs13.readFileSync(configPath, "utf-8"));
|
|
1510
4177
|
const client = new LumioApiClient(store.getApiUrl(), token);
|
|
1511
4178
|
try {
|
|
1512
4179
|
const versions = await client.get(
|
|
@@ -1535,8 +4202,8 @@ function registerStatus(program2) {
|
|
|
1535
4202
|
}
|
|
1536
4203
|
|
|
1537
4204
|
// src/commands/logs.ts
|
|
1538
|
-
import * as
|
|
1539
|
-
import * as
|
|
4205
|
+
import * as fs14 from "fs";
|
|
4206
|
+
import * as path15 from "path";
|
|
1540
4207
|
function registerLogs(program2) {
|
|
1541
4208
|
program2.command("logs").description("Stream server function logs via SSE").option("-f, --follow", "Follow log output", true).option("-n, --lines <count>", "Number of recent lines", "50").action(async (options) => {
|
|
1542
4209
|
const store = new CredentialStore();
|
|
@@ -1545,12 +4212,12 @@ function registerLogs(program2) {
|
|
|
1545
4212
|
console.error("Not logged in. Run `lumio login` first.");
|
|
1546
4213
|
process.exit(1);
|
|
1547
4214
|
}
|
|
1548
|
-
const configPath =
|
|
1549
|
-
if (!
|
|
4215
|
+
const configPath = path15.resolve("lumio.config.json");
|
|
4216
|
+
if (!fs14.existsSync(configPath)) {
|
|
1550
4217
|
console.error("lumio.config.json not found.");
|
|
1551
4218
|
process.exit(1);
|
|
1552
4219
|
}
|
|
1553
|
-
const config = parseConfigFile(
|
|
4220
|
+
const config = parseConfigFile(fs14.readFileSync(configPath, "utf-8"));
|
|
1554
4221
|
const apiUrl = store.getApiUrl();
|
|
1555
4222
|
const logsUrl = `${apiUrl}/v1/developer/extensions/${config.extensionId}/logs?lines=${options.lines}`;
|
|
1556
4223
|
console.log(`Streaming logs for ${config.name}...`);
|
|
@@ -1587,22 +4254,31 @@ function registerLogs(program2) {
|
|
|
1587
4254
|
}
|
|
1588
4255
|
|
|
1589
4256
|
// src/commands/env.ts
|
|
1590
|
-
import * as
|
|
1591
|
-
import * as
|
|
4257
|
+
import * as fs15 from "fs";
|
|
4258
|
+
import * as path16 from "path";
|
|
4259
|
+
var MAX_SECRET_KEY_LEN = 64;
|
|
4260
|
+
var MAX_SECRET_VALUE_BYTES = 4096;
|
|
1592
4261
|
function validateSecretKey(key) {
|
|
1593
|
-
return
|
|
4262
|
+
return /^[A-Z0-9_]{1,64}$/.test(key);
|
|
4263
|
+
}
|
|
4264
|
+
function validateSecretValue(value) {
|
|
4265
|
+
const bytes = Buffer.byteLength(value, "utf8");
|
|
4266
|
+
return bytes > 0 && bytes <= MAX_SECRET_VALUE_BYTES;
|
|
4267
|
+
}
|
|
4268
|
+
function secretEndpoint(extensionId, key) {
|
|
4269
|
+
return `/v1/developer/extensions/${extensionId}/secrets/${encodeURIComponent(key)}`;
|
|
1594
4270
|
}
|
|
1595
4271
|
function formatSecretList(secrets) {
|
|
1596
4272
|
if (secrets.length === 0) return "No secrets configured.";
|
|
1597
4273
|
return secrets.map((s) => ` ${s.key} (created ${s.created_at})`).join("\n");
|
|
1598
4274
|
}
|
|
1599
4275
|
function loadConfig() {
|
|
1600
|
-
const configPath =
|
|
1601
|
-
if (!
|
|
4276
|
+
const configPath = path16.resolve("lumio.config.json");
|
|
4277
|
+
if (!fs15.existsSync(configPath)) {
|
|
1602
4278
|
console.error("lumio.config.json not found. Run `lumio init` first.");
|
|
1603
4279
|
process.exit(1);
|
|
1604
4280
|
}
|
|
1605
|
-
return parseConfigFile(
|
|
4281
|
+
return parseConfigFile(fs15.readFileSync(configPath, "utf-8"));
|
|
1606
4282
|
}
|
|
1607
4283
|
function requireClient() {
|
|
1608
4284
|
const store = new CredentialStore();
|
|
@@ -1615,10 +4291,10 @@ function requireClient() {
|
|
|
1615
4291
|
}
|
|
1616
4292
|
function registerEnv(program2) {
|
|
1617
4293
|
const envCmd = program2.command("env").description("Manage server-side secrets for the extension");
|
|
1618
|
-
envCmd.command("set <key> [value]").description("Set a secret (
|
|
4294
|
+
envCmd.command("set <key> [value]").description("Set a secret (PUT /v1/developer/extensions/{id}/secrets/{key}). 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) => {
|
|
1619
4295
|
if (!validateSecretKey(key)) {
|
|
1620
4296
|
console.error(
|
|
1621
|
-
|
|
4297
|
+
`Secret key must match ^[A-Z0-9_]{1,${MAX_SECRET_KEY_LEN}}$ (uppercase letters, digits, underscores; e.g. API_KEY).`
|
|
1622
4298
|
);
|
|
1623
4299
|
process.exit(1);
|
|
1624
4300
|
}
|
|
@@ -1633,9 +4309,13 @@ function registerEnv(program2) {
|
|
|
1633
4309
|
}]);
|
|
1634
4310
|
secretValue = answer.value;
|
|
1635
4311
|
}
|
|
4312
|
+
if (!validateSecretValue(secretValue)) {
|
|
4313
|
+
console.error(`Secret value must be between 1 and ${MAX_SECRET_VALUE_BYTES} bytes.`);
|
|
4314
|
+
process.exit(1);
|
|
4315
|
+
}
|
|
1636
4316
|
const config = loadConfig();
|
|
1637
4317
|
const client = requireClient();
|
|
1638
|
-
await client.
|
|
4318
|
+
await client.put(secretEndpoint(config.extensionId, key), { value: secretValue });
|
|
1639
4319
|
console.log(`Secret ${key} set.`);
|
|
1640
4320
|
});
|
|
1641
4321
|
envCmd.command("list").description("List all secrets (keys only, no values)").action(async () => {
|
|
@@ -1649,14 +4329,14 @@ function registerEnv(program2) {
|
|
|
1649
4329
|
envCmd.command("delete <key>").description("Delete a secret").action(async (key) => {
|
|
1650
4330
|
const config = loadConfig();
|
|
1651
4331
|
const client = requireClient();
|
|
1652
|
-
await client.delete(
|
|
4332
|
+
await client.delete(secretEndpoint(config.extensionId, key));
|
|
1653
4333
|
console.log(`Secret ${key} deleted.`);
|
|
1654
4334
|
});
|
|
1655
4335
|
}
|
|
1656
4336
|
|
|
1657
4337
|
// src/commands/run.ts
|
|
1658
|
-
import * as
|
|
1659
|
-
import * as
|
|
4338
|
+
import * as fs16 from "fs";
|
|
4339
|
+
import * as path17 from "path";
|
|
1660
4340
|
var VALID_FUNCTION_TYPES = [
|
|
1661
4341
|
"query",
|
|
1662
4342
|
"mutation",
|
|
@@ -1693,12 +4373,12 @@ function parseFunctionRef(ref) {
|
|
|
1693
4373
|
function registerRun(program2) {
|
|
1694
4374
|
program2.command("run <type:name> [args]").description("Execute a server function locally (e.g. lumio run query:getPlayers)").option("--user <name>", "Mock user name for bot handlers", "testuser").option("--role <role>", "Mock user role for bot handlers", "everyone").option("--message <text>", "Mock chat message for bot handlers").option("--platform <platform>", "Mock platform for bot handlers", "twitch").action(async (typeAndName, argsJson, options) => {
|
|
1695
4375
|
const fnRef = parseFunctionRef(typeAndName);
|
|
1696
|
-
const configPath =
|
|
1697
|
-
if (!
|
|
4376
|
+
const configPath = path17.resolve("lumio.config.json");
|
|
4377
|
+
if (!fs16.existsSync(configPath)) {
|
|
1698
4378
|
console.error("lumio.config.json not found. Run `lumio init` first.");
|
|
1699
4379
|
process.exit(1);
|
|
1700
4380
|
}
|
|
1701
|
-
const config = parseConfigFile(
|
|
4381
|
+
const config = parseConfigFile(fs16.readFileSync(configPath, "utf-8"));
|
|
1702
4382
|
const store = new CredentialStore();
|
|
1703
4383
|
const token = store.getToken();
|
|
1704
4384
|
if (!token) {
|
|
@@ -1747,16 +4427,16 @@ function registerRun(program2) {
|
|
|
1747
4427
|
}
|
|
1748
4428
|
|
|
1749
4429
|
// src/commands/dashboard.ts
|
|
1750
|
-
import * as
|
|
1751
|
-
import * as
|
|
4430
|
+
import * as fs17 from "fs";
|
|
4431
|
+
import * as path18 from "path";
|
|
1752
4432
|
function registerDashboard(program2) {
|
|
1753
4433
|
program2.command("dashboard").description("Open the extension dashboard in browser").action(async () => {
|
|
1754
|
-
const configPath =
|
|
1755
|
-
if (!
|
|
4434
|
+
const configPath = path18.resolve("lumio.config.json");
|
|
4435
|
+
if (!fs17.existsSync(configPath)) {
|
|
1756
4436
|
console.error("lumio.config.json not found. Run `lumio init` first.");
|
|
1757
4437
|
process.exit(1);
|
|
1758
4438
|
}
|
|
1759
|
-
const config = parseConfigFile(
|
|
4439
|
+
const config = parseConfigFile(fs17.readFileSync(configPath, "utf-8"));
|
|
1760
4440
|
const baseUrl = process.env.LUMIO_WEB_URL ?? "https://lumio.vision";
|
|
1761
4441
|
const url = `${baseUrl}/dashboard/extensions/${config.extensionId}`;
|
|
1762
4442
|
console.log(`Opening ${url}...`);
|
|
@@ -1767,10 +4447,10 @@ function registerDashboard(program2) {
|
|
|
1767
4447
|
|
|
1768
4448
|
// src/program.ts
|
|
1769
4449
|
function createProgram() {
|
|
1770
|
-
const program2 = new
|
|
4450
|
+
const program2 = new Command2();
|
|
1771
4451
|
program2.name("lumio");
|
|
1772
4452
|
program2.description("Lumio Extension CLI -- Build, test, and deploy Lumio extensions");
|
|
1773
|
-
const require2 =
|
|
4453
|
+
const require2 = createRequire3(import.meta.url);
|
|
1774
4454
|
const { version } = require2("../package.json");
|
|
1775
4455
|
program2.version(version);
|
|
1776
4456
|
registerLogin(program2);
|
|
@@ -1778,6 +4458,7 @@ function createProgram() {
|
|
|
1778
4458
|
registerWhoami(program2);
|
|
1779
4459
|
registerInit(program2);
|
|
1780
4460
|
registerDev(program2);
|
|
4461
|
+
registerTrigger(program2);
|
|
1781
4462
|
registerBuild(program2);
|
|
1782
4463
|
registerDeploy(program2);
|
|
1783
4464
|
registerStatus(program2);
|