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.
- package/README.md +17 -6
- package/dist/cli.js +721 -143
- package/dist/templates.js +612 -66
- package/package.json +4 -4
- package/schemas/result-v3.schema.json +6 -2
- package/schemas/result-v4.schema.json +170 -0
- package/templates/cloudflare-livekit/agent/env.example +3 -0
- package/templates/cloudflare-livekit/agent/src/agent.py +5 -3
- package/templates/cloudflare-livekit/package-lock.json +70 -142
- package/templates/cloudflare-livekit/package.json +1 -1
- package/templates/cloudflare-livekit/pnpm-lock.yaml +96 -153
- package/templates/cloudflare-livekit/tests/dev-events.test.mjs +23 -0
- package/templates/cloudflare-livekit/tests/dev-fixtures.mjs +7 -3
- package/templates/cloudflare-livekit/tsconfig.web.json +1 -0
- package/templates/cloudflare-livekit/tsconfig.worker.json +1 -0
- package/templates/cloudflare-livekit/web/api.ts +12 -0
- package/templates/cloudflare-livekit/web/components/agents-ui/agent-chat-transcript.tsx +6 -90
- package/templates/cloudflare-livekit/web/components/transcript-view.tsx +90 -0
- package/templates/cloudflare-livekit/web/session-attempt.ts +23 -2
- package/templates/cloudflare-livekit/worker/capability.test.ts +31 -0
- package/templates/cloudflare-livekit/worker/capability.ts +72 -0
- package/templates/cloudflare-livekit/worker/index.test.ts +2 -2
- package/templates/cloudflare-livekit/worker/index.ts +15 -3
- package/templates/cloudflare-livekit/worker/session.ts +34 -0
- package/templates/layers/agora/AGENTS.md +8 -0
- package/templates/layers/agora/README.md +19 -0
- package/templates/layers/agora/env.example +9 -0
- package/templates/layers/agora/package-lock.json +6068 -0
- package/templates/layers/agora/package.json +54 -0
- package/templates/layers/agora/pnpm-lock.yaml +3816 -0
- package/templates/layers/agora/tsconfig.json +16 -0
- package/templates/layers/agora/vitest.config.ts +4 -0
- package/templates/layers/agora/web/App.tsx +174 -0
- package/templates/layers/agora/web/agora-session.test.ts +70 -0
- package/templates/layers/agora/web/agora-session.ts +267 -0
- package/templates/layers/agora/worker/agora.test.ts +60 -0
- package/templates/layers/agora/worker/agora.ts +165 -0
- package/templates/layers/agora/worker/index.ts +50 -0
- package/templates/layers/catalog/DEPLOYMENT.md +3 -0
- package/templates/layers/catalog/spatius.config.json +1 -0
- package/templates/layers/node/Dockerfile +12 -0
- package/templates/layers/node/dockerignore +10 -0
- package/templates/layers/node/package-lock.json +5569 -0
- package/templates/layers/node/pnpm-lock.yaml +3470 -0
- package/templates/layers/node/railway.json +8 -0
- package/templates/layers/node/scripts/dev-service.mjs +14 -0
- package/templates/layers/node/scripts/dev-web.mjs +22 -0
- package/templates/layers/node/server/index.ts +92 -0
- package/templates/layers/node/vite.config.ts +17 -0
- package/templates/layers/railway-agent/agent/railway.json +5 -0
- package/dist/cli.js.map +0 -1
- package/dist/templates.js.map +0 -1
package/dist/templates.js
CHANGED
|
@@ -79,6 +79,96 @@ var init_errors = __esm({
|
|
|
79
79
|
}
|
|
80
80
|
});
|
|
81
81
|
|
|
82
|
+
// src/catalog.ts
|
|
83
|
+
function validateSelection(stack) {
|
|
84
|
+
if (!Object.hasOwn(stacks, stack)) {
|
|
85
|
+
throw new CliError("INVALID_ARGUMENT", `Unsupported stack: ${stack}`, {
|
|
86
|
+
exitCode: EXIT_CODES.invalidArgument
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
return { stack };
|
|
90
|
+
}
|
|
91
|
+
var stacks;
|
|
92
|
+
var init_catalog = __esm({
|
|
93
|
+
"src/catalog.ts"() {
|
|
94
|
+
"use strict";
|
|
95
|
+
init_errors();
|
|
96
|
+
stacks = {
|
|
97
|
+
"cloudflare-livekit": {
|
|
98
|
+
label: "Cloudflare + LiveKit Cloud Agents",
|
|
99
|
+
web: "cloudflare",
|
|
100
|
+
agent: "livekit",
|
|
101
|
+
provider: "livekit"
|
|
102
|
+
},
|
|
103
|
+
"cloudflare-livekit-railway": {
|
|
104
|
+
label: "Cloudflare + LiveKit Agents on Railway",
|
|
105
|
+
web: "cloudflare",
|
|
106
|
+
agent: "railway",
|
|
107
|
+
provider: "livekit"
|
|
108
|
+
},
|
|
109
|
+
"railway-livekit-cloud": {
|
|
110
|
+
label: "Railway + LiveKit Cloud Agents",
|
|
111
|
+
web: "railway",
|
|
112
|
+
agent: "livekit",
|
|
113
|
+
provider: "livekit"
|
|
114
|
+
},
|
|
115
|
+
"railway-livekit": {
|
|
116
|
+
label: "Railway + LiveKit Agents on Railway",
|
|
117
|
+
web: "railway",
|
|
118
|
+
agent: "railway",
|
|
119
|
+
provider: "livekit"
|
|
120
|
+
},
|
|
121
|
+
"zeabur-agora": {
|
|
122
|
+
label: "Zeabur + Agora Conversational AI",
|
|
123
|
+
web: "zeabur",
|
|
124
|
+
agent: "agora",
|
|
125
|
+
provider: "agora"
|
|
126
|
+
}
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
// src/project-config.ts
|
|
132
|
+
import { lstat, readFile } from "fs/promises";
|
|
133
|
+
import { join } from "path";
|
|
134
|
+
async function readProjectConfig(directory) {
|
|
135
|
+
const path = join(directory, "spatius.config.json");
|
|
136
|
+
try {
|
|
137
|
+
const stats = await lstat(path);
|
|
138
|
+
if (!stats.isFile() || stats.isSymbolicLink())
|
|
139
|
+
throw new Error("Expected a regular file");
|
|
140
|
+
const value = JSON.parse(await readFile(path, "utf8"));
|
|
141
|
+
if (value && typeof value === "object" && "version" in value && value.version !== 2)
|
|
142
|
+
throw new CliError(
|
|
143
|
+
"INVALID_ARGUMENT",
|
|
144
|
+
"Unsupported spatius.config.json version. Expected version 2.",
|
|
145
|
+
{ path, exitCode: EXIT_CODES.invalidArgument }
|
|
146
|
+
);
|
|
147
|
+
if (!value || typeof value !== "object" || !("version" in value) || value.version !== 2 || !("stack" in value) || typeof value.stack !== "string")
|
|
148
|
+
throw new Error("Unsupported project configuration");
|
|
149
|
+
return { version: 2, ...validateSelection(value.stack) };
|
|
150
|
+
} catch (error) {
|
|
151
|
+
if (error instanceof CliError) throw error;
|
|
152
|
+
if (error instanceof Error && "code" in error && error.code === "ENOENT")
|
|
153
|
+
return void 0;
|
|
154
|
+
throw new CliError("INVALID_ARGUMENT", "Invalid spatius.config.json.", {
|
|
155
|
+
path,
|
|
156
|
+
exitCode: EXIT_CODES.invalidArgument
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
async function webEnvironmentPath(directory) {
|
|
161
|
+
const config = await readProjectConfig(directory);
|
|
162
|
+
return config && !config.stack.startsWith("cloudflare") ? ".env.local" : ".dev.vars";
|
|
163
|
+
}
|
|
164
|
+
var init_project_config = __esm({
|
|
165
|
+
"src/project-config.ts"() {
|
|
166
|
+
"use strict";
|
|
167
|
+
init_catalog();
|
|
168
|
+
init_errors();
|
|
169
|
+
}
|
|
170
|
+
});
|
|
171
|
+
|
|
82
172
|
// src/package-managers.ts
|
|
83
173
|
import { execFile, spawn } from "child_process";
|
|
84
174
|
function requiresWindowsCommandShell(command, platform = process.platform) {
|
|
@@ -201,23 +291,32 @@ var init_package_managers = __esm({
|
|
|
201
291
|
});
|
|
202
292
|
|
|
203
293
|
// src/setup/project.ts
|
|
204
|
-
import { lstat } from "fs/promises";
|
|
205
|
-
import { join } from "path";
|
|
294
|
+
import { lstat as lstat2 } from "fs/promises";
|
|
295
|
+
import { join as join3 } from "path";
|
|
206
296
|
async function assertSpatiusProject(targetDirectory) {
|
|
207
297
|
try {
|
|
208
|
-
const target = await
|
|
298
|
+
const target = await lstat2(targetDirectory);
|
|
209
299
|
if (!target.isDirectory() || target.isSymbolicLink()) {
|
|
210
300
|
throw new Error("not a regular directory");
|
|
211
301
|
}
|
|
302
|
+
const config = await readProjectConfig(targetDirectory);
|
|
303
|
+
const paths = config ? [
|
|
304
|
+
"package.json",
|
|
305
|
+
"spatius.config.json",
|
|
306
|
+
await webEnvironmentPath(targetDirectory) + ".example",
|
|
307
|
+
"worker/index.ts",
|
|
308
|
+
"agent/src/agent.py",
|
|
309
|
+
"agent/.env.example"
|
|
310
|
+
] : requiredProjectPaths;
|
|
212
311
|
await Promise.all([
|
|
213
312
|
...requiredProjectDirectories.map(async (relativePath) => {
|
|
214
|
-
const stats = await
|
|
313
|
+
const stats = await lstat2(join3(targetDirectory, relativePath));
|
|
215
314
|
if (!stats.isDirectory() || stats.isSymbolicLink()) {
|
|
216
315
|
throw new Error(`${relativePath} is not a regular directory`);
|
|
217
316
|
}
|
|
218
317
|
}),
|
|
219
|
-
...
|
|
220
|
-
const stats = await
|
|
318
|
+
...paths.map(async (relativePath) => {
|
|
319
|
+
const stats = await lstat2(join3(targetDirectory, relativePath));
|
|
221
320
|
if (!stats.isFile() || stats.isSymbolicLink()) {
|
|
222
321
|
throw new Error(`${relativePath} is not a regular file`);
|
|
223
322
|
}
|
|
@@ -239,6 +338,7 @@ var requiredProjectPaths, requiredProjectDirectories;
|
|
|
239
338
|
var init_project = __esm({
|
|
240
339
|
"src/setup/project.ts"() {
|
|
241
340
|
"use strict";
|
|
341
|
+
init_project_config();
|
|
242
342
|
init_errors();
|
|
243
343
|
requiredProjectPaths = [
|
|
244
344
|
"package.json",
|
|
@@ -277,14 +377,14 @@ var init_voices = __esm({
|
|
|
277
377
|
// src/setup/environment.ts
|
|
278
378
|
import {
|
|
279
379
|
chmod,
|
|
280
|
-
lstat as
|
|
281
|
-
readFile as
|
|
380
|
+
lstat as lstat3,
|
|
381
|
+
readFile as readFile3,
|
|
282
382
|
rename,
|
|
283
383
|
rm,
|
|
284
384
|
writeFile as writeFile2
|
|
285
385
|
} from "fs/promises";
|
|
286
386
|
import { randomUUID as randomUUID2 } from "crypto";
|
|
287
|
-
import { join as
|
|
387
|
+
import { join as join4 } from "path";
|
|
288
388
|
function parseQuotedValue(rawValue) {
|
|
289
389
|
const value = rawValue.trim();
|
|
290
390
|
if (value.startsWith('"')) {
|
|
@@ -439,7 +539,7 @@ function isMissingFile(error) {
|
|
|
439
539
|
}
|
|
440
540
|
async function readOptionalCredentialFile(path) {
|
|
441
541
|
try {
|
|
442
|
-
const stats = await
|
|
542
|
+
const stats = await lstat3(path);
|
|
443
543
|
if (stats.isSymbolicLink() || !stats.isFile()) {
|
|
444
544
|
throw new CliError(
|
|
445
545
|
"FILESYSTEM_ERROR",
|
|
@@ -451,7 +551,7 @@ async function readOptionalCredentialFile(path) {
|
|
|
451
551
|
}
|
|
452
552
|
);
|
|
453
553
|
}
|
|
454
|
-
return await
|
|
554
|
+
return await readFile3(path, "utf8");
|
|
455
555
|
} catch (error) {
|
|
456
556
|
if (isMissingFile(error)) {
|
|
457
557
|
return void 0;
|
|
@@ -460,10 +560,11 @@ async function readOptionalCredentialFile(path) {
|
|
|
460
560
|
}
|
|
461
561
|
}
|
|
462
562
|
async function readCredentialFileState(targetDirectory) {
|
|
563
|
+
const webPath = await webEnvironmentPath(targetDirectory);
|
|
463
564
|
const [worker, agent, workerExample] = await Promise.all([
|
|
464
|
-
readOptionalCredentialFile(
|
|
465
|
-
readOptionalCredentialFile(
|
|
466
|
-
readOptionalCredentialFile(
|
|
565
|
+
readOptionalCredentialFile(join4(targetDirectory, webPath)),
|
|
566
|
+
readOptionalCredentialFile(join4(targetDirectory, "agent", ".env.local")),
|
|
567
|
+
readOptionalCredentialFile(join4(targetDirectory, webPath + ".example"))
|
|
467
568
|
]);
|
|
468
569
|
return {
|
|
469
570
|
agent,
|
|
@@ -535,23 +636,24 @@ async function pathExists(path, fileSystem) {
|
|
|
535
636
|
}
|
|
536
637
|
}
|
|
537
638
|
async function writeCredentialFilesAtomically(targetDirectory, contents, fileSystem = defaultAtomicFileSystem) {
|
|
639
|
+
const webPath = await webEnvironmentPath(targetDirectory);
|
|
538
640
|
const nonce = randomUUID2();
|
|
539
641
|
const targets = [
|
|
540
642
|
{
|
|
541
643
|
backupMoved: false,
|
|
542
|
-
backupPath:
|
|
543
|
-
destinationPath:
|
|
644
|
+
backupPath: join4(targetDirectory, `.dev.vars.${nonce}.backup`),
|
|
645
|
+
destinationPath: join4(targetDirectory, webPath),
|
|
544
646
|
hadOriginal: false,
|
|
545
647
|
installed: false,
|
|
546
|
-
stagedPath:
|
|
648
|
+
stagedPath: join4(targetDirectory, `.dev.vars.${nonce}.tmp`)
|
|
547
649
|
},
|
|
548
650
|
{
|
|
549
651
|
backupMoved: false,
|
|
550
|
-
backupPath:
|
|
551
|
-
destinationPath:
|
|
652
|
+
backupPath: join4(targetDirectory, "agent", `.env.local.${nonce}.backup`),
|
|
653
|
+
destinationPath: join4(targetDirectory, "agent", ".env.local"),
|
|
552
654
|
hadOriginal: false,
|
|
553
655
|
installed: false,
|
|
554
|
-
stagedPath:
|
|
656
|
+
stagedPath: join4(targetDirectory, "agent", `.env.local.${nonce}.tmp`)
|
|
555
657
|
}
|
|
556
658
|
];
|
|
557
659
|
const payloads = [contents.worker, contents.agent];
|
|
@@ -612,9 +714,10 @@ async function writeCredentialFilesAtomically(targetDirectory, contents, fileSys
|
|
|
612
714
|
}
|
|
613
715
|
}
|
|
614
716
|
async function readCredentialExamples(targetDirectory) {
|
|
717
|
+
const webPath = await webEnvironmentPath(targetDirectory);
|
|
615
718
|
const [worker, agent] = await Promise.all([
|
|
616
|
-
|
|
617
|
-
|
|
719
|
+
readFile3(join4(targetDirectory, webPath + ".example"), "utf8"),
|
|
720
|
+
readFile3(join4(targetDirectory, "agent", ".env.example"), "utf8")
|
|
618
721
|
]);
|
|
619
722
|
return { agent, worker };
|
|
620
723
|
}
|
|
@@ -622,6 +725,7 @@ var WORKER_MANAGED_KEYS, AGENT_MANAGED_KEYS, assignmentPattern, defaultAtomicFil
|
|
|
622
725
|
var init_environment = __esm({
|
|
623
726
|
"src/setup/environment.ts"() {
|
|
624
727
|
"use strict";
|
|
728
|
+
init_project_config();
|
|
625
729
|
init_errors();
|
|
626
730
|
WORKER_MANAGED_KEYS = [
|
|
627
731
|
"LIVEKIT_URL",
|
|
@@ -641,8 +745,8 @@ var init_environment = __esm({
|
|
|
641
745
|
assignmentPattern = /^\s*(?:export\s+)?([A-Za-z_][A-Za-z\d_]*)\s*=\s*(.*)$/u;
|
|
642
746
|
defaultAtomicFileSystem = {
|
|
643
747
|
chmod,
|
|
644
|
-
lstat:
|
|
645
|
-
readFile: (path) =>
|
|
748
|
+
lstat: lstat3,
|
|
749
|
+
readFile: (path) => readFile3(path),
|
|
646
750
|
rename,
|
|
647
751
|
rm: (path) => rm(path, { force: true }),
|
|
648
752
|
writeFile: writeFile2
|
|
@@ -7980,12 +8084,12 @@ var require_dist = __commonJS({
|
|
|
7980
8084
|
// src/setup/livekit.ts
|
|
7981
8085
|
import { createHmac } from "crypto";
|
|
7982
8086
|
import { spawn as spawn2 } from "child_process";
|
|
7983
|
-
import { chmod as chmod2, mkdtemp, readFile as
|
|
8087
|
+
import { chmod as chmod2, mkdtemp, readFile as readFile4, rm as rm2, writeFile as writeFile3 } from "fs/promises";
|
|
7984
8088
|
import { homedir, tmpdir } from "os";
|
|
7985
|
-
import { join as
|
|
7986
|
-
async function readLiveKitProjects(configPath =
|
|
8089
|
+
import { join as join5 } from "path";
|
|
8090
|
+
async function readLiveKitProjects(configPath = join5(homedir(), ".livekit", "cli-config.yaml")) {
|
|
7987
8091
|
try {
|
|
7988
|
-
const config = (0, import_yaml.parse)(await
|
|
8092
|
+
const config = (0, import_yaml.parse)(await readFile4(configPath, "utf8"));
|
|
7989
8093
|
if (config == null) return [];
|
|
7990
8094
|
if (typeof config !== "object" || !("projects" in config) || !Array.isArray(config.projects))
|
|
7991
8095
|
throw new Error();
|
|
@@ -8148,12 +8252,12 @@ async function loadLiveKitCredentialsWithCli({
|
|
|
8148
8252
|
projectName
|
|
8149
8253
|
} = {}) {
|
|
8150
8254
|
const directory = await fileSystem.mkdtemp(
|
|
8151
|
-
|
|
8255
|
+
join5(temporaryRoot, "create-spatius-app-livekit-")
|
|
8152
8256
|
);
|
|
8153
8257
|
try {
|
|
8154
8258
|
await fileSystem.chmod(directory, 448);
|
|
8155
8259
|
await fileSystem.writeFile(
|
|
8156
|
-
|
|
8260
|
+
join5(directory, ".env.example"),
|
|
8157
8261
|
"LIVEKIT_URL=\nLIVEKIT_API_KEY=\nLIVEKIT_API_SECRET=\n",
|
|
8158
8262
|
384
|
|
8159
8263
|
);
|
|
@@ -8172,7 +8276,7 @@ async function loadLiveKitCredentialsWithCli({
|
|
|
8172
8276
|
}
|
|
8173
8277
|
);
|
|
8174
8278
|
const values = parseDotenv(
|
|
8175
|
-
await fileSystem.readFile(
|
|
8279
|
+
await fileSystem.readFile(join5(directory, ".env.local"))
|
|
8176
8280
|
);
|
|
8177
8281
|
const validated = validateCredentialBundle({
|
|
8178
8282
|
liveKit: {
|
|
@@ -8272,7 +8376,7 @@ var init_livekit = __esm({
|
|
|
8272
8376
|
defaultTemporaryFileSystem = {
|
|
8273
8377
|
chmod: chmod2,
|
|
8274
8378
|
mkdtemp,
|
|
8275
|
-
readFile: (path) =>
|
|
8379
|
+
readFile: (path) => readFile4(path, "utf8"),
|
|
8276
8380
|
remove: (path) => rm2(path, {
|
|
8277
8381
|
force: true,
|
|
8278
8382
|
maxRetries: 3,
|
|
@@ -9723,7 +9827,7 @@ async function runCredentialSetup({
|
|
|
9723
9827
|
const examples = await readExamples(targetDirectory);
|
|
9724
9828
|
const rendered = buildCredentialFileContents(state, examples, credentials);
|
|
9725
9829
|
await writeFiles(targetDirectory, rendered);
|
|
9726
|
-
onStatus("Saved
|
|
9830
|
+
onStatus("Saved local web and agent credentials for local development only.");
|
|
9727
9831
|
return "configured";
|
|
9728
9832
|
}
|
|
9729
9833
|
var init_wizard = __esm({
|
|
@@ -9743,18 +9847,18 @@ var init_wizard = __esm({
|
|
|
9743
9847
|
});
|
|
9744
9848
|
|
|
9745
9849
|
// src/templates.ts
|
|
9746
|
-
|
|
9747
|
-
|
|
9850
|
+
init_project_config();
|
|
9851
|
+
init_catalog();
|
|
9748
9852
|
|
|
9749
|
-
// src/templates/
|
|
9750
|
-
init_errors();
|
|
9853
|
+
// src/templates/agora.ts
|
|
9751
9854
|
init_package_managers();
|
|
9752
|
-
|
|
9855
|
+
import { readFile as readFile6, writeFile as writeFile5 } from "fs/promises";
|
|
9856
|
+
import { basename as basename3, join as join7 } from "path";
|
|
9753
9857
|
|
|
9754
9858
|
// src/templates/cloudflare-livekit/configure.ts
|
|
9755
9859
|
init_package_managers();
|
|
9756
9860
|
import { randomUUID } from "crypto";
|
|
9757
|
-
import { readFile, writeFile } from "fs/promises";
|
|
9861
|
+
import { access, readFile as readFile2, writeFile } from "fs/promises";
|
|
9758
9862
|
import { basename, join as join2 } from "path";
|
|
9759
9863
|
function toValidPackageName(value) {
|
|
9760
9864
|
const normalized = value.trim().toLowerCase().replace(/\s+/gu, "-").replace(/^[._-]+/u, "").replace(/[^a-z\d~._-]+/gu, "-").replace(/-+/gu, "-").slice(0, 214).replace(/-$/u, "");
|
|
@@ -9770,6 +9874,7 @@ function npmRequiresScriptAllowlist(version) {
|
|
|
9770
9874
|
function createReplacements(packageManagers, platform) {
|
|
9771
9875
|
const javascript = packageManagers.javascript;
|
|
9772
9876
|
const python = packageManagers.python;
|
|
9877
|
+
if (!python) throw new Error("LiveKit templates require Python tooling.");
|
|
9773
9878
|
return /* @__PURE__ */ new Map([
|
|
9774
9879
|
[
|
|
9775
9880
|
"{{SPATIUS_JAVASCRIPT_PACKAGE_MANAGER}}",
|
|
@@ -9841,7 +9946,7 @@ function createReplacements(packageManagers, platform) {
|
|
|
9841
9946
|
]);
|
|
9842
9947
|
}
|
|
9843
9948
|
async function renderFile(path, replacements) {
|
|
9844
|
-
let contents = await
|
|
9949
|
+
let contents = await readFile2(path, "utf8");
|
|
9845
9950
|
for (const [token, replacement] of replacements) {
|
|
9846
9951
|
contents = contents.replaceAll(token, replacement);
|
|
9847
9952
|
}
|
|
@@ -9853,7 +9958,7 @@ async function configureGeneratedTemplate(targetDirectory, {
|
|
|
9853
9958
|
projectName = basename(targetDirectory)
|
|
9854
9959
|
}) {
|
|
9855
9960
|
const packagePath = join2(targetDirectory, "package.json");
|
|
9856
|
-
const packageContents = await
|
|
9961
|
+
const packageContents = await readFile2(packagePath, "utf8");
|
|
9857
9962
|
const metadata = JSON.parse(packageContents);
|
|
9858
9963
|
const javascript = packageManagers.javascript;
|
|
9859
9964
|
const run = (script) => javascriptRunCommand(javascript.name, script);
|
|
@@ -9892,7 +9997,7 @@ async function configureGeneratedTemplate(targetDirectory, {
|
|
|
9892
9997
|
if (javascript.name === "npm") {
|
|
9893
9998
|
const packageLockPath = join2(targetDirectory, "package-lock.json");
|
|
9894
9999
|
const packageLock = JSON.parse(
|
|
9895
|
-
await
|
|
10000
|
+
await readFile2(packageLockPath, "utf8")
|
|
9896
10001
|
);
|
|
9897
10002
|
packageLock.name = metadata.name;
|
|
9898
10003
|
if (packageLock.packages?.[""] !== void 0) {
|
|
@@ -9911,12 +10016,18 @@ async function configureGeneratedTemplate(targetDirectory, {
|
|
|
9911
10016
|
await Promise.all(
|
|
9912
10017
|
[
|
|
9913
10018
|
".dev.vars.example",
|
|
10019
|
+
".env.local.example",
|
|
9914
10020
|
"wrangler.jsonc",
|
|
9915
10021
|
"agent/src/agent.py",
|
|
9916
10022
|
"agent/README.md",
|
|
9917
10023
|
"worker/index.test.ts"
|
|
9918
10024
|
].map(
|
|
9919
|
-
async (relativePath) =>
|
|
10025
|
+
async (relativePath) => access(join2(targetDirectory, relativePath)).then(
|
|
10026
|
+
() => renderFile(join2(targetDirectory, relativePath), agentReplacements)
|
|
10027
|
+
).catch((error) => {
|
|
10028
|
+
if (!(error instanceof Error && "code" in error && error.code === "ENOENT"))
|
|
10029
|
+
throw error;
|
|
10030
|
+
})
|
|
9920
10031
|
)
|
|
9921
10032
|
);
|
|
9922
10033
|
await Promise.all(
|
|
@@ -9951,6 +10062,7 @@ function createInstallPlan(targetDirectory, packageManagers, platform = process.
|
|
|
9951
10062
|
`Installing JavaScript dependencies with ${javascript.name}`
|
|
9952
10063
|
)
|
|
9953
10064
|
];
|
|
10065
|
+
if (!python) return steps;
|
|
9954
10066
|
if (python.name === "uv") {
|
|
9955
10067
|
steps.push(
|
|
9956
10068
|
commandStep(
|
|
@@ -9989,6 +10101,9 @@ function createInstallPlan(targetDirectory, packageManagers, platform = process.
|
|
|
9989
10101
|
}
|
|
9990
10102
|
|
|
9991
10103
|
// src/templates/cloudflare-livekit/index.ts
|
|
10104
|
+
init_errors();
|
|
10105
|
+
init_package_managers();
|
|
10106
|
+
init_project();
|
|
9992
10107
|
var credentialSetupStep = {
|
|
9993
10108
|
command: "npx create-spatius-app setup . --interactive",
|
|
9994
10109
|
reason: "Connect LiveKit and Spatius accounts and select an avatar and voice in a secure interactive terminal. Never paste secrets into chat.",
|
|
@@ -10017,7 +10132,7 @@ var cloudflareLivekitTemplate = {
|
|
|
10017
10132
|
],
|
|
10018
10133
|
includeFile(path, configuration) {
|
|
10019
10134
|
const javascript = configuration?.packageManagers.javascript.name ?? "pnpm";
|
|
10020
|
-
const python = configuration?.packageManagers.python
|
|
10135
|
+
const python = configuration?.packageManagers.python?.name ?? "uv";
|
|
10021
10136
|
if (path === "package-lock.json") return javascript === "npm";
|
|
10022
10137
|
if (["npmrc", "pnpm-lock.yaml", "pnpm-workspace.yaml"].includes(path)) {
|
|
10023
10138
|
return javascript === "pnpm";
|
|
@@ -10041,6 +10156,8 @@ var cloudflareLivekitTemplate = {
|
|
|
10041
10156
|
platform,
|
|
10042
10157
|
pythonPackageManager
|
|
10043
10158
|
}) {
|
|
10159
|
+
if (!pythonPackageManager)
|
|
10160
|
+
throw new Error("LiveKit templates require Python tooling.");
|
|
10044
10161
|
return [
|
|
10045
10162
|
...dependenciesInstalled ? [] : [
|
|
10046
10163
|
javascriptInstallCommand(javascriptPackageManager),
|
|
@@ -10073,16 +10190,409 @@ var cloudflareLivekitTemplate = {
|
|
|
10073
10190
|
]
|
|
10074
10191
|
};
|
|
10075
10192
|
|
|
10193
|
+
// src/setup/agora.ts
|
|
10194
|
+
init_environment();
|
|
10195
|
+
init_errors();
|
|
10196
|
+
init_project_config();
|
|
10197
|
+
import { lstat as lstat4, readFile as readFile5, rename as rename2, rm as rm3, writeFile as writeFile4 } from "fs/promises";
|
|
10198
|
+
import { join as join6 } from "path";
|
|
10199
|
+
import { randomUUID as randomUUID3 } from "crypto";
|
|
10200
|
+
var runAgoraSetup = async ({
|
|
10201
|
+
targetDirectory,
|
|
10202
|
+
prompts,
|
|
10203
|
+
onStatus = () => void 0
|
|
10204
|
+
}) => {
|
|
10205
|
+
const target = await lstat4(targetDirectory);
|
|
10206
|
+
if (!target.isDirectory() || target.isSymbolicLink())
|
|
10207
|
+
throw new CliError(
|
|
10208
|
+
"INVALID_ARGUMENT",
|
|
10209
|
+
"Expected a regular project directory.",
|
|
10210
|
+
{ exitCode: EXIT_CODES.invalidArgument }
|
|
10211
|
+
);
|
|
10212
|
+
const config = await readProjectConfig(targetDirectory);
|
|
10213
|
+
if (config?.stack !== "zeabur-agora")
|
|
10214
|
+
throw new CliError(
|
|
10215
|
+
"INVALID_ARGUMENT",
|
|
10216
|
+
"Expected a Zeabur + Agora project.",
|
|
10217
|
+
{ exitCode: EXIT_CODES.invalidArgument }
|
|
10218
|
+
);
|
|
10219
|
+
for (const name of [
|
|
10220
|
+
"package.json",
|
|
10221
|
+
".env.local.example",
|
|
10222
|
+
"worker/agora.ts"
|
|
10223
|
+
]) {
|
|
10224
|
+
const stats = await lstat4(join6(targetDirectory, name));
|
|
10225
|
+
if (!stats.isFile() || stats.isSymbolicLink())
|
|
10226
|
+
throw new CliError(
|
|
10227
|
+
"INVALID_ARGUMENT",
|
|
10228
|
+
"Expected regular project files.",
|
|
10229
|
+
{ exitCode: EXIT_CODES.invalidArgument }
|
|
10230
|
+
);
|
|
10231
|
+
}
|
|
10232
|
+
const path = join6(targetDirectory, ".env.local");
|
|
10233
|
+
let existing;
|
|
10234
|
+
try {
|
|
10235
|
+
const stats = await lstat4(path);
|
|
10236
|
+
if (!stats.isFile() || stats.isSymbolicLink())
|
|
10237
|
+
throw new Error("Unsafe credential path");
|
|
10238
|
+
existing = await readFile5(path, "utf8");
|
|
10239
|
+
} catch (error) {
|
|
10240
|
+
if (!(error instanceof Error && "code" in error && error.code === "ENOENT"))
|
|
10241
|
+
throw new CliError("FILESYSTEM_ERROR", "Cannot safely read .env.local.", {
|
|
10242
|
+
exitCode: EXIT_CODES.filesystem,
|
|
10243
|
+
path
|
|
10244
|
+
});
|
|
10245
|
+
}
|
|
10246
|
+
if (existing && !await prompts.confirm(
|
|
10247
|
+
"Replace existing Agora and Spatius credentials? Other variables are preserved.",
|
|
10248
|
+
false
|
|
10249
|
+
))
|
|
10250
|
+
return "unchanged";
|
|
10251
|
+
const values = {};
|
|
10252
|
+
const previous = parseDotenv(existing ?? "");
|
|
10253
|
+
const secretKeys = ["AGORA_APP_CERTIFICATE", "SPATIUS_API_KEY"];
|
|
10254
|
+
for (const key of [
|
|
10255
|
+
"AGORA_APP_ID",
|
|
10256
|
+
"AGORA_APP_CERTIFICATE",
|
|
10257
|
+
"AGORA_PIPELINE_ID",
|
|
10258
|
+
"SPATIUS_APP_ID",
|
|
10259
|
+
"SPATIUS_API_KEY",
|
|
10260
|
+
"SPATIUS_AVATAR_ID",
|
|
10261
|
+
"SPATIUS_REGION",
|
|
10262
|
+
"AGORA_AVATAR_SAMPLE_RATE"
|
|
10263
|
+
]) {
|
|
10264
|
+
const defaults = {
|
|
10265
|
+
SPATIUS_REGION: "cn-beijing",
|
|
10266
|
+
AGORA_AVATAR_SAMPLE_RATE: "24000"
|
|
10267
|
+
};
|
|
10268
|
+
while (true) {
|
|
10269
|
+
const value = secretKeys.includes(key) ? await prompts.password(key) : await prompts.input(key, {
|
|
10270
|
+
initialValue: previous.get(key) ?? defaults[key]
|
|
10271
|
+
});
|
|
10272
|
+
if (isPlaceholderValue(value) || new RegExp("\\p{Cc}", "u").test(value)) {
|
|
10273
|
+
onStatus("Enter a non-empty value without control characters.");
|
|
10274
|
+
continue;
|
|
10275
|
+
}
|
|
10276
|
+
if (["AGORA_APP_ID", "AGORA_APP_CERTIFICATE"].includes(key) && !/^[a-f\d]{32}$/iu.test(value)) {
|
|
10277
|
+
onStatus(
|
|
10278
|
+
"Agora App ID and certificate must contain 32 hexadecimal characters."
|
|
10279
|
+
);
|
|
10280
|
+
continue;
|
|
10281
|
+
}
|
|
10282
|
+
if (key === "AGORA_AVATAR_SAMPLE_RATE" && ![8e3, 16e3, 22050, 24e3, 32e3, 44100, 48e3].includes(
|
|
10283
|
+
Number(value)
|
|
10284
|
+
)) {
|
|
10285
|
+
onStatus("Choose a supported audio sample rate.");
|
|
10286
|
+
continue;
|
|
10287
|
+
}
|
|
10288
|
+
values[key] = value;
|
|
10289
|
+
break;
|
|
10290
|
+
}
|
|
10291
|
+
}
|
|
10292
|
+
const rendered = mergeDotenv(
|
|
10293
|
+
existing ?? await readFile5(join6(targetDirectory, ".env.local.example"), "utf8"),
|
|
10294
|
+
values
|
|
10295
|
+
);
|
|
10296
|
+
const temporary = join6(targetDirectory, `.env.local.${randomUUID3()}.tmp`);
|
|
10297
|
+
try {
|
|
10298
|
+
await writeFile4(temporary, rendered, { flag: "wx", mode: 384 });
|
|
10299
|
+
await rename2(temporary, path);
|
|
10300
|
+
} finally {
|
|
10301
|
+
await rm3(temporary, { force: true });
|
|
10302
|
+
}
|
|
10303
|
+
onStatus(
|
|
10304
|
+
"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."
|
|
10305
|
+
);
|
|
10306
|
+
return "configured";
|
|
10307
|
+
};
|
|
10308
|
+
|
|
10309
|
+
// src/templates/agora.ts
|
|
10310
|
+
init_project_config();
|
|
10311
|
+
var files = /* @__PURE__ */ new Set([
|
|
10312
|
+
"package.json",
|
|
10313
|
+
"package-lock.json",
|
|
10314
|
+
"pnpm-lock.yaml",
|
|
10315
|
+
"pnpm-workspace.yaml",
|
|
10316
|
+
"npmrc",
|
|
10317
|
+
"gitignore",
|
|
10318
|
+
"dockerignore",
|
|
10319
|
+
"Dockerfile",
|
|
10320
|
+
".editorconfig",
|
|
10321
|
+
".prettierrc.json",
|
|
10322
|
+
".prettierignore",
|
|
10323
|
+
"README.md",
|
|
10324
|
+
"AGENTS.md",
|
|
10325
|
+
"DEPLOYMENT.md",
|
|
10326
|
+
"spatius.config.json",
|
|
10327
|
+
"index.html",
|
|
10328
|
+
"tsconfig.json",
|
|
10329
|
+
"vite.config.ts",
|
|
10330
|
+
"vitest.config.ts",
|
|
10331
|
+
"eslint.config.mjs",
|
|
10332
|
+
"env.example",
|
|
10333
|
+
"server/index.ts",
|
|
10334
|
+
"worker/index.ts",
|
|
10335
|
+
"worker/agora.ts",
|
|
10336
|
+
"worker/agora.test.ts",
|
|
10337
|
+
"worker/capability.ts",
|
|
10338
|
+
"worker/capability.test.ts",
|
|
10339
|
+
"web/App.tsx",
|
|
10340
|
+
"web/main.tsx",
|
|
10341
|
+
"web/styles.css",
|
|
10342
|
+
"web/agora-session.ts",
|
|
10343
|
+
"web/agora-session.test.ts",
|
|
10344
|
+
"web/components/transcript-view.tsx",
|
|
10345
|
+
"web/components/transcript-panel.tsx",
|
|
10346
|
+
"web/components/icons.tsx",
|
|
10347
|
+
"web/components/agents-ui/agent-chat-input.tsx",
|
|
10348
|
+
"web/components/agents-ui/LICENSE",
|
|
10349
|
+
"web/components/agents-ui/PROVENANCE.md"
|
|
10350
|
+
]);
|
|
10351
|
+
var agoraTemplate = {
|
|
10352
|
+
id: "zeabur-agora",
|
|
10353
|
+
stack: "zeabur-agora",
|
|
10354
|
+
requiresPython: false,
|
|
10355
|
+
humanSteps: [
|
|
10356
|
+
{
|
|
10357
|
+
command: "npx create-spatius-app setup . --interactive",
|
|
10358
|
+
reason: "Enter Agora and Spatius credentials in a secure interactive terminal and configure a published English Agora pipeline. Never paste secrets into chat.",
|
|
10359
|
+
requiresHuman: true,
|
|
10360
|
+
requiresTty: true
|
|
10361
|
+
}
|
|
10362
|
+
],
|
|
10363
|
+
directory: "templates/cloudflare-livekit",
|
|
10364
|
+
description: "Zeabur + Agora Conversational AI",
|
|
10365
|
+
components: [
|
|
10366
|
+
"- web/ React + Spatius AvatarKit + Agora",
|
|
10367
|
+
"- server/ Node HTTP API; Agora hosts the conversational agent"
|
|
10368
|
+
],
|
|
10369
|
+
layers: [
|
|
10370
|
+
{ directory: "templates/layers/catalog" },
|
|
10371
|
+
{
|
|
10372
|
+
directory: "templates/layers/node",
|
|
10373
|
+
overrides: ["vite.config.ts", "package-lock.json", "pnpm-lock.yaml"]
|
|
10374
|
+
},
|
|
10375
|
+
{
|
|
10376
|
+
directory: "templates/layers/agora",
|
|
10377
|
+
overrides: [
|
|
10378
|
+
"package.json",
|
|
10379
|
+
"package-lock.json",
|
|
10380
|
+
"pnpm-lock.yaml",
|
|
10381
|
+
"web/App.tsx",
|
|
10382
|
+
"worker/index.ts",
|
|
10383
|
+
"tsconfig.json",
|
|
10384
|
+
"vitest.config.ts",
|
|
10385
|
+
"README.md",
|
|
10386
|
+
"AGENTS.md"
|
|
10387
|
+
]
|
|
10388
|
+
}
|
|
10389
|
+
],
|
|
10390
|
+
includeFile(path, configuration) {
|
|
10391
|
+
if (!cloudflareLivekitTemplate.includeFile(path, configuration)) return false;
|
|
10392
|
+
return files.has(path) || [...files].some((file) => file.startsWith(path + "/")) || path === "web/assets" || path.startsWith("web/assets/");
|
|
10393
|
+
},
|
|
10394
|
+
mapFile(path) {
|
|
10395
|
+
return path === "env.example" ? ".env.local.example" : cloudflareLivekitTemplate.mapFile(path);
|
|
10396
|
+
},
|
|
10397
|
+
async configure(directory, configuration) {
|
|
10398
|
+
const manager = configuration.packageManagers.javascript;
|
|
10399
|
+
const run = (script) => javascriptRunCommand(manager.name, script);
|
|
10400
|
+
const path = join7(directory, "package.json");
|
|
10401
|
+
const pkg = JSON.parse(await readFile6(path, "utf8"));
|
|
10402
|
+
pkg.name = toValidPackageName(
|
|
10403
|
+
configuration.projectName ?? basename3(directory)
|
|
10404
|
+
);
|
|
10405
|
+
if (manager.version && manager.version !== "unknown")
|
|
10406
|
+
pkg.packageManager = `${manager.name}@${manager.version}`;
|
|
10407
|
+
pkg.scripts.check = ["format:check", "lint", "typecheck", "test", "build"].map(run).join(" && ");
|
|
10408
|
+
if (manager.name === "bun") pkg.trustedDependencies = ["esbuild"];
|
|
10409
|
+
await writeFile5(path, JSON.stringify(pkg, null, 2) + "\n");
|
|
10410
|
+
if (manager.name === "npm") {
|
|
10411
|
+
const lockPath = join7(directory, "package-lock.json");
|
|
10412
|
+
const lock = JSON.parse(await readFile6(lockPath, "utf8"));
|
|
10413
|
+
lock.name = pkg.name;
|
|
10414
|
+
lock.packages[""].name = pkg.name;
|
|
10415
|
+
await writeFile5(lockPath, JSON.stringify(lock, null, 2) + "\n");
|
|
10416
|
+
}
|
|
10417
|
+
await writeFile5(
|
|
10418
|
+
join7(directory, "spatius.config.json"),
|
|
10419
|
+
JSON.stringify({ version: 2, stack: "zeabur-agora" }, null, 2) + "\n"
|
|
10420
|
+
);
|
|
10421
|
+
const serverPath = join7(directory, "server/index.ts");
|
|
10422
|
+
await writeFile5(
|
|
10423
|
+
serverPath,
|
|
10424
|
+
(await readFile6(serverPath, "utf8")).replace(
|
|
10425
|
+
"import { handleRequest }",
|
|
10426
|
+
"import type { AgoraEnvironment } from '../worker/agora.js';\nimport { handleRequest }"
|
|
10427
|
+
).replace(
|
|
10428
|
+
"const env = process.env;",
|
|
10429
|
+
"const env = process.env as unknown as AgoraEnvironment;"
|
|
10430
|
+
)
|
|
10431
|
+
);
|
|
10432
|
+
const dockerPath = join7(directory, "Dockerfile");
|
|
10433
|
+
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";
|
|
10434
|
+
await writeFile5(
|
|
10435
|
+
dockerPath,
|
|
10436
|
+
(await readFile6(dockerPath, "utf8")).replace("__SPATIUS_CONTAINER_INSTALL__", install).replace("__SPATIUS_CONTAINER_BUILD__", run("build"))
|
|
10437
|
+
);
|
|
10438
|
+
for (const name of ["README.md", "AGENTS.md"]) {
|
|
10439
|
+
const path2 = join7(directory, name);
|
|
10440
|
+
await writeFile5(
|
|
10441
|
+
path2,
|
|
10442
|
+
(await readFile6(path2, "utf8")).replaceAll("__INSTALL__", javascriptInstallCommand(manager.name)).replaceAll("__DEV__", run("dev")).replaceAll("__CHECK__", run("check"))
|
|
10443
|
+
);
|
|
10444
|
+
}
|
|
10445
|
+
await writeFile5(
|
|
10446
|
+
join7(directory, "DEPLOYMENT.md"),
|
|
10447
|
+
"# 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"
|
|
10448
|
+
);
|
|
10449
|
+
},
|
|
10450
|
+
createInstallPlan(directory, managers, platform) {
|
|
10451
|
+
return createInstallPlan(
|
|
10452
|
+
directory,
|
|
10453
|
+
{ javascript: managers.javascript },
|
|
10454
|
+
platform
|
|
10455
|
+
);
|
|
10456
|
+
},
|
|
10457
|
+
nextSteps({
|
|
10458
|
+
dependenciesInstalled,
|
|
10459
|
+
credentialsConfigured,
|
|
10460
|
+
javascriptPackageManager
|
|
10461
|
+
}) {
|
|
10462
|
+
return [
|
|
10463
|
+
...dependenciesInstalled ? [] : [javascriptInstallCommand(javascriptPackageManager)],
|
|
10464
|
+
...credentialsConfigured ? [] : ["npx create-spatius-app setup . --interactive"],
|
|
10465
|
+
javascriptRunCommand(javascriptPackageManager, "dev")
|
|
10466
|
+
];
|
|
10467
|
+
},
|
|
10468
|
+
setup: {
|
|
10469
|
+
recognizes: async (directory) => (await readProjectConfig(directory))?.stack === "zeabur-agora",
|
|
10470
|
+
run: runAgoraSetup
|
|
10471
|
+
},
|
|
10472
|
+
verification: [
|
|
10473
|
+
{ javascript: "npm", scripts: ["check"] },
|
|
10474
|
+
{ javascript: "pnpm", scripts: ["check"] }
|
|
10475
|
+
]
|
|
10476
|
+
};
|
|
10477
|
+
|
|
10478
|
+
// src/templates/composed.ts
|
|
10479
|
+
init_catalog();
|
|
10480
|
+
import { readFile as readFile7, writeFile as writeFile6 } from "fs/promises";
|
|
10481
|
+
import { join as join8 } from "path";
|
|
10482
|
+
init_package_managers();
|
|
10483
|
+
function composeTemplate(stack) {
|
|
10484
|
+
if (stack === "zeabur-agora") return agoraTemplate;
|
|
10485
|
+
const selected = stacks[stack];
|
|
10486
|
+
const node = selected.web === "railway";
|
|
10487
|
+
return {
|
|
10488
|
+
...cloudflareLivekitTemplate,
|
|
10489
|
+
id: stack,
|
|
10490
|
+
stack,
|
|
10491
|
+
requiresPython: true,
|
|
10492
|
+
description: selected.label,
|
|
10493
|
+
components: [
|
|
10494
|
+
"- web/ React + Spatius AvatarKit",
|
|
10495
|
+
`- ${node ? "server/" : "worker/"} ${selected.web} API`,
|
|
10496
|
+
"- agent/ Python LiveKit agent"
|
|
10497
|
+
],
|
|
10498
|
+
layers: [
|
|
10499
|
+
{ directory: "templates/layers/catalog" },
|
|
10500
|
+
...selected.agent === "railway" ? [{ directory: "templates/layers/railway-agent" }] : [],
|
|
10501
|
+
...node ? [
|
|
10502
|
+
{
|
|
10503
|
+
directory: "templates/layers/node",
|
|
10504
|
+
overrides: [
|
|
10505
|
+
"vite.config.ts",
|
|
10506
|
+
"scripts/dev-web.mjs",
|
|
10507
|
+
"package-lock.json",
|
|
10508
|
+
"pnpm-lock.yaml"
|
|
10509
|
+
]
|
|
10510
|
+
}
|
|
10511
|
+
] : []
|
|
10512
|
+
],
|
|
10513
|
+
includeFile(path, configuration) {
|
|
10514
|
+
if (node && ["wrangler.jsonc"].includes(path)) return false;
|
|
10515
|
+
return cloudflareLivekitTemplate.includeFile(path, configuration);
|
|
10516
|
+
},
|
|
10517
|
+
mapFile(path) {
|
|
10518
|
+
return node && path === "dev.vars.example" ? ".env.local.example" : cloudflareLivekitTemplate.mapFile(path);
|
|
10519
|
+
},
|
|
10520
|
+
async configure(directory, configuration) {
|
|
10521
|
+
await cloudflareLivekitTemplate.configure(directory, configuration);
|
|
10522
|
+
await writeFile6(
|
|
10523
|
+
join8(directory, "spatius.config.json"),
|
|
10524
|
+
JSON.stringify({ version: 2, stack }, null, 2) + "\n"
|
|
10525
|
+
);
|
|
10526
|
+
const packagePath = join8(directory, "package.json");
|
|
10527
|
+
const pkg = JSON.parse(await readFile7(packagePath, "utf8"));
|
|
10528
|
+
const manager = configuration.packageManagers.javascript.name;
|
|
10529
|
+
const run = (script) => javascriptRunCommand(manager, script);
|
|
10530
|
+
let deployment = `# ${selected.label}
|
|
10531
|
+
|
|
10532
|
+
`;
|
|
10533
|
+
if (node) {
|
|
10534
|
+
for (const name of ["README.md", "AGENTS.md", "agent/README.md"]) {
|
|
10535
|
+
const path = join8(directory, name);
|
|
10536
|
+
await writeFile6(
|
|
10537
|
+
path,
|
|
10538
|
+
(await readFile7(path, "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"
|
|
10539
|
+
);
|
|
10540
|
+
}
|
|
10541
|
+
const workerConfigPath = join8(directory, "tsconfig.worker.json");
|
|
10542
|
+
await writeFile6(
|
|
10543
|
+
workerConfigPath,
|
|
10544
|
+
(await readFile7(workerConfigPath, "utf8")).replace(
|
|
10545
|
+
'"types": ["./worker-configuration.d.ts"]',
|
|
10546
|
+
'"types": ["./worker-configuration.d.ts", "node"]'
|
|
10547
|
+
).replace(
|
|
10548
|
+
'"include": ["worker", "worker-configuration.d.ts"]',
|
|
10549
|
+
'"include": ["worker", "worker-configuration.d.ts", "server"]'
|
|
10550
|
+
)
|
|
10551
|
+
);
|
|
10552
|
+
delete pkg.devDependencies["@cloudflare/vite-plugin"];
|
|
10553
|
+
delete pkg.devDependencies.wrangler;
|
|
10554
|
+
pkg.dependencies.tsx = "4.23.13";
|
|
10555
|
+
delete pkg.scripts["cf-typegen"];
|
|
10556
|
+
delete pkg.scripts.wrangler;
|
|
10557
|
+
pkg.scripts.start = "node --env-file-if-exists=.env.local node_modules/tsx/dist/cli.mjs server/index.ts";
|
|
10558
|
+
pkg.scripts.deploy = "railway up";
|
|
10559
|
+
const dockerPath = join8(directory, "Dockerfile");
|
|
10560
|
+
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";
|
|
10561
|
+
await writeFile6(
|
|
10562
|
+
dockerPath,
|
|
10563
|
+
(await readFile7(dockerPath, "utf8")).replace("__SPATIUS_CONTAINER_INSTALL__", install).replace("__SPATIUS_CONTAINER_BUILD__", run("build"))
|
|
10564
|
+
);
|
|
10565
|
+
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";
|
|
10566
|
+
} else {
|
|
10567
|
+
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")}\`.
|
|
10568
|
+
|
|
10569
|
+
`;
|
|
10570
|
+
}
|
|
10571
|
+
if (selected.agent === "railway") {
|
|
10572
|
+
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";
|
|
10573
|
+
} else
|
|
10574
|
+
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";
|
|
10575
|
+
deployment += "\nDeploy the agent before opening conversations. Use separate LiveKit projects for development and production. Local setup never uploads secrets.\n";
|
|
10576
|
+
await writeFile6(join8(directory, "DEPLOYMENT.md"), deployment);
|
|
10577
|
+
await writeFile6(packagePath, JSON.stringify(pkg, null, 2) + "\n");
|
|
10578
|
+
}
|
|
10579
|
+
};
|
|
10580
|
+
}
|
|
10581
|
+
|
|
10582
|
+
// src/templates.ts
|
|
10583
|
+
init_errors();
|
|
10584
|
+
import { fileURLToPath } from "url";
|
|
10585
|
+
|
|
10076
10586
|
// src/scaffold.ts
|
|
10077
10587
|
init_errors();
|
|
10078
10588
|
import { constants } from "fs";
|
|
10079
|
-
import { copyFile, lstat as
|
|
10080
|
-
import { basename as
|
|
10589
|
+
import { copyFile, lstat as lstat6, mkdir, readdir as readdir2, rm as rm4 } from "fs/promises";
|
|
10590
|
+
import { basename as basename4, dirname, isAbsolute, join as join9, win32 as win322 } from "path";
|
|
10081
10591
|
|
|
10082
10592
|
// src/project-directory.ts
|
|
10083
10593
|
init_errors();
|
|
10084
10594
|
import { homedir as homedir2 } from "os";
|
|
10085
|
-
import { lstat as
|
|
10595
|
+
import { lstat as lstat5, readdir } from "fs/promises";
|
|
10086
10596
|
import { parse as parse2, resolve } from "path";
|
|
10087
10597
|
function normalizeTargetDirectory(input, currentWorkingDirectory = process.cwd()) {
|
|
10088
10598
|
return resolve(currentWorkingDirectory, input);
|
|
@@ -10114,7 +10624,7 @@ function assertSafeTargetDirectory(targetDirectory, homeDirectory = homedir2())
|
|
|
10114
10624
|
}
|
|
10115
10625
|
async function inspectTargetDirectory(targetDirectory) {
|
|
10116
10626
|
try {
|
|
10117
|
-
const stats = await
|
|
10627
|
+
const stats = await lstat5(targetDirectory);
|
|
10118
10628
|
if (!stats.isDirectory() || stats.isSymbolicLink()) {
|
|
10119
10629
|
return "not-a-directory";
|
|
10120
10630
|
}
|
|
@@ -10186,10 +10696,10 @@ function assertRelativeDestination(path) {
|
|
|
10186
10696
|
}
|
|
10187
10697
|
}
|
|
10188
10698
|
async function listTemplateFiles(templateDirectory, template, configuration, relativeDirectory = "") {
|
|
10189
|
-
const entries = await readdir2(
|
|
10699
|
+
const entries = await readdir2(join9(templateDirectory, relativeDirectory), {
|
|
10190
10700
|
withFileTypes: true
|
|
10191
10701
|
});
|
|
10192
|
-
const
|
|
10702
|
+
const files2 = [];
|
|
10193
10703
|
for (const entry of entries) {
|
|
10194
10704
|
if (shouldIgnoreTemplateEntry(entry.name)) continue;
|
|
10195
10705
|
const source = [relativeDirectory, entry.name].filter(Boolean).join("/");
|
|
@@ -10198,7 +10708,7 @@ async function listTemplateFiles(templateDirectory, template, configuration, rel
|
|
|
10198
10708
|
throw new Error("Bundled templates must not contain symbolic links.");
|
|
10199
10709
|
}
|
|
10200
10710
|
if (entry.isDirectory()) {
|
|
10201
|
-
|
|
10711
|
+
files2.push(
|
|
10202
10712
|
...await listTemplateFiles(
|
|
10203
10713
|
templateDirectory,
|
|
10204
10714
|
template,
|
|
@@ -10209,12 +10719,12 @@ async function listTemplateFiles(templateDirectory, template, configuration, rel
|
|
|
10209
10719
|
} else if (entry.isFile()) {
|
|
10210
10720
|
const destination = template.mapFile(source);
|
|
10211
10721
|
assertRelativeDestination(destination);
|
|
10212
|
-
|
|
10722
|
+
files2.push({ source, destination });
|
|
10213
10723
|
} else {
|
|
10214
10724
|
throw new Error("Bundled templates must contain regular files.");
|
|
10215
10725
|
}
|
|
10216
10726
|
}
|
|
10217
|
-
return
|
|
10727
|
+
return files2;
|
|
10218
10728
|
}
|
|
10219
10729
|
async function createScaffoldPlan({
|
|
10220
10730
|
configuration,
|
|
@@ -10226,7 +10736,7 @@ async function createScaffoldPlan({
|
|
|
10226
10736
|
const targetState = await assertTargetDirectoryAvailable(targetDirectory);
|
|
10227
10737
|
let entries;
|
|
10228
10738
|
try {
|
|
10229
|
-
const root = await
|
|
10739
|
+
const root = await lstat6(templateDirectory);
|
|
10230
10740
|
if (!root.isDirectory() || root.isSymbolicLink()) {
|
|
10231
10741
|
throw new Error("The template must be a regular directory.");
|
|
10232
10742
|
}
|
|
@@ -10235,6 +10745,33 @@ async function createScaffoldPlan({
|
|
|
10235
10745
|
template,
|
|
10236
10746
|
configuration
|
|
10237
10747
|
);
|
|
10748
|
+
for (const layer of template.layers ?? []) {
|
|
10749
|
+
const directory = resolveTemplateDirectory({
|
|
10750
|
+
...template,
|
|
10751
|
+
directory: layer.directory
|
|
10752
|
+
});
|
|
10753
|
+
const root2 = await lstat6(directory);
|
|
10754
|
+
if (!root2.isDirectory() || root2.isSymbolicLink())
|
|
10755
|
+
throw new Error("Invalid template layer.");
|
|
10756
|
+
const additions = await listTemplateFiles(
|
|
10757
|
+
directory,
|
|
10758
|
+
template,
|
|
10759
|
+
configuration
|
|
10760
|
+
);
|
|
10761
|
+
for (const addition of additions) {
|
|
10762
|
+
const index = entries.findIndex(
|
|
10763
|
+
(entry) => entry.destination === addition.destination
|
|
10764
|
+
);
|
|
10765
|
+
if (index >= 0) {
|
|
10766
|
+
if (!layer.overrides?.includes(addition.destination))
|
|
10767
|
+
throw new Error(
|
|
10768
|
+
`Undeclared template override: ${addition.destination}`
|
|
10769
|
+
);
|
|
10770
|
+
entries.splice(index, 1);
|
|
10771
|
+
}
|
|
10772
|
+
entries.push({ ...addition, directory });
|
|
10773
|
+
}
|
|
10774
|
+
}
|
|
10238
10775
|
const destinations = /* @__PURE__ */ new Set();
|
|
10239
10776
|
const directories = /* @__PURE__ */ new Map();
|
|
10240
10777
|
for (const { destination } of entries) {
|
|
@@ -10293,11 +10830,11 @@ async function createScaffoldPlan({
|
|
|
10293
10830
|
}
|
|
10294
10831
|
async function rollback(targetDirectory, targetCreated, createdPaths) {
|
|
10295
10832
|
if (targetCreated) {
|
|
10296
|
-
await
|
|
10833
|
+
await rm4(targetDirectory, { force: true, recursive: true });
|
|
10297
10834
|
return;
|
|
10298
10835
|
}
|
|
10299
10836
|
for (const path of [...createdPaths].reverse()) {
|
|
10300
|
-
await
|
|
10837
|
+
await rm4(path, { force: true, recursive: true });
|
|
10301
10838
|
}
|
|
10302
10839
|
}
|
|
10303
10840
|
async function scaffoldProject({
|
|
@@ -10332,9 +10869,9 @@ async function scaffoldProject({
|
|
|
10332
10869
|
targetCreated = true;
|
|
10333
10870
|
}
|
|
10334
10871
|
for (const entry of plan.entries) {
|
|
10335
|
-
const source =
|
|
10336
|
-
const destination =
|
|
10337
|
-
const stats = await
|
|
10872
|
+
const source = join9(entry.directory ?? templateDirectory, entry.source);
|
|
10873
|
+
const destination = join9(targetDirectory, entry.destination);
|
|
10874
|
+
const stats = await lstat6(source);
|
|
10338
10875
|
if (!stats.isFile() || stats.isSymbolicLink()) {
|
|
10339
10876
|
throw new Error("A template source is no longer a regular file.");
|
|
10340
10877
|
}
|
|
@@ -10351,7 +10888,7 @@ async function scaffoldProject({
|
|
|
10351
10888
|
);
|
|
10352
10889
|
throw new CliError(
|
|
10353
10890
|
"FILESYSTEM_ERROR",
|
|
10354
|
-
"Could not create " +
|
|
10891
|
+
"Could not create " + basename4(targetDirectory) + ": " + (error instanceof Error ? error.message : "unknown filesystem error"),
|
|
10355
10892
|
{
|
|
10356
10893
|
exitCode: EXIT_CODES.filesystem,
|
|
10357
10894
|
path: targetDirectory,
|
|
@@ -10365,16 +10902,26 @@ async function scaffoldProject({
|
|
|
10365
10902
|
// src/templates.ts
|
|
10366
10903
|
init_package_managers();
|
|
10367
10904
|
var DEFAULT_TEMPLATE_ID = "cloudflare-livekit";
|
|
10368
|
-
var templateRegistry =
|
|
10369
|
-
[
|
|
10370
|
-
|
|
10905
|
+
var templateRegistry = Object.fromEntries(
|
|
10906
|
+
Object.keys(stacks).map((stack) => [
|
|
10907
|
+
stack,
|
|
10908
|
+
composeTemplate(stack)
|
|
10909
|
+
])
|
|
10910
|
+
);
|
|
10371
10911
|
function getTemplate(id = DEFAULT_TEMPLATE_ID) {
|
|
10372
|
-
|
|
10912
|
+
const template = templateRegistry[id];
|
|
10913
|
+
if (!template)
|
|
10914
|
+
throw new CliError("INVALID_ARGUMENT", `Unknown template: ${id}`, {
|
|
10915
|
+
exitCode: EXIT_CODES.invalidArgument
|
|
10916
|
+
});
|
|
10917
|
+
return template;
|
|
10373
10918
|
}
|
|
10374
10919
|
function resolveTemplateDirectory(template = getTemplate()) {
|
|
10375
10920
|
return fileURLToPath(new URL(`../${template.directory}/`, import.meta.url));
|
|
10376
10921
|
}
|
|
10377
|
-
async function resolveProjectTemplate(targetDirectory, templates =
|
|
10922
|
+
async function resolveProjectTemplate(targetDirectory, templates = [getTemplate()]) {
|
|
10923
|
+
const config = await readProjectConfig(targetDirectory);
|
|
10924
|
+
if (config) return getTemplate(config.stack);
|
|
10378
10925
|
const matches = [];
|
|
10379
10926
|
for (const template of templates) {
|
|
10380
10927
|
if (await template.setup.recognizes(targetDirectory))
|
|
@@ -10405,4 +10952,3 @@ export {
|
|
|
10405
10952
|
selectPythonPackageManager,
|
|
10406
10953
|
templateRegistry
|
|
10407
10954
|
};
|
|
10408
|
-
//# sourceMappingURL=templates.js.map
|