create-spatius-app 0.1.2 → 0.1.3-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/README.md +17 -6
  2. package/dist/cli.js +721 -143
  3. package/dist/templates.js +612 -66
  4. package/package.json +4 -4
  5. package/schemas/result-v3.schema.json +6 -2
  6. package/schemas/result-v4.schema.json +170 -0
  7. package/templates/cloudflare-livekit/agent/env.example +3 -0
  8. package/templates/cloudflare-livekit/agent/src/agent.py +5 -3
  9. package/templates/cloudflare-livekit/package-lock.json +70 -142
  10. package/templates/cloudflare-livekit/package.json +1 -1
  11. package/templates/cloudflare-livekit/pnpm-lock.yaml +96 -153
  12. package/templates/cloudflare-livekit/tests/dev-events.test.mjs +23 -0
  13. package/templates/cloudflare-livekit/tests/dev-fixtures.mjs +7 -3
  14. package/templates/cloudflare-livekit/tsconfig.web.json +1 -0
  15. package/templates/cloudflare-livekit/tsconfig.worker.json +1 -0
  16. package/templates/cloudflare-livekit/web/api.ts +12 -0
  17. package/templates/cloudflare-livekit/web/components/agents-ui/agent-chat-transcript.tsx +6 -90
  18. package/templates/cloudflare-livekit/web/components/transcript-view.tsx +90 -0
  19. package/templates/cloudflare-livekit/web/session-attempt.ts +23 -2
  20. package/templates/cloudflare-livekit/worker/capability.test.ts +31 -0
  21. package/templates/cloudflare-livekit/worker/capability.ts +72 -0
  22. package/templates/cloudflare-livekit/worker/index.test.ts +2 -2
  23. package/templates/cloudflare-livekit/worker/index.ts +15 -3
  24. package/templates/cloudflare-livekit/worker/session.ts +34 -0
  25. package/templates/layers/agora/AGENTS.md +8 -0
  26. package/templates/layers/agora/README.md +19 -0
  27. package/templates/layers/agora/env.example +9 -0
  28. package/templates/layers/agora/package-lock.json +6068 -0
  29. package/templates/layers/agora/package.json +54 -0
  30. package/templates/layers/agora/pnpm-lock.yaml +3816 -0
  31. package/templates/layers/agora/tsconfig.json +16 -0
  32. package/templates/layers/agora/vitest.config.ts +4 -0
  33. package/templates/layers/agora/web/App.tsx +174 -0
  34. package/templates/layers/agora/web/agora-session.test.ts +70 -0
  35. package/templates/layers/agora/web/agora-session.ts +267 -0
  36. package/templates/layers/agora/worker/agora.test.ts +60 -0
  37. package/templates/layers/agora/worker/agora.ts +165 -0
  38. package/templates/layers/agora/worker/index.ts +50 -0
  39. package/templates/layers/catalog/DEPLOYMENT.md +3 -0
  40. package/templates/layers/catalog/spatius.config.json +1 -0
  41. package/templates/layers/node/Dockerfile +12 -0
  42. package/templates/layers/node/dockerignore +10 -0
  43. package/templates/layers/node/package-lock.json +5569 -0
  44. package/templates/layers/node/pnpm-lock.yaml +3470 -0
  45. package/templates/layers/node/railway.json +8 -0
  46. package/templates/layers/node/scripts/dev-service.mjs +14 -0
  47. package/templates/layers/node/scripts/dev-web.mjs +22 -0
  48. package/templates/layers/node/server/index.ts +92 -0
  49. package/templates/layers/node/vite.config.ts +17 -0
  50. package/templates/layers/railway-agent/agent/railway.json +5 -0
  51. package/dist/cli.js.map +0 -1
  52. package/dist/templates.js.map +0 -1
package/dist/cli.js CHANGED
@@ -40,6 +40,117 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
40
40
  mod
41
41
  ));
42
42
 
43
+ // src/errors.ts
44
+ function normalizeError(error) {
45
+ if (error instanceof CliError) {
46
+ return error;
47
+ }
48
+ if (error instanceof Error && error.message.trim() !== "") {
49
+ return new CliError("UNKNOWN_ERROR", error.message);
50
+ }
51
+ return new CliError(
52
+ "UNKNOWN_ERROR",
53
+ "An unexpected error occurred while creating the project."
54
+ );
55
+ }
56
+ var EXIT_CODES, CliError, PromptCancelledError;
57
+ var init_errors = __esm({
58
+ "src/errors.ts"() {
59
+ "use strict";
60
+ EXIT_CODES = {
61
+ cancelled: 130,
62
+ filesystem: 4,
63
+ general: 1,
64
+ invalidArgument: 2,
65
+ success: 0,
66
+ targetConflict: 3
67
+ };
68
+ CliError = class extends Error {
69
+ code;
70
+ exitCode;
71
+ path;
72
+ recovery;
73
+ constructor(code, message, { exitCode = EXIT_CODES.general, path: path2, recovery } = {}) {
74
+ super(message);
75
+ this.name = "CliError";
76
+ this.code = code;
77
+ this.exitCode = exitCode;
78
+ this.path = path2;
79
+ this.recovery = recovery;
80
+ }
81
+ };
82
+ PromptCancelledError = class extends CliError {
83
+ constructor(message = "Project creation was cancelled.") {
84
+ super("CANCELLED", message, {
85
+ exitCode: EXIT_CODES.cancelled,
86
+ recovery: "Run the command again when you are ready."
87
+ });
88
+ this.name = "PromptCancelledError";
89
+ }
90
+ };
91
+ }
92
+ });
93
+
94
+ // src/catalog.ts
95
+ function validateSelection(stack) {
96
+ if (!Object.hasOwn(stacks, stack)) {
97
+ throw new CliError("INVALID_ARGUMENT", `Unsupported stack: ${stack}`, {
98
+ exitCode: EXIT_CODES.invalidArgument
99
+ });
100
+ }
101
+ return { stack };
102
+ }
103
+ async function selectCatalog(options) {
104
+ if (options.stack !== void 0) return validateSelection(options.stack);
105
+ const choices = Object.entries(stacks).map(([value, stack]) => ({
106
+ value,
107
+ label: stack.label
108
+ }));
109
+ return validateSelection(
110
+ options.interactive ? await options.prompts.choose("Which stack?", choices, DEFAULT_STACK) : DEFAULT_STACK
111
+ );
112
+ }
113
+ var stacks, DEFAULT_STACK;
114
+ var init_catalog = __esm({
115
+ "src/catalog.ts"() {
116
+ "use strict";
117
+ init_errors();
118
+ stacks = {
119
+ "cloudflare-livekit": {
120
+ label: "Cloudflare + LiveKit Cloud Agents",
121
+ web: "cloudflare",
122
+ agent: "livekit",
123
+ provider: "livekit"
124
+ },
125
+ "cloudflare-livekit-railway": {
126
+ label: "Cloudflare + LiveKit Agents on Railway",
127
+ web: "cloudflare",
128
+ agent: "railway",
129
+ provider: "livekit"
130
+ },
131
+ "railway-livekit-cloud": {
132
+ label: "Railway + LiveKit Cloud Agents",
133
+ web: "railway",
134
+ agent: "livekit",
135
+ provider: "livekit"
136
+ },
137
+ "railway-livekit": {
138
+ label: "Railway + LiveKit Agents on Railway",
139
+ web: "railway",
140
+ agent: "railway",
141
+ provider: "livekit"
142
+ },
143
+ "zeabur-agora": {
144
+ label: "Zeabur + Agora Conversational AI",
145
+ web: "zeabur",
146
+ agent: "agora",
147
+ provider: "agora"
148
+ }
149
+ };
150
+ DEFAULT_STACK = "cloudflare-livekit";
151
+ }
152
+ });
153
+
43
154
  // node_modules/.pnpm/sisteransi@1.0.5/node_modules/sisteransi/src/index.js
44
155
  var require_src = __commonJS({
45
156
  "node_modules/.pnpm/sisteransi@1.0.5/node_modules/sisteransi/src/index.js"(exports, module) {
@@ -123,7 +234,7 @@ var require_dist = __commonJS({
123
234
  determineAgent: () => determineAgent2
124
235
  });
125
236
  module.exports = __toCommonJS(src_exports);
126
- var import_promises9 = __require("fs/promises");
237
+ var import_promises13 = __require("fs/promises");
127
238
  var import_node_fs4 = __require("fs");
128
239
  var DEVIN_LOCAL_PATH = "/opt/.devin";
129
240
  var CURSOR = "cursor";
@@ -211,7 +322,7 @@ var require_dist = __commonJS({
211
322
  return { isAgent: true, agent: { name: GITHUB_COPILOT } };
212
323
  }
213
324
  try {
214
- await (0, import_promises9.access)(DEVIN_LOCAL_PATH, import_node_fs4.constants.F_OK);
325
+ await (0, import_promises13.access)(DEVIN_LOCAL_PATH, import_node_fs4.constants.F_OK);
215
326
  return { isAgent: true, agent: { name: DEVIN } };
216
327
  } catch (_error) {
217
328
  }
@@ -220,57 +331,6 @@ var require_dist = __commonJS({
220
331
  }
221
332
  });
222
333
 
223
- // src/errors.ts
224
- function normalizeError(error) {
225
- if (error instanceof CliError) {
226
- return error;
227
- }
228
- if (error instanceof Error && error.message.trim() !== "") {
229
- return new CliError("UNKNOWN_ERROR", error.message);
230
- }
231
- return new CliError(
232
- "UNKNOWN_ERROR",
233
- "An unexpected error occurred while creating the project."
234
- );
235
- }
236
- var EXIT_CODES, CliError, PromptCancelledError;
237
- var init_errors = __esm({
238
- "src/errors.ts"() {
239
- "use strict";
240
- EXIT_CODES = {
241
- cancelled: 130,
242
- filesystem: 4,
243
- general: 1,
244
- invalidArgument: 2,
245
- success: 0,
246
- targetConflict: 3
247
- };
248
- CliError = class extends Error {
249
- code;
250
- exitCode;
251
- path;
252
- recovery;
253
- constructor(code, message, { exitCode = EXIT_CODES.general, path: path2, recovery } = {}) {
254
- super(message);
255
- this.name = "CliError";
256
- this.code = code;
257
- this.exitCode = exitCode;
258
- this.path = path2;
259
- this.recovery = recovery;
260
- }
261
- };
262
- PromptCancelledError = class extends CliError {
263
- constructor(message = "Project creation was cancelled.") {
264
- super("CANCELLED", message, {
265
- exitCode: EXIT_CODES.cancelled,
266
- recovery: "Run the command again when you are ready."
267
- });
268
- this.name = "PromptCancelledError";
269
- }
270
- };
271
- }
272
- });
273
-
274
334
  // src/package-managers.ts
275
335
  import { execFile, spawn } from "child_process";
276
336
  function requiresWindowsCommandShell(command, platform = process.platform) {
@@ -531,7 +591,7 @@ function selectPythonPackageManager(inventory, explicit, platform = process.plat
531
591
  }
532
592
  function assertPackageManagersAvailable(packageManagers) {
533
593
  const unavailable = [packageManagers.javascript, packageManagers.python].find(
534
- (manager) => !manager.available
594
+ (manager) => manager !== void 0 && !manager.available
535
595
  );
536
596
  if (unavailable !== void 0) {
537
597
  throw new CliError(
@@ -571,24 +631,74 @@ var init_package_managers = __esm({
571
631
  }
572
632
  });
573
633
 
574
- // src/setup/project.ts
575
- import { lstat } from "fs/promises";
634
+ // src/project-config.ts
635
+ import { lstat, readFile } from "fs/promises";
576
636
  import { join as join2 } from "path";
637
+ async function readProjectConfig(directory) {
638
+ const path2 = join2(directory, "spatius.config.json");
639
+ try {
640
+ const stats = await lstat(path2);
641
+ if (!stats.isFile() || stats.isSymbolicLink())
642
+ throw new Error("Expected a regular file");
643
+ const value = JSON.parse(await readFile(path2, "utf8"));
644
+ if (value && typeof value === "object" && "version" in value && value.version !== 2)
645
+ throw new CliError(
646
+ "INVALID_ARGUMENT",
647
+ "Unsupported spatius.config.json version. Expected version 2.",
648
+ { path: path2, exitCode: EXIT_CODES.invalidArgument }
649
+ );
650
+ if (!value || typeof value !== "object" || !("version" in value) || value.version !== 2 || !("stack" in value) || typeof value.stack !== "string")
651
+ throw new Error("Unsupported project configuration");
652
+ return { version: 2, ...validateSelection(value.stack) };
653
+ } catch (error) {
654
+ if (error instanceof CliError) throw error;
655
+ if (error instanceof Error && "code" in error && error.code === "ENOENT")
656
+ return void 0;
657
+ throw new CliError("INVALID_ARGUMENT", "Invalid spatius.config.json.", {
658
+ path: path2,
659
+ exitCode: EXIT_CODES.invalidArgument
660
+ });
661
+ }
662
+ }
663
+ async function webEnvironmentPath(directory) {
664
+ const config = await readProjectConfig(directory);
665
+ return config && !config.stack.startsWith("cloudflare") ? ".env.local" : ".dev.vars";
666
+ }
667
+ var init_project_config = __esm({
668
+ "src/project-config.ts"() {
669
+ "use strict";
670
+ init_catalog();
671
+ init_errors();
672
+ }
673
+ });
674
+
675
+ // src/setup/project.ts
676
+ import { lstat as lstat2 } from "fs/promises";
677
+ import { join as join4 } from "path";
577
678
  async function assertSpatiusProject(targetDirectory) {
578
679
  try {
579
- const target = await lstat(targetDirectory);
680
+ const target = await lstat2(targetDirectory);
580
681
  if (!target.isDirectory() || target.isSymbolicLink()) {
581
682
  throw new Error("not a regular directory");
582
683
  }
684
+ const config = await readProjectConfig(targetDirectory);
685
+ const paths = config ? [
686
+ "package.json",
687
+ "spatius.config.json",
688
+ await webEnvironmentPath(targetDirectory) + ".example",
689
+ "worker/index.ts",
690
+ "agent/src/agent.py",
691
+ "agent/.env.example"
692
+ ] : requiredProjectPaths;
583
693
  await Promise.all([
584
694
  ...requiredProjectDirectories.map(async (relativePath) => {
585
- const stats = await lstat(join2(targetDirectory, relativePath));
695
+ const stats = await lstat2(join4(targetDirectory, relativePath));
586
696
  if (!stats.isDirectory() || stats.isSymbolicLink()) {
587
697
  throw new Error(`${relativePath} is not a regular directory`);
588
698
  }
589
699
  }),
590
- ...requiredProjectPaths.map(async (relativePath) => {
591
- const stats = await lstat(join2(targetDirectory, relativePath));
700
+ ...paths.map(async (relativePath) => {
701
+ const stats = await lstat2(join4(targetDirectory, relativePath));
592
702
  if (!stats.isFile() || stats.isSymbolicLink()) {
593
703
  throw new Error(`${relativePath} is not a regular file`);
594
704
  }
@@ -610,6 +720,7 @@ var requiredProjectPaths, requiredProjectDirectories;
610
720
  var init_project = __esm({
611
721
  "src/setup/project.ts"() {
612
722
  "use strict";
723
+ init_project_config();
613
724
  init_errors();
614
725
  requiredProjectPaths = [
615
726
  "package.json",
@@ -648,14 +759,14 @@ var init_voices = __esm({
648
759
  // src/setup/environment.ts
649
760
  import {
650
761
  chmod,
651
- lstat as lstat2,
652
- readFile as readFile2,
762
+ lstat as lstat3,
763
+ readFile as readFile3,
653
764
  rename,
654
765
  rm,
655
766
  writeFile as writeFile2
656
767
  } from "fs/promises";
657
768
  import { randomUUID as randomUUID2 } from "crypto";
658
- import { join as join4 } from "path";
769
+ import { join as join5 } from "path";
659
770
  function parseQuotedValue(rawValue) {
660
771
  const value = rawValue.trim();
661
772
  if (value.startsWith('"')) {
@@ -810,7 +921,7 @@ function isMissingFile(error) {
810
921
  }
811
922
  async function readOptionalCredentialFile(path2) {
812
923
  try {
813
- const stats = await lstat2(path2);
924
+ const stats = await lstat3(path2);
814
925
  if (stats.isSymbolicLink() || !stats.isFile()) {
815
926
  throw new CliError(
816
927
  "FILESYSTEM_ERROR",
@@ -822,7 +933,7 @@ async function readOptionalCredentialFile(path2) {
822
933
  }
823
934
  );
824
935
  }
825
- return await readFile2(path2, "utf8");
936
+ return await readFile3(path2, "utf8");
826
937
  } catch (error) {
827
938
  if (isMissingFile(error)) {
828
939
  return void 0;
@@ -831,10 +942,11 @@ async function readOptionalCredentialFile(path2) {
831
942
  }
832
943
  }
833
944
  async function readCredentialFileState(targetDirectory) {
945
+ const webPath = await webEnvironmentPath(targetDirectory);
834
946
  const [worker, agent, workerExample] = await Promise.all([
835
- readOptionalCredentialFile(join4(targetDirectory, ".dev.vars")),
836
- readOptionalCredentialFile(join4(targetDirectory, "agent", ".env.local")),
837
- readOptionalCredentialFile(join4(targetDirectory, ".dev.vars.example"))
947
+ readOptionalCredentialFile(join5(targetDirectory, webPath)),
948
+ readOptionalCredentialFile(join5(targetDirectory, "agent", ".env.local")),
949
+ readOptionalCredentialFile(join5(targetDirectory, webPath + ".example"))
838
950
  ]);
839
951
  return {
840
952
  agent,
@@ -906,23 +1018,24 @@ async function pathExists(path2, fileSystem) {
906
1018
  }
907
1019
  }
908
1020
  async function writeCredentialFilesAtomically(targetDirectory, contents, fileSystem = defaultAtomicFileSystem) {
1021
+ const webPath = await webEnvironmentPath(targetDirectory);
909
1022
  const nonce = randomUUID2();
910
1023
  const targets = [
911
1024
  {
912
1025
  backupMoved: false,
913
- backupPath: join4(targetDirectory, `.dev.vars.${nonce}.backup`),
914
- destinationPath: join4(targetDirectory, ".dev.vars"),
1026
+ backupPath: join5(targetDirectory, `.dev.vars.${nonce}.backup`),
1027
+ destinationPath: join5(targetDirectory, webPath),
915
1028
  hadOriginal: false,
916
1029
  installed: false,
917
- stagedPath: join4(targetDirectory, `.dev.vars.${nonce}.tmp`)
1030
+ stagedPath: join5(targetDirectory, `.dev.vars.${nonce}.tmp`)
918
1031
  },
919
1032
  {
920
1033
  backupMoved: false,
921
- backupPath: join4(targetDirectory, "agent", `.env.local.${nonce}.backup`),
922
- destinationPath: join4(targetDirectory, "agent", ".env.local"),
1034
+ backupPath: join5(targetDirectory, "agent", `.env.local.${nonce}.backup`),
1035
+ destinationPath: join5(targetDirectory, "agent", ".env.local"),
923
1036
  hadOriginal: false,
924
1037
  installed: false,
925
- stagedPath: join4(targetDirectory, "agent", `.env.local.${nonce}.tmp`)
1038
+ stagedPath: join5(targetDirectory, "agent", `.env.local.${nonce}.tmp`)
926
1039
  }
927
1040
  ];
928
1041
  const payloads = [contents.worker, contents.agent];
@@ -983,9 +1096,10 @@ async function writeCredentialFilesAtomically(targetDirectory, contents, fileSys
983
1096
  }
984
1097
  }
985
1098
  async function readCredentialExamples(targetDirectory) {
1099
+ const webPath = await webEnvironmentPath(targetDirectory);
986
1100
  const [worker, agent] = await Promise.all([
987
- readFile2(join4(targetDirectory, ".dev.vars.example"), "utf8"),
988
- readFile2(join4(targetDirectory, "agent", ".env.example"), "utf8")
1101
+ readFile3(join5(targetDirectory, webPath + ".example"), "utf8"),
1102
+ readFile3(join5(targetDirectory, "agent", ".env.example"), "utf8")
989
1103
  ]);
990
1104
  return { agent, worker };
991
1105
  }
@@ -993,6 +1107,7 @@ var WORKER_MANAGED_KEYS, AGENT_MANAGED_KEYS, assignmentPattern, defaultAtomicFil
993
1107
  var init_environment = __esm({
994
1108
  "src/setup/environment.ts"() {
995
1109
  "use strict";
1110
+ init_project_config();
996
1111
  init_errors();
997
1112
  WORKER_MANAGED_KEYS = [
998
1113
  "LIVEKIT_URL",
@@ -1012,8 +1127,8 @@ var init_environment = __esm({
1012
1127
  assignmentPattern = /^\s*(?:export\s+)?([A-Za-z_][A-Za-z\d_]*)\s*=\s*(.*)$/u;
1013
1128
  defaultAtomicFileSystem = {
1014
1129
  chmod,
1015
- lstat: lstat2,
1016
- readFile: (path2) => readFile2(path2),
1130
+ lstat: lstat3,
1131
+ readFile: (path2) => readFile3(path2),
1017
1132
  rename,
1018
1133
  rm: (path2) => rm(path2, { force: true }),
1019
1134
  writeFile: writeFile2
@@ -8351,12 +8466,12 @@ var require_dist2 = __commonJS({
8351
8466
  // src/setup/livekit.ts
8352
8467
  import { createHmac } from "crypto";
8353
8468
  import { spawn as spawn2 } from "child_process";
8354
- import { chmod as chmod2, mkdtemp, readFile as readFile3, rm as rm2, writeFile as writeFile3 } from "fs/promises";
8469
+ import { chmod as chmod2, mkdtemp, readFile as readFile4, rm as rm2, writeFile as writeFile3 } from "fs/promises";
8355
8470
  import { homedir, tmpdir } from "os";
8356
- import { join as join5 } from "path";
8357
- async function readLiveKitProjects(configPath = join5(homedir(), ".livekit", "cli-config.yaml")) {
8471
+ import { join as join6 } from "path";
8472
+ async function readLiveKitProjects(configPath = join6(homedir(), ".livekit", "cli-config.yaml")) {
8358
8473
  try {
8359
- const config = (0, import_yaml.parse)(await readFile3(configPath, "utf8"));
8474
+ const config = (0, import_yaml.parse)(await readFile4(configPath, "utf8"));
8360
8475
  if (config == null) return [];
8361
8476
  if (typeof config !== "object" || !("projects" in config) || !Array.isArray(config.projects))
8362
8477
  throw new Error();
@@ -8519,12 +8634,12 @@ async function loadLiveKitCredentialsWithCli({
8519
8634
  projectName
8520
8635
  } = {}) {
8521
8636
  const directory = await fileSystem.mkdtemp(
8522
- join5(temporaryRoot, "create-spatius-app-livekit-")
8637
+ join6(temporaryRoot, "create-spatius-app-livekit-")
8523
8638
  );
8524
8639
  try {
8525
8640
  await fileSystem.chmod(directory, 448);
8526
8641
  await fileSystem.writeFile(
8527
- join5(directory, ".env.example"),
8642
+ join6(directory, ".env.example"),
8528
8643
  "LIVEKIT_URL=\nLIVEKIT_API_KEY=\nLIVEKIT_API_SECRET=\n",
8529
8644
  384
8530
8645
  );
@@ -8543,7 +8658,7 @@ async function loadLiveKitCredentialsWithCli({
8543
8658
  }
8544
8659
  );
8545
8660
  const values = parseDotenv(
8546
- await fileSystem.readFile(join5(directory, ".env.local"))
8661
+ await fileSystem.readFile(join6(directory, ".env.local"))
8547
8662
  );
8548
8663
  const validated = validateCredentialBundle({
8549
8664
  liveKit: {
@@ -8643,7 +8758,7 @@ var init_livekit = __esm({
8643
8758
  defaultTemporaryFileSystem = {
8644
8759
  chmod: chmod2,
8645
8760
  mkdtemp,
8646
- readFile: (path2) => readFile3(path2, "utf8"),
8761
+ readFile: (path2) => readFile4(path2, "utf8"),
8647
8762
  remove: (path2) => rm2(path2, {
8648
8763
  force: true,
8649
8764
  maxRetries: 3,
@@ -10094,7 +10209,7 @@ async function runCredentialSetup({
10094
10209
  const examples = await readExamples(targetDirectory);
10095
10210
  const rendered = buildCredentialFileContents(state, examples, credentials);
10096
10211
  await writeFiles(targetDirectory, rendered);
10097
- onStatus("Saved .dev.vars and agent/.env.local for local development only.");
10212
+ onStatus("Saved local web and agent credentials for local development only.");
10098
10213
  return "configured";
10099
10214
  }
10100
10215
  var init_wizard = __esm({
@@ -10113,6 +10228,9 @@ var init_wizard = __esm({
10113
10228
  }
10114
10229
  });
10115
10230
 
10231
+ // src/cli.ts
10232
+ init_catalog();
10233
+
10116
10234
  // node_modules/.pnpm/@clack+core@1.4.3/node_modules/@clack/core/dist/index.mjs
10117
10235
  import { styleText } from "util";
10118
10236
  import { stdout, stdin } from "process";
@@ -14699,7 +14817,7 @@ var program = new Command();
14699
14817
 
14700
14818
  // src/cli.ts
14701
14819
  init_errors();
14702
- import { readFile as readFile4 } from "fs/promises";
14820
+ import { readFile as readFile8 } from "fs/promises";
14703
14821
  import process5 from "process";
14704
14822
  import { fileURLToPath as fileURLToPath2 } from "url";
14705
14823
 
@@ -14744,18 +14862,18 @@ init_package_managers();
14744
14862
  import { spawn as spawn3 } from "child_process";
14745
14863
 
14746
14864
  // src/templates.ts
14747
- init_errors();
14748
- import { fileURLToPath } from "url";
14865
+ init_project_config();
14866
+ init_catalog();
14749
14867
 
14750
- // src/templates/cloudflare-livekit/index.ts
14751
- init_errors();
14868
+ // src/templates/agora.ts
14752
14869
  init_package_managers();
14753
- init_project();
14870
+ import { readFile as readFile6, writeFile as writeFile5 } from "fs/promises";
14871
+ import { basename as basename3, join as join8 } from "path";
14754
14872
 
14755
14873
  // src/templates/cloudflare-livekit/configure.ts
14756
14874
  init_package_managers();
14757
14875
  import { randomUUID } from "crypto";
14758
- import { readFile, writeFile } from "fs/promises";
14876
+ import { access, readFile as readFile2, writeFile } from "fs/promises";
14759
14877
  import { basename, join as join3 } from "path";
14760
14878
  function toValidPackageName(value) {
14761
14879
  const normalized = value.trim().toLowerCase().replace(/\s+/gu, "-").replace(/^[._-]+/u, "").replace(/[^a-z\d~._-]+/gu, "-").replace(/-+/gu, "-").slice(0, 214).replace(/-$/u, "");
@@ -14771,6 +14889,7 @@ function npmRequiresScriptAllowlist(version) {
14771
14889
  function createReplacements(packageManagers, platform) {
14772
14890
  const javascript = packageManagers.javascript;
14773
14891
  const python = packageManagers.python;
14892
+ if (!python) throw new Error("LiveKit templates require Python tooling.");
14774
14893
  return /* @__PURE__ */ new Map([
14775
14894
  [
14776
14895
  "{{SPATIUS_JAVASCRIPT_PACKAGE_MANAGER}}",
@@ -14842,7 +14961,7 @@ function createReplacements(packageManagers, platform) {
14842
14961
  ]);
14843
14962
  }
14844
14963
  async function renderFile(path2, replacements) {
14845
- let contents = await readFile(path2, "utf8");
14964
+ let contents = await readFile2(path2, "utf8");
14846
14965
  for (const [token, replacement] of replacements) {
14847
14966
  contents = contents.replaceAll(token, replacement);
14848
14967
  }
@@ -14854,7 +14973,7 @@ async function configureGeneratedTemplate(targetDirectory, {
14854
14973
  projectName = basename(targetDirectory)
14855
14974
  }) {
14856
14975
  const packagePath = join3(targetDirectory, "package.json");
14857
- const packageContents = await readFile(packagePath, "utf8");
14976
+ const packageContents = await readFile2(packagePath, "utf8");
14858
14977
  const metadata = JSON.parse(packageContents);
14859
14978
  const javascript = packageManagers.javascript;
14860
14979
  const run = (script) => javascriptRunCommand(javascript.name, script);
@@ -14893,7 +15012,7 @@ async function configureGeneratedTemplate(targetDirectory, {
14893
15012
  if (javascript.name === "npm") {
14894
15013
  const packageLockPath = join3(targetDirectory, "package-lock.json");
14895
15014
  const packageLock = JSON.parse(
14896
- await readFile(packageLockPath, "utf8")
15015
+ await readFile2(packageLockPath, "utf8")
14897
15016
  );
14898
15017
  packageLock.name = metadata.name;
14899
15018
  if (packageLock.packages?.[""] !== void 0) {
@@ -14912,12 +15031,18 @@ async function configureGeneratedTemplate(targetDirectory, {
14912
15031
  await Promise.all(
14913
15032
  [
14914
15033
  ".dev.vars.example",
15034
+ ".env.local.example",
14915
15035
  "wrangler.jsonc",
14916
15036
  "agent/src/agent.py",
14917
15037
  "agent/README.md",
14918
15038
  "worker/index.test.ts"
14919
15039
  ].map(
14920
- async (relativePath) => renderFile(join3(targetDirectory, relativePath), agentReplacements)
15040
+ async (relativePath) => access(join3(targetDirectory, relativePath)).then(
15041
+ () => renderFile(join3(targetDirectory, relativePath), agentReplacements)
15042
+ ).catch((error) => {
15043
+ if (!(error instanceof Error && "code" in error && error.code === "ENOENT"))
15044
+ throw error;
15045
+ })
14921
15046
  )
14922
15047
  );
14923
15048
  await Promise.all(
@@ -14952,6 +15077,7 @@ function createInstallPlan(targetDirectory, packageManagers, platform = process.
14952
15077
  `Installing JavaScript dependencies with ${javascript.name}`
14953
15078
  )
14954
15079
  ];
15080
+ if (!python) return steps;
14955
15081
  if (python.name === "uv") {
14956
15082
  steps.push(
14957
15083
  commandStep(
@@ -14990,6 +15116,9 @@ function createInstallPlan(targetDirectory, packageManagers, platform = process.
14990
15116
  }
14991
15117
 
14992
15118
  // src/templates/cloudflare-livekit/index.ts
15119
+ init_errors();
15120
+ init_package_managers();
15121
+ init_project();
14993
15122
  var credentialSetupStep = {
14994
15123
  command: "npx create-spatius-app setup . --interactive",
14995
15124
  reason: "Connect LiveKit and Spatius accounts and select an avatar and voice in a secure interactive terminal. Never paste secrets into chat.",
@@ -15018,7 +15147,7 @@ var cloudflareLivekitTemplate = {
15018
15147
  ],
15019
15148
  includeFile(path2, configuration) {
15020
15149
  const javascript = configuration?.packageManagers.javascript.name ?? "pnpm";
15021
- const python = configuration?.packageManagers.python.name ?? "uv";
15150
+ const python = configuration?.packageManagers.python?.name ?? "uv";
15022
15151
  if (path2 === "package-lock.json") return javascript === "npm";
15023
15152
  if (["npmrc", "pnpm-lock.yaml", "pnpm-workspace.yaml"].includes(path2)) {
15024
15153
  return javascript === "pnpm";
@@ -15042,6 +15171,8 @@ var cloudflareLivekitTemplate = {
15042
15171
  platform,
15043
15172
  pythonPackageManager
15044
15173
  }) {
15174
+ if (!pythonPackageManager)
15175
+ throw new Error("LiveKit templates require Python tooling.");
15045
15176
  return [
15046
15177
  ...dependenciesInstalled ? [] : [
15047
15178
  javascriptInstallCommand(javascriptPackageManager),
@@ -15074,16 +15205,409 @@ var cloudflareLivekitTemplate = {
15074
15205
  ]
15075
15206
  };
15076
15207
 
15208
+ // src/setup/agora.ts
15209
+ init_environment();
15210
+ init_errors();
15211
+ init_project_config();
15212
+ import { lstat as lstat4, readFile as readFile5, rename as rename2, rm as rm3, writeFile as writeFile4 } from "fs/promises";
15213
+ import { join as join7 } from "path";
15214
+ import { randomUUID as randomUUID3 } from "crypto";
15215
+ var runAgoraSetup = async ({
15216
+ targetDirectory,
15217
+ prompts,
15218
+ onStatus = () => void 0
15219
+ }) => {
15220
+ const target = await lstat4(targetDirectory);
15221
+ if (!target.isDirectory() || target.isSymbolicLink())
15222
+ throw new CliError(
15223
+ "INVALID_ARGUMENT",
15224
+ "Expected a regular project directory.",
15225
+ { exitCode: EXIT_CODES.invalidArgument }
15226
+ );
15227
+ const config = await readProjectConfig(targetDirectory);
15228
+ if (config?.stack !== "zeabur-agora")
15229
+ throw new CliError(
15230
+ "INVALID_ARGUMENT",
15231
+ "Expected a Zeabur + Agora project.",
15232
+ { exitCode: EXIT_CODES.invalidArgument }
15233
+ );
15234
+ for (const name of [
15235
+ "package.json",
15236
+ ".env.local.example",
15237
+ "worker/agora.ts"
15238
+ ]) {
15239
+ const stats = await lstat4(join7(targetDirectory, name));
15240
+ if (!stats.isFile() || stats.isSymbolicLink())
15241
+ throw new CliError(
15242
+ "INVALID_ARGUMENT",
15243
+ "Expected regular project files.",
15244
+ { exitCode: EXIT_CODES.invalidArgument }
15245
+ );
15246
+ }
15247
+ const path2 = join7(targetDirectory, ".env.local");
15248
+ let existing;
15249
+ try {
15250
+ const stats = await lstat4(path2);
15251
+ if (!stats.isFile() || stats.isSymbolicLink())
15252
+ throw new Error("Unsafe credential path");
15253
+ existing = await readFile5(path2, "utf8");
15254
+ } catch (error) {
15255
+ if (!(error instanceof Error && "code" in error && error.code === "ENOENT"))
15256
+ throw new CliError("FILESYSTEM_ERROR", "Cannot safely read .env.local.", {
15257
+ exitCode: EXIT_CODES.filesystem,
15258
+ path: path2
15259
+ });
15260
+ }
15261
+ if (existing && !await prompts.confirm(
15262
+ "Replace existing Agora and Spatius credentials? Other variables are preserved.",
15263
+ false
15264
+ ))
15265
+ return "unchanged";
15266
+ const values = {};
15267
+ const previous = parseDotenv(existing ?? "");
15268
+ const secretKeys = ["AGORA_APP_CERTIFICATE", "SPATIUS_API_KEY"];
15269
+ for (const key of [
15270
+ "AGORA_APP_ID",
15271
+ "AGORA_APP_CERTIFICATE",
15272
+ "AGORA_PIPELINE_ID",
15273
+ "SPATIUS_APP_ID",
15274
+ "SPATIUS_API_KEY",
15275
+ "SPATIUS_AVATAR_ID",
15276
+ "SPATIUS_REGION",
15277
+ "AGORA_AVATAR_SAMPLE_RATE"
15278
+ ]) {
15279
+ const defaults = {
15280
+ SPATIUS_REGION: "cn-beijing",
15281
+ AGORA_AVATAR_SAMPLE_RATE: "24000"
15282
+ };
15283
+ while (true) {
15284
+ const value = secretKeys.includes(key) ? await prompts.password(key) : await prompts.input(key, {
15285
+ initialValue: previous.get(key) ?? defaults[key]
15286
+ });
15287
+ if (isPlaceholderValue(value) || new RegExp("\\p{Cc}", "u").test(value)) {
15288
+ onStatus("Enter a non-empty value without control characters.");
15289
+ continue;
15290
+ }
15291
+ if (["AGORA_APP_ID", "AGORA_APP_CERTIFICATE"].includes(key) && !/^[a-f\d]{32}$/iu.test(value)) {
15292
+ onStatus(
15293
+ "Agora App ID and certificate must contain 32 hexadecimal characters."
15294
+ );
15295
+ continue;
15296
+ }
15297
+ if (key === "AGORA_AVATAR_SAMPLE_RATE" && ![8e3, 16e3, 22050, 24e3, 32e3, 44100, 48e3].includes(
15298
+ Number(value)
15299
+ )) {
15300
+ onStatus("Choose a supported audio sample rate.");
15301
+ continue;
15302
+ }
15303
+ values[key] = value;
15304
+ break;
15305
+ }
15306
+ }
15307
+ const rendered = mergeDotenv(
15308
+ existing ?? await readFile5(join7(targetDirectory, ".env.local.example"), "utf8"),
15309
+ values
15310
+ );
15311
+ const temporary = join7(targetDirectory, `.env.local.${randomUUID3()}.tmp`);
15312
+ try {
15313
+ await writeFile4(temporary, rendered, { flag: "wx", mode: 384 });
15314
+ await rename2(temporary, path2);
15315
+ } finally {
15316
+ await rm3(temporary, { force: true });
15317
+ }
15318
+ onStatus(
15319
+ "Saved .env.local. Configure an English assistant in the published Agora pipeline, with its TTS sample rate matching AGORA_AVATAR_SAMPLE_RATE. No cloud services were started."
15320
+ );
15321
+ return "configured";
15322
+ };
15323
+
15324
+ // src/templates/agora.ts
15325
+ init_project_config();
15326
+ var files = /* @__PURE__ */ new Set([
15327
+ "package.json",
15328
+ "package-lock.json",
15329
+ "pnpm-lock.yaml",
15330
+ "pnpm-workspace.yaml",
15331
+ "npmrc",
15332
+ "gitignore",
15333
+ "dockerignore",
15334
+ "Dockerfile",
15335
+ ".editorconfig",
15336
+ ".prettierrc.json",
15337
+ ".prettierignore",
15338
+ "README.md",
15339
+ "AGENTS.md",
15340
+ "DEPLOYMENT.md",
15341
+ "spatius.config.json",
15342
+ "index.html",
15343
+ "tsconfig.json",
15344
+ "vite.config.ts",
15345
+ "vitest.config.ts",
15346
+ "eslint.config.mjs",
15347
+ "env.example",
15348
+ "server/index.ts",
15349
+ "worker/index.ts",
15350
+ "worker/agora.ts",
15351
+ "worker/agora.test.ts",
15352
+ "worker/capability.ts",
15353
+ "worker/capability.test.ts",
15354
+ "web/App.tsx",
15355
+ "web/main.tsx",
15356
+ "web/styles.css",
15357
+ "web/agora-session.ts",
15358
+ "web/agora-session.test.ts",
15359
+ "web/components/transcript-view.tsx",
15360
+ "web/components/transcript-panel.tsx",
15361
+ "web/components/icons.tsx",
15362
+ "web/components/agents-ui/agent-chat-input.tsx",
15363
+ "web/components/agents-ui/LICENSE",
15364
+ "web/components/agents-ui/PROVENANCE.md"
15365
+ ]);
15366
+ var agoraTemplate = {
15367
+ id: "zeabur-agora",
15368
+ stack: "zeabur-agora",
15369
+ requiresPython: false,
15370
+ humanSteps: [
15371
+ {
15372
+ command: "npx create-spatius-app setup . --interactive",
15373
+ reason: "Enter Agora and Spatius credentials in a secure interactive terminal and configure a published English Agora pipeline. Never paste secrets into chat.",
15374
+ requiresHuman: true,
15375
+ requiresTty: true
15376
+ }
15377
+ ],
15378
+ directory: "templates/cloudflare-livekit",
15379
+ description: "Zeabur + Agora Conversational AI",
15380
+ components: [
15381
+ "- web/ React + Spatius AvatarKit + Agora",
15382
+ "- server/ Node HTTP API; Agora hosts the conversational agent"
15383
+ ],
15384
+ layers: [
15385
+ { directory: "templates/layers/catalog" },
15386
+ {
15387
+ directory: "templates/layers/node",
15388
+ overrides: ["vite.config.ts", "package-lock.json", "pnpm-lock.yaml"]
15389
+ },
15390
+ {
15391
+ directory: "templates/layers/agora",
15392
+ overrides: [
15393
+ "package.json",
15394
+ "package-lock.json",
15395
+ "pnpm-lock.yaml",
15396
+ "web/App.tsx",
15397
+ "worker/index.ts",
15398
+ "tsconfig.json",
15399
+ "vitest.config.ts",
15400
+ "README.md",
15401
+ "AGENTS.md"
15402
+ ]
15403
+ }
15404
+ ],
15405
+ includeFile(path2, configuration) {
15406
+ if (!cloudflareLivekitTemplate.includeFile(path2, configuration)) return false;
15407
+ return files.has(path2) || [...files].some((file) => file.startsWith(path2 + "/")) || path2 === "web/assets" || path2.startsWith("web/assets/");
15408
+ },
15409
+ mapFile(path2) {
15410
+ return path2 === "env.example" ? ".env.local.example" : cloudflareLivekitTemplate.mapFile(path2);
15411
+ },
15412
+ async configure(directory, configuration) {
15413
+ const manager = configuration.packageManagers.javascript;
15414
+ const run = (script) => javascriptRunCommand(manager.name, script);
15415
+ const path2 = join8(directory, "package.json");
15416
+ const pkg = JSON.parse(await readFile6(path2, "utf8"));
15417
+ pkg.name = toValidPackageName(
15418
+ configuration.projectName ?? basename3(directory)
15419
+ );
15420
+ if (manager.version && manager.version !== "unknown")
15421
+ pkg.packageManager = `${manager.name}@${manager.version}`;
15422
+ pkg.scripts.check = ["format:check", "lint", "typecheck", "test", "build"].map(run).join(" && ");
15423
+ if (manager.name === "bun") pkg.trustedDependencies = ["esbuild"];
15424
+ await writeFile5(path2, JSON.stringify(pkg, null, 2) + "\n");
15425
+ if (manager.name === "npm") {
15426
+ const lockPath = join8(directory, "package-lock.json");
15427
+ const lock = JSON.parse(await readFile6(lockPath, "utf8"));
15428
+ lock.name = pkg.name;
15429
+ lock.packages[""].name = pkg.name;
15430
+ await writeFile5(lockPath, JSON.stringify(lock, null, 2) + "\n");
15431
+ }
15432
+ await writeFile5(
15433
+ join8(directory, "spatius.config.json"),
15434
+ JSON.stringify({ version: 2, stack: "zeabur-agora" }, null, 2) + "\n"
15435
+ );
15436
+ const serverPath = join8(directory, "server/index.ts");
15437
+ await writeFile5(
15438
+ serverPath,
15439
+ (await readFile6(serverPath, "utf8")).replace(
15440
+ "import { handleRequest }",
15441
+ "import type { AgoraEnvironment } from '../worker/agora.js';\nimport { handleRequest }"
15442
+ ).replace(
15443
+ "const env = process.env;",
15444
+ "const env = process.env as unknown as AgoraEnvironment;"
15445
+ )
15446
+ );
15447
+ const dockerPath = join8(directory, "Dockerfile");
15448
+ const install = manager.name === "pnpm" ? "npm install --global pnpm@12.3.4 && pnpm install --frozen-lockfile" : manager.name === "bun" ? "npm install --global bun && bun install" : "npm ci";
15449
+ await writeFile5(
15450
+ dockerPath,
15451
+ (await readFile6(dockerPath, "utf8")).replace("__SPATIUS_CONTAINER_INSTALL__", install).replace("__SPATIUS_CONTAINER_BUILD__", run("build"))
15452
+ );
15453
+ for (const name of ["README.md", "AGENTS.md"]) {
15454
+ const path3 = join8(directory, name);
15455
+ await writeFile5(
15456
+ path3,
15457
+ (await readFile6(path3, "utf8")).replaceAll("__INSTALL__", javascriptInstallCommand(manager.name)).replaceAll("__DEV__", run("dev")).replaceAll("__CHECK__", run("check"))
15458
+ );
15459
+ }
15460
+ await writeFile5(
15461
+ join8(directory, "DEPLOYMENT.md"),
15462
+ "# Zeabur + Agora Conversational AI\n\nCreate one Zeabur service from this repository with the repository root as build root. Set ZBPACK_DOCKERFILE_PATH=Dockerfile and PORT=8787. Copy the environment variables from .env.local.example into Zeabur service variables, then generate an HTTPS domain. Use /api/health for the health check. Git redeployments rebuild the same service.\n\nThe Node service serves both frontend and API. There is no Python worker or database. Enable Agora Conversational AI and RTM, publish an English assistant pipeline, and set AGORA_PIPELINE_ID. The pipeline owns its models and voice. Match AGORA_AVATAR_SAMPLE_RATE to its TTS output.\n\nSession creation starts a hosted agent. Disconnect and initialization failures stop it through the backend. Page-close cleanup is best effort, with a 60-second idle timeout fallback. Sessions and client tokens last 30 minutes; start a fresh conversation afterward.\n\nLocal setup writes .env.local only. It never provisions Zeabur or uploads secrets.\n"
15463
+ );
15464
+ },
15465
+ createInstallPlan(directory, managers, platform) {
15466
+ return createInstallPlan(
15467
+ directory,
15468
+ { javascript: managers.javascript },
15469
+ platform
15470
+ );
15471
+ },
15472
+ nextSteps({
15473
+ dependenciesInstalled,
15474
+ credentialsConfigured,
15475
+ javascriptPackageManager
15476
+ }) {
15477
+ return [
15478
+ ...dependenciesInstalled ? [] : [javascriptInstallCommand(javascriptPackageManager)],
15479
+ ...credentialsConfigured ? [] : ["npx create-spatius-app setup . --interactive"],
15480
+ javascriptRunCommand(javascriptPackageManager, "dev")
15481
+ ];
15482
+ },
15483
+ setup: {
15484
+ recognizes: async (directory) => (await readProjectConfig(directory))?.stack === "zeabur-agora",
15485
+ run: runAgoraSetup
15486
+ },
15487
+ verification: [
15488
+ { javascript: "npm", scripts: ["check"] },
15489
+ { javascript: "pnpm", scripts: ["check"] }
15490
+ ]
15491
+ };
15492
+
15493
+ // src/templates/composed.ts
15494
+ init_catalog();
15495
+ import { readFile as readFile7, writeFile as writeFile6 } from "fs/promises";
15496
+ import { join as join9 } from "path";
15497
+ init_package_managers();
15498
+ function composeTemplate(stack) {
15499
+ if (stack === "zeabur-agora") return agoraTemplate;
15500
+ const selected = stacks[stack];
15501
+ const node = selected.web === "railway";
15502
+ return {
15503
+ ...cloudflareLivekitTemplate,
15504
+ id: stack,
15505
+ stack,
15506
+ requiresPython: true,
15507
+ description: selected.label,
15508
+ components: [
15509
+ "- web/ React + Spatius AvatarKit",
15510
+ `- ${node ? "server/" : "worker/"} ${selected.web} API`,
15511
+ "- agent/ Python LiveKit agent"
15512
+ ],
15513
+ layers: [
15514
+ { directory: "templates/layers/catalog" },
15515
+ ...selected.agent === "railway" ? [{ directory: "templates/layers/railway-agent" }] : [],
15516
+ ...node ? [
15517
+ {
15518
+ directory: "templates/layers/node",
15519
+ overrides: [
15520
+ "vite.config.ts",
15521
+ "scripts/dev-web.mjs",
15522
+ "package-lock.json",
15523
+ "pnpm-lock.yaml"
15524
+ ]
15525
+ }
15526
+ ] : []
15527
+ ],
15528
+ includeFile(path2, configuration) {
15529
+ if (node && ["wrangler.jsonc"].includes(path2)) return false;
15530
+ return cloudflareLivekitTemplate.includeFile(path2, configuration);
15531
+ },
15532
+ mapFile(path2) {
15533
+ return node && path2 === "dev.vars.example" ? ".env.local.example" : cloudflareLivekitTemplate.mapFile(path2);
15534
+ },
15535
+ async configure(directory, configuration) {
15536
+ await cloudflareLivekitTemplate.configure(directory, configuration);
15537
+ await writeFile6(
15538
+ join9(directory, "spatius.config.json"),
15539
+ JSON.stringify({ version: 2, stack }, null, 2) + "\n"
15540
+ );
15541
+ const packagePath = join9(directory, "package.json");
15542
+ const pkg = JSON.parse(await readFile7(packagePath, "utf8"));
15543
+ const manager = configuration.packageManagers.javascript.name;
15544
+ const run = (script) => javascriptRunCommand(manager, script);
15545
+ let deployment = `# ${selected.label}
15546
+
15547
+ `;
15548
+ if (node) {
15549
+ for (const name of ["README.md", "AGENTS.md", "agent/README.md"]) {
15550
+ const path2 = join9(directory, name);
15551
+ await writeFile6(
15552
+ path2,
15553
+ (await readFile7(path2, "utf8")).replaceAll("Cloudflare Workers", "Railway").replaceAll("Cloudflare Worker", "Node API").replaceAll(".dev.vars", ".env.local") + "\nSee DEPLOYMENT.md for this stack\u2019s deployment steps.\n"
15554
+ );
15555
+ }
15556
+ const workerConfigPath = join9(directory, "tsconfig.worker.json");
15557
+ await writeFile6(
15558
+ workerConfigPath,
15559
+ (await readFile7(workerConfigPath, "utf8")).replace(
15560
+ '"types": ["./worker-configuration.d.ts"]',
15561
+ '"types": ["./worker-configuration.d.ts", "node"]'
15562
+ ).replace(
15563
+ '"include": ["worker", "worker-configuration.d.ts"]',
15564
+ '"include": ["worker", "worker-configuration.d.ts", "server"]'
15565
+ )
15566
+ );
15567
+ delete pkg.devDependencies["@cloudflare/vite-plugin"];
15568
+ delete pkg.devDependencies.wrangler;
15569
+ pkg.dependencies.tsx = "4.23.13";
15570
+ delete pkg.scripts["cf-typegen"];
15571
+ delete pkg.scripts.wrangler;
15572
+ pkg.scripts.start = "node --env-file-if-exists=.env.local node_modules/tsx/dist/cli.mjs server/index.ts";
15573
+ pkg.scripts.deploy = "railway up";
15574
+ const dockerPath = join9(directory, "Dockerfile");
15575
+ const install = manager === "pnpm" ? "npm install --global pnpm@12.3.4 && pnpm install --frozen-lockfile" : manager === "bun" ? "npm install --global bun && bun install" : "npm ci";
15576
+ await writeFile6(
15577
+ dockerPath,
15578
+ (await readFile7(dockerPath, "utf8")).replace("__SPATIUS_CONTAINER_INSTALL__", install).replace("__SPATIUS_CONTAINER_BUILD__", run("build"))
15579
+ );
15580
+ deployment += "Create a Railway service from this repository with the repository root as its build root and railway.json as its configuration. Add the variables from .env.local.example in Railway. Generate a public domain. The web service serves both the frontend and /api. Deploy with `railway up`; subsequent Git deployments rebuild the service.\n\n";
15581
+ } else {
15582
+ deployment += `Run \`${run("wrangler -- login")}\`, set the non-secret configuration from .dev.vars.example in wrangler.jsonc, and upload LIVEKIT_API_KEY and LIVEKIT_API_SECRET using \`${run("wrangler -- secret put LIVEKIT_API_SECRET")}\` (repeat for the key). Run \`${run("deploy")}\`.
15583
+
15584
+ `;
15585
+ }
15586
+ if (selected.agent === "railway") {
15587
+ deployment += "Create a separate Railway agent service from this repository, with root directory /agent and configuration /agent/railway.json. Add the variables from agent/.env.example. Keep it running continuously; do not enable sleeping. No public domain is needed. Set PORT=8081 for the agent health check. Deploy this service independently of the web service.\n";
15588
+ } else
15589
+ deployment += "From agent/, run `lk cloud auth` and `lk agent create` for the first deployment. Configure the agent secrets from agent/.env.example. Use `lk agent deploy` for updates.\n";
15590
+ deployment += "\nDeploy the agent before opening conversations. Use separate LiveKit projects for development and production. Local setup never uploads secrets.\n";
15591
+ await writeFile6(join9(directory, "DEPLOYMENT.md"), deployment);
15592
+ await writeFile6(packagePath, JSON.stringify(pkg, null, 2) + "\n");
15593
+ }
15594
+ };
15595
+ }
15596
+
15597
+ // src/templates.ts
15598
+ init_errors();
15599
+ import { fileURLToPath } from "url";
15600
+
15077
15601
  // src/scaffold.ts
15078
15602
  init_errors();
15079
15603
  import { constants } from "fs";
15080
- import { copyFile, lstat as lstat4, mkdir, readdir as readdir2, rm as rm3 } from "fs/promises";
15081
- import { basename as basename3, dirname as dirname2, isAbsolute, join as join6, win32 as win322 } from "path";
15604
+ import { copyFile, lstat as lstat6, mkdir, readdir as readdir2, rm as rm4 } from "fs/promises";
15605
+ import { basename as basename4, dirname as dirname2, isAbsolute, join as join10, win32 as win322 } from "path";
15082
15606
 
15083
15607
  // src/project-directory.ts
15084
15608
  init_errors();
15085
15609
  import { homedir as homedir2 } from "os";
15086
- import { lstat as lstat3, readdir } from "fs/promises";
15610
+ import { lstat as lstat5, readdir } from "fs/promises";
15087
15611
  import { parse as parse2, resolve } from "path";
15088
15612
  function normalizeTargetDirectory(input, currentWorkingDirectory = process.cwd()) {
15089
15613
  return resolve(currentWorkingDirectory, input);
@@ -15115,7 +15639,7 @@ function assertSafeTargetDirectory(targetDirectory, homeDirectory = homedir2())
15115
15639
  }
15116
15640
  async function inspectTargetDirectory(targetDirectory) {
15117
15641
  try {
15118
- const stats = await lstat3(targetDirectory);
15642
+ const stats = await lstat5(targetDirectory);
15119
15643
  if (!stats.isDirectory() || stats.isSymbolicLink()) {
15120
15644
  return "not-a-directory";
15121
15645
  }
@@ -15187,10 +15711,10 @@ function assertRelativeDestination(path2) {
15187
15711
  }
15188
15712
  }
15189
15713
  async function listTemplateFiles(templateDirectory, template, configuration, relativeDirectory = "") {
15190
- const entries = await readdir2(join6(templateDirectory, relativeDirectory), {
15714
+ const entries = await readdir2(join10(templateDirectory, relativeDirectory), {
15191
15715
  withFileTypes: true
15192
15716
  });
15193
- const files = [];
15717
+ const files2 = [];
15194
15718
  for (const entry of entries) {
15195
15719
  if (shouldIgnoreTemplateEntry(entry.name)) continue;
15196
15720
  const source = [relativeDirectory, entry.name].filter(Boolean).join("/");
@@ -15199,7 +15723,7 @@ async function listTemplateFiles(templateDirectory, template, configuration, rel
15199
15723
  throw new Error("Bundled templates must not contain symbolic links.");
15200
15724
  }
15201
15725
  if (entry.isDirectory()) {
15202
- files.push(
15726
+ files2.push(
15203
15727
  ...await listTemplateFiles(
15204
15728
  templateDirectory,
15205
15729
  template,
@@ -15210,12 +15734,12 @@ async function listTemplateFiles(templateDirectory, template, configuration, rel
15210
15734
  } else if (entry.isFile()) {
15211
15735
  const destination = template.mapFile(source);
15212
15736
  assertRelativeDestination(destination);
15213
- files.push({ source, destination });
15737
+ files2.push({ source, destination });
15214
15738
  } else {
15215
15739
  throw new Error("Bundled templates must contain regular files.");
15216
15740
  }
15217
15741
  }
15218
- return files;
15742
+ return files2;
15219
15743
  }
15220
15744
  async function createScaffoldPlan({
15221
15745
  configuration,
@@ -15227,7 +15751,7 @@ async function createScaffoldPlan({
15227
15751
  const targetState = await assertTargetDirectoryAvailable(targetDirectory);
15228
15752
  let entries;
15229
15753
  try {
15230
- const root = await lstat4(templateDirectory);
15754
+ const root = await lstat6(templateDirectory);
15231
15755
  if (!root.isDirectory() || root.isSymbolicLink()) {
15232
15756
  throw new Error("The template must be a regular directory.");
15233
15757
  }
@@ -15236,6 +15760,33 @@ async function createScaffoldPlan({
15236
15760
  template,
15237
15761
  configuration
15238
15762
  );
15763
+ for (const layer of template.layers ?? []) {
15764
+ const directory = resolveTemplateDirectory({
15765
+ ...template,
15766
+ directory: layer.directory
15767
+ });
15768
+ const root2 = await lstat6(directory);
15769
+ if (!root2.isDirectory() || root2.isSymbolicLink())
15770
+ throw new Error("Invalid template layer.");
15771
+ const additions = await listTemplateFiles(
15772
+ directory,
15773
+ template,
15774
+ configuration
15775
+ );
15776
+ for (const addition of additions) {
15777
+ const index = entries.findIndex(
15778
+ (entry) => entry.destination === addition.destination
15779
+ );
15780
+ if (index >= 0) {
15781
+ if (!layer.overrides?.includes(addition.destination))
15782
+ throw new Error(
15783
+ `Undeclared template override: ${addition.destination}`
15784
+ );
15785
+ entries.splice(index, 1);
15786
+ }
15787
+ entries.push({ ...addition, directory });
15788
+ }
15789
+ }
15239
15790
  const destinations = /* @__PURE__ */ new Set();
15240
15791
  const directories = /* @__PURE__ */ new Map();
15241
15792
  for (const { destination } of entries) {
@@ -15294,11 +15845,11 @@ async function createScaffoldPlan({
15294
15845
  }
15295
15846
  async function rollback(targetDirectory, targetCreated, createdPaths) {
15296
15847
  if (targetCreated) {
15297
- await rm3(targetDirectory, { force: true, recursive: true });
15848
+ await rm4(targetDirectory, { force: true, recursive: true });
15298
15849
  return;
15299
15850
  }
15300
15851
  for (const path2 of [...createdPaths].reverse()) {
15301
- await rm3(path2, { force: true, recursive: true });
15852
+ await rm4(path2, { force: true, recursive: true });
15302
15853
  }
15303
15854
  }
15304
15855
  async function scaffoldProject({
@@ -15333,9 +15884,9 @@ async function scaffoldProject({
15333
15884
  targetCreated = true;
15334
15885
  }
15335
15886
  for (const entry of plan.entries) {
15336
- const source = join6(templateDirectory, entry.source);
15337
- const destination = join6(targetDirectory, entry.destination);
15338
- const stats = await lstat4(source);
15887
+ const source = join10(entry.directory ?? templateDirectory, entry.source);
15888
+ const destination = join10(targetDirectory, entry.destination);
15889
+ const stats = await lstat6(source);
15339
15890
  if (!stats.isFile() || stats.isSymbolicLink()) {
15340
15891
  throw new Error("A template source is no longer a regular file.");
15341
15892
  }
@@ -15352,7 +15903,7 @@ async function scaffoldProject({
15352
15903
  );
15353
15904
  throw new CliError(
15354
15905
  "FILESYSTEM_ERROR",
15355
- "Could not create " + basename3(targetDirectory) + ": " + (error instanceof Error ? error.message : "unknown filesystem error"),
15906
+ "Could not create " + basename4(targetDirectory) + ": " + (error instanceof Error ? error.message : "unknown filesystem error"),
15356
15907
  {
15357
15908
  exitCode: EXIT_CODES.filesystem,
15358
15909
  path: targetDirectory,
@@ -15366,16 +15917,26 @@ async function scaffoldProject({
15366
15917
  // src/templates.ts
15367
15918
  init_package_managers();
15368
15919
  var DEFAULT_TEMPLATE_ID = "cloudflare-livekit";
15369
- var templateRegistry = {
15370
- [DEFAULT_TEMPLATE_ID]: cloudflareLivekitTemplate
15371
- };
15920
+ var templateRegistry = Object.fromEntries(
15921
+ Object.keys(stacks).map((stack) => [
15922
+ stack,
15923
+ composeTemplate(stack)
15924
+ ])
15925
+ );
15372
15926
  function getTemplate(id = DEFAULT_TEMPLATE_ID) {
15373
- return templateRegistry[id];
15927
+ const template = templateRegistry[id];
15928
+ if (!template)
15929
+ throw new CliError("INVALID_ARGUMENT", `Unknown template: ${id}`, {
15930
+ exitCode: EXIT_CODES.invalidArgument
15931
+ });
15932
+ return template;
15374
15933
  }
15375
15934
  function resolveTemplateDirectory(template = getTemplate()) {
15376
15935
  return fileURLToPath(new URL(`../${template.directory}/`, import.meta.url));
15377
15936
  }
15378
- async function resolveProjectTemplate(targetDirectory, templates = Object.values(templateRegistry)) {
15937
+ async function resolveProjectTemplate(targetDirectory, templates = [getTemplate()]) {
15938
+ const config = await readProjectConfig(targetDirectory);
15939
+ if (config) return getTemplate(config.stack);
15379
15940
  const matches = [];
15380
15941
  for (const template of templates) {
15381
15942
  if (await template.setup.recognizes(targetDirectory))
@@ -15568,10 +16129,10 @@ function resolveInteractionMode({
15568
16129
  init_package_managers();
15569
16130
 
15570
16131
  // src/output.ts
15571
- var OUTPUT_SCHEMA_VERSION = 3;
16132
+ var OUTPUT_SCHEMA_VERSION = 4;
15572
16133
  function createSuccessResult({
15573
16134
  dryRun,
15574
- files,
16135
+ files: files2,
15575
16136
  generatorVersion,
15576
16137
  dependenciesInstalled,
15577
16138
  javascriptPackageManager,
@@ -15587,7 +16148,7 @@ function createSuccessResult({
15587
16148
  gitInitialized: false
15588
16149
  },
15589
16150
  command: "create",
15590
- created: dryRun ? [] : [...files],
16151
+ created: dryRun ? [] : [...files2],
15591
16152
  dryRun,
15592
16153
  generatorVersion,
15593
16154
  humanSteps: template.humanSteps.map((step) => ({ ...step })),
@@ -15600,12 +16161,12 @@ function createSuccessResult({
15600
16161
  ok: true,
15601
16162
  packageManagers: {
15602
16163
  javascript: javascriptPackageManager,
15603
- python: pythonPackageManager.name
16164
+ ...pythonPackageManager ? { python: pythonPackageManager.name } : {}
15604
16165
  },
15605
16166
  projectDirectory,
15606
16167
  schemaVersion: OUTPUT_SCHEMA_VERSION,
15607
- template: "default",
15608
- wouldCreate: dryRun ? [...files] : []
16168
+ stack: template.stack ?? "cloudflare-livekit",
16169
+ wouldCreate: dryRun ? [...files2] : []
15609
16170
  };
15610
16171
  }
15611
16172
  function createFailureResult(error) {
@@ -16035,7 +16596,10 @@ var PromptSession = class {
16035
16596
  return this.confirm("Install dependencies now?", true);
16036
16597
  }
16037
16598
  shouldConfigureCredentials() {
16038
- return this.confirm("Configure LiveKit and Spatius credentials now?", true);
16599
+ return this.confirm(
16600
+ "Configure provider and Spatius credentials now?",
16601
+ true
16602
+ );
16039
16603
  }
16040
16604
  };
16041
16605
 
@@ -16079,7 +16643,7 @@ async function readPackageVersion() {
16079
16643
  const packagePath = fileURLToPath2(
16080
16644
  new URL("../package.json", import.meta.url)
16081
16645
  );
16082
- const contents = await readFile4(packagePath, "utf8");
16646
+ const contents = await readFile8(packagePath, "utf8");
16083
16647
  const metadata = JSON.parse(contents);
16084
16648
  return metadata.version;
16085
16649
  }
@@ -16177,7 +16741,7 @@ function managerOptions(managers) {
16177
16741
  value: manager.name
16178
16742
  }));
16179
16743
  }
16180
- async function resolvePackageManagers(options, interactive, prompts) {
16744
+ async function resolvePackageManagers(options, interactive, prompts, requiresPython = true) {
16181
16745
  if (!interactive && (options.install !== true || options.dryRun)) {
16182
16746
  const inventory2 = { javascript: [], python: [] };
16183
16747
  const javascriptDefault2 = options.packageManager ?? inferInvokingPackageManager(process5.env.npm_config_user_agent) ?? "npm";
@@ -16187,15 +16751,17 @@ async function resolvePackageManagers(options, interactive, prompts) {
16187
16751
  javascriptDefault2,
16188
16752
  process5.env.npm_config_user_agent
16189
16753
  ),
16190
- python: selectPythonPackageManager(
16191
- inventory2,
16192
- options.pythonPackageManager ?? "uv"
16193
- )
16754
+ ...requiresPython ? {
16755
+ python: selectPythonPackageManager(
16756
+ inventory2,
16757
+ options.pythonPackageManager ?? "uv"
16758
+ )
16759
+ } : {}
16194
16760
  };
16195
16761
  }
16196
16762
  const inventory = await detectPackageManagers({
16197
16763
  ...options.packageManager === void 0 ? {} : { javascript: [options.packageManager] },
16198
- ...options.pythonPackageManager === void 0 ? {} : { python: [options.pythonPackageManager] }
16764
+ ...!requiresPython ? { python: [] } : options.pythonPackageManager === void 0 ? {} : { python: [options.pythonPackageManager] }
16199
16765
  });
16200
16766
  const javascriptDefault = !interactive && options.packageManager === void 0 && inventory.javascript.length === 0 ? "npm" : options.packageManager;
16201
16767
  const pythonDefault = !interactive && options.pythonPackageManager === void 0 && inventory.python.length === 0 ? "uv" : options.pythonPackageManager;
@@ -16204,7 +16770,7 @@ async function resolvePackageManagers(options, interactive, prompts) {
16204
16770
  javascriptDefault,
16205
16771
  process5.env.npm_config_user_agent
16206
16772
  );
16207
- let python = selectPythonPackageManager(inventory, pythonDefault);
16773
+ let python = requiresPython ? selectPythonPackageManager(inventory, pythonDefault) : void 0;
16208
16774
  if (interactive && options.packageManager === void 0) {
16209
16775
  const selectedName = await prompts.choose(
16210
16776
  "Which JavaScript package manager?",
@@ -16215,7 +16781,7 @@ async function resolvePackageManagers(options, interactive, prompts) {
16215
16781
  (manager) => manager.name === selectedName
16216
16782
  );
16217
16783
  }
16218
- if (interactive && options.pythonPackageManager === void 0) {
16784
+ if (interactive && python && options.pythonPackageManager === void 0) {
16219
16785
  const selectedName = await prompts.choose(
16220
16786
  "Which Python package manager?",
16221
16787
  managerOptions(inventory.python),
@@ -16226,7 +16792,7 @@ async function resolvePackageManagers(options, interactive, prompts) {
16226
16792
  return { javascript, python };
16227
16793
  }
16228
16794
  function renderManagerSummary(javascript, python) {
16229
- return `${javascript.name} + ${python.name}`;
16795
+ return python ? `${javascript.name} + ${python.name}` : javascript.name;
16230
16796
  }
16231
16797
  function isTestTerminalOverride() {
16232
16798
  return process5.env.NODE_ENV === "test" && process5.env.CREATE_SPATIUS_APP_TEST_INTERACTIVE === "1";
@@ -16256,6 +16822,7 @@ async function detectExecutionAgent() {
16256
16822
  }
16257
16823
  async function runCreateCommand(version, projectDirectoryArgument, rawOptions) {
16258
16824
  const options = {
16825
+ stack: rawOptions.stack,
16259
16826
  debug: rawOptions.debug === true,
16260
16827
  dryRun: rawOptions.dryRun === true,
16261
16828
  install: rawOptions.install,
@@ -16300,6 +16867,14 @@ async function runCreateCommand(version, projectDirectoryArgument, rawOptions) {
16300
16867
  `${theme.accent("create-spatius-app")} ${theme.highlight(`v${version}`)}`
16301
16868
  );
16302
16869
  }
16870
+ const selection = await selectCatalog({ ...options, interactive, prompts });
16871
+ const template = getTemplate(selection.stack);
16872
+ if (template.requiresPython === false && options.pythonPackageManager)
16873
+ throw new CliError(
16874
+ "INVALID_ARGUMENT",
16875
+ "This stack does not use Python. Remove --python-package-manager.",
16876
+ { exitCode: EXIT_CODES.invalidArgument }
16877
+ );
16303
16878
  const projectDirectoryInput = await resolveProjectDirectoryInput({
16304
16879
  argument: projectDirectoryArgument,
16305
16880
  interactive,
@@ -16309,13 +16884,13 @@ async function runCreateCommand(version, projectDirectoryArgument, rawOptions) {
16309
16884
  const packageManagers = await resolvePackageManagers(
16310
16885
  options,
16311
16886
  interactive,
16312
- prompts
16887
+ prompts,
16888
+ template.requiresPython !== false
16313
16889
  );
16314
16890
  const shouldInstall = !options.dryRun && (options.install ?? (interactive ? await prompts.shouldInstall() : false));
16315
16891
  if (shouldInstall) {
16316
16892
  assertPackageManagersAvailable(packageManagers);
16317
16893
  }
16318
- const template = getTemplate();
16319
16894
  const configuration = { packageManagers };
16320
16895
  const scaffold = options.dryRun ? await createScaffoldPlan({
16321
16896
  configuration,
@@ -16494,7 +17069,11 @@ async function runSetupCommand(version, projectDirectoryArgument, rawOptions) {
16494
17069
  }
16495
17070
  }
16496
17071
  function addCreateOptions(command) {
16497
- return command.option("-y, --yes", "accept safe defaults without prompting").addOption(new Option("--interactive", "force interactive prompts")).addOption(new Option("--no-interactive", "disable interactive prompts")).addOption(
17072
+ return command.addOption(
17073
+ new Option("--stack <stack>", "application and deployment stack").choices(
17074
+ Object.keys(stacks)
17075
+ )
17076
+ ).option("-y, --yes", "accept safe defaults without prompting").addOption(new Option("--interactive", "force interactive prompts")).addOption(new Option("--no-interactive", "disable interactive prompts")).addOption(
16498
17077
  new Option(
16499
17078
  "--package-manager <manager>",
16500
17079
  "use pnpm, bun, or npm for JavaScript dependencies"
@@ -16504,7 +17083,7 @@ function addCreateOptions(command) {
16504
17083
  "--python-package-manager <manager>",
16505
17084
  "use uv or pip for Python dependencies"
16506
17085
  ).choices(PYTHON_PACKAGE_MANAGERS)
16507
- ).option("--install", "install JavaScript and Python dependencies").option("--no-install", "skip dependency installation").option("--setup", "configure local LiveKit and Spatius credentials").option("--no-setup", "skip credential setup").option("--debug", "write safe Spatius request diagnostics to stderr").option("--json", "emit one machine-readable JSON result").option("--dry-run", "validate and show planned files without writing");
17086
+ ).option("--install", "install JavaScript and Python dependencies").option("--no-install", "skip dependency installation").option("--setup", "configure local provider and Spatius credentials").option("--no-setup", "skip credential setup").option("--debug", "write safe Spatius request diagnostics to stderr").option("--json", "emit one machine-readable JSON result").option("--dry-run", "validate and show planned files without writing");
16508
17087
  }
16509
17088
  async function main() {
16510
17089
  const version = await readPackageVersion();
@@ -16529,7 +17108,7 @@ Examples:
16529
17108
  async (projectDirectoryArgument, rawOptions) => runCreateCommand(version, projectDirectoryArgument, rawOptions)
16530
17109
  );
16531
17110
  program2.command("setup").description(
16532
- "securely configure local LiveKit and Spatius credentials for an existing generated project"
17111
+ "securely configure local provider credentials for an existing generated project"
16533
17112
  ).argument("[project-directory]", "generated project to configure", ".").addOption(new Option("--interactive", "allow browser and secret prompts")).addOption(new Option("--no-interactive", "disable interactive prompts")).option("--debug", "write safe Spatius request diagnostics to stderr").addHelpText(
16534
17113
  "after",
16535
17114
  `
@@ -16567,4 +17146,3 @@ try {
16567
17146
  }
16568
17147
  process5.exitCode = normalizedError.exitCode;
16569
17148
  }
16570
- //# sourceMappingURL=cli.js.map