create-spatius-app 0.1.2 → 0.1.3-beta.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +18 -6
- package/dist/cli.js +647 -144
- package/dist/templates.js +529 -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/AGENTS.md +2 -0
- package/templates/cloudflare-livekit/agent/README.md +13 -0
- package/templates/cloudflare-livekit/agent/env.example +3 -0
- package/templates/cloudflare-livekit/agent/pyproject.toml +1 -1
- package/templates/cloudflare-livekit/agent/src/agent.py +7 -4
- package/templates/cloudflare-livekit/agent/tests/test_agent_startup.py +49 -0
- package/templates/cloudflare-livekit/agent/uv.lock +14 -14
- package/templates/cloudflare-livekit/gitignore +1 -0
- package/templates/cloudflare-livekit/package-lock.json +247 -158
- package/templates/cloudflare-livekit/package.json +9 -8
- package/templates/cloudflare-livekit/pnpm-lock.yaml +200 -177
- package/templates/cloudflare-livekit/tests/dev-events.test.mjs +23 -0
- package/templates/cloudflare-livekit/tests/dev-fixtures.mjs +14 -5
- package/templates/cloudflare-livekit/tests/dev-supervision.test.mjs +17 -14
- 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 +13 -0
- package/templates/layers/agora/README.md +25 -0
- package/templates/layers/agora/dev.vars.example +9 -0
- package/templates/layers/agora/package-lock.json +6988 -0
- package/templates/layers/agora/package.json +60 -0
- package/templates/layers/agora/pnpm-lock.yaml +4648 -0
- package/templates/layers/agora/scripts/check-worker.mjs +98 -0
- package/templates/layers/agora/tsconfig.worker.json +23 -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 +62 -0
- package/templates/layers/agora/worker/agora.ts +156 -0
- package/templates/layers/agora/worker/index.test.ts +66 -0
- package/templates/layers/agora/worker/index.ts +54 -0
- package/templates/layers/agora/wrangler.jsonc +24 -0
- package/templates/layers/catalog/DEPLOYMENT.md +3 -0
- package/templates/layers/catalog/spatius.config.json +1 -0
- package/dist/cli.js.map +0 -1
- package/dist/templates.js.map +0 -1
- package/templates/cloudflare-livekit/worker-configuration.d.ts +0 -17184
package/dist/templates.js
CHANGED
|
@@ -79,6 +79,78 @@ 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: "LiveKit",
|
|
99
|
+
web: "cloudflare",
|
|
100
|
+
agent: "livekit",
|
|
101
|
+
provider: "livekit"
|
|
102
|
+
},
|
|
103
|
+
"cloudflare-agora": {
|
|
104
|
+
label: "Agora Conversational AI",
|
|
105
|
+
web: "cloudflare",
|
|
106
|
+
agent: "agora",
|
|
107
|
+
provider: "agora"
|
|
108
|
+
}
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
// src/project-config.ts
|
|
114
|
+
import { lstat, readFile } from "fs/promises";
|
|
115
|
+
import { join } from "path";
|
|
116
|
+
async function readProjectConfig(directory) {
|
|
117
|
+
const path = join(directory, "spatius.config.json");
|
|
118
|
+
try {
|
|
119
|
+
const stats = await lstat(path);
|
|
120
|
+
if (!stats.isFile() || stats.isSymbolicLink())
|
|
121
|
+
throw new Error("Expected a regular file");
|
|
122
|
+
const value = JSON.parse(await readFile(path, "utf8"));
|
|
123
|
+
if (value && typeof value === "object" && "version" in value && value.version !== 2)
|
|
124
|
+
throw new CliError(
|
|
125
|
+
"INVALID_ARGUMENT",
|
|
126
|
+
"Unsupported spatius.config.json version. Expected version 2.",
|
|
127
|
+
{ path, exitCode: EXIT_CODES.invalidArgument }
|
|
128
|
+
);
|
|
129
|
+
if (!value || typeof value !== "object" || !("version" in value) || value.version !== 2 || !("stack" in value) || typeof value.stack !== "string")
|
|
130
|
+
throw new Error("Unsupported project configuration");
|
|
131
|
+
return { version: 2, ...validateSelection(value.stack) };
|
|
132
|
+
} catch (error) {
|
|
133
|
+
if (error instanceof CliError) throw error;
|
|
134
|
+
if (error instanceof Error && "code" in error && error.code === "ENOENT")
|
|
135
|
+
return void 0;
|
|
136
|
+
throw new CliError("INVALID_ARGUMENT", "Invalid spatius.config.json.", {
|
|
137
|
+
path,
|
|
138
|
+
exitCode: EXIT_CODES.invalidArgument
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
async function webEnvironmentPath(directory) {
|
|
143
|
+
await readProjectConfig(directory);
|
|
144
|
+
return ".dev.vars";
|
|
145
|
+
}
|
|
146
|
+
var init_project_config = __esm({
|
|
147
|
+
"src/project-config.ts"() {
|
|
148
|
+
"use strict";
|
|
149
|
+
init_catalog();
|
|
150
|
+
init_errors();
|
|
151
|
+
}
|
|
152
|
+
});
|
|
153
|
+
|
|
82
154
|
// src/package-managers.ts
|
|
83
155
|
import { execFile, spawn } from "child_process";
|
|
84
156
|
function requiresWindowsCommandShell(command, platform = process.platform) {
|
|
@@ -201,23 +273,32 @@ var init_package_managers = __esm({
|
|
|
201
273
|
});
|
|
202
274
|
|
|
203
275
|
// src/setup/project.ts
|
|
204
|
-
import { lstat } from "fs/promises";
|
|
205
|
-
import { join } from "path";
|
|
276
|
+
import { lstat as lstat2 } from "fs/promises";
|
|
277
|
+
import { join as join3 } from "path";
|
|
206
278
|
async function assertSpatiusProject(targetDirectory) {
|
|
207
279
|
try {
|
|
208
|
-
const target = await
|
|
280
|
+
const target = await lstat2(targetDirectory);
|
|
209
281
|
if (!target.isDirectory() || target.isSymbolicLink()) {
|
|
210
282
|
throw new Error("not a regular directory");
|
|
211
283
|
}
|
|
284
|
+
const config = await readProjectConfig(targetDirectory);
|
|
285
|
+
const paths = config ? [
|
|
286
|
+
"package.json",
|
|
287
|
+
"spatius.config.json",
|
|
288
|
+
await webEnvironmentPath(targetDirectory) + ".example",
|
|
289
|
+
"worker/index.ts",
|
|
290
|
+
"agent/src/agent.py",
|
|
291
|
+
"agent/.env.example"
|
|
292
|
+
] : requiredProjectPaths;
|
|
212
293
|
await Promise.all([
|
|
213
294
|
...requiredProjectDirectories.map(async (relativePath) => {
|
|
214
|
-
const stats = await
|
|
295
|
+
const stats = await lstat2(join3(targetDirectory, relativePath));
|
|
215
296
|
if (!stats.isDirectory() || stats.isSymbolicLink()) {
|
|
216
297
|
throw new Error(`${relativePath} is not a regular directory`);
|
|
217
298
|
}
|
|
218
299
|
}),
|
|
219
|
-
...
|
|
220
|
-
const stats = await
|
|
300
|
+
...paths.map(async (relativePath) => {
|
|
301
|
+
const stats = await lstat2(join3(targetDirectory, relativePath));
|
|
221
302
|
if (!stats.isFile() || stats.isSymbolicLink()) {
|
|
222
303
|
throw new Error(`${relativePath} is not a regular file`);
|
|
223
304
|
}
|
|
@@ -239,6 +320,7 @@ var requiredProjectPaths, requiredProjectDirectories;
|
|
|
239
320
|
var init_project = __esm({
|
|
240
321
|
"src/setup/project.ts"() {
|
|
241
322
|
"use strict";
|
|
323
|
+
init_project_config();
|
|
242
324
|
init_errors();
|
|
243
325
|
requiredProjectPaths = [
|
|
244
326
|
"package.json",
|
|
@@ -277,14 +359,14 @@ var init_voices = __esm({
|
|
|
277
359
|
// src/setup/environment.ts
|
|
278
360
|
import {
|
|
279
361
|
chmod,
|
|
280
|
-
lstat as
|
|
281
|
-
readFile as
|
|
362
|
+
lstat as lstat3,
|
|
363
|
+
readFile as readFile3,
|
|
282
364
|
rename,
|
|
283
365
|
rm,
|
|
284
366
|
writeFile as writeFile2
|
|
285
367
|
} from "fs/promises";
|
|
286
368
|
import { randomUUID as randomUUID2 } from "crypto";
|
|
287
|
-
import { join as
|
|
369
|
+
import { join as join4 } from "path";
|
|
288
370
|
function parseQuotedValue(rawValue) {
|
|
289
371
|
const value = rawValue.trim();
|
|
290
372
|
if (value.startsWith('"')) {
|
|
@@ -439,7 +521,7 @@ function isMissingFile(error) {
|
|
|
439
521
|
}
|
|
440
522
|
async function readOptionalCredentialFile(path) {
|
|
441
523
|
try {
|
|
442
|
-
const stats = await
|
|
524
|
+
const stats = await lstat3(path);
|
|
443
525
|
if (stats.isSymbolicLink() || !stats.isFile()) {
|
|
444
526
|
throw new CliError(
|
|
445
527
|
"FILESYSTEM_ERROR",
|
|
@@ -451,7 +533,7 @@ async function readOptionalCredentialFile(path) {
|
|
|
451
533
|
}
|
|
452
534
|
);
|
|
453
535
|
}
|
|
454
|
-
return await
|
|
536
|
+
return await readFile3(path, "utf8");
|
|
455
537
|
} catch (error) {
|
|
456
538
|
if (isMissingFile(error)) {
|
|
457
539
|
return void 0;
|
|
@@ -460,10 +542,11 @@ async function readOptionalCredentialFile(path) {
|
|
|
460
542
|
}
|
|
461
543
|
}
|
|
462
544
|
async function readCredentialFileState(targetDirectory) {
|
|
545
|
+
const webPath = await webEnvironmentPath(targetDirectory);
|
|
463
546
|
const [worker, agent, workerExample] = await Promise.all([
|
|
464
|
-
readOptionalCredentialFile(
|
|
465
|
-
readOptionalCredentialFile(
|
|
466
|
-
readOptionalCredentialFile(
|
|
547
|
+
readOptionalCredentialFile(join4(targetDirectory, webPath)),
|
|
548
|
+
readOptionalCredentialFile(join4(targetDirectory, "agent", ".env.local")),
|
|
549
|
+
readOptionalCredentialFile(join4(targetDirectory, webPath + ".example"))
|
|
467
550
|
]);
|
|
468
551
|
return {
|
|
469
552
|
agent,
|
|
@@ -535,23 +618,24 @@ async function pathExists(path, fileSystem) {
|
|
|
535
618
|
}
|
|
536
619
|
}
|
|
537
620
|
async function writeCredentialFilesAtomically(targetDirectory, contents, fileSystem = defaultAtomicFileSystem) {
|
|
621
|
+
const webPath = await webEnvironmentPath(targetDirectory);
|
|
538
622
|
const nonce = randomUUID2();
|
|
539
623
|
const targets = [
|
|
540
624
|
{
|
|
541
625
|
backupMoved: false,
|
|
542
|
-
backupPath:
|
|
543
|
-
destinationPath:
|
|
626
|
+
backupPath: join4(targetDirectory, `.dev.vars.${nonce}.backup`),
|
|
627
|
+
destinationPath: join4(targetDirectory, webPath),
|
|
544
628
|
hadOriginal: false,
|
|
545
629
|
installed: false,
|
|
546
|
-
stagedPath:
|
|
630
|
+
stagedPath: join4(targetDirectory, `.dev.vars.${nonce}.tmp`)
|
|
547
631
|
},
|
|
548
632
|
{
|
|
549
633
|
backupMoved: false,
|
|
550
|
-
backupPath:
|
|
551
|
-
destinationPath:
|
|
634
|
+
backupPath: join4(targetDirectory, "agent", `.env.local.${nonce}.backup`),
|
|
635
|
+
destinationPath: join4(targetDirectory, "agent", ".env.local"),
|
|
552
636
|
hadOriginal: false,
|
|
553
637
|
installed: false,
|
|
554
|
-
stagedPath:
|
|
638
|
+
stagedPath: join4(targetDirectory, "agent", `.env.local.${nonce}.tmp`)
|
|
555
639
|
}
|
|
556
640
|
];
|
|
557
641
|
const payloads = [contents.worker, contents.agent];
|
|
@@ -612,9 +696,10 @@ async function writeCredentialFilesAtomically(targetDirectory, contents, fileSys
|
|
|
612
696
|
}
|
|
613
697
|
}
|
|
614
698
|
async function readCredentialExamples(targetDirectory) {
|
|
699
|
+
const webPath = await webEnvironmentPath(targetDirectory);
|
|
615
700
|
const [worker, agent] = await Promise.all([
|
|
616
|
-
|
|
617
|
-
|
|
701
|
+
readFile3(join4(targetDirectory, webPath + ".example"), "utf8"),
|
|
702
|
+
readFile3(join4(targetDirectory, "agent", ".env.example"), "utf8")
|
|
618
703
|
]);
|
|
619
704
|
return { agent, worker };
|
|
620
705
|
}
|
|
@@ -622,6 +707,7 @@ var WORKER_MANAGED_KEYS, AGENT_MANAGED_KEYS, assignmentPattern, defaultAtomicFil
|
|
|
622
707
|
var init_environment = __esm({
|
|
623
708
|
"src/setup/environment.ts"() {
|
|
624
709
|
"use strict";
|
|
710
|
+
init_project_config();
|
|
625
711
|
init_errors();
|
|
626
712
|
WORKER_MANAGED_KEYS = [
|
|
627
713
|
"LIVEKIT_URL",
|
|
@@ -641,8 +727,8 @@ var init_environment = __esm({
|
|
|
641
727
|
assignmentPattern = /^\s*(?:export\s+)?([A-Za-z_][A-Za-z\d_]*)\s*=\s*(.*)$/u;
|
|
642
728
|
defaultAtomicFileSystem = {
|
|
643
729
|
chmod,
|
|
644
|
-
lstat:
|
|
645
|
-
readFile: (path) =>
|
|
730
|
+
lstat: lstat3,
|
|
731
|
+
readFile: (path) => readFile3(path),
|
|
646
732
|
rename,
|
|
647
733
|
rm: (path) => rm(path, { force: true }),
|
|
648
734
|
writeFile: writeFile2
|
|
@@ -7980,12 +8066,12 @@ var require_dist = __commonJS({
|
|
|
7980
8066
|
// src/setup/livekit.ts
|
|
7981
8067
|
import { createHmac } from "crypto";
|
|
7982
8068
|
import { spawn as spawn2 } from "child_process";
|
|
7983
|
-
import { chmod as chmod2, mkdtemp, readFile as
|
|
8069
|
+
import { chmod as chmod2, mkdtemp, readFile as readFile4, rm as rm2, writeFile as writeFile3 } from "fs/promises";
|
|
7984
8070
|
import { homedir, tmpdir } from "os";
|
|
7985
|
-
import { join as
|
|
7986
|
-
async function readLiveKitProjects(configPath =
|
|
8071
|
+
import { join as join5 } from "path";
|
|
8072
|
+
async function readLiveKitProjects(configPath = join5(homedir(), ".livekit", "cli-config.yaml")) {
|
|
7987
8073
|
try {
|
|
7988
|
-
const config = (0, import_yaml.parse)(await
|
|
8074
|
+
const config = (0, import_yaml.parse)(await readFile4(configPath, "utf8"));
|
|
7989
8075
|
if (config == null) return [];
|
|
7990
8076
|
if (typeof config !== "object" || !("projects" in config) || !Array.isArray(config.projects))
|
|
7991
8077
|
throw new Error();
|
|
@@ -8148,12 +8234,12 @@ async function loadLiveKitCredentialsWithCli({
|
|
|
8148
8234
|
projectName
|
|
8149
8235
|
} = {}) {
|
|
8150
8236
|
const directory = await fileSystem.mkdtemp(
|
|
8151
|
-
|
|
8237
|
+
join5(temporaryRoot, "create-spatius-app-livekit-")
|
|
8152
8238
|
);
|
|
8153
8239
|
try {
|
|
8154
8240
|
await fileSystem.chmod(directory, 448);
|
|
8155
8241
|
await fileSystem.writeFile(
|
|
8156
|
-
|
|
8242
|
+
join5(directory, ".env.example"),
|
|
8157
8243
|
"LIVEKIT_URL=\nLIVEKIT_API_KEY=\nLIVEKIT_API_SECRET=\n",
|
|
8158
8244
|
384
|
|
8159
8245
|
);
|
|
@@ -8172,7 +8258,7 @@ async function loadLiveKitCredentialsWithCli({
|
|
|
8172
8258
|
}
|
|
8173
8259
|
);
|
|
8174
8260
|
const values = parseDotenv(
|
|
8175
|
-
await fileSystem.readFile(
|
|
8261
|
+
await fileSystem.readFile(join5(directory, ".env.local"))
|
|
8176
8262
|
);
|
|
8177
8263
|
const validated = validateCredentialBundle({
|
|
8178
8264
|
liveKit: {
|
|
@@ -8272,7 +8358,7 @@ var init_livekit = __esm({
|
|
|
8272
8358
|
defaultTemporaryFileSystem = {
|
|
8273
8359
|
chmod: chmod2,
|
|
8274
8360
|
mkdtemp,
|
|
8275
|
-
readFile: (path) =>
|
|
8361
|
+
readFile: (path) => readFile4(path, "utf8"),
|
|
8276
8362
|
remove: (path) => rm2(path, {
|
|
8277
8363
|
force: true,
|
|
8278
8364
|
maxRetries: 3,
|
|
@@ -9723,7 +9809,7 @@ async function runCredentialSetup({
|
|
|
9723
9809
|
const examples = await readExamples(targetDirectory);
|
|
9724
9810
|
const rendered = buildCredentialFileContents(state, examples, credentials);
|
|
9725
9811
|
await writeFiles(targetDirectory, rendered);
|
|
9726
|
-
onStatus("Saved
|
|
9812
|
+
onStatus("Saved local web and agent credentials for local development only.");
|
|
9727
9813
|
return "configured";
|
|
9728
9814
|
}
|
|
9729
9815
|
var init_wizard = __esm({
|
|
@@ -9743,18 +9829,22 @@ var init_wizard = __esm({
|
|
|
9743
9829
|
});
|
|
9744
9830
|
|
|
9745
9831
|
// src/templates.ts
|
|
9746
|
-
|
|
9747
|
-
|
|
9832
|
+
init_project_config();
|
|
9833
|
+
init_catalog();
|
|
9748
9834
|
|
|
9749
|
-
// src/templates/
|
|
9750
|
-
|
|
9835
|
+
// src/templates/composed.ts
|
|
9836
|
+
import { writeFile as writeFile6 } from "fs/promises";
|
|
9837
|
+
import { join as join8 } from "path";
|
|
9838
|
+
|
|
9839
|
+
// src/templates/agora.ts
|
|
9751
9840
|
init_package_managers();
|
|
9752
|
-
|
|
9841
|
+
import { readFile as readFile6, writeFile as writeFile5 } from "fs/promises";
|
|
9842
|
+
import { basename as basename3, join as join7 } from "path";
|
|
9753
9843
|
|
|
9754
9844
|
// src/templates/cloudflare-livekit/configure.ts
|
|
9755
9845
|
init_package_managers();
|
|
9756
9846
|
import { randomUUID } from "crypto";
|
|
9757
|
-
import { readFile, writeFile } from "fs/promises";
|
|
9847
|
+
import { access, readFile as readFile2, writeFile } from "fs/promises";
|
|
9758
9848
|
import { basename, join as join2 } from "path";
|
|
9759
9849
|
function toValidPackageName(value) {
|
|
9760
9850
|
const normalized = value.trim().toLowerCase().replace(/\s+/gu, "-").replace(/^[._-]+/u, "").replace(/[^a-z\d~._-]+/gu, "-").replace(/-+/gu, "-").slice(0, 214).replace(/-$/u, "");
|
|
@@ -9770,6 +9860,7 @@ function npmRequiresScriptAllowlist(version) {
|
|
|
9770
9860
|
function createReplacements(packageManagers, platform) {
|
|
9771
9861
|
const javascript = packageManagers.javascript;
|
|
9772
9862
|
const python = packageManagers.python;
|
|
9863
|
+
if (!python) throw new Error("LiveKit templates require Python tooling.");
|
|
9773
9864
|
return /* @__PURE__ */ new Map([
|
|
9774
9865
|
[
|
|
9775
9866
|
"{{SPATIUS_JAVASCRIPT_PACKAGE_MANAGER}}",
|
|
@@ -9841,7 +9932,7 @@ function createReplacements(packageManagers, platform) {
|
|
|
9841
9932
|
]);
|
|
9842
9933
|
}
|
|
9843
9934
|
async function renderFile(path, replacements) {
|
|
9844
|
-
let contents = await
|
|
9935
|
+
let contents = await readFile2(path, "utf8");
|
|
9845
9936
|
for (const [token, replacement] of replacements) {
|
|
9846
9937
|
contents = contents.replaceAll(token, replacement);
|
|
9847
9938
|
}
|
|
@@ -9853,7 +9944,7 @@ async function configureGeneratedTemplate(targetDirectory, {
|
|
|
9853
9944
|
projectName = basename(targetDirectory)
|
|
9854
9945
|
}) {
|
|
9855
9946
|
const packagePath = join2(targetDirectory, "package.json");
|
|
9856
|
-
const packageContents = await
|
|
9947
|
+
const packageContents = await readFile2(packagePath, "utf8");
|
|
9857
9948
|
const metadata = JSON.parse(packageContents);
|
|
9858
9949
|
const javascript = packageManagers.javascript;
|
|
9859
9950
|
const run = (script) => javascriptRunCommand(javascript.name, script);
|
|
@@ -9892,7 +9983,7 @@ async function configureGeneratedTemplate(targetDirectory, {
|
|
|
9892
9983
|
if (javascript.name === "npm") {
|
|
9893
9984
|
const packageLockPath = join2(targetDirectory, "package-lock.json");
|
|
9894
9985
|
const packageLock = JSON.parse(
|
|
9895
|
-
await
|
|
9986
|
+
await readFile2(packageLockPath, "utf8")
|
|
9896
9987
|
);
|
|
9897
9988
|
packageLock.name = metadata.name;
|
|
9898
9989
|
if (packageLock.packages?.[""] !== void 0) {
|
|
@@ -9911,12 +10002,18 @@ async function configureGeneratedTemplate(targetDirectory, {
|
|
|
9911
10002
|
await Promise.all(
|
|
9912
10003
|
[
|
|
9913
10004
|
".dev.vars.example",
|
|
10005
|
+
".env.local.example",
|
|
9914
10006
|
"wrangler.jsonc",
|
|
9915
10007
|
"agent/src/agent.py",
|
|
9916
10008
|
"agent/README.md",
|
|
9917
10009
|
"worker/index.test.ts"
|
|
9918
10010
|
].map(
|
|
9919
|
-
async (relativePath) =>
|
|
10011
|
+
async (relativePath) => access(join2(targetDirectory, relativePath)).then(
|
|
10012
|
+
() => renderFile(join2(targetDirectory, relativePath), agentReplacements)
|
|
10013
|
+
).catch((error) => {
|
|
10014
|
+
if (!(error instanceof Error && "code" in error && error.code === "ENOENT"))
|
|
10015
|
+
throw error;
|
|
10016
|
+
})
|
|
9920
10017
|
)
|
|
9921
10018
|
);
|
|
9922
10019
|
await Promise.all(
|
|
@@ -9951,6 +10048,7 @@ function createInstallPlan(targetDirectory, packageManagers, platform = process.
|
|
|
9951
10048
|
`Installing JavaScript dependencies with ${javascript.name}`
|
|
9952
10049
|
)
|
|
9953
10050
|
];
|
|
10051
|
+
if (!python) return steps;
|
|
9954
10052
|
if (python.name === "uv") {
|
|
9955
10053
|
steps.push(
|
|
9956
10054
|
commandStep(
|
|
@@ -9989,6 +10087,9 @@ function createInstallPlan(targetDirectory, packageManagers, platform = process.
|
|
|
9989
10087
|
}
|
|
9990
10088
|
|
|
9991
10089
|
// src/templates/cloudflare-livekit/index.ts
|
|
10090
|
+
init_errors();
|
|
10091
|
+
init_package_managers();
|
|
10092
|
+
init_project();
|
|
9992
10093
|
var credentialSetupStep = {
|
|
9993
10094
|
command: "npx create-spatius-app setup . --interactive",
|
|
9994
10095
|
reason: "Connect LiveKit and Spatius accounts and select an avatar and voice in a secure interactive terminal. Never paste secrets into chat.",
|
|
@@ -10016,8 +10117,9 @@ var cloudflareLivekitTemplate = {
|
|
|
10016
10117
|
"- agent/ Python LiveKit agent"
|
|
10017
10118
|
],
|
|
10018
10119
|
includeFile(path, configuration) {
|
|
10120
|
+
if (path === "worker-configuration.d.ts") return false;
|
|
10019
10121
|
const javascript = configuration?.packageManagers.javascript.name ?? "pnpm";
|
|
10020
|
-
const python = configuration?.packageManagers.python
|
|
10122
|
+
const python = configuration?.packageManagers.python?.name ?? "uv";
|
|
10021
10123
|
if (path === "package-lock.json") return javascript === "npm";
|
|
10022
10124
|
if (["npmrc", "pnpm-lock.yaml", "pnpm-workspace.yaml"].includes(path)) {
|
|
10023
10125
|
return javascript === "pnpm";
|
|
@@ -10041,6 +10143,8 @@ var cloudflareLivekitTemplate = {
|
|
|
10041
10143
|
platform,
|
|
10042
10144
|
pythonPackageManager
|
|
10043
10145
|
}) {
|
|
10146
|
+
if (!pythonPackageManager)
|
|
10147
|
+
throw new Error("LiveKit templates require Python tooling.");
|
|
10044
10148
|
return [
|
|
10045
10149
|
...dependenciesInstalled ? [] : [
|
|
10046
10150
|
javascriptInstallCommand(javascriptPackageManager),
|
|
@@ -10073,16 +10177,339 @@ var cloudflareLivekitTemplate = {
|
|
|
10073
10177
|
]
|
|
10074
10178
|
};
|
|
10075
10179
|
|
|
10180
|
+
// src/setup/agora.ts
|
|
10181
|
+
init_environment();
|
|
10182
|
+
init_errors();
|
|
10183
|
+
init_project_config();
|
|
10184
|
+
import { lstat as lstat4, readFile as readFile5, rename as rename2, rm as rm3, writeFile as writeFile4 } from "fs/promises";
|
|
10185
|
+
import { join as join6 } from "path";
|
|
10186
|
+
import { randomUUID as randomUUID3 } from "crypto";
|
|
10187
|
+
var runAgoraSetup = async ({
|
|
10188
|
+
targetDirectory,
|
|
10189
|
+
prompts,
|
|
10190
|
+
onStatus = () => void 0
|
|
10191
|
+
}) => {
|
|
10192
|
+
const target = await lstat4(targetDirectory);
|
|
10193
|
+
if (!target.isDirectory() || target.isSymbolicLink())
|
|
10194
|
+
throw new CliError(
|
|
10195
|
+
"INVALID_ARGUMENT",
|
|
10196
|
+
"Expected a regular project directory.",
|
|
10197
|
+
{ exitCode: EXIT_CODES.invalidArgument }
|
|
10198
|
+
);
|
|
10199
|
+
const config = await readProjectConfig(targetDirectory);
|
|
10200
|
+
if (config?.stack !== "cloudflare-agora")
|
|
10201
|
+
throw new CliError(
|
|
10202
|
+
"INVALID_ARGUMENT",
|
|
10203
|
+
"Expected a Cloudflare + Agora project.",
|
|
10204
|
+
{ exitCode: EXIT_CODES.invalidArgument }
|
|
10205
|
+
);
|
|
10206
|
+
for (const name of ["package.json", ".dev.vars.example", "worker/agora.ts"]) {
|
|
10207
|
+
const stats = await lstat4(join6(targetDirectory, name));
|
|
10208
|
+
if (!stats.isFile() || stats.isSymbolicLink())
|
|
10209
|
+
throw new CliError(
|
|
10210
|
+
"INVALID_ARGUMENT",
|
|
10211
|
+
"Expected regular project files.",
|
|
10212
|
+
{ exitCode: EXIT_CODES.invalidArgument }
|
|
10213
|
+
);
|
|
10214
|
+
}
|
|
10215
|
+
const path = join6(targetDirectory, ".dev.vars");
|
|
10216
|
+
let existing;
|
|
10217
|
+
try {
|
|
10218
|
+
const stats = await lstat4(path);
|
|
10219
|
+
if (!stats.isFile() || stats.isSymbolicLink())
|
|
10220
|
+
throw new Error("Unsafe credential path");
|
|
10221
|
+
existing = await readFile5(path, "utf8");
|
|
10222
|
+
} catch (error) {
|
|
10223
|
+
if (!(error instanceof Error && "code" in error && error.code === "ENOENT"))
|
|
10224
|
+
throw new CliError("FILESYSTEM_ERROR", "Cannot safely read .dev.vars.", {
|
|
10225
|
+
exitCode: EXIT_CODES.filesystem,
|
|
10226
|
+
path
|
|
10227
|
+
});
|
|
10228
|
+
}
|
|
10229
|
+
if (existing && !await prompts.confirm(
|
|
10230
|
+
"Replace existing Agora and Spatius credentials? Other variables are preserved.",
|
|
10231
|
+
false
|
|
10232
|
+
))
|
|
10233
|
+
return "unchanged";
|
|
10234
|
+
const values = {};
|
|
10235
|
+
const previous = parseDotenv(existing ?? "");
|
|
10236
|
+
const secretKeys = ["AGORA_APP_CERTIFICATE", "SPATIUS_API_KEY"];
|
|
10237
|
+
for (const key of [
|
|
10238
|
+
"AGORA_APP_ID",
|
|
10239
|
+
"AGORA_APP_CERTIFICATE",
|
|
10240
|
+
"AGORA_PIPELINE_ID",
|
|
10241
|
+
"SPATIUS_APP_ID",
|
|
10242
|
+
"SPATIUS_API_KEY",
|
|
10243
|
+
"SPATIUS_AVATAR_ID",
|
|
10244
|
+
"SPATIUS_REGION",
|
|
10245
|
+
"AGORA_AVATAR_SAMPLE_RATE"
|
|
10246
|
+
]) {
|
|
10247
|
+
const defaults = {
|
|
10248
|
+
SPATIUS_REGION: "cn-beijing",
|
|
10249
|
+
AGORA_AVATAR_SAMPLE_RATE: "24000"
|
|
10250
|
+
};
|
|
10251
|
+
while (true) {
|
|
10252
|
+
const value = secretKeys.includes(key) ? await prompts.password(key) : await prompts.input(key, {
|
|
10253
|
+
initialValue: previous.get(key) ?? defaults[key]
|
|
10254
|
+
});
|
|
10255
|
+
if (isPlaceholderValue(value) || new RegExp("\\p{Cc}", "u").test(value)) {
|
|
10256
|
+
onStatus("Enter a non-empty value without control characters.");
|
|
10257
|
+
continue;
|
|
10258
|
+
}
|
|
10259
|
+
if (["AGORA_APP_ID", "AGORA_APP_CERTIFICATE"].includes(key) && !/^[a-f\d]{32}$/iu.test(value)) {
|
|
10260
|
+
onStatus(
|
|
10261
|
+
"Agora App ID and certificate must contain 32 hexadecimal characters."
|
|
10262
|
+
);
|
|
10263
|
+
continue;
|
|
10264
|
+
}
|
|
10265
|
+
if (key === "AGORA_AVATAR_SAMPLE_RATE" && ![8e3, 16e3, 22050, 24e3, 32e3, 44100, 48e3].includes(
|
|
10266
|
+
Number(value)
|
|
10267
|
+
)) {
|
|
10268
|
+
onStatus("Choose a supported audio sample rate.");
|
|
10269
|
+
continue;
|
|
10270
|
+
}
|
|
10271
|
+
values[key] = value;
|
|
10272
|
+
break;
|
|
10273
|
+
}
|
|
10274
|
+
}
|
|
10275
|
+
const rendered = mergeDotenv(
|
|
10276
|
+
existing ?? await readFile5(join6(targetDirectory, ".dev.vars.example"), "utf8"),
|
|
10277
|
+
values
|
|
10278
|
+
);
|
|
10279
|
+
const temporary = join6(targetDirectory, `.dev.vars.${randomUUID3()}.tmp`);
|
|
10280
|
+
try {
|
|
10281
|
+
await writeFile4(temporary, rendered, { flag: "wx", mode: 384 });
|
|
10282
|
+
await rename2(temporary, path);
|
|
10283
|
+
} finally {
|
|
10284
|
+
await rm3(temporary, { force: true });
|
|
10285
|
+
}
|
|
10286
|
+
onStatus(
|
|
10287
|
+
"Saved .dev.vars. Configure an English assistant in the published Agora pipeline, with its TTS sample rate matching AGORA_AVATAR_SAMPLE_RATE. No cloud services were started."
|
|
10288
|
+
);
|
|
10289
|
+
return "configured";
|
|
10290
|
+
};
|
|
10291
|
+
|
|
10292
|
+
// src/templates/agora.ts
|
|
10293
|
+
init_project_config();
|
|
10294
|
+
var files = /* @__PURE__ */ new Set([
|
|
10295
|
+
"package.json",
|
|
10296
|
+
"package-lock.json",
|
|
10297
|
+
"pnpm-lock.yaml",
|
|
10298
|
+
"pnpm-workspace.yaml",
|
|
10299
|
+
"npmrc",
|
|
10300
|
+
"gitignore",
|
|
10301
|
+
".editorconfig",
|
|
10302
|
+
".prettierrc.json",
|
|
10303
|
+
".prettierignore",
|
|
10304
|
+
"README.md",
|
|
10305
|
+
"AGENTS.md",
|
|
10306
|
+
"DEPLOYMENT.md",
|
|
10307
|
+
"spatius.config.json",
|
|
10308
|
+
"index.html",
|
|
10309
|
+
"tsconfig.json",
|
|
10310
|
+
"tsconfig.web.json",
|
|
10311
|
+
"tsconfig.worker.json",
|
|
10312
|
+
"tsconfig.node.json",
|
|
10313
|
+
"vite.config.ts",
|
|
10314
|
+
"vitest.config.ts",
|
|
10315
|
+
"eslint.config.mjs",
|
|
10316
|
+
"dev.vars.example",
|
|
10317
|
+
"wrangler.jsonc",
|
|
10318
|
+
"scripts/check-worker.mjs",
|
|
10319
|
+
"worker/index.ts",
|
|
10320
|
+
"worker/index.test.ts",
|
|
10321
|
+
"worker/agora.ts",
|
|
10322
|
+
"worker/agora.test.ts",
|
|
10323
|
+
"worker/capability.ts",
|
|
10324
|
+
"worker/capability.test.ts",
|
|
10325
|
+
"web/App.tsx",
|
|
10326
|
+
"web/main.tsx",
|
|
10327
|
+
"web/styles.css",
|
|
10328
|
+
"web/agora-session.ts",
|
|
10329
|
+
"web/agora-session.test.ts",
|
|
10330
|
+
"web/components/transcript-view.tsx",
|
|
10331
|
+
"web/components/transcript-panel.tsx",
|
|
10332
|
+
"web/components/icons.tsx",
|
|
10333
|
+
"web/components/agents-ui/agent-chat-input.tsx",
|
|
10334
|
+
"web/components/agents-ui/LICENSE",
|
|
10335
|
+
"web/components/agents-ui/PROVENANCE.md"
|
|
10336
|
+
]);
|
|
10337
|
+
var agoraTemplate = {
|
|
10338
|
+
id: "cloudflare-agora",
|
|
10339
|
+
stack: "cloudflare-agora",
|
|
10340
|
+
requiresPython: false,
|
|
10341
|
+
humanSteps: [
|
|
10342
|
+
{
|
|
10343
|
+
command: "npx create-spatius-app setup . --interactive",
|
|
10344
|
+
reason: "Enter Agora and Spatius credentials in a secure interactive terminal and configure a published English Agora pipeline. Never paste secrets into chat.",
|
|
10345
|
+
requiresHuman: true,
|
|
10346
|
+
requiresTty: true
|
|
10347
|
+
}
|
|
10348
|
+
],
|
|
10349
|
+
directory: "templates/cloudflare-livekit",
|
|
10350
|
+
description: "Cloudflare Workers + Agora Conversational AI",
|
|
10351
|
+
components: [
|
|
10352
|
+
"- web/ React + Spatius AvatarKit + Agora",
|
|
10353
|
+
"- worker/ Cloudflare API; Agora hosts the conversational agent"
|
|
10354
|
+
],
|
|
10355
|
+
layers: [
|
|
10356
|
+
{ directory: "templates/layers/catalog" },
|
|
10357
|
+
{
|
|
10358
|
+
directory: "templates/layers/agora",
|
|
10359
|
+
overrides: [
|
|
10360
|
+
"package.json",
|
|
10361
|
+
"package-lock.json",
|
|
10362
|
+
"pnpm-lock.yaml",
|
|
10363
|
+
"web/App.tsx",
|
|
10364
|
+
"worker/index.ts",
|
|
10365
|
+
"worker/index.test.ts",
|
|
10366
|
+
"tsconfig.worker.json",
|
|
10367
|
+
".dev.vars.example",
|
|
10368
|
+
"wrangler.jsonc",
|
|
10369
|
+
"vitest.config.ts",
|
|
10370
|
+
"README.md",
|
|
10371
|
+
"AGENTS.md"
|
|
10372
|
+
]
|
|
10373
|
+
}
|
|
10374
|
+
],
|
|
10375
|
+
includeFile(path, configuration) {
|
|
10376
|
+
if (!cloudflareLivekitTemplate.includeFile(path, configuration)) return false;
|
|
10377
|
+
return files.has(path) || [...files].some((file) => file.startsWith(path + "/")) || path === "web/assets" || path.startsWith("web/assets/");
|
|
10378
|
+
},
|
|
10379
|
+
mapFile(path) {
|
|
10380
|
+
return cloudflareLivekitTemplate.mapFile(path);
|
|
10381
|
+
},
|
|
10382
|
+
async configure(directory, configuration) {
|
|
10383
|
+
const manager = configuration.packageManagers.javascript;
|
|
10384
|
+
const run = (script) => javascriptRunCommand(manager.name, script);
|
|
10385
|
+
const path = join7(directory, "package.json");
|
|
10386
|
+
const pkg = JSON.parse(await readFile6(path, "utf8"));
|
|
10387
|
+
pkg.name = toValidPackageName(
|
|
10388
|
+
configuration.projectName ?? basename3(directory)
|
|
10389
|
+
);
|
|
10390
|
+
if (manager.version && manager.version !== "unknown")
|
|
10391
|
+
pkg.packageManager = `${manager.name}@${manager.version}`;
|
|
10392
|
+
else delete pkg.packageManager;
|
|
10393
|
+
pkg.scripts.check = [
|
|
10394
|
+
"format:check",
|
|
10395
|
+
"lint",
|
|
10396
|
+
"typecheck",
|
|
10397
|
+
"test",
|
|
10398
|
+
"build",
|
|
10399
|
+
"test:worker"
|
|
10400
|
+
].map(run).join(" && ");
|
|
10401
|
+
pkg.scripts.deploy = `${run("build")} && wrangler deploy`;
|
|
10402
|
+
if (manager.name === "bun")
|
|
10403
|
+
pkg.trustedDependencies = ["core-js", "esbuild", "workerd"];
|
|
10404
|
+
else if (manager.name === "npm" && Number.parseInt(manager.version ?? "", 10) >= 12)
|
|
10405
|
+
pkg.allowScripts = { "core-js": true, esbuild: true, workerd: true };
|
|
10406
|
+
await writeFile5(path, JSON.stringify(pkg, null, 2) + "\n");
|
|
10407
|
+
if (manager.name === "npm") {
|
|
10408
|
+
const lockPath = join7(directory, "package-lock.json");
|
|
10409
|
+
const lock = JSON.parse(await readFile6(lockPath, "utf8"));
|
|
10410
|
+
lock.name = pkg.name;
|
|
10411
|
+
lock.packages[""].name = pkg.name;
|
|
10412
|
+
await writeFile5(lockPath, JSON.stringify(lock, null, 2) + "\n");
|
|
10413
|
+
}
|
|
10414
|
+
await writeFile5(
|
|
10415
|
+
join7(directory, "spatius.config.json"),
|
|
10416
|
+
JSON.stringify({ version: 2, stack: "cloudflare-agora" }, null, 2) + "\n"
|
|
10417
|
+
);
|
|
10418
|
+
for (const name of ["README.md", "AGENTS.md"]) {
|
|
10419
|
+
const path2 = join7(directory, name);
|
|
10420
|
+
await writeFile5(
|
|
10421
|
+
path2,
|
|
10422
|
+
(await readFile6(path2, "utf8")).replaceAll("__INSTALL__", javascriptInstallCommand(manager.name)).replaceAll("__DEV__", run("dev")).replaceAll("__CHECK__", run("check"))
|
|
10423
|
+
);
|
|
10424
|
+
}
|
|
10425
|
+
await writeFile5(
|
|
10426
|
+
join7(directory, "DEPLOYMENT.md"),
|
|
10427
|
+
`# Cloudflare Workers + Agora Conversational AI
|
|
10428
|
+
|
|
10429
|
+
Run \`${run("wrangler -- login")}\`. Set your non-secret configuration from .dev.vars in the vars section of wrangler.jsonc. Upload AGORA_APP_CERTIFICATE and SPATIUS_API_KEY with \`${run("wrangler -- secret put AGORA_APP_CERTIFICATE")}\` (repeat for SPATIUS_API_KEY). Run \`${run("deploy")}\` to deploy the React frontend and API as one Cloudflare Worker.
|
|
10430
|
+
|
|
10431
|
+
Agora hosts the conversational agent. There is no Python worker, Node server, container, or database to deploy. 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.
|
|
10432
|
+
|
|
10433
|
+
Session creation starts a hosted agent. Disconnect and initialization failures stop it through the Worker API. 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.
|
|
10434
|
+
|
|
10435
|
+
Local setup writes .dev.vars only. It never provisions Cloudflare services or uploads secrets.
|
|
10436
|
+
`
|
|
10437
|
+
);
|
|
10438
|
+
},
|
|
10439
|
+
createInstallPlan(directory, managers, platform) {
|
|
10440
|
+
return createInstallPlan(
|
|
10441
|
+
directory,
|
|
10442
|
+
{ javascript: managers.javascript },
|
|
10443
|
+
platform
|
|
10444
|
+
);
|
|
10445
|
+
},
|
|
10446
|
+
nextSteps({
|
|
10447
|
+
dependenciesInstalled,
|
|
10448
|
+
credentialsConfigured,
|
|
10449
|
+
javascriptPackageManager
|
|
10450
|
+
}) {
|
|
10451
|
+
return [
|
|
10452
|
+
...dependenciesInstalled ? [] : [javascriptInstallCommand(javascriptPackageManager)],
|
|
10453
|
+
...credentialsConfigured ? [] : ["npx create-spatius-app setup . --interactive"],
|
|
10454
|
+
javascriptRunCommand(javascriptPackageManager, "dev")
|
|
10455
|
+
];
|
|
10456
|
+
},
|
|
10457
|
+
setup: {
|
|
10458
|
+
recognizes: async (directory) => (await readProjectConfig(directory))?.stack === "cloudflare-agora",
|
|
10459
|
+
run: runAgoraSetup
|
|
10460
|
+
},
|
|
10461
|
+
verification: [
|
|
10462
|
+
{ javascript: "npm", scripts: ["check"] },
|
|
10463
|
+
{ javascript: "pnpm", scripts: ["check"] }
|
|
10464
|
+
]
|
|
10465
|
+
};
|
|
10466
|
+
|
|
10467
|
+
// src/templates/composed.ts
|
|
10468
|
+
init_package_managers();
|
|
10469
|
+
function composeTemplate(stack) {
|
|
10470
|
+
if (stack === "cloudflare-agora") return agoraTemplate;
|
|
10471
|
+
return {
|
|
10472
|
+
...cloudflareLivekitTemplate,
|
|
10473
|
+
stack,
|
|
10474
|
+
requiresPython: true,
|
|
10475
|
+
layers: [{ directory: "templates/layers/catalog" }],
|
|
10476
|
+
async configure(directory, configuration) {
|
|
10477
|
+
await cloudflareLivekitTemplate.configure(directory, configuration);
|
|
10478
|
+
await writeFile6(
|
|
10479
|
+
join8(directory, "spatius.config.json"),
|
|
10480
|
+
JSON.stringify({ version: 2, stack }, null, 2) + "\n"
|
|
10481
|
+
);
|
|
10482
|
+
const manager = configuration.packageManagers.javascript.name;
|
|
10483
|
+
const run = (script) => javascriptRunCommand(manager, script);
|
|
10484
|
+
await writeFile6(
|
|
10485
|
+
join8(directory, "DEPLOYMENT.md"),
|
|
10486
|
+
`# Cloudflare Workers + LiveKit Cloud
|
|
10487
|
+
|
|
10488
|
+
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")}\` to deploy the frontend and API together.
|
|
10489
|
+
|
|
10490
|
+
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.
|
|
10491
|
+
|
|
10492
|
+
Deploy the agent before opening conversations. Use separate LiveKit projects for development and production. Local setup never uploads secrets.
|
|
10493
|
+
`
|
|
10494
|
+
);
|
|
10495
|
+
}
|
|
10496
|
+
};
|
|
10497
|
+
}
|
|
10498
|
+
|
|
10499
|
+
// src/templates.ts
|
|
10500
|
+
init_errors();
|
|
10501
|
+
import { fileURLToPath } from "url";
|
|
10502
|
+
|
|
10076
10503
|
// src/scaffold.ts
|
|
10077
10504
|
init_errors();
|
|
10078
10505
|
import { constants } from "fs";
|
|
10079
|
-
import { copyFile, lstat as
|
|
10080
|
-
import { basename as
|
|
10506
|
+
import { copyFile, lstat as lstat6, mkdir, readdir as readdir2, rm as rm4 } from "fs/promises";
|
|
10507
|
+
import { basename as basename4, dirname, isAbsolute, join as join9, win32 as win322 } from "path";
|
|
10081
10508
|
|
|
10082
10509
|
// src/project-directory.ts
|
|
10083
10510
|
init_errors();
|
|
10084
10511
|
import { homedir as homedir2 } from "os";
|
|
10085
|
-
import { lstat as
|
|
10512
|
+
import { lstat as lstat5, readdir } from "fs/promises";
|
|
10086
10513
|
import { parse as parse2, resolve } from "path";
|
|
10087
10514
|
function normalizeTargetDirectory(input, currentWorkingDirectory = process.cwd()) {
|
|
10088
10515
|
return resolve(currentWorkingDirectory, input);
|
|
@@ -10114,7 +10541,7 @@ function assertSafeTargetDirectory(targetDirectory, homeDirectory = homedir2())
|
|
|
10114
10541
|
}
|
|
10115
10542
|
async function inspectTargetDirectory(targetDirectory) {
|
|
10116
10543
|
try {
|
|
10117
|
-
const stats = await
|
|
10544
|
+
const stats = await lstat5(targetDirectory);
|
|
10118
10545
|
if (!stats.isDirectory() || stats.isSymbolicLink()) {
|
|
10119
10546
|
return "not-a-directory";
|
|
10120
10547
|
}
|
|
@@ -10186,10 +10613,10 @@ function assertRelativeDestination(path) {
|
|
|
10186
10613
|
}
|
|
10187
10614
|
}
|
|
10188
10615
|
async function listTemplateFiles(templateDirectory, template, configuration, relativeDirectory = "") {
|
|
10189
|
-
const entries = await readdir2(
|
|
10616
|
+
const entries = await readdir2(join9(templateDirectory, relativeDirectory), {
|
|
10190
10617
|
withFileTypes: true
|
|
10191
10618
|
});
|
|
10192
|
-
const
|
|
10619
|
+
const files2 = [];
|
|
10193
10620
|
for (const entry of entries) {
|
|
10194
10621
|
if (shouldIgnoreTemplateEntry(entry.name)) continue;
|
|
10195
10622
|
const source = [relativeDirectory, entry.name].filter(Boolean).join("/");
|
|
@@ -10198,7 +10625,7 @@ async function listTemplateFiles(templateDirectory, template, configuration, rel
|
|
|
10198
10625
|
throw new Error("Bundled templates must not contain symbolic links.");
|
|
10199
10626
|
}
|
|
10200
10627
|
if (entry.isDirectory()) {
|
|
10201
|
-
|
|
10628
|
+
files2.push(
|
|
10202
10629
|
...await listTemplateFiles(
|
|
10203
10630
|
templateDirectory,
|
|
10204
10631
|
template,
|
|
@@ -10209,12 +10636,12 @@ async function listTemplateFiles(templateDirectory, template, configuration, rel
|
|
|
10209
10636
|
} else if (entry.isFile()) {
|
|
10210
10637
|
const destination = template.mapFile(source);
|
|
10211
10638
|
assertRelativeDestination(destination);
|
|
10212
|
-
|
|
10639
|
+
files2.push({ source, destination });
|
|
10213
10640
|
} else {
|
|
10214
10641
|
throw new Error("Bundled templates must contain regular files.");
|
|
10215
10642
|
}
|
|
10216
10643
|
}
|
|
10217
|
-
return
|
|
10644
|
+
return files2;
|
|
10218
10645
|
}
|
|
10219
10646
|
async function createScaffoldPlan({
|
|
10220
10647
|
configuration,
|
|
@@ -10226,7 +10653,7 @@ async function createScaffoldPlan({
|
|
|
10226
10653
|
const targetState = await assertTargetDirectoryAvailable(targetDirectory);
|
|
10227
10654
|
let entries;
|
|
10228
10655
|
try {
|
|
10229
|
-
const root = await
|
|
10656
|
+
const root = await lstat6(templateDirectory);
|
|
10230
10657
|
if (!root.isDirectory() || root.isSymbolicLink()) {
|
|
10231
10658
|
throw new Error("The template must be a regular directory.");
|
|
10232
10659
|
}
|
|
@@ -10235,6 +10662,33 @@ async function createScaffoldPlan({
|
|
|
10235
10662
|
template,
|
|
10236
10663
|
configuration
|
|
10237
10664
|
);
|
|
10665
|
+
for (const layer of template.layers ?? []) {
|
|
10666
|
+
const directory = resolveTemplateDirectory({
|
|
10667
|
+
...template,
|
|
10668
|
+
directory: layer.directory
|
|
10669
|
+
});
|
|
10670
|
+
const root2 = await lstat6(directory);
|
|
10671
|
+
if (!root2.isDirectory() || root2.isSymbolicLink())
|
|
10672
|
+
throw new Error("Invalid template layer.");
|
|
10673
|
+
const additions = await listTemplateFiles(
|
|
10674
|
+
directory,
|
|
10675
|
+
template,
|
|
10676
|
+
configuration
|
|
10677
|
+
);
|
|
10678
|
+
for (const addition of additions) {
|
|
10679
|
+
const index = entries.findIndex(
|
|
10680
|
+
(entry) => entry.destination === addition.destination
|
|
10681
|
+
);
|
|
10682
|
+
if (index >= 0) {
|
|
10683
|
+
if (!layer.overrides?.includes(addition.destination))
|
|
10684
|
+
throw new Error(
|
|
10685
|
+
`Undeclared template override: ${addition.destination}`
|
|
10686
|
+
);
|
|
10687
|
+
entries.splice(index, 1);
|
|
10688
|
+
}
|
|
10689
|
+
entries.push({ ...addition, directory });
|
|
10690
|
+
}
|
|
10691
|
+
}
|
|
10238
10692
|
const destinations = /* @__PURE__ */ new Set();
|
|
10239
10693
|
const directories = /* @__PURE__ */ new Map();
|
|
10240
10694
|
for (const { destination } of entries) {
|
|
@@ -10293,11 +10747,11 @@ async function createScaffoldPlan({
|
|
|
10293
10747
|
}
|
|
10294
10748
|
async function rollback(targetDirectory, targetCreated, createdPaths) {
|
|
10295
10749
|
if (targetCreated) {
|
|
10296
|
-
await
|
|
10750
|
+
await rm4(targetDirectory, { force: true, recursive: true });
|
|
10297
10751
|
return;
|
|
10298
10752
|
}
|
|
10299
10753
|
for (const path of [...createdPaths].reverse()) {
|
|
10300
|
-
await
|
|
10754
|
+
await rm4(path, { force: true, recursive: true });
|
|
10301
10755
|
}
|
|
10302
10756
|
}
|
|
10303
10757
|
async function scaffoldProject({
|
|
@@ -10332,9 +10786,9 @@ async function scaffoldProject({
|
|
|
10332
10786
|
targetCreated = true;
|
|
10333
10787
|
}
|
|
10334
10788
|
for (const entry of plan.entries) {
|
|
10335
|
-
const source =
|
|
10336
|
-
const destination =
|
|
10337
|
-
const stats = await
|
|
10789
|
+
const source = join9(entry.directory ?? templateDirectory, entry.source);
|
|
10790
|
+
const destination = join9(targetDirectory, entry.destination);
|
|
10791
|
+
const stats = await lstat6(source);
|
|
10338
10792
|
if (!stats.isFile() || stats.isSymbolicLink()) {
|
|
10339
10793
|
throw new Error("A template source is no longer a regular file.");
|
|
10340
10794
|
}
|
|
@@ -10351,7 +10805,7 @@ async function scaffoldProject({
|
|
|
10351
10805
|
);
|
|
10352
10806
|
throw new CliError(
|
|
10353
10807
|
"FILESYSTEM_ERROR",
|
|
10354
|
-
"Could not create " +
|
|
10808
|
+
"Could not create " + basename4(targetDirectory) + ": " + (error instanceof Error ? error.message : "unknown filesystem error"),
|
|
10355
10809
|
{
|
|
10356
10810
|
exitCode: EXIT_CODES.filesystem,
|
|
10357
10811
|
path: targetDirectory,
|
|
@@ -10365,16 +10819,26 @@ async function scaffoldProject({
|
|
|
10365
10819
|
// src/templates.ts
|
|
10366
10820
|
init_package_managers();
|
|
10367
10821
|
var DEFAULT_TEMPLATE_ID = "cloudflare-livekit";
|
|
10368
|
-
var templateRegistry =
|
|
10369
|
-
[
|
|
10370
|
-
|
|
10822
|
+
var templateRegistry = Object.fromEntries(
|
|
10823
|
+
Object.keys(stacks).map((stack) => [
|
|
10824
|
+
stack,
|
|
10825
|
+
composeTemplate(stack)
|
|
10826
|
+
])
|
|
10827
|
+
);
|
|
10371
10828
|
function getTemplate(id = DEFAULT_TEMPLATE_ID) {
|
|
10372
|
-
|
|
10829
|
+
const template = templateRegistry[id];
|
|
10830
|
+
if (!template)
|
|
10831
|
+
throw new CliError("INVALID_ARGUMENT", `Unknown template: ${id}`, {
|
|
10832
|
+
exitCode: EXIT_CODES.invalidArgument
|
|
10833
|
+
});
|
|
10834
|
+
return template;
|
|
10373
10835
|
}
|
|
10374
10836
|
function resolveTemplateDirectory(template = getTemplate()) {
|
|
10375
10837
|
return fileURLToPath(new URL(`../${template.directory}/`, import.meta.url));
|
|
10376
10838
|
}
|
|
10377
|
-
async function resolveProjectTemplate(targetDirectory, templates =
|
|
10839
|
+
async function resolveProjectTemplate(targetDirectory, templates = [getTemplate()]) {
|
|
10840
|
+
const config = await readProjectConfig(targetDirectory);
|
|
10841
|
+
if (config) return getTemplate(config.stack);
|
|
10378
10842
|
const matches = [];
|
|
10379
10843
|
for (const template of templates) {
|
|
10380
10844
|
if (await template.setup.recognizes(targetDirectory))
|
|
@@ -10405,4 +10869,3 @@ export {
|
|
|
10405
10869
|
selectPythonPackageManager,
|
|
10406
10870
|
templateRegistry
|
|
10407
10871
|
};
|
|
10408
|
-
//# sourceMappingURL=templates.js.map
|