create-spatius-app 0.1.0-beta.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +57 -0
- package/dist/cli.js +9046 -0
- package/dist/cli.js.map +1 -0
- package/dist/templates.js +2856 -0
- package/dist/templates.js.map +1 -0
- package/package.json +79 -0
- package/schemas/result-v1.schema.json +129 -0
- package/schemas/result-v2.schema.json +146 -0
- package/templates/cloudflare-livekit/.editorconfig +15 -0
- package/templates/cloudflare-livekit/.prettierignore +6 -0
- package/templates/cloudflare-livekit/.prettierrc.json +4 -0
- package/templates/cloudflare-livekit/AGENTS.md +41 -0
- package/templates/cloudflare-livekit/README.md +37 -0
- package/templates/cloudflare-livekit/agent/Dockerfile.pip +31 -0
- package/templates/cloudflare-livekit/agent/Dockerfile.uv +29 -0
- package/templates/cloudflare-livekit/agent/README.md +84 -0
- package/templates/cloudflare-livekit/agent/dockerignore +11 -0
- package/templates/cloudflare-livekit/agent/env.example +8 -0
- package/templates/cloudflare-livekit/agent/pyproject.toml +21 -0
- package/templates/cloudflare-livekit/agent/python-version +1 -0
- package/templates/cloudflare-livekit/agent/src/__init__.py +1 -0
- package/templates/cloudflare-livekit/agent/src/agent.py +97 -0
- package/templates/cloudflare-livekit/agent/src/dispatch_metadata.py +64 -0
- package/templates/cloudflare-livekit/agent/tests/test_agent_startup.py +211 -0
- package/templates/cloudflare-livekit/agent/tests/test_dispatch_metadata.py +97 -0
- package/templates/cloudflare-livekit/agent/uv.lock +2272 -0
- package/templates/cloudflare-livekit/components.json +23 -0
- package/templates/cloudflare-livekit/contracts/agent-dispatch-metadata.schema.json +40 -0
- package/templates/cloudflare-livekit/dev.vars.example +12 -0
- package/templates/cloudflare-livekit/eslint.config.mjs +41 -0
- package/templates/cloudflare-livekit/gitignore +30 -0
- package/templates/cloudflare-livekit/index.html +17 -0
- package/templates/cloudflare-livekit/npmrc +4 -0
- package/templates/cloudflare-livekit/package-lock.json +6514 -0
- package/templates/cloudflare-livekit/package.json +66 -0
- package/templates/cloudflare-livekit/playwright.config.ts +29 -0
- package/templates/cloudflare-livekit/pnpm-lock.yaml +4012 -0
- package/templates/cloudflare-livekit/pnpm-workspace.yaml +8 -0
- package/templates/cloudflare-livekit/scripts/agent-runtime.mjs +129 -0
- package/templates/cloudflare-livekit/scripts/command.mjs +52 -0
- package/templates/cloudflare-livekit/scripts/dev-agent.mjs +77 -0
- package/templates/cloudflare-livekit/scripts/dev-web.mjs +18 -0
- package/templates/cloudflare-livekit/scripts/managed-process.mjs +144 -0
- package/templates/cloudflare-livekit/scripts/process-tree.mjs +111 -0
- package/templates/cloudflare-livekit/scripts/run-agent-python.mjs +25 -0
- package/templates/cloudflare-livekit/tests/dev-agent.test.mjs +327 -0
- package/templates/cloudflare-livekit/tests/dev-fixtures.mjs +235 -0
- package/templates/cloudflare-livekit/tests/dev-supervision.test.mjs +203 -0
- package/templates/cloudflare-livekit/tests/e2e/conversation.spec.ts +376 -0
- package/templates/cloudflare-livekit/tests/e2e/fake-avatar.ts +24 -0
- package/templates/cloudflare-livekit/tests/e2e/fake-components.tsx +173 -0
- package/templates/cloudflare-livekit/tests/e2e/fake-livekit.ts +123 -0
- package/templates/cloudflare-livekit/tests/e2e/harness.html +12 -0
- package/templates/cloudflare-livekit/tests/e2e/harness.tsx +1 -0
- package/templates/cloudflare-livekit/tests/e2e/vite.config.ts +48 -0
- package/templates/cloudflare-livekit/tests/web/api.test.ts +64 -0
- package/templates/cloudflare-livekit/tests/web/app.test.tsx +125 -0
- package/templates/cloudflare-livekit/tests/web/audio-visualizer.test.tsx +53 -0
- package/templates/cloudflare-livekit/tests/web/avatar-session.test.ts +119 -0
- package/templates/cloudflare-livekit/tests/web/components.test.tsx +152 -0
- package/templates/cloudflare-livekit/tests/web/conversation.test.tsx +76 -0
- package/templates/cloudflare-livekit/tests/web/session-attempt.test.ts +178 -0
- package/templates/cloudflare-livekit/tsconfig.json +9 -0
- package/templates/cloudflare-livekit/tsconfig.node.json +23 -0
- package/templates/cloudflare-livekit/tsconfig.web.json +26 -0
- package/templates/cloudflare-livekit/tsconfig.worker.json +22 -0
- package/templates/cloudflare-livekit/vite.config.ts +49 -0
- package/templates/cloudflare-livekit/vitest.config.ts +11 -0
- package/templates/cloudflare-livekit/web/App.tsx +176 -0
- package/templates/cloudflare-livekit/web/api.ts +69 -0
- package/templates/cloudflare-livekit/web/assets/FONT-PROVENANCE.md +8 -0
- package/templates/cloudflare-livekit/web/assets/OFL.txt +92 -0
- package/templates/cloudflare-livekit/web/assets/inter-latin.woff2 +0 -0
- package/templates/cloudflare-livekit/web/avatar-session.ts +67 -0
- package/templates/cloudflare-livekit/web/components/agents-ui/LICENSE +201 -0
- package/templates/cloudflare-livekit/web/components/agents-ui/PROVENANCE.md +32 -0
- package/templates/cloudflare-livekit/web/components/agents-ui/agent-audio-visualizer-bar.tsx +43 -0
- package/templates/cloudflare-livekit/web/components/agents-ui/agent-chat-input.tsx +78 -0
- package/templates/cloudflare-livekit/web/components/agents-ui/agent-chat-transcript.tsx +145 -0
- package/templates/cloudflare-livekit/web/components/agents-ui/agent-disconnect-button.tsx +27 -0
- package/templates/cloudflare-livekit/web/components/agents-ui/agent-session-provider.tsx +29 -0
- package/templates/cloudflare-livekit/web/components/agents-ui/agent-track-toggle.tsx +51 -0
- package/templates/cloudflare-livekit/web/components/agents-ui/start-audio-button.tsx +13 -0
- package/templates/cloudflare-livekit/web/components/conversation.tsx +352 -0
- package/templates/cloudflare-livekit/web/components/icons.tsx +39 -0
- package/templates/cloudflare-livekit/web/components/transcript-panel.tsx +12 -0
- package/templates/cloudflare-livekit/web/main.tsx +17 -0
- package/templates/cloudflare-livekit/web/session-attempt.ts +110 -0
- package/templates/cloudflare-livekit/web/styles.css +571 -0
- package/templates/cloudflare-livekit/worker/dispatch-metadata.ts +22 -0
- package/templates/cloudflare-livekit/worker/index.test.ts +152 -0
- package/templates/cloudflare-livekit/worker/index.ts +61 -0
- package/templates/cloudflare-livekit/worker/session.ts +103 -0
- package/templates/cloudflare-livekit/worker-configuration.d.ts +17184 -0
- package/templates/cloudflare-livekit/wrangler.jsonc +27 -0
|
@@ -0,0 +1,2856 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { createRequire as __createRequire } from 'node:module';
|
|
3
|
+
const require = __createRequire(import.meta.url);
|
|
4
|
+
var __defProp = Object.defineProperty;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __esm = (fn, res) => function __init() {
|
|
7
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
8
|
+
};
|
|
9
|
+
var __export = (target, all) => {
|
|
10
|
+
for (var name in all)
|
|
11
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
// src/errors.ts
|
|
15
|
+
var EXIT_CODES, CliError, PromptCancelledError;
|
|
16
|
+
var init_errors = __esm({
|
|
17
|
+
"src/errors.ts"() {
|
|
18
|
+
"use strict";
|
|
19
|
+
EXIT_CODES = {
|
|
20
|
+
cancelled: 130,
|
|
21
|
+
filesystem: 4,
|
|
22
|
+
general: 1,
|
|
23
|
+
invalidArgument: 2,
|
|
24
|
+
success: 0,
|
|
25
|
+
targetConflict: 3
|
|
26
|
+
};
|
|
27
|
+
CliError = class extends Error {
|
|
28
|
+
code;
|
|
29
|
+
exitCode;
|
|
30
|
+
path;
|
|
31
|
+
recovery;
|
|
32
|
+
constructor(code, message, { exitCode = EXIT_CODES.general, path, recovery } = {}) {
|
|
33
|
+
super(message);
|
|
34
|
+
this.name = "CliError";
|
|
35
|
+
this.code = code;
|
|
36
|
+
this.exitCode = exitCode;
|
|
37
|
+
this.path = path;
|
|
38
|
+
this.recovery = recovery;
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
PromptCancelledError = class extends CliError {
|
|
42
|
+
constructor(message = "Project creation was cancelled.") {
|
|
43
|
+
super("CANCELLED", message, {
|
|
44
|
+
exitCode: EXIT_CODES.cancelled,
|
|
45
|
+
recovery: "Run the command again when you are ready."
|
|
46
|
+
});
|
|
47
|
+
this.name = "PromptCancelledError";
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
// src/package-managers.ts
|
|
54
|
+
import { execFile, spawn } from "child_process";
|
|
55
|
+
function requiresWindowsCommandShell(command, platform = process.platform) {
|
|
56
|
+
if (platform !== "win32") {
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
59
|
+
const executableName = command.split(/[\\/]/u).at(-1)?.toLowerCase();
|
|
60
|
+
return executableName !== void 0 && (["npm", "pnpm", "bun", "uv", "lk"].includes(executableName) || executableName.endsWith(".cmd") || executableName.endsWith(".bat"));
|
|
61
|
+
}
|
|
62
|
+
function createProcessInvocation(command, args, platform = process.platform, environment = process.env) {
|
|
63
|
+
if (!requiresWindowsCommandShell(command, platform)) {
|
|
64
|
+
return { args: [...args], command };
|
|
65
|
+
}
|
|
66
|
+
return {
|
|
67
|
+
args: ["/d", "/s", "/c", command, ...args],
|
|
68
|
+
command: environment.ComSpec ?? environment.COMSPEC ?? "cmd.exe"
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
function executable(command, ...argsPrefix) {
|
|
72
|
+
return {
|
|
73
|
+
argsPrefix,
|
|
74
|
+
command,
|
|
75
|
+
displayName: [command, ...argsPrefix].join(" ")
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
function pythonRuntimeCandidates(platform) {
|
|
79
|
+
return platform === "win32" ? [executable("py", "-3"), executable("python"), executable("python3")] : [executable("python3"), executable("python")];
|
|
80
|
+
}
|
|
81
|
+
function inferInvokingPackageManager(userAgent) {
|
|
82
|
+
const name = userAgent?.split(" ")[0]?.split("/")[0];
|
|
83
|
+
return JAVASCRIPT_PACKAGE_MANAGERS.find((manager) => manager === name);
|
|
84
|
+
}
|
|
85
|
+
function unavailableJavaScriptManager(name) {
|
|
86
|
+
return {
|
|
87
|
+
available: false,
|
|
88
|
+
command: executable(name),
|
|
89
|
+
name
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
function unavailablePythonManager(name, platform) {
|
|
93
|
+
if (name === "uv") {
|
|
94
|
+
return {
|
|
95
|
+
available: false,
|
|
96
|
+
command: executable("uv"),
|
|
97
|
+
name
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
const pythonRuntime = pythonRuntimeCandidates(platform)[0];
|
|
101
|
+
return {
|
|
102
|
+
available: false,
|
|
103
|
+
command: executable(
|
|
104
|
+
pythonRuntime.command,
|
|
105
|
+
...pythonRuntime.argsPrefix,
|
|
106
|
+
"-m",
|
|
107
|
+
"pip"
|
|
108
|
+
),
|
|
109
|
+
name,
|
|
110
|
+
pythonRuntime
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
function selectJavaScriptPackageManager(inventory, explicit, userAgent) {
|
|
114
|
+
if (explicit !== void 0) {
|
|
115
|
+
return inventory.javascript.find((manager) => manager.name === explicit) ?? unavailableJavaScriptManager(explicit);
|
|
116
|
+
}
|
|
117
|
+
const invoked = inferInvokingPackageManager(userAgent);
|
|
118
|
+
const selected = inventory.javascript.find((manager) => manager.name === invoked) ?? inventory.javascript[0];
|
|
119
|
+
if (selected === void 0) {
|
|
120
|
+
throw new CliError(
|
|
121
|
+
"PACKAGE_MANAGER_NOT_FOUND",
|
|
122
|
+
"No supported JavaScript package manager was found.",
|
|
123
|
+
{
|
|
124
|
+
exitCode: EXIT_CODES.invalidArgument,
|
|
125
|
+
recovery: "Install pnpm, Bun, or npm, then run the command again."
|
|
126
|
+
}
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
return selected;
|
|
130
|
+
}
|
|
131
|
+
function selectPythonPackageManager(inventory, explicit, platform = process.platform) {
|
|
132
|
+
if (explicit !== void 0) {
|
|
133
|
+
return inventory.python.find((manager) => manager.name === explicit) ?? unavailablePythonManager(explicit, platform);
|
|
134
|
+
}
|
|
135
|
+
const selected = inventory.python[0];
|
|
136
|
+
if (selected === void 0) {
|
|
137
|
+
throw new CliError(
|
|
138
|
+
"PACKAGE_MANAGER_NOT_FOUND",
|
|
139
|
+
"No supported Python package manager was found.",
|
|
140
|
+
{
|
|
141
|
+
exitCode: EXIT_CODES.invalidArgument,
|
|
142
|
+
recovery: "Install uv, or install Python with pip, then run the command again."
|
|
143
|
+
}
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
return selected;
|
|
147
|
+
}
|
|
148
|
+
function javascriptInstallCommand(packageManager) {
|
|
149
|
+
return `${packageManager} install`;
|
|
150
|
+
}
|
|
151
|
+
function javascriptRunCommand(packageManager, script) {
|
|
152
|
+
return `${packageManager} run ${script}`;
|
|
153
|
+
}
|
|
154
|
+
function pythonInstallCommands(packageManager, platform = process.platform) {
|
|
155
|
+
if (packageManager.name === "uv") {
|
|
156
|
+
return ["uv sync --directory agent --extra dev"];
|
|
157
|
+
}
|
|
158
|
+
const python = packageManager.pythonRuntime?.displayName ?? (platform === "win32" ? "py -3" : "python3");
|
|
159
|
+
const virtualEnvironmentPython = platform === "win32" ? "agent\\.venv\\Scripts\\python.exe" : "agent/.venv/bin/python";
|
|
160
|
+
return [
|
|
161
|
+
`${python} -m venv agent/.venv`,
|
|
162
|
+
`${virtualEnvironmentPython} -m pip install -e "./agent[dev]"`
|
|
163
|
+
];
|
|
164
|
+
}
|
|
165
|
+
var JAVASCRIPT_PACKAGE_MANAGERS;
|
|
166
|
+
var init_package_managers = __esm({
|
|
167
|
+
"src/package-managers.ts"() {
|
|
168
|
+
"use strict";
|
|
169
|
+
init_errors();
|
|
170
|
+
JAVASCRIPT_PACKAGE_MANAGERS = ["pnpm", "bun", "npm"];
|
|
171
|
+
}
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
// src/setup/project.ts
|
|
175
|
+
import { lstat } from "fs/promises";
|
|
176
|
+
import { join } from "path";
|
|
177
|
+
async function assertSpatiusProject(targetDirectory) {
|
|
178
|
+
try {
|
|
179
|
+
const target = await lstat(targetDirectory);
|
|
180
|
+
if (!target.isDirectory() || target.isSymbolicLink()) {
|
|
181
|
+
throw new Error("not a regular directory");
|
|
182
|
+
}
|
|
183
|
+
await Promise.all([
|
|
184
|
+
...requiredProjectDirectories.map(async (relativePath) => {
|
|
185
|
+
const stats = await lstat(join(targetDirectory, relativePath));
|
|
186
|
+
if (!stats.isDirectory() || stats.isSymbolicLink()) {
|
|
187
|
+
throw new Error(`${relativePath} is not a regular directory`);
|
|
188
|
+
}
|
|
189
|
+
}),
|
|
190
|
+
...requiredProjectPaths.map(async (relativePath) => {
|
|
191
|
+
const stats = await lstat(join(targetDirectory, relativePath));
|
|
192
|
+
if (!stats.isFile() || stats.isSymbolicLink()) {
|
|
193
|
+
throw new Error(`${relativePath} is not a regular file`);
|
|
194
|
+
}
|
|
195
|
+
})
|
|
196
|
+
]);
|
|
197
|
+
} catch {
|
|
198
|
+
throw new CliError(
|
|
199
|
+
"INVALID_ARGUMENT",
|
|
200
|
+
"The target is not a compatible create-spatius-app project.",
|
|
201
|
+
{
|
|
202
|
+
exitCode: EXIT_CODES.invalidArgument,
|
|
203
|
+
path: targetDirectory,
|
|
204
|
+
recovery: "Run this command from a generated project, or pass its directory explicitly."
|
|
205
|
+
}
|
|
206
|
+
);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
var requiredProjectPaths, requiredProjectDirectories;
|
|
210
|
+
var init_project = __esm({
|
|
211
|
+
"src/setup/project.ts"() {
|
|
212
|
+
"use strict";
|
|
213
|
+
init_errors();
|
|
214
|
+
requiredProjectPaths = [
|
|
215
|
+
"package.json",
|
|
216
|
+
"wrangler.jsonc",
|
|
217
|
+
".dev.vars.example",
|
|
218
|
+
"worker/index.ts",
|
|
219
|
+
"agent/src/agent.py",
|
|
220
|
+
"agent/.env.example"
|
|
221
|
+
];
|
|
222
|
+
requiredProjectDirectories = ["agent", "worker"];
|
|
223
|
+
}
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
// src/templates/cloudflare-livekit/voices.ts
|
|
227
|
+
var CHARACTER_VOICES;
|
|
228
|
+
var init_voices = __esm({
|
|
229
|
+
"src/templates/cloudflare-livekit/voices.ts"() {
|
|
230
|
+
"use strict";
|
|
231
|
+
CHARACTER_VOICES = [
|
|
232
|
+
{
|
|
233
|
+
value: "feminine",
|
|
234
|
+
label: "Feminine \u2014 Jacqueline",
|
|
235
|
+
hint: "Standard female voice \xB7 English (US)",
|
|
236
|
+
voiceId: "9626c31c-bec5-4cca-baa8-f8ba9e84c8bc"
|
|
237
|
+
},
|
|
238
|
+
{
|
|
239
|
+
value: "masculine",
|
|
240
|
+
label: "Masculine \u2014 Blake",
|
|
241
|
+
hint: "Standard male voice \xB7 English (US)",
|
|
242
|
+
voiceId: "a167e0f3-df7e-4d52-a9c3-f949145efdab"
|
|
243
|
+
}
|
|
244
|
+
];
|
|
245
|
+
}
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
// src/setup/environment.ts
|
|
249
|
+
import {
|
|
250
|
+
chmod,
|
|
251
|
+
lstat as lstat2,
|
|
252
|
+
readFile as readFile2,
|
|
253
|
+
rename,
|
|
254
|
+
rm,
|
|
255
|
+
writeFile as writeFile2
|
|
256
|
+
} from "fs/promises";
|
|
257
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
258
|
+
import { join as join3 } from "path";
|
|
259
|
+
function parseQuotedValue(rawValue) {
|
|
260
|
+
const value = rawValue.trim();
|
|
261
|
+
if (value.startsWith('"')) {
|
|
262
|
+
const match = value.match(/^((?:"(?:\\.|[^"\\])*")).*$/u);
|
|
263
|
+
if (match?.[1] !== void 0) {
|
|
264
|
+
try {
|
|
265
|
+
return JSON.parse(match[1]);
|
|
266
|
+
} catch {
|
|
267
|
+
return match[1].slice(1, -1);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
if (value.startsWith("'")) {
|
|
272
|
+
const closingQuote = value.indexOf("'", 1);
|
|
273
|
+
if (closingQuote !== -1) {
|
|
274
|
+
return value.slice(1, closingQuote);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
return value.replace(/\s+#.*$/u, "").trim();
|
|
278
|
+
}
|
|
279
|
+
function parseDotenv(contents) {
|
|
280
|
+
const values = /* @__PURE__ */ new Map();
|
|
281
|
+
for (const line of contents.split(/\r?\n/u)) {
|
|
282
|
+
const match = line.match(assignmentPattern);
|
|
283
|
+
if (match?.[1] !== void 0 && match[2] !== void 0) {
|
|
284
|
+
values.set(match[1], parseQuotedValue(match[2]));
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
return values;
|
|
288
|
+
}
|
|
289
|
+
function isPlaceholderValue(value) {
|
|
290
|
+
if (value === void 0 || value.trim() === "") {
|
|
291
|
+
return true;
|
|
292
|
+
}
|
|
293
|
+
const normalized = value.trim().toLowerCase();
|
|
294
|
+
return normalized.includes("your-") || normalized.includes("your_") || normalized.includes("replace-me") || normalized.includes("changeme") || normalized.includes("placeholder") || /^<[^>]+>$/u.test(normalized);
|
|
295
|
+
}
|
|
296
|
+
function assertSafeValue(name, value) {
|
|
297
|
+
const normalized = value.trim();
|
|
298
|
+
if (isPlaceholderValue(normalized)) {
|
|
299
|
+
throw new CliError(
|
|
300
|
+
"INVALID_ARGUMENT",
|
|
301
|
+
`${name} must be a non-placeholder value.`,
|
|
302
|
+
{ exitCode: EXIT_CODES.invalidArgument }
|
|
303
|
+
);
|
|
304
|
+
}
|
|
305
|
+
if (new RegExp("\\p{Cc}", "u").test(normalized)) {
|
|
306
|
+
throw new CliError(
|
|
307
|
+
"INVALID_ARGUMENT",
|
|
308
|
+
`${name} cannot contain control characters.`,
|
|
309
|
+
{ exitCode: EXIT_CODES.invalidArgument }
|
|
310
|
+
);
|
|
311
|
+
}
|
|
312
|
+
return normalized;
|
|
313
|
+
}
|
|
314
|
+
function validateLiveKitUrl(value) {
|
|
315
|
+
const normalized = assertSafeValue("LIVEKIT_URL", value);
|
|
316
|
+
let parsed;
|
|
317
|
+
try {
|
|
318
|
+
parsed = new URL(normalized);
|
|
319
|
+
} catch {
|
|
320
|
+
throw new CliError(
|
|
321
|
+
"INVALID_ARGUMENT",
|
|
322
|
+
"LIVEKIT_URL must be a valid https:// or wss:// URL.",
|
|
323
|
+
{ exitCode: EXIT_CODES.invalidArgument }
|
|
324
|
+
);
|
|
325
|
+
}
|
|
326
|
+
if (!["https:", "wss:"].includes(parsed.protocol) || parsed.hostname === "") {
|
|
327
|
+
throw new CliError(
|
|
328
|
+
"INVALID_ARGUMENT",
|
|
329
|
+
"LIVEKIT_URL must use https:// or wss://.",
|
|
330
|
+
{ exitCode: EXIT_CODES.invalidArgument }
|
|
331
|
+
);
|
|
332
|
+
}
|
|
333
|
+
return normalized;
|
|
334
|
+
}
|
|
335
|
+
function publicBackgroundUrl(value) {
|
|
336
|
+
if (!value) return void 0;
|
|
337
|
+
try {
|
|
338
|
+
const url = new URL(value);
|
|
339
|
+
return url.protocol === "https:" && !url.username && !url.password ? url.href : void 0;
|
|
340
|
+
} catch {
|
|
341
|
+
return void 0;
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
function validateCredentialBundle(credentials) {
|
|
345
|
+
return {
|
|
346
|
+
...credentials.voiceId === void 0 ? {} : {
|
|
347
|
+
voiceId: assertSafeValue("CARTESIA_VOICE_ID", credentials.voiceId)
|
|
348
|
+
},
|
|
349
|
+
liveKit: {
|
|
350
|
+
apiKey: assertSafeValue("LIVEKIT_API_KEY", credentials.liveKit.apiKey),
|
|
351
|
+
apiSecret: assertSafeValue(
|
|
352
|
+
"LIVEKIT_API_SECRET",
|
|
353
|
+
credentials.liveKit.apiSecret
|
|
354
|
+
),
|
|
355
|
+
url: validateLiveKitUrl(credentials.liveKit.url)
|
|
356
|
+
},
|
|
357
|
+
spatius: {
|
|
358
|
+
backgroundUrl: publicBackgroundUrl(credentials.spatius.backgroundUrl),
|
|
359
|
+
apiKey: assertSafeValue("SPATIUS_API_KEY", credentials.spatius.apiKey),
|
|
360
|
+
appId: assertSafeValue("SPATIUS_APP_ID", credentials.spatius.appId),
|
|
361
|
+
avatarId: assertSafeValue(
|
|
362
|
+
"SPATIUS_AVATAR_ID",
|
|
363
|
+
credentials.spatius.avatarId
|
|
364
|
+
)
|
|
365
|
+
}
|
|
366
|
+
};
|
|
367
|
+
}
|
|
368
|
+
function managedValues(contents, keys) {
|
|
369
|
+
const parsed = parseDotenv(contents ?? "");
|
|
370
|
+
return new Map(
|
|
371
|
+
keys.flatMap((key) => {
|
|
372
|
+
const value = parsed.get(key);
|
|
373
|
+
return value === void 0 ? [] : [[key, value]];
|
|
374
|
+
})
|
|
375
|
+
);
|
|
376
|
+
}
|
|
377
|
+
function inspectCredentialConfiguration(contents, expectedAgentName = "spatius-agent") {
|
|
378
|
+
const worker = managedValues(contents.worker, WORKER_MANAGED_KEYS);
|
|
379
|
+
const agent = managedValues(contents.agent, AGENT_MANAGED_KEYS);
|
|
380
|
+
const allEntries = [...worker.entries(), ...agent.entries()];
|
|
381
|
+
const hasManagedValues = allEntries.length > 0 || parseDotenv(contents.worker ?? "").has("CARTESIA_VOICE_ID");
|
|
382
|
+
const hasPlaceholder = allEntries.some(
|
|
383
|
+
([, value]) => isPlaceholderValue(value)
|
|
384
|
+
);
|
|
385
|
+
const allPresent = WORKER_MANAGED_KEYS.every((key) => worker.has(key)) && AGENT_MANAGED_KEYS.every((key) => agent.has(key));
|
|
386
|
+
const sharedKeys = [
|
|
387
|
+
"LIVEKIT_URL",
|
|
388
|
+
"LIVEKIT_API_KEY",
|
|
389
|
+
"LIVEKIT_API_SECRET",
|
|
390
|
+
"SPATIUS_APP_ID"
|
|
391
|
+
];
|
|
392
|
+
const inconsistent = sharedKeys.some((key) => {
|
|
393
|
+
const workerValue = worker.get(key);
|
|
394
|
+
const agentValue = agent.get(key);
|
|
395
|
+
return workerValue !== void 0 && agentValue !== void 0 && !isPlaceholderValue(workerValue) && !isPlaceholderValue(agentValue) && workerValue !== agentValue;
|
|
396
|
+
}) || worker.get("LIVEKIT_AGENT_NAME") !== void 0 && !isPlaceholderValue(worker.get("LIVEKIT_AGENT_NAME")) && worker.get("LIVEKIT_AGENT_NAME") !== expectedAgentName;
|
|
397
|
+
if (inconsistent) {
|
|
398
|
+
return { hasManagedValues, status: "inconsistent" };
|
|
399
|
+
}
|
|
400
|
+
if (!allPresent) {
|
|
401
|
+
return { hasManagedValues, status: "missing" };
|
|
402
|
+
}
|
|
403
|
+
if (hasPlaceholder) {
|
|
404
|
+
return { hasManagedValues, status: "placeholder" };
|
|
405
|
+
}
|
|
406
|
+
return { hasManagedValues, status: "complete" };
|
|
407
|
+
}
|
|
408
|
+
function isMissingFile(error) {
|
|
409
|
+
return error instanceof Error && "code" in error && error.code === "ENOENT";
|
|
410
|
+
}
|
|
411
|
+
async function readOptionalCredentialFile(path) {
|
|
412
|
+
try {
|
|
413
|
+
const stats = await lstat2(path);
|
|
414
|
+
if (stats.isSymbolicLink() || !stats.isFile()) {
|
|
415
|
+
throw new CliError(
|
|
416
|
+
"FILESYSTEM_ERROR",
|
|
417
|
+
`Refusing to manage a credential path that is not a regular file: ${path}`,
|
|
418
|
+
{
|
|
419
|
+
exitCode: EXIT_CODES.filesystem,
|
|
420
|
+
path,
|
|
421
|
+
recovery: "Replace it with a regular local file and rerun setup."
|
|
422
|
+
}
|
|
423
|
+
);
|
|
424
|
+
}
|
|
425
|
+
return await readFile2(path, "utf8");
|
|
426
|
+
} catch (error) {
|
|
427
|
+
if (isMissingFile(error)) {
|
|
428
|
+
return void 0;
|
|
429
|
+
}
|
|
430
|
+
throw error;
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
async function readCredentialFileState(targetDirectory) {
|
|
434
|
+
const [worker, agent, workerExample] = await Promise.all([
|
|
435
|
+
readOptionalCredentialFile(join3(targetDirectory, ".dev.vars")),
|
|
436
|
+
readOptionalCredentialFile(join3(targetDirectory, "agent", ".env.local")),
|
|
437
|
+
readOptionalCredentialFile(join3(targetDirectory, ".dev.vars.example"))
|
|
438
|
+
]);
|
|
439
|
+
return {
|
|
440
|
+
agent,
|
|
441
|
+
...inspectCredentialConfiguration(
|
|
442
|
+
{ agent, worker },
|
|
443
|
+
parseDotenv(workerExample ?? "").get("LIVEKIT_AGENT_NAME")
|
|
444
|
+
),
|
|
445
|
+
worker
|
|
446
|
+
};
|
|
447
|
+
}
|
|
448
|
+
function serializeDotenvValue(value) {
|
|
449
|
+
return JSON.stringify(value);
|
|
450
|
+
}
|
|
451
|
+
function mergeDotenv(baseContents, values) {
|
|
452
|
+
const seen = /* @__PURE__ */ new Set();
|
|
453
|
+
const lines = baseContents.replace(/\r\n/gu, "\n").split("\n");
|
|
454
|
+
const rendered = lines.map((line) => {
|
|
455
|
+
const match = line.match(assignmentPattern);
|
|
456
|
+
const key = match?.[1];
|
|
457
|
+
if (key === void 0 || !(key in values)) {
|
|
458
|
+
return line;
|
|
459
|
+
}
|
|
460
|
+
seen.add(key);
|
|
461
|
+
return `${key}=${serializeDotenvValue(values[key])}`;
|
|
462
|
+
});
|
|
463
|
+
while (rendered.at(-1) === "") {
|
|
464
|
+
rendered.pop();
|
|
465
|
+
}
|
|
466
|
+
for (const [key, value] of Object.entries(values)) {
|
|
467
|
+
if (!seen.has(key)) {
|
|
468
|
+
rendered.push(`${key}=${serializeDotenvValue(value)}`);
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
return `${rendered.join("\n")}
|
|
472
|
+
`;
|
|
473
|
+
}
|
|
474
|
+
function buildCredentialFileContents(state, examples, rawCredentials) {
|
|
475
|
+
const credentials = validateCredentialBundle(rawCredentials);
|
|
476
|
+
const shared = {
|
|
477
|
+
LIVEKIT_API_KEY: credentials.liveKit.apiKey,
|
|
478
|
+
LIVEKIT_API_SECRET: credentials.liveKit.apiSecret,
|
|
479
|
+
LIVEKIT_URL: credentials.liveKit.url,
|
|
480
|
+
SPATIUS_APP_ID: credentials.spatius.appId
|
|
481
|
+
};
|
|
482
|
+
return {
|
|
483
|
+
agent: mergeDotenv(state.agent ?? examples.agent, {
|
|
484
|
+
...shared,
|
|
485
|
+
SPATIUS_API_KEY: credentials.spatius.apiKey
|
|
486
|
+
}),
|
|
487
|
+
worker: mergeDotenv(state.worker ?? examples.worker, {
|
|
488
|
+
...credentials.voiceId === void 0 ? {} : { CARTESIA_VOICE_ID: credentials.voiceId },
|
|
489
|
+
...shared,
|
|
490
|
+
LIVEKIT_AGENT_NAME: parseDotenv(examples.worker).get("LIVEKIT_AGENT_NAME") ?? "spatius-agent",
|
|
491
|
+
SPATIUS_AVATAR_ID: credentials.spatius.avatarId,
|
|
492
|
+
// Clear a previous avatar’s background when a new selection has none.
|
|
493
|
+
SPATIUS_AVATAR_BACKGROUND_URL: credentials.spatius.backgroundUrl ?? ""
|
|
494
|
+
})
|
|
495
|
+
};
|
|
496
|
+
}
|
|
497
|
+
async function pathExists(path, fileSystem) {
|
|
498
|
+
try {
|
|
499
|
+
await fileSystem.lstat(path);
|
|
500
|
+
return true;
|
|
501
|
+
} catch (error) {
|
|
502
|
+
if (isMissingFile(error)) {
|
|
503
|
+
return false;
|
|
504
|
+
}
|
|
505
|
+
throw error;
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
async function writeCredentialFilesAtomically(targetDirectory, contents, fileSystem = defaultAtomicFileSystem) {
|
|
509
|
+
const nonce = randomUUID2();
|
|
510
|
+
const targets = [
|
|
511
|
+
{
|
|
512
|
+
backupMoved: false,
|
|
513
|
+
backupPath: join3(targetDirectory, `.dev.vars.${nonce}.backup`),
|
|
514
|
+
destinationPath: join3(targetDirectory, ".dev.vars"),
|
|
515
|
+
hadOriginal: false,
|
|
516
|
+
installed: false,
|
|
517
|
+
stagedPath: join3(targetDirectory, `.dev.vars.${nonce}.tmp`)
|
|
518
|
+
},
|
|
519
|
+
{
|
|
520
|
+
backupMoved: false,
|
|
521
|
+
backupPath: join3(targetDirectory, "agent", `.env.local.${nonce}.backup`),
|
|
522
|
+
destinationPath: join3(targetDirectory, "agent", ".env.local"),
|
|
523
|
+
hadOriginal: false,
|
|
524
|
+
installed: false,
|
|
525
|
+
stagedPath: join3(targetDirectory, "agent", `.env.local.${nonce}.tmp`)
|
|
526
|
+
}
|
|
527
|
+
];
|
|
528
|
+
const payloads = [contents.worker, contents.agent];
|
|
529
|
+
try {
|
|
530
|
+
for (const [index, target] of targets.entries()) {
|
|
531
|
+
await fileSystem.writeFile(target.stagedPath, payloads[index], {
|
|
532
|
+
flag: "wx",
|
|
533
|
+
mode: 384
|
|
534
|
+
});
|
|
535
|
+
target.hadOriginal = await pathExists(target.destinationPath, fileSystem);
|
|
536
|
+
if (target.hadOriginal) {
|
|
537
|
+
target.original = await fileSystem.readFile(target.destinationPath);
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
for (const target of targets) {
|
|
541
|
+
if (target.hadOriginal) {
|
|
542
|
+
await fileSystem.rename(target.destinationPath, target.backupPath);
|
|
543
|
+
target.backupMoved = true;
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
for (const target of targets) {
|
|
547
|
+
await fileSystem.rename(target.stagedPath, target.destinationPath);
|
|
548
|
+
target.installed = true;
|
|
549
|
+
await fileSystem.chmod(target.destinationPath, 384);
|
|
550
|
+
}
|
|
551
|
+
for (const target of targets) {
|
|
552
|
+
await fileSystem.rm(target.backupPath);
|
|
553
|
+
}
|
|
554
|
+
} catch {
|
|
555
|
+
const unrestoredPaths = [];
|
|
556
|
+
for (const target of [...targets].reverse()) {
|
|
557
|
+
try {
|
|
558
|
+
if (target.backupMoved) {
|
|
559
|
+
if (!await pathExists(target.backupPath, fileSystem)) {
|
|
560
|
+
await fileSystem.writeFile(target.backupPath, target.original, {
|
|
561
|
+
flag: "wx",
|
|
562
|
+
mode: 384
|
|
563
|
+
});
|
|
564
|
+
}
|
|
565
|
+
await fileSystem.rename(target.backupPath, target.destinationPath);
|
|
566
|
+
} else if (target.installed) {
|
|
567
|
+
await fileSystem.rm(target.destinationPath);
|
|
568
|
+
}
|
|
569
|
+
} catch {
|
|
570
|
+
unrestoredPaths.push(target.destinationPath);
|
|
571
|
+
}
|
|
572
|
+
await fileSystem.rm(target.stagedPath).catch(() => void 0);
|
|
573
|
+
}
|
|
574
|
+
throw new CliError(
|
|
575
|
+
"FILESYSTEM_ERROR",
|
|
576
|
+
"Could not save the local credential files. " + (unrestoredPaths.length === 0 ? "Existing files were left unchanged or restored." : "Some credential files could not be restored. Any remaining backups were kept."),
|
|
577
|
+
{
|
|
578
|
+
exitCode: EXIT_CODES.filesystem,
|
|
579
|
+
path: targetDirectory,
|
|
580
|
+
recovery: unrestoredPaths.length === 0 ? "Check file permissions, then rerun create-spatius-app setup . --interactive." : `Check file permissions and inspect the affected files (${unrestoredPaths.join(", ")}) and remaining .backup files before rerunning setup.`
|
|
581
|
+
}
|
|
582
|
+
);
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
async function readCredentialExamples(targetDirectory) {
|
|
586
|
+
const [worker, agent] = await Promise.all([
|
|
587
|
+
readFile2(join3(targetDirectory, ".dev.vars.example"), "utf8"),
|
|
588
|
+
readFile2(join3(targetDirectory, "agent", ".env.example"), "utf8")
|
|
589
|
+
]);
|
|
590
|
+
return { agent, worker };
|
|
591
|
+
}
|
|
592
|
+
var WORKER_MANAGED_KEYS, AGENT_MANAGED_KEYS, assignmentPattern, defaultAtomicFileSystem;
|
|
593
|
+
var init_environment = __esm({
|
|
594
|
+
"src/setup/environment.ts"() {
|
|
595
|
+
"use strict";
|
|
596
|
+
init_errors();
|
|
597
|
+
WORKER_MANAGED_KEYS = [
|
|
598
|
+
"LIVEKIT_URL",
|
|
599
|
+
"LIVEKIT_API_KEY",
|
|
600
|
+
"LIVEKIT_API_SECRET",
|
|
601
|
+
"LIVEKIT_AGENT_NAME",
|
|
602
|
+
"SPATIUS_APP_ID",
|
|
603
|
+
"SPATIUS_AVATAR_ID"
|
|
604
|
+
];
|
|
605
|
+
AGENT_MANAGED_KEYS = [
|
|
606
|
+
"LIVEKIT_URL",
|
|
607
|
+
"LIVEKIT_API_KEY",
|
|
608
|
+
"LIVEKIT_API_SECRET",
|
|
609
|
+
"SPATIUS_API_KEY",
|
|
610
|
+
"SPATIUS_APP_ID"
|
|
611
|
+
];
|
|
612
|
+
assignmentPattern = /^\s*(?:export\s+)?([A-Za-z_][A-Za-z\d_]*)\s*=\s*(.*)$/u;
|
|
613
|
+
defaultAtomicFileSystem = {
|
|
614
|
+
chmod,
|
|
615
|
+
lstat: lstat2,
|
|
616
|
+
readFile: (path) => readFile2(path),
|
|
617
|
+
rename,
|
|
618
|
+
rm: (path) => rm(path, { force: true }),
|
|
619
|
+
writeFile: writeFile2
|
|
620
|
+
};
|
|
621
|
+
}
|
|
622
|
+
});
|
|
623
|
+
|
|
624
|
+
// src/setup/livekit.ts
|
|
625
|
+
import { spawn as spawn2 } from "child_process";
|
|
626
|
+
import { chmod as chmod2, mkdtemp, readFile as readFile3, rm as rm2, writeFile as writeFile3 } from "fs/promises";
|
|
627
|
+
import { tmpdir } from "os";
|
|
628
|
+
import { join as join4 } from "path";
|
|
629
|
+
async function terminate(child) {
|
|
630
|
+
if (child.pid === void 0) {
|
|
631
|
+
child.kill("SIGKILL");
|
|
632
|
+
return;
|
|
633
|
+
}
|
|
634
|
+
if (process.platform === "win32") {
|
|
635
|
+
await new Promise((resolvePromise) => {
|
|
636
|
+
const killer = spawn2(
|
|
637
|
+
"taskkill.exe",
|
|
638
|
+
["/pid", String(child.pid), "/t", "/f"],
|
|
639
|
+
{ stdio: "ignore", windowsHide: true }
|
|
640
|
+
);
|
|
641
|
+
killer.once("close", () => resolvePromise());
|
|
642
|
+
killer.once("error", () => resolvePromise());
|
|
643
|
+
});
|
|
644
|
+
return;
|
|
645
|
+
}
|
|
646
|
+
try {
|
|
647
|
+
process.kill(-child.pid, "SIGKILL");
|
|
648
|
+
} catch {
|
|
649
|
+
child.kill("SIGKILL");
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
function captureCommand(command, args, { cwd, timeoutMs = 3e3 } = {}) {
|
|
653
|
+
return new Promise((resolvePromise, reject) => {
|
|
654
|
+
const invocation = createProcessInvocation(command, args);
|
|
655
|
+
const child = spawn2(invocation.command, invocation.args, {
|
|
656
|
+
...cwd === void 0 ? {} : { cwd },
|
|
657
|
+
detached: process.platform !== "win32",
|
|
658
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
659
|
+
windowsHide: true
|
|
660
|
+
});
|
|
661
|
+
let output = "";
|
|
662
|
+
let settled = false;
|
|
663
|
+
const onData = (chunk) => {
|
|
664
|
+
if (output.length < 128e3) {
|
|
665
|
+
output += chunk.toString();
|
|
666
|
+
}
|
|
667
|
+
};
|
|
668
|
+
child.stdout?.on("data", onData);
|
|
669
|
+
child.stderr?.on("data", onData);
|
|
670
|
+
const timeout = setTimeout(() => {
|
|
671
|
+
if (settled) return;
|
|
672
|
+
settled = true;
|
|
673
|
+
void terminate(child).finally(() => {
|
|
674
|
+
reject(new Error(`${command} timed out.`));
|
|
675
|
+
});
|
|
676
|
+
}, timeoutMs);
|
|
677
|
+
child.once("error", (error) => {
|
|
678
|
+
if (settled) return;
|
|
679
|
+
settled = true;
|
|
680
|
+
clearTimeout(timeout);
|
|
681
|
+
reject(error);
|
|
682
|
+
});
|
|
683
|
+
child.once("close", (code) => {
|
|
684
|
+
if (settled) return;
|
|
685
|
+
settled = true;
|
|
686
|
+
clearTimeout(timeout);
|
|
687
|
+
if (code !== 0) {
|
|
688
|
+
reject(new Error(`${command} exited unsuccessfully.`));
|
|
689
|
+
return;
|
|
690
|
+
}
|
|
691
|
+
resolvePromise(output.trim());
|
|
692
|
+
});
|
|
693
|
+
});
|
|
694
|
+
}
|
|
695
|
+
function interactiveCommand(command, args, { cwd }) {
|
|
696
|
+
return new Promise((resolvePromise, reject) => {
|
|
697
|
+
const invocation = createProcessInvocation(command, args);
|
|
698
|
+
const child = spawn2(invocation.command, invocation.args, {
|
|
699
|
+
cwd,
|
|
700
|
+
stdio: "inherit",
|
|
701
|
+
windowsHide: true
|
|
702
|
+
});
|
|
703
|
+
child.once("error", () => {
|
|
704
|
+
reject(new Error("The LiveKit CLI could not be started."));
|
|
705
|
+
});
|
|
706
|
+
child.once("close", (code, signal) => {
|
|
707
|
+
if (signal === "SIGINT" || signal === "SIGTERM" || code === 130) {
|
|
708
|
+
reject(new PromptCancelledError("LiveKit setup was cancelled."));
|
|
709
|
+
return;
|
|
710
|
+
}
|
|
711
|
+
if (code === 0) {
|
|
712
|
+
resolvePromise();
|
|
713
|
+
return;
|
|
714
|
+
}
|
|
715
|
+
reject(new Error("The LiveKit CLI credential flow did not complete."));
|
|
716
|
+
});
|
|
717
|
+
});
|
|
718
|
+
}
|
|
719
|
+
function parseVersion(output) {
|
|
720
|
+
return output.match(/\d+\.\d+(?:\.\d+)?(?:-[\w.-]+)?/u)?.[0];
|
|
721
|
+
}
|
|
722
|
+
async function probeLiveKitCli(runner = defaultLiveKitCommandRunner) {
|
|
723
|
+
let versionOutput;
|
|
724
|
+
try {
|
|
725
|
+
versionOutput = await runner.capture("lk", ["--version"]);
|
|
726
|
+
} catch {
|
|
727
|
+
return { available: false, reason: "missing" };
|
|
728
|
+
}
|
|
729
|
+
try {
|
|
730
|
+
const help = await runner.capture("lk", ["app", "env", "--help"]);
|
|
731
|
+
if (!help.includes("--write") || !help.includes("--destination") || !help.includes("--example")) {
|
|
732
|
+
return {
|
|
733
|
+
available: false,
|
|
734
|
+
reason: "incompatible",
|
|
735
|
+
...parseVersion(versionOutput) === void 0 ? {} : { version: parseVersion(versionOutput) }
|
|
736
|
+
};
|
|
737
|
+
}
|
|
738
|
+
return {
|
|
739
|
+
available: true,
|
|
740
|
+
...parseVersion(versionOutput) === void 0 ? {} : { version: parseVersion(versionOutput) }
|
|
741
|
+
};
|
|
742
|
+
} catch {
|
|
743
|
+
return {
|
|
744
|
+
available: false,
|
|
745
|
+
reason: "incompatible",
|
|
746
|
+
...parseVersion(versionOutput) === void 0 ? {} : { version: parseVersion(versionOutput) }
|
|
747
|
+
};
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
function createLiveKitEnvArguments(temporaryDirectory) {
|
|
751
|
+
return [
|
|
752
|
+
"app",
|
|
753
|
+
"env",
|
|
754
|
+
"--write",
|
|
755
|
+
"--destination",
|
|
756
|
+
".env.local",
|
|
757
|
+
"--example",
|
|
758
|
+
".env.example",
|
|
759
|
+
temporaryDirectory
|
|
760
|
+
];
|
|
761
|
+
}
|
|
762
|
+
async function loadLiveKitCredentialsWithCli({
|
|
763
|
+
fileSystem = defaultTemporaryFileSystem,
|
|
764
|
+
runner = defaultLiveKitCommandRunner,
|
|
765
|
+
temporaryRoot = tmpdir()
|
|
766
|
+
} = {}) {
|
|
767
|
+
const directory = await fileSystem.mkdtemp(
|
|
768
|
+
join4(temporaryRoot, "create-spatius-app-livekit-")
|
|
769
|
+
);
|
|
770
|
+
try {
|
|
771
|
+
await fileSystem.chmod(directory, 448);
|
|
772
|
+
await fileSystem.writeFile(
|
|
773
|
+
join4(directory, ".env.example"),
|
|
774
|
+
"LIVEKIT_URL=\nLIVEKIT_API_KEY=\nLIVEKIT_API_SECRET=\n",
|
|
775
|
+
384
|
|
776
|
+
);
|
|
777
|
+
await runner.interactive("lk", createLiveKitEnvArguments(directory), {
|
|
778
|
+
cwd: directory
|
|
779
|
+
});
|
|
780
|
+
const values = parseDotenv(
|
|
781
|
+
await fileSystem.readFile(join4(directory, ".env.local"))
|
|
782
|
+
);
|
|
783
|
+
const validated = validateCredentialBundle({
|
|
784
|
+
liveKit: {
|
|
785
|
+
apiKey: values.get("LIVEKIT_API_KEY") ?? "",
|
|
786
|
+
apiSecret: values.get("LIVEKIT_API_SECRET") ?? "",
|
|
787
|
+
url: values.get("LIVEKIT_URL") ?? ""
|
|
788
|
+
},
|
|
789
|
+
spatius: {
|
|
790
|
+
apiKey: "validation-only",
|
|
791
|
+
appId: "validation-only",
|
|
792
|
+
avatarId: "validation-only"
|
|
793
|
+
}
|
|
794
|
+
});
|
|
795
|
+
return validated.liveKit;
|
|
796
|
+
} catch (error) {
|
|
797
|
+
if (error instanceof PromptCancelledError) throw error;
|
|
798
|
+
throw new Error(
|
|
799
|
+
"LiveKit CLI did not return a complete, valid credential set.",
|
|
800
|
+
{ cause: error }
|
|
801
|
+
);
|
|
802
|
+
} finally {
|
|
803
|
+
await fileSystem.remove(directory).catch(() => {
|
|
804
|
+
throw new Error(
|
|
805
|
+
"The temporary LiveKit credential directory could not be removed."
|
|
806
|
+
);
|
|
807
|
+
});
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
var defaultLiveKitCommandRunner, defaultTemporaryFileSystem;
|
|
811
|
+
var init_livekit = __esm({
|
|
812
|
+
"src/setup/livekit.ts"() {
|
|
813
|
+
"use strict";
|
|
814
|
+
init_errors();
|
|
815
|
+
init_package_managers();
|
|
816
|
+
init_environment();
|
|
817
|
+
defaultLiveKitCommandRunner = {
|
|
818
|
+
capture: captureCommand,
|
|
819
|
+
interactive: interactiveCommand
|
|
820
|
+
};
|
|
821
|
+
defaultTemporaryFileSystem = {
|
|
822
|
+
chmod: chmod2,
|
|
823
|
+
mkdtemp,
|
|
824
|
+
readFile: (path) => readFile3(path, "utf8"),
|
|
825
|
+
remove: (path) => rm2(path, {
|
|
826
|
+
force: true,
|
|
827
|
+
maxRetries: 3,
|
|
828
|
+
recursive: true,
|
|
829
|
+
retryDelay: 100
|
|
830
|
+
}),
|
|
831
|
+
writeFile: (path, contents, mode) => writeFile3(path, contents, { mode })
|
|
832
|
+
};
|
|
833
|
+
}
|
|
834
|
+
});
|
|
835
|
+
|
|
836
|
+
// src/setup/livekit-install.ts
|
|
837
|
+
import { tmpdir as tmpdir2 } from "os";
|
|
838
|
+
async function planLiveKitInstall(update, platform = process.platform, runner = defaultLiveKitCommandRunner) {
|
|
839
|
+
try {
|
|
840
|
+
if (platform === "darwin") {
|
|
841
|
+
await runner.capture("brew", ["--version"]);
|
|
842
|
+
const args = [update ? "upgrade" : "install", "livekit-cli"];
|
|
843
|
+
return {
|
|
844
|
+
command: "brew",
|
|
845
|
+
args,
|
|
846
|
+
displayCommand: `brew ${args.join(" ")}`
|
|
847
|
+
};
|
|
848
|
+
}
|
|
849
|
+
if (platform === "win32") {
|
|
850
|
+
await runner.capture("winget", ["--version"]);
|
|
851
|
+
const args = [
|
|
852
|
+
update ? "upgrade" : "install",
|
|
853
|
+
"--id",
|
|
854
|
+
"LiveKit.LiveKitCLI",
|
|
855
|
+
"--exact"
|
|
856
|
+
];
|
|
857
|
+
return {
|
|
858
|
+
command: "winget",
|
|
859
|
+
args,
|
|
860
|
+
displayCommand: `winget ${args.join(" ")}`
|
|
861
|
+
};
|
|
862
|
+
}
|
|
863
|
+
if (platform === "linux") {
|
|
864
|
+
await runner.capture("bash", ["--version"]);
|
|
865
|
+
await runner.capture("curl", ["--version"]);
|
|
866
|
+
const script = "curl -fsSL https://get.livekit.io/cli | bash";
|
|
867
|
+
return {
|
|
868
|
+
command: "bash",
|
|
869
|
+
args: ["-o", "pipefail", "-c", script],
|
|
870
|
+
displayCommand: script
|
|
871
|
+
};
|
|
872
|
+
}
|
|
873
|
+
} catch {
|
|
874
|
+
}
|
|
875
|
+
return void 0;
|
|
876
|
+
}
|
|
877
|
+
function installLiveKitCli(plan, runner = defaultLiveKitCommandRunner) {
|
|
878
|
+
return runner.interactive(plan.command, plan.args, { cwd: tmpdir2() });
|
|
879
|
+
}
|
|
880
|
+
function authenticateLiveKitCli(runner = defaultLiveKitCommandRunner) {
|
|
881
|
+
return runner.interactive("lk", ["cloud", "auth"], { cwd: tmpdir2() });
|
|
882
|
+
}
|
|
883
|
+
var LIVEKIT_INSTALL_GUIDE;
|
|
884
|
+
var init_livekit_install = __esm({
|
|
885
|
+
"src/setup/livekit-install.ts"() {
|
|
886
|
+
"use strict";
|
|
887
|
+
init_livekit();
|
|
888
|
+
LIVEKIT_INSTALL_GUIDE = "https://docs.livekit.io/reference/developer-tools/livekit-cli/";
|
|
889
|
+
}
|
|
890
|
+
});
|
|
891
|
+
|
|
892
|
+
// src/setup/redaction.ts
|
|
893
|
+
function maskSecret(value) {
|
|
894
|
+
const normalized = value.trim();
|
|
895
|
+
if (normalized.length <= 4) {
|
|
896
|
+
return "\u2022\u2022\u2022\u2022";
|
|
897
|
+
}
|
|
898
|
+
return `\u2022\u2022\u2022\u2022${normalized.slice(-4)}`;
|
|
899
|
+
}
|
|
900
|
+
var SENSITIVE_ASSIGNMENT, SENSITIVE_QUERY, BEARER_TOKEN, SecretRedactor;
|
|
901
|
+
var init_redaction = __esm({
|
|
902
|
+
"src/setup/redaction.ts"() {
|
|
903
|
+
"use strict";
|
|
904
|
+
SENSITIVE_ASSIGNMENT = /\b(authorization|auth[_-]?code|code[_-]?verifier|access[_-]?token|refresh[_-]?token|api[_-]?key|api[_-]?secret|password|secret)\b(\s*[:=]\s*)(["']?)([^\s,"'&}]+)\3/giu;
|
|
905
|
+
SENSITIVE_QUERY = /([?&](?:auth_code|code_verifier|access_token|refresh_token|api_key|api_secret)=)[^&#\s]+/giu;
|
|
906
|
+
BEARER_TOKEN = /\bBearer\s+[A-Za-z\d._~+/=-]+/giu;
|
|
907
|
+
SecretRedactor = class {
|
|
908
|
+
#values = /* @__PURE__ */ new Set();
|
|
909
|
+
add(...values) {
|
|
910
|
+
for (const value of values) {
|
|
911
|
+
const trimmed = value?.trim();
|
|
912
|
+
if (trimmed !== void 0 && trimmed.length >= 4) {
|
|
913
|
+
this.#values.add(trimmed);
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
}
|
|
917
|
+
redact(value) {
|
|
918
|
+
let redacted = value;
|
|
919
|
+
for (const secret of [...this.#values].sort(
|
|
920
|
+
(left, right) => right.length - left.length
|
|
921
|
+
)) {
|
|
922
|
+
redacted = redacted.replaceAll(secret, "[REDACTED]");
|
|
923
|
+
}
|
|
924
|
+
return redacted.replace(BEARER_TOKEN, "Bearer [REDACTED]").replace(SENSITIVE_QUERY, "$1[REDACTED]").replace(SENSITIVE_ASSIGNMENT, "$1$2$3[REDACTED]$3");
|
|
925
|
+
}
|
|
926
|
+
error(error, fallback) {
|
|
927
|
+
const message = error instanceof Error ? error.message : fallback;
|
|
928
|
+
return new Error(this.redact(message), { cause: error });
|
|
929
|
+
}
|
|
930
|
+
};
|
|
931
|
+
}
|
|
932
|
+
});
|
|
933
|
+
|
|
934
|
+
// src/setup/success-page.ts
|
|
935
|
+
var successPage;
|
|
936
|
+
var init_success_page = __esm({
|
|
937
|
+
"src/setup/success-page.ts"() {
|
|
938
|
+
"use strict";
|
|
939
|
+
successPage = `<!doctype html>
|
|
940
|
+
<html lang="en">
|
|
941
|
+
<head>
|
|
942
|
+
<meta charset="utf-8" />
|
|
943
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
944
|
+
<meta name="referrer" content="no-referrer" />
|
|
945
|
+
<title>Spatius authorization complete</title>
|
|
946
|
+
<style>
|
|
947
|
+
:root { color-scheme: light; font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; color: #151131; background: #f7f5f1; }
|
|
948
|
+
* { box-sizing: border-box; }
|
|
949
|
+
body { margin: 0; min-height: 100vh; min-height: 100svh; display: flex; flex-direction: column; background: radial-gradient(ellipse at 50% 38%, #eae6f3 0, transparent 58%); }
|
|
950
|
+
a { color: inherit; text-decoration: none; }
|
|
951
|
+
a:focus-visible { outline: 3px solid #6363a7; outline-offset: 5px; }
|
|
952
|
+
header, footer { width: min(100% - 48px, 1120px); margin-inline: auto; display: flex; align-items: center; justify-content: space-between; gap: 20px; padding-block: 28px; }
|
|
953
|
+
.brand { font-size: 24px; font-weight: 650; letter-spacing: -.9px; }
|
|
954
|
+
.brand { display: inline-flex; align-items: center; gap: 8px; }
|
|
955
|
+
.eyebrow { font: 11px ui-monospace, SFMono-Regular, Consolas, monospace; letter-spacing: .12em; text-transform: uppercase; color: #636174; }
|
|
956
|
+
main { flex: 1; display: grid; place-items: center; padding: 48px 24px; }
|
|
957
|
+
.card { width: min(100%, 620px); padding: 48px; background: #ffffffed; border: 1px solid #e8e4ee; border-radius: 24px; box-shadow: 0 24px 80px -32px #30234d30; }
|
|
958
|
+
.check { display: grid; place-items: center; width: 52px; height: 52px; border: 1px solid #dcd7ed; border-radius: 50%; background: #f1eef8; color: #6363a7; margin-bottom: 28px; }
|
|
959
|
+
.check svg { width: 24px; height: 24px; }
|
|
960
|
+
h1 { font-size: clamp(30px, 5vw, 42px); font-weight: 550; letter-spacing: -.045em; line-height: 1.15; margin: 12px 0 16px; }
|
|
961
|
+
.description { color: #636174; line-height: 1.7; font-size: 15px; margin: 0; max-width: 420px; }
|
|
962
|
+
.terminal { display: flex; align-items: center; gap: 16px; padding: 20px; margin: 28px 0 32px; background: #f7f5f9; border: 1px solid #e8e4ee; border-radius: 12px; }
|
|
963
|
+
.terminal-icon { font: 20px ui-monospace, SFMono-Regular, Consolas, monospace; color: #6363a7; }
|
|
964
|
+
.terminal strong { display: block; font-size: 14px; font-weight: 600; }
|
|
965
|
+
.terminal p { margin: 5px 0 0; font-size: 13px; color: #636174; line-height: 1.5; }
|
|
966
|
+
nav { border-top: 1px solid #eeebf1; padding-top: 24px; }
|
|
967
|
+
nav .eyebrow { display: block; margin-bottom: 14px; }
|
|
968
|
+
.links { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; }
|
|
969
|
+
.links a { display: flex; align-items: center; justify-content: space-between; gap: 8px; min-height: 46px; padding: 12px 14px; border: 1px solid #e8e4ee; border-radius: 9px; font-size: 13px; font-weight: 550; }
|
|
970
|
+
.links a:hover { background: #f1eef8; border-color: #bab2d4; }
|
|
971
|
+
.link-label { display: flex; align-items: center; gap: 10px; }
|
|
972
|
+
.links svg { width: 18px; height: 18px; flex-shrink: 0; fill: none; stroke: currentColor; stroke-width: 1.6; stroke-linecap: round; stroke-linejoin: round; color: #6363a7; }
|
|
973
|
+
.links .external-icon { width: 14px; height: 14px; }
|
|
974
|
+
footer { justify-content: flex-end; font-size: 12px; color: #636174; }
|
|
975
|
+
footer a:hover { color: #151131; text-decoration: underline; }
|
|
976
|
+
@media (max-width: 520px) {
|
|
977
|
+
header, footer { width: calc(100% - 40px); padding-block: 22px; }
|
|
978
|
+
main { padding: 20px; }
|
|
979
|
+
.card { padding: 28px 24px; border-radius: 18px; }
|
|
980
|
+
.links { grid-template-columns: 1fr; }
|
|
981
|
+
footer { flex-wrap: wrap; gap: 12px; }
|
|
982
|
+
}
|
|
983
|
+
</style>
|
|
984
|
+
</head>
|
|
985
|
+
<body>
|
|
986
|
+
<header>
|
|
987
|
+
<!-- Inline brand mark from spatius-site/src/assets/brand/spatius-logo-mark-black.svg. -->
|
|
988
|
+
<a class="brand" href="https://www.spatius.ai/" target="_blank" rel="noopener noreferrer"><svg width="24" height="20" aria-hidden="true" viewBox="0 0 265 220" fill="none" xmlns="http://www.w3.org/2000/svg"><g clip-path="url(#clip0_279_158)"><path d="M178.56 7.19C182.19 8.35 184.62 11.78 184.62 15.59V39.43C184.62 44.17 180.77 48 176.06 48H151.07C148.71 48 146.57 48.96 145.02 50.5C144.92 50.6 144.82 50.69 144.73 50.8C143.35 52.33 142.5 54.35 142.5 56.56V94.22C142.5 98.94 138.66 102.78 133.94 102.78H108.96C104.22 102.78 100.4 106.62 100.4 111.34V149C100.4 153.74 96.56 157.57 91.84 157.57H66.86C62.12 157.57 58.29 161.41 58.29 166.14V188.47C58.29 195.77 49.94 199.95 44.12 195.55C17.04 175.04 0 145.2 0 111.99C0 50.13 59.1 0 132 0C148.41 0 164.11 2.53999 178.59 7.17999L178.56 7.19Z" fill="currentColor"/><path d="M230.22 59.4799V98.2699C230.22 103.15 226.27 107.09 221.4 107.09H195.68C190.8 107.09 186.86 111.04 186.86 115.91V154.7C186.86 159.58 182.9 163.52 178.04 163.52H152.31C149.89 163.52 147.67 164.52 146.08 166.1C145.98 166.2 145.88 166.3 145.78 166.41C144.35 167.98 143.49 170.06 143.49 172.34V211.13C143.49 216.01 139.54 219.95 134.67 219.95H108.95C104.07 219.95 100.13 216 100.13 211.13V172.34C100.13 167.46 104.08 163.52 108.95 163.52H134.67C139.55 163.52 143.49 159.57 143.49 154.7V115.91C143.49 115.62 143.49 115.35 143.53 115.06C143.53 114.93 143.55 114.81 143.57 114.67C143.57 114.55 143.6 114.42 143.63 114.29C143.63 114.23 143.65 114.18 143.66 114.12C143.68 114 143.72 113.88 143.75 113.77C143.77 113.66 143.81 113.55 143.84 113.43C143.92 113.18 144.01 112.91 144.11 112.67C144.19 112.46 144.28 112.26 144.36 112.07C144.36 112.05 144.38 112.04 144.38 112.03C144.44 111.92 144.49 111.81 144.56 111.7C144.73 111.39 144.93 111.08 145.12 110.79C145.21 110.67 145.29 110.55 145.39 110.44C145.51 110.27 145.64 110.12 145.79 109.96C145.89 109.85 145.99 109.75 146.09 109.65C146.29 109.45 146.5 109.26 146.71 109.07C146.93 108.89 147.15 108.73 147.38 108.56C147.61 108.41 147.86 108.26 148.1 108.12C148.34 107.99 148.6 107.87 148.85 107.75C149.12 107.63 149.38 107.53 149.66 107.45C149.87 107.38 150.08 107.33 150.29 107.28C150.5 107.22 150.72 107.18 150.94 107.15C151.12 107.13 151.29 107.11 151.47 107.08H178.02C182.89 107.08 186.84 103.13 186.84 98.2599V59.4699C186.84 54.5899 190.79 50.6499 195.66 50.6499H221.38C226.26 50.6499 230.2 54.5999 230.2 59.4699H230.21L230.22 59.4799Z" fill="currentColor"/><path d="M264.34 122.11V154.32C264.34 158.37 261.07 161.64 257.02 161.64H235.66C231.61 161.64 228.34 164.91 228.34 168.96V201.16C228.34 205.21 225.07 208.49 221.02 208.49H199.66C195.61 208.49 192.34 205.22 192.34 201.16V168.96C192.34 164.91 195.61 161.64 199.66 161.64H221.02C225.07 161.64 228.34 158.37 228.34 154.32V122.11C228.34 118.06 231.61 114.79 235.66 114.79H257.02C261.07 114.79 264.34 118.06 264.34 122.11Z" fill="currentColor"/></g><defs><clipPath id="clip0_279_158"><rect width="265" height="220" fill="white"/></clipPath></defs></svg>Spatius</a>
|
|
989
|
+
</header>
|
|
990
|
+
<main>
|
|
991
|
+
<section class="card" aria-labelledby="success-title">
|
|
992
|
+
<div class="check" aria-hidden="true"><svg viewBox="0 0 24 24" fill="none"><path d="m5 12 4 4L19 6" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" /></svg></div>
|
|
993
|
+
<span class="eyebrow">You're connected</span>
|
|
994
|
+
<h1 id="success-title">Authorization complete.</h1>
|
|
995
|
+
<p class="description">Your next idea starts here. Spatius is ready to help you bring it to life.</p>
|
|
996
|
+
<div class="terminal">
|
|
997
|
+
<span class="terminal-icon" aria-hidden="true">>_</span>
|
|
998
|
+
<div><strong>Continue in your terminal</strong><p>You can close this tab and finish your project setup.</p></div>
|
|
999
|
+
</div>
|
|
1000
|
+
<nav aria-label="Spatius resources">
|
|
1001
|
+
<span class="eyebrow">While you're building</span>
|
|
1002
|
+
<div class="links">
|
|
1003
|
+
<a href="https://docs.spatius.ai/" target="_blank" rel="noopener noreferrer"><span class="link-label"><svg aria-hidden="true" viewBox="0 0 24 24"><path d="M12 5v16M12 5C9 3 5 3 2 4v15c3-1 7-1 10 2 3-3 7-3 10-2V4c-3-1-7-1-10 1Z" /></svg>Documentation</span><svg class="external-icon" aria-hidden="true" viewBox="0 0 24 24"><path d="M14 3h7v7m0-7L10 14M10 3H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-5" /></svg></a>
|
|
1004
|
+
<a href="https://app.spatius.ai" target="_blank" rel="noopener noreferrer"><span class="link-label"><svg aria-hidden="true" viewBox="0 0 24 24"><rect x="3" y="3" width="18" height="18" rx="3" /><path d="M3 9h18M9 9v12" /></svg>Open Studio</span><svg class="external-icon" aria-hidden="true" viewBox="0 0 24 24"><path d="M14 3h7v7m0-7L10 14M10 3H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-5" /></svg></a>
|
|
1005
|
+
<a href="https://github.com/spatius-ai" target="_blank" rel="noopener noreferrer"><span class="link-label"><svg aria-hidden="true" viewBox="0 0 24 24"><path d="m8 7-5 5 5 5m8-10 5 5-5 5m-3-13-2 16" /></svg>GitHub</span><svg class="external-icon" aria-hidden="true" viewBox="0 0 24 24"><path d="M14 3h7v7m0-7L10 14M10 3H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-5" /></svg></a>
|
|
1006
|
+
<a href="https://discord.gg/9HGhZfHZh9" target="_blank" rel="noopener noreferrer"><span class="link-label"><svg aria-hidden="true" viewBox="0 0 24 24"><path d="M21 11a8 8 0 0 1-8 8H8l-5 3V7a4 4 0 0 1 4-4h6a8 8 0 0 1 8 8Z" /><path d="M7 10h10M7 14h6" /></svg>Join our Discord</span><svg class="external-icon" aria-hidden="true" viewBox="0 0 24 24"><path d="M14 3h7v7m0-7L10 14M10 3H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-5" /></svg></a>
|
|
1007
|
+
</div>
|
|
1008
|
+
</nav>
|
|
1009
|
+
</section>
|
|
1010
|
+
</main>
|
|
1011
|
+
<footer><a href="https://www.spatius.ai/" target="_blank" rel="noopener noreferrer">Explore Spatius \u2197</a></footer>
|
|
1012
|
+
</body>
|
|
1013
|
+
</html>`;
|
|
1014
|
+
}
|
|
1015
|
+
});
|
|
1016
|
+
|
|
1017
|
+
// src/setup/spatius-api.ts
|
|
1018
|
+
function diagnosticRoute(path) {
|
|
1019
|
+
return path.replace(/\/apps\/[^/]+/u, "/apps/{appId}");
|
|
1020
|
+
}
|
|
1021
|
+
function apiError(payload, httpStatus, method, path) {
|
|
1022
|
+
const errors = payload.errors;
|
|
1023
|
+
const hasErrors = Array.isArray(errors) && errors.length > 0;
|
|
1024
|
+
const raw = hasErrors ? errors[0] : payload.error;
|
|
1025
|
+
if (!hasErrors && (raw === void 0 || raw === null)) return void 0;
|
|
1026
|
+
const error = raw !== null && typeof raw === "object" && !Array.isArray(raw) ? raw : {};
|
|
1027
|
+
const code = typeof error.code === "string" && Object.hasOwn(knownApiStatuses, error.code) ? error.code : "API_ERROR";
|
|
1028
|
+
const suppliedStatus = typeof error.status === "number" ? error.status : typeof error.status === "string" && /^\d{3}$/u.test(error.status) ? Number(error.status) : void 0;
|
|
1029
|
+
const status = suppliedStatus !== void 0 && Number.isInteger(suppliedStatus) && suppliedStatus >= 400 && suppliedStatus <= 599 ? suppliedStatus : knownApiStatuses[code] ?? (httpStatus >= 400 ? httpStatus : 500);
|
|
1030
|
+
return new SpatiusHttpError(status, method, path, {
|
|
1031
|
+
apiCode: code,
|
|
1032
|
+
httpStatus,
|
|
1033
|
+
missingFrontendUrl: code === "INTERNAL_SERVER_ERROR" && error.detail === "server open-platform frontend URL is not configured"
|
|
1034
|
+
});
|
|
1035
|
+
}
|
|
1036
|
+
function object(value, label) {
|
|
1037
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
|
1038
|
+
throw new Error(`Spatius returned an invalid ${label} response.`);
|
|
1039
|
+
}
|
|
1040
|
+
return value;
|
|
1041
|
+
}
|
|
1042
|
+
function field(value, camelCase, snakeCase) {
|
|
1043
|
+
return value[camelCase] ?? value[snakeCase];
|
|
1044
|
+
}
|
|
1045
|
+
function requiredString(value, camelCase, snakeCase = camelCase) {
|
|
1046
|
+
const result = field(value, camelCase, snakeCase);
|
|
1047
|
+
if (typeof result !== "string" || result.trim() === "") {
|
|
1048
|
+
throw new Error(`Spatius returned a response without ${camelCase}.`);
|
|
1049
|
+
}
|
|
1050
|
+
return result.trim();
|
|
1051
|
+
}
|
|
1052
|
+
function optionalString(value, camelCase, snakeCase = camelCase) {
|
|
1053
|
+
const result = field(value, camelCase, snakeCase);
|
|
1054
|
+
return typeof result === "string" && result.trim() !== "" ? result.trim() : void 0;
|
|
1055
|
+
}
|
|
1056
|
+
function optionalNumber(value, camelCase, snakeCase = camelCase) {
|
|
1057
|
+
const result = field(value, camelCase, snakeCase);
|
|
1058
|
+
return typeof result === "number" && Number.isFinite(result) ? result : void 0;
|
|
1059
|
+
}
|
|
1060
|
+
function arrayField(value, camelCase, snakeCase) {
|
|
1061
|
+
const result = field(value, camelCase, snakeCase);
|
|
1062
|
+
if (!Array.isArray(result)) return [];
|
|
1063
|
+
const values = [];
|
|
1064
|
+
for (const entry of result) values.push(entry);
|
|
1065
|
+
return values;
|
|
1066
|
+
}
|
|
1067
|
+
function nextPageToken(value) {
|
|
1068
|
+
const paginationValue = value.pagination;
|
|
1069
|
+
if (paginationValue === null || typeof paginationValue !== "object" || Array.isArray(paginationValue)) {
|
|
1070
|
+
return void 0;
|
|
1071
|
+
}
|
|
1072
|
+
return optionalString(
|
|
1073
|
+
paginationValue,
|
|
1074
|
+
"nextPageToken",
|
|
1075
|
+
"next_page_token"
|
|
1076
|
+
);
|
|
1077
|
+
}
|
|
1078
|
+
function parseTokenSet(value, refreshTokenFallback) {
|
|
1079
|
+
const refreshToken = optionalString(value, "refreshToken", "refresh_token") ?? refreshTokenFallback;
|
|
1080
|
+
if (refreshToken === void 0) {
|
|
1081
|
+
throw new Error("Spatius returned a response without refreshToken.");
|
|
1082
|
+
}
|
|
1083
|
+
return {
|
|
1084
|
+
accessToken: requiredString(value, "accessToken", "access_token"),
|
|
1085
|
+
refreshToken
|
|
1086
|
+
};
|
|
1087
|
+
}
|
|
1088
|
+
function parseApiKey(value) {
|
|
1089
|
+
return {
|
|
1090
|
+
apiKey: requiredString(value, "apiKey", "api_key"),
|
|
1091
|
+
...optionalString(value, "createdAt", "created_at") === void 0 ? {} : { createdAt: optionalString(value, "createdAt", "created_at") }
|
|
1092
|
+
};
|
|
1093
|
+
}
|
|
1094
|
+
var SPATIUS_CONSOLE_BASE_URL, knownApiStatuses, SpatiusHttpError, SpatiusApiClient, SpatiusAuthenticatedSession;
|
|
1095
|
+
var init_spatius_api = __esm({
|
|
1096
|
+
"src/setup/spatius-api.ts"() {
|
|
1097
|
+
"use strict";
|
|
1098
|
+
SPATIUS_CONSOLE_BASE_URL = "https://api.studio.spatius.ai";
|
|
1099
|
+
knownApiStatuses = {
|
|
1100
|
+
BAD_REQUEST: 400,
|
|
1101
|
+
INVALID_ARGUMENT: 400,
|
|
1102
|
+
FAILED_PRECONDITION: 400,
|
|
1103
|
+
UNAUTHORIZED: 401,
|
|
1104
|
+
PAYMENT_REQUIRED: 402,
|
|
1105
|
+
FORBIDDEN: 403,
|
|
1106
|
+
PERMISSION_DENIED: 403,
|
|
1107
|
+
NOT_FOUND: 404,
|
|
1108
|
+
TIMEOUT: 408,
|
|
1109
|
+
ALREADY_EXISTS: 409,
|
|
1110
|
+
FAILED_DEPENDENCY: 424,
|
|
1111
|
+
QUOTA_EXCEEDED: 429,
|
|
1112
|
+
INTERNAL_SERVER_ERROR: 500,
|
|
1113
|
+
UNIMPLEMENTED: 501,
|
|
1114
|
+
UNAVAILABLE: 503,
|
|
1115
|
+
unauthorized: 401,
|
|
1116
|
+
forbidden: 403,
|
|
1117
|
+
invalid_request: 400,
|
|
1118
|
+
not_found: 404,
|
|
1119
|
+
internal_error: 500
|
|
1120
|
+
};
|
|
1121
|
+
SpatiusHttpError = class extends Error {
|
|
1122
|
+
status;
|
|
1123
|
+
httpStatus;
|
|
1124
|
+
apiCode;
|
|
1125
|
+
constructor(status, method, path, options = {}) {
|
|
1126
|
+
const httpStatus = options.httpStatus ?? status;
|
|
1127
|
+
super(
|
|
1128
|
+
`Spatius request ${method} ${diagnosticRoute(path)} failed (HTTP ${String(httpStatus)}${options.apiCode === void 0 ? "" : `, API ${String(status)} ${options.apiCode}`}).` + (options.missingFrontendUrl === true ? " The Spatius console server is missing SERVER_OPEN_PLATFORM_FRONTEND_URL. Configure it with the Studio URL in the console deployment, then retry." : "")
|
|
1129
|
+
);
|
|
1130
|
+
this.name = "SpatiusHttpError";
|
|
1131
|
+
this.status = status;
|
|
1132
|
+
this.httpStatus = httpStatus;
|
|
1133
|
+
this.apiCode = options.apiCode;
|
|
1134
|
+
}
|
|
1135
|
+
};
|
|
1136
|
+
SpatiusApiClient = class {
|
|
1137
|
+
#baseUrl;
|
|
1138
|
+
#fetch;
|
|
1139
|
+
#onDiagnostic;
|
|
1140
|
+
constructor(baseUrl = SPATIUS_CONSOLE_BASE_URL, fetchImplementation = fetch, onDiagnostic) {
|
|
1141
|
+
this.#baseUrl = baseUrl.replace(/\/+$/u, "");
|
|
1142
|
+
this.#fetch = fetchImplementation;
|
|
1143
|
+
this.#onDiagnostic = onDiagnostic;
|
|
1144
|
+
}
|
|
1145
|
+
async #request(path, {
|
|
1146
|
+
accessToken,
|
|
1147
|
+
appCredentials,
|
|
1148
|
+
body,
|
|
1149
|
+
method = "GET",
|
|
1150
|
+
query
|
|
1151
|
+
} = {}) {
|
|
1152
|
+
const startedAt = performance.now();
|
|
1153
|
+
const report = (details) => {
|
|
1154
|
+
this.#onDiagnostic?.({
|
|
1155
|
+
method,
|
|
1156
|
+
route: diagnosticRoute(path),
|
|
1157
|
+
durationMs: Math.round(performance.now() - startedAt),
|
|
1158
|
+
...details
|
|
1159
|
+
});
|
|
1160
|
+
};
|
|
1161
|
+
const url = new URL(`${this.#baseUrl}${path}`);
|
|
1162
|
+
if (query !== void 0) {
|
|
1163
|
+
url.search = query.toString();
|
|
1164
|
+
}
|
|
1165
|
+
const headers = new Headers({ Accept: "application/json" });
|
|
1166
|
+
if (body !== void 0) {
|
|
1167
|
+
headers.set("Content-Type", "application/json");
|
|
1168
|
+
}
|
|
1169
|
+
if (accessToken !== void 0) {
|
|
1170
|
+
headers.set("Authorization", `Bearer ${accessToken}`);
|
|
1171
|
+
}
|
|
1172
|
+
if (appCredentials !== void 0) {
|
|
1173
|
+
headers.set("X-App-ID", appCredentials.appId);
|
|
1174
|
+
headers.set("X-API-Key", appCredentials.apiKey);
|
|
1175
|
+
}
|
|
1176
|
+
let response;
|
|
1177
|
+
try {
|
|
1178
|
+
response = await this.#fetch(url, {
|
|
1179
|
+
...body === void 0 ? {} : { body: JSON.stringify(body) },
|
|
1180
|
+
headers,
|
|
1181
|
+
method,
|
|
1182
|
+
redirect: "error",
|
|
1183
|
+
signal: AbortSignal.timeout(3e4)
|
|
1184
|
+
});
|
|
1185
|
+
} catch {
|
|
1186
|
+
report({ outcome: "network-error" });
|
|
1187
|
+
throw new Error(
|
|
1188
|
+
`Could not reach the Spatius API for ${method} ${diagnosticRoute(path)}.`
|
|
1189
|
+
);
|
|
1190
|
+
}
|
|
1191
|
+
if (response.status === 204) {
|
|
1192
|
+
report({ outcome: "response", httpStatus: response.status });
|
|
1193
|
+
return {};
|
|
1194
|
+
}
|
|
1195
|
+
let payload;
|
|
1196
|
+
try {
|
|
1197
|
+
const contents = await response.text();
|
|
1198
|
+
payload = contents.trim() === "" ? {} : object(JSON.parse(contents), "API");
|
|
1199
|
+
} catch {
|
|
1200
|
+
report({ outcome: "invalid-response", httpStatus: response.status });
|
|
1201
|
+
if (!response.ok)
|
|
1202
|
+
throw new SpatiusHttpError(response.status, method, path);
|
|
1203
|
+
throw new Error(
|
|
1204
|
+
`Spatius returned an invalid JSON object response for ${method} ${diagnosticRoute(path)}.`
|
|
1205
|
+
);
|
|
1206
|
+
}
|
|
1207
|
+
const error = apiError(payload, response.status, method, path);
|
|
1208
|
+
report({
|
|
1209
|
+
outcome: "response",
|
|
1210
|
+
httpStatus: response.status,
|
|
1211
|
+
...error === void 0 ? {} : { apiStatus: error.status, apiCode: error.apiCode }
|
|
1212
|
+
});
|
|
1213
|
+
if (error !== void 0) throw error;
|
|
1214
|
+
if (!response.ok) throw new SpatiusHttpError(response.status, method, path);
|
|
1215
|
+
return payload;
|
|
1216
|
+
}
|
|
1217
|
+
async createAuthSession(input) {
|
|
1218
|
+
const response = await this.#request("/v1/cli/auth/sessions", {
|
|
1219
|
+
body: {
|
|
1220
|
+
clientName: "create-spatius-app",
|
|
1221
|
+
codeChallenge: input.codeChallenge,
|
|
1222
|
+
codeChallengeMethod: "CLI_AUTH_CODE_CHALLENGE_METHOD_S256",
|
|
1223
|
+
redirectUri: input.redirectUri,
|
|
1224
|
+
state: input.state
|
|
1225
|
+
},
|
|
1226
|
+
method: "POST"
|
|
1227
|
+
});
|
|
1228
|
+
return {
|
|
1229
|
+
authRequestId: requiredString(
|
|
1230
|
+
response,
|
|
1231
|
+
"authRequestId",
|
|
1232
|
+
"auth_request_id"
|
|
1233
|
+
),
|
|
1234
|
+
authorizeUrl: requiredString(response, "authorizeUrl", "authorize_url"),
|
|
1235
|
+
...optionalString(response, "expiresAt", "expires_at") === void 0 ? {} : { expiresAt: optionalString(response, "expiresAt", "expires_at") },
|
|
1236
|
+
...optionalNumber(response, "expiresIn", "expires_in") === void 0 ? {} : { expiresIn: optionalNumber(response, "expiresIn", "expires_in") }
|
|
1237
|
+
};
|
|
1238
|
+
}
|
|
1239
|
+
async exchangeAuthCode(input) {
|
|
1240
|
+
const response = await this.#request("/v1/cli/auth/token", {
|
|
1241
|
+
body: {
|
|
1242
|
+
authCode: input.authCode,
|
|
1243
|
+
authRequestId: input.authRequestId,
|
|
1244
|
+
codeVerifier: input.codeVerifier
|
|
1245
|
+
},
|
|
1246
|
+
method: "POST"
|
|
1247
|
+
});
|
|
1248
|
+
return parseTokenSet(object(response.token, "token"));
|
|
1249
|
+
}
|
|
1250
|
+
async refresh(refreshToken) {
|
|
1251
|
+
const response = await this.#request("/v1/cli/auth/token:refresh", {
|
|
1252
|
+
body: { refreshToken },
|
|
1253
|
+
method: "POST"
|
|
1254
|
+
});
|
|
1255
|
+
return parseTokenSet(object(response.token, "token"), refreshToken);
|
|
1256
|
+
}
|
|
1257
|
+
async revoke(tokens) {
|
|
1258
|
+
await this.#request("/v1/cli/auth/token:revoke", {
|
|
1259
|
+
body: {
|
|
1260
|
+
accessToken: tokens.accessToken,
|
|
1261
|
+
refreshToken: tokens.refreshToken
|
|
1262
|
+
},
|
|
1263
|
+
method: "POST"
|
|
1264
|
+
});
|
|
1265
|
+
}
|
|
1266
|
+
async listApps(accessToken) {
|
|
1267
|
+
const results = [];
|
|
1268
|
+
let pageToken;
|
|
1269
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1270
|
+
do {
|
|
1271
|
+
const query = new URLSearchParams({ "pagination.pageSize": "100" });
|
|
1272
|
+
if (pageToken !== void 0) {
|
|
1273
|
+
query.set("pagination.pageToken", pageToken);
|
|
1274
|
+
}
|
|
1275
|
+
const response = await this.#request("/v1/apps", {
|
|
1276
|
+
accessToken,
|
|
1277
|
+
query
|
|
1278
|
+
});
|
|
1279
|
+
for (const raw of arrayField(response, "apps", "apps")) {
|
|
1280
|
+
const app = object(raw, "app");
|
|
1281
|
+
results.push({
|
|
1282
|
+
appId: requiredString(app, "appId", "app_id"),
|
|
1283
|
+
...optionalString(app, "createdAt", "created_at") === void 0 ? {} : { createdAt: optionalString(app, "createdAt", "created_at") },
|
|
1284
|
+
name: optionalString(app, "name") ?? "Unnamed app"
|
|
1285
|
+
});
|
|
1286
|
+
}
|
|
1287
|
+
pageToken = nextPageToken(response);
|
|
1288
|
+
if (pageToken !== void 0 && seen.has(pageToken)) break;
|
|
1289
|
+
if (pageToken !== void 0) seen.add(pageToken);
|
|
1290
|
+
} while (pageToken !== void 0);
|
|
1291
|
+
return results;
|
|
1292
|
+
}
|
|
1293
|
+
async createApp(accessToken, name) {
|
|
1294
|
+
const response = await this.#request("/v1/apps", {
|
|
1295
|
+
accessToken,
|
|
1296
|
+
body: { name },
|
|
1297
|
+
method: "POST"
|
|
1298
|
+
});
|
|
1299
|
+
return {
|
|
1300
|
+
appId: requiredString(response, "appId", "app_id"),
|
|
1301
|
+
name
|
|
1302
|
+
};
|
|
1303
|
+
}
|
|
1304
|
+
async listApiKeys(accessToken, appId) {
|
|
1305
|
+
const results = [];
|
|
1306
|
+
let pageToken;
|
|
1307
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1308
|
+
do {
|
|
1309
|
+
const query = new URLSearchParams({ "pagination.pageSize": "100" });
|
|
1310
|
+
if (pageToken !== void 0) {
|
|
1311
|
+
query.set("pagination.pageToken", pageToken);
|
|
1312
|
+
}
|
|
1313
|
+
const path = `/v1/apps/${encodeURIComponent(appId)}/api-keys`;
|
|
1314
|
+
const response = await this.#request(path, { accessToken, query });
|
|
1315
|
+
for (const raw of arrayField(response, "apiKeys", "api_keys")) {
|
|
1316
|
+
results.push(parseApiKey(object(raw, "API key")));
|
|
1317
|
+
}
|
|
1318
|
+
pageToken = nextPageToken(response);
|
|
1319
|
+
if (pageToken !== void 0 && seen.has(pageToken)) break;
|
|
1320
|
+
if (pageToken !== void 0) seen.add(pageToken);
|
|
1321
|
+
} while (pageToken !== void 0);
|
|
1322
|
+
return results;
|
|
1323
|
+
}
|
|
1324
|
+
async createApiKey(accessToken, appId) {
|
|
1325
|
+
const path = `/v1/apps/${encodeURIComponent(appId)}/api-keys`;
|
|
1326
|
+
const response = await this.#request(path, {
|
|
1327
|
+
accessToken,
|
|
1328
|
+
body: { appId },
|
|
1329
|
+
method: "POST"
|
|
1330
|
+
});
|
|
1331
|
+
return parseApiKey(object(field(response, "apiKey", "api_key"), "API key"));
|
|
1332
|
+
}
|
|
1333
|
+
async listPublicAvatars(accessToken) {
|
|
1334
|
+
const results = [];
|
|
1335
|
+
let pageToken;
|
|
1336
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1337
|
+
do {
|
|
1338
|
+
const query = new URLSearchParams({ "pagination.pageSize": "100" });
|
|
1339
|
+
if (pageToken !== void 0) {
|
|
1340
|
+
query.set("pagination.pageToken", pageToken);
|
|
1341
|
+
}
|
|
1342
|
+
const response = await this.#request("/v2/console/public-avatars", {
|
|
1343
|
+
accessToken,
|
|
1344
|
+
query
|
|
1345
|
+
});
|
|
1346
|
+
for (const raw of arrayField(
|
|
1347
|
+
response,
|
|
1348
|
+
"publicAvatars",
|
|
1349
|
+
"public_avatars"
|
|
1350
|
+
)) {
|
|
1351
|
+
const avatar = object(raw, "public avatar");
|
|
1352
|
+
results.push({
|
|
1353
|
+
id: requiredString(avatar, "id"),
|
|
1354
|
+
name: optionalString(avatar, "name") ?? "Unnamed avatar",
|
|
1355
|
+
source: "public",
|
|
1356
|
+
backgroundUrl: optionalString(
|
|
1357
|
+
avatar,
|
|
1358
|
+
"backgroundImageUrl",
|
|
1359
|
+
"background_image_url"
|
|
1360
|
+
)
|
|
1361
|
+
});
|
|
1362
|
+
}
|
|
1363
|
+
pageToken = nextPageToken(response);
|
|
1364
|
+
if (pageToken !== void 0 && seen.has(pageToken)) break;
|
|
1365
|
+
if (pageToken !== void 0) seen.add(pageToken);
|
|
1366
|
+
} while (pageToken !== void 0);
|
|
1367
|
+
return results;
|
|
1368
|
+
}
|
|
1369
|
+
async listPersonalAvatars(appId, apiKey) {
|
|
1370
|
+
const results = [];
|
|
1371
|
+
let pageToken;
|
|
1372
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1373
|
+
do {
|
|
1374
|
+
const query = new URLSearchParams({ "pagination.pageSize": "100" });
|
|
1375
|
+
if (pageToken !== void 0) {
|
|
1376
|
+
query.set("pagination.pageToken", pageToken);
|
|
1377
|
+
}
|
|
1378
|
+
const response = await this.#request("/v1/open/avatars", {
|
|
1379
|
+
appCredentials: { apiKey, appId },
|
|
1380
|
+
query
|
|
1381
|
+
});
|
|
1382
|
+
for (const raw of arrayField(response, "avatars", "avatars")) {
|
|
1383
|
+
const avatar = object(raw, "personal avatar");
|
|
1384
|
+
results.push({
|
|
1385
|
+
id: requiredString(avatar, "id"),
|
|
1386
|
+
name: optionalString(avatar, "name") ?? "Unnamed avatar",
|
|
1387
|
+
source: "personal"
|
|
1388
|
+
});
|
|
1389
|
+
}
|
|
1390
|
+
pageToken = nextPageToken(response);
|
|
1391
|
+
if (pageToken !== void 0 && seen.has(pageToken)) break;
|
|
1392
|
+
if (pageToken !== void 0) seen.add(pageToken);
|
|
1393
|
+
} while (pageToken !== void 0);
|
|
1394
|
+
return results;
|
|
1395
|
+
}
|
|
1396
|
+
};
|
|
1397
|
+
SpatiusAuthenticatedSession = class {
|
|
1398
|
+
#client;
|
|
1399
|
+
#tokens;
|
|
1400
|
+
constructor(client, tokens) {
|
|
1401
|
+
this.#client = client;
|
|
1402
|
+
this.#tokens = tokens;
|
|
1403
|
+
}
|
|
1404
|
+
async authorized(operation) {
|
|
1405
|
+
try {
|
|
1406
|
+
return await operation(this.#tokens.accessToken);
|
|
1407
|
+
} catch (error) {
|
|
1408
|
+
if (!(error instanceof SpatiusHttpError) || error.status !== 401) {
|
|
1409
|
+
throw error;
|
|
1410
|
+
}
|
|
1411
|
+
this.#tokens = await this.#client.refresh(this.#tokens.refreshToken);
|
|
1412
|
+
return operation(this.#tokens.accessToken);
|
|
1413
|
+
}
|
|
1414
|
+
}
|
|
1415
|
+
revoke() {
|
|
1416
|
+
return this.#client.revoke(this.#tokens);
|
|
1417
|
+
}
|
|
1418
|
+
};
|
|
1419
|
+
}
|
|
1420
|
+
});
|
|
1421
|
+
|
|
1422
|
+
// src/setup/spatius-auth.ts
|
|
1423
|
+
import { execFile as execFile2 } from "child_process";
|
|
1424
|
+
import { createHash, randomBytes } from "crypto";
|
|
1425
|
+
import { createServer } from "http";
|
|
1426
|
+
import { promisify } from "util";
|
|
1427
|
+
function base64Url(value) {
|
|
1428
|
+
return value.toString("base64url");
|
|
1429
|
+
}
|
|
1430
|
+
function createPkceMaterial(random = randomBytes) {
|
|
1431
|
+
const verifier = base64Url(random(32));
|
|
1432
|
+
return {
|
|
1433
|
+
challenge: base64Url(createHash("sha256").update(verifier).digest()),
|
|
1434
|
+
state: base64Url(random(18)),
|
|
1435
|
+
verifier
|
|
1436
|
+
};
|
|
1437
|
+
}
|
|
1438
|
+
function validateSpatiusCallback(requestUrl, redirectUri, expectedState, expectedRequestId) {
|
|
1439
|
+
const url = new URL(requestUrl, redirectUri);
|
|
1440
|
+
if (url.pathname !== callbackPath) {
|
|
1441
|
+
throw new Error("The Spatius callback used an unexpected path.");
|
|
1442
|
+
}
|
|
1443
|
+
const providerError = url.searchParams.get("error");
|
|
1444
|
+
if (providerError !== null) {
|
|
1445
|
+
const safeCode = providerError.replace(/[^A-Za-z\d_.-]/gu, "").slice(0, 80);
|
|
1446
|
+
throw new Error(
|
|
1447
|
+
safeCode === "" ? "Spatius authorization was declined." : `Spatius authorization was declined (${safeCode}).`
|
|
1448
|
+
);
|
|
1449
|
+
}
|
|
1450
|
+
if (url.searchParams.get("state") !== expectedState) {
|
|
1451
|
+
throw new Error("The Spatius callback state did not match.");
|
|
1452
|
+
}
|
|
1453
|
+
const authRequestId = url.searchParams.get("auth_request_id")?.trim();
|
|
1454
|
+
if (authRequestId !== expectedRequestId) {
|
|
1455
|
+
throw new Error("The Spatius callback request ID did not match.");
|
|
1456
|
+
}
|
|
1457
|
+
const authCode = url.searchParams.get("auth_code")?.trim();
|
|
1458
|
+
if (authCode === void 0 || authCode === "") {
|
|
1459
|
+
throw new Error(
|
|
1460
|
+
"The Spatius callback did not include an authorization code."
|
|
1461
|
+
);
|
|
1462
|
+
}
|
|
1463
|
+
return { authCode, authRequestId };
|
|
1464
|
+
}
|
|
1465
|
+
function closeServer(server) {
|
|
1466
|
+
return new Promise((resolvePromise) => {
|
|
1467
|
+
server.close(() => resolvePromise());
|
|
1468
|
+
server.closeAllConnections();
|
|
1469
|
+
});
|
|
1470
|
+
}
|
|
1471
|
+
async function createLoopbackCallbackServer() {
|
|
1472
|
+
let expectation;
|
|
1473
|
+
let redirectUri = "";
|
|
1474
|
+
const server = createServer((request, response) => {
|
|
1475
|
+
if (request.method !== "GET" || request.url === void 0) {
|
|
1476
|
+
response.writeHead(404).end("Not found");
|
|
1477
|
+
return;
|
|
1478
|
+
}
|
|
1479
|
+
const requestUrl = new URL(request.url, redirectUri);
|
|
1480
|
+
if (requestUrl.pathname !== callbackPath || expectation === void 0) {
|
|
1481
|
+
response.writeHead(404).end("Not found");
|
|
1482
|
+
return;
|
|
1483
|
+
}
|
|
1484
|
+
const current = expectation;
|
|
1485
|
+
expectation = void 0;
|
|
1486
|
+
try {
|
|
1487
|
+
const result = validateSpatiusCallback(
|
|
1488
|
+
request.url,
|
|
1489
|
+
redirectUri,
|
|
1490
|
+
current.expectedState,
|
|
1491
|
+
current.expectedRequestId
|
|
1492
|
+
);
|
|
1493
|
+
response.writeHead(200, {
|
|
1494
|
+
"Cache-Control": "no-store",
|
|
1495
|
+
"Content-Security-Policy": "default-src 'none'; style-src 'unsafe-inline'",
|
|
1496
|
+
"Content-Type": "text/html; charset=utf-8",
|
|
1497
|
+
"Referrer-Policy": "no-referrer"
|
|
1498
|
+
}).end(successPage);
|
|
1499
|
+
current.resolve(result);
|
|
1500
|
+
} catch (error) {
|
|
1501
|
+
response.writeHead(400, {
|
|
1502
|
+
"Cache-Control": "no-store",
|
|
1503
|
+
"Content-Type": "text/plain; charset=utf-8"
|
|
1504
|
+
}).end("Authorization could not be completed. Return to the terminal.");
|
|
1505
|
+
current.reject(
|
|
1506
|
+
error instanceof Error ? error : new Error("The Spatius callback was invalid.")
|
|
1507
|
+
);
|
|
1508
|
+
}
|
|
1509
|
+
});
|
|
1510
|
+
await new Promise((resolvePromise, reject) => {
|
|
1511
|
+
server.once("error", reject);
|
|
1512
|
+
server.listen(0, "127.0.0.1", () => {
|
|
1513
|
+
server.off("error", reject);
|
|
1514
|
+
resolvePromise();
|
|
1515
|
+
});
|
|
1516
|
+
});
|
|
1517
|
+
const address = server.address();
|
|
1518
|
+
redirectUri = `http://127.0.0.1:${String(address.port)}${callbackPath}`;
|
|
1519
|
+
return {
|
|
1520
|
+
close: () => closeServer(server),
|
|
1521
|
+
redirectUri,
|
|
1522
|
+
waitForCallback: ({ expectedRequestId, expectedState, timeoutMs }) => new Promise((resolvePromise, reject) => {
|
|
1523
|
+
if (expectation !== void 0) {
|
|
1524
|
+
reject(new Error("A Spatius callback is already pending."));
|
|
1525
|
+
return;
|
|
1526
|
+
}
|
|
1527
|
+
const timeout = setTimeout(() => {
|
|
1528
|
+
expectation = void 0;
|
|
1529
|
+
reject(new Error("Timed out waiting for Spatius authorization."));
|
|
1530
|
+
}, timeoutMs);
|
|
1531
|
+
expectation = {
|
|
1532
|
+
expectedRequestId,
|
|
1533
|
+
expectedState,
|
|
1534
|
+
reject: (error) => {
|
|
1535
|
+
clearTimeout(timeout);
|
|
1536
|
+
reject(error);
|
|
1537
|
+
},
|
|
1538
|
+
resolve: (result) => {
|
|
1539
|
+
clearTimeout(timeout);
|
|
1540
|
+
resolvePromise(result);
|
|
1541
|
+
}
|
|
1542
|
+
};
|
|
1543
|
+
})
|
|
1544
|
+
};
|
|
1545
|
+
}
|
|
1546
|
+
async function openSystemBrowser(url) {
|
|
1547
|
+
const command = process.platform === "darwin" ? "open" : process.platform === "win32" ? "rundll32.exe" : "xdg-open";
|
|
1548
|
+
const args = process.platform === "win32" ? ["url.dll,FileProtocolHandler", url] : [url];
|
|
1549
|
+
try {
|
|
1550
|
+
await execFileAsync(command, args, {
|
|
1551
|
+
timeout: 1e4,
|
|
1552
|
+
windowsHide: true
|
|
1553
|
+
});
|
|
1554
|
+
} catch {
|
|
1555
|
+
throw new Error("The browser could not be opened automatically.");
|
|
1556
|
+
}
|
|
1557
|
+
}
|
|
1558
|
+
function sessionTimeoutMs(response, now, maximumMs) {
|
|
1559
|
+
const candidates = [maximumMs];
|
|
1560
|
+
if (response.expiresIn !== void 0 && response.expiresIn > 0) {
|
|
1561
|
+
candidates.push(response.expiresIn * 1e3);
|
|
1562
|
+
}
|
|
1563
|
+
if (response.expiresAt !== void 0) {
|
|
1564
|
+
const expiresAt = Date.parse(response.expiresAt);
|
|
1565
|
+
if (Number.isFinite(expiresAt)) {
|
|
1566
|
+
candidates.push(Math.max(1, expiresAt - now()));
|
|
1567
|
+
}
|
|
1568
|
+
}
|
|
1569
|
+
return Math.max(1, Math.min(...candidates.filter((value) => value > 0)));
|
|
1570
|
+
}
|
|
1571
|
+
function validateAuthorizationUrl(value) {
|
|
1572
|
+
const url = new URL(value);
|
|
1573
|
+
const loopbackHost = ["127.0.0.1", "[::1]", "localhost"].includes(
|
|
1574
|
+
url.hostname
|
|
1575
|
+
);
|
|
1576
|
+
if (url.protocol !== "https:" && !(url.protocol === "http:" && loopbackHost) || url.username !== "" || url.password !== "") {
|
|
1577
|
+
throw new Error("Spatius returned an unsafe authorization URL.");
|
|
1578
|
+
}
|
|
1579
|
+
return value;
|
|
1580
|
+
}
|
|
1581
|
+
async function loginToSpatius({
|
|
1582
|
+
callbackServerFactory = createLoopbackCallbackServer,
|
|
1583
|
+
client,
|
|
1584
|
+
maximumWaitMs = 5 * 6e4,
|
|
1585
|
+
now = Date.now,
|
|
1586
|
+
onAuthorizationUrl,
|
|
1587
|
+
onBrowserOpenFailure,
|
|
1588
|
+
openBrowser = openSystemBrowser,
|
|
1589
|
+
redactor = new SecretRedactor()
|
|
1590
|
+
}) {
|
|
1591
|
+
const callback = await callbackServerFactory();
|
|
1592
|
+
const pkce = createPkceMaterial();
|
|
1593
|
+
redactor.add(pkce.verifier, pkce.state);
|
|
1594
|
+
try {
|
|
1595
|
+
const auth = await client.createAuthSession({
|
|
1596
|
+
codeChallenge: pkce.challenge,
|
|
1597
|
+
redirectUri: callback.redirectUri,
|
|
1598
|
+
state: pkce.state
|
|
1599
|
+
});
|
|
1600
|
+
const authorizationUrl = validateAuthorizationUrl(auth.authorizeUrl);
|
|
1601
|
+
onAuthorizationUrl(authorizationUrl);
|
|
1602
|
+
const callbackResult = callback.waitForCallback({
|
|
1603
|
+
expectedRequestId: auth.authRequestId,
|
|
1604
|
+
expectedState: pkce.state,
|
|
1605
|
+
timeoutMs: sessionTimeoutMs(auth, now, maximumWaitMs)
|
|
1606
|
+
});
|
|
1607
|
+
void callbackResult.catch(() => void 0);
|
|
1608
|
+
try {
|
|
1609
|
+
await openBrowser(authorizationUrl);
|
|
1610
|
+
} catch (error) {
|
|
1611
|
+
onBrowserOpenFailure?.(
|
|
1612
|
+
error instanceof Error ? error : new Error("The browser could not be opened automatically.")
|
|
1613
|
+
);
|
|
1614
|
+
}
|
|
1615
|
+
const result = await callbackResult;
|
|
1616
|
+
redactor.add(result.authCode);
|
|
1617
|
+
const tokens = await client.exchangeAuthCode({
|
|
1618
|
+
authCode: result.authCode,
|
|
1619
|
+
authRequestId: result.authRequestId,
|
|
1620
|
+
codeVerifier: pkce.verifier
|
|
1621
|
+
});
|
|
1622
|
+
redactor.add(tokens.accessToken, tokens.refreshToken);
|
|
1623
|
+
return new SpatiusAuthenticatedSession(client, tokens);
|
|
1624
|
+
} finally {
|
|
1625
|
+
await callback.close().catch(() => void 0);
|
|
1626
|
+
}
|
|
1627
|
+
}
|
|
1628
|
+
var execFileAsync, callbackPath;
|
|
1629
|
+
var init_spatius_auth = __esm({
|
|
1630
|
+
"src/setup/spatius-auth.ts"() {
|
|
1631
|
+
"use strict";
|
|
1632
|
+
init_redaction();
|
|
1633
|
+
init_success_page();
|
|
1634
|
+
init_spatius_api();
|
|
1635
|
+
execFileAsync = promisify(execFile2);
|
|
1636
|
+
callbackPath = "/callback";
|
|
1637
|
+
}
|
|
1638
|
+
});
|
|
1639
|
+
|
|
1640
|
+
// src/setup/spatius-resources.ts
|
|
1641
|
+
function combineAvatars(publicAvatars, personalAvatars) {
|
|
1642
|
+
const byId = /* @__PURE__ */ new Map();
|
|
1643
|
+
for (const avatar of publicAvatars) {
|
|
1644
|
+
byId.set(avatar.id, avatar);
|
|
1645
|
+
}
|
|
1646
|
+
for (const avatar of personalAvatars) {
|
|
1647
|
+
byId.set(avatar.id, { ...byId.get(avatar.id), ...avatar });
|
|
1648
|
+
}
|
|
1649
|
+
return [...byId.values()];
|
|
1650
|
+
}
|
|
1651
|
+
function dateLabel(value) {
|
|
1652
|
+
if (value === void 0) return "creation date unavailable";
|
|
1653
|
+
const date = new Date(value);
|
|
1654
|
+
return Number.isNaN(date.getTime()) ? "creation date unavailable" : `created ${date.toISOString().slice(0, 10)}`;
|
|
1655
|
+
}
|
|
1656
|
+
function apiKeyLabel(key) {
|
|
1657
|
+
return `${maskSecret(key.apiKey)} \xB7 ${dateLabel(key.createdAt)}`;
|
|
1658
|
+
}
|
|
1659
|
+
function assertResourceId(value, label) {
|
|
1660
|
+
const normalized = value.trim();
|
|
1661
|
+
if (isPlaceholderValue(normalized)) {
|
|
1662
|
+
throw new Error(`${label} must be a non-placeholder value.`);
|
|
1663
|
+
}
|
|
1664
|
+
return normalized;
|
|
1665
|
+
}
|
|
1666
|
+
async function createApiKey(session, client, app, prompts, redactor) {
|
|
1667
|
+
const confirmed = await prompts.confirm(
|
|
1668
|
+
`Create a new API key for ${app.name} (${app.appId})?`,
|
|
1669
|
+
false
|
|
1670
|
+
);
|
|
1671
|
+
if (!confirmed) {
|
|
1672
|
+
throw new PromptCancelledError("Credential setup was cancelled.");
|
|
1673
|
+
}
|
|
1674
|
+
const key = await session.authorized(
|
|
1675
|
+
(accessToken) => client.createApiKey(accessToken, app.appId)
|
|
1676
|
+
);
|
|
1677
|
+
redactor.add(key.apiKey);
|
|
1678
|
+
return { apiKey: key.apiKey, app };
|
|
1679
|
+
}
|
|
1680
|
+
async function selectExistingAppKey(session, client, app, prompts, redactor) {
|
|
1681
|
+
const keys = await session.authorized(
|
|
1682
|
+
(accessToken) => client.listApiKeys(accessToken, app.appId)
|
|
1683
|
+
);
|
|
1684
|
+
redactor.add(...keys.map((key2) => key2.apiKey));
|
|
1685
|
+
const options = [
|
|
1686
|
+
...keys.map((key2, index2) => ({
|
|
1687
|
+
label: apiKeyLabel(key2),
|
|
1688
|
+
value: `key-${String(index2)}`
|
|
1689
|
+
})),
|
|
1690
|
+
{
|
|
1691
|
+
hint: "Creates a new secret after confirmation",
|
|
1692
|
+
label: "Create a new API key for this app",
|
|
1693
|
+
value: "create-key"
|
|
1694
|
+
}
|
|
1695
|
+
];
|
|
1696
|
+
const selection = await prompts.choose(
|
|
1697
|
+
`Which API key should ${app.name} use?`,
|
|
1698
|
+
options,
|
|
1699
|
+
keys.length > 0 ? "key-0" : "create-key"
|
|
1700
|
+
);
|
|
1701
|
+
if (selection === "create-key") {
|
|
1702
|
+
return createApiKey(session, client, app, prompts, redactor);
|
|
1703
|
+
}
|
|
1704
|
+
const index = Number.parseInt(selection.slice("key-".length), 10);
|
|
1705
|
+
const key = keys[index];
|
|
1706
|
+
if (key === void 0) {
|
|
1707
|
+
throw new Error("The selected Spatius API key is no longer available.");
|
|
1708
|
+
}
|
|
1709
|
+
return { apiKey: key.apiKey, app };
|
|
1710
|
+
}
|
|
1711
|
+
async function createAppAndKey(session, client, projectName, prompts, redactor) {
|
|
1712
|
+
const requestedName = await prompts.input("Name for the new Spatius app", {
|
|
1713
|
+
initialValue: projectName,
|
|
1714
|
+
placeholder: projectName
|
|
1715
|
+
});
|
|
1716
|
+
const name = requestedName.trim();
|
|
1717
|
+
if (name === "" || name.length > 128) {
|
|
1718
|
+
throw new Error(
|
|
1719
|
+
"The Spatius app name must be between 1 and 128 characters."
|
|
1720
|
+
);
|
|
1721
|
+
}
|
|
1722
|
+
const confirmed = await prompts.confirm(
|
|
1723
|
+
`Create the Spatius app "${name}" and its first API key?`,
|
|
1724
|
+
false
|
|
1725
|
+
);
|
|
1726
|
+
if (!confirmed) {
|
|
1727
|
+
throw new PromptCancelledError("Credential setup was cancelled.");
|
|
1728
|
+
}
|
|
1729
|
+
const app = await session.authorized(
|
|
1730
|
+
(accessToken) => client.createApp(accessToken, name)
|
|
1731
|
+
);
|
|
1732
|
+
try {
|
|
1733
|
+
const key = await session.authorized(
|
|
1734
|
+
(accessToken) => client.createApiKey(accessToken, app.appId)
|
|
1735
|
+
);
|
|
1736
|
+
redactor.add(key.apiKey);
|
|
1737
|
+
return { apiKey: key.apiKey, app };
|
|
1738
|
+
} catch (error) {
|
|
1739
|
+
throw new Error(
|
|
1740
|
+
`The Spatius app was created with ID ${app.appId}, but its API key could not be created. Rerun setup and select that app to create a key.`,
|
|
1741
|
+
{ cause: error }
|
|
1742
|
+
);
|
|
1743
|
+
}
|
|
1744
|
+
}
|
|
1745
|
+
async function selectAppAndKey(session, client, apps, projectName, prompts, redactor) {
|
|
1746
|
+
const options = [
|
|
1747
|
+
...apps.map((app2, index2) => ({
|
|
1748
|
+
hint: app2.appId,
|
|
1749
|
+
label: app2.name,
|
|
1750
|
+
value: `app-${String(index2)}`
|
|
1751
|
+
})),
|
|
1752
|
+
{
|
|
1753
|
+
hint: "Creates remote resources only after confirmation",
|
|
1754
|
+
label: "Create a new Spatius app and API key",
|
|
1755
|
+
value: "create-app"
|
|
1756
|
+
}
|
|
1757
|
+
];
|
|
1758
|
+
const selection = await prompts.choose(
|
|
1759
|
+
"Which Spatius app should this project use?",
|
|
1760
|
+
options,
|
|
1761
|
+
apps.length > 0 ? "app-0" : "create-app"
|
|
1762
|
+
);
|
|
1763
|
+
if (selection === "create-app") {
|
|
1764
|
+
return createAppAndKey(session, client, projectName, prompts, redactor);
|
|
1765
|
+
}
|
|
1766
|
+
const index = Number.parseInt(selection.slice("app-".length), 10);
|
|
1767
|
+
const app = apps[index];
|
|
1768
|
+
if (app === void 0) {
|
|
1769
|
+
throw new Error("The selected Spatius app is no longer available.");
|
|
1770
|
+
}
|
|
1771
|
+
return selectExistingAppKey(session, client, app, prompts, redactor);
|
|
1772
|
+
}
|
|
1773
|
+
async function selectAvatar(publicAvatars, personalAvatars, prompts) {
|
|
1774
|
+
const avatars = combineAvatars(publicAvatars, personalAvatars);
|
|
1775
|
+
const options = [
|
|
1776
|
+
...avatars.map((avatar2, index2) => ({
|
|
1777
|
+
hint: avatar2.id,
|
|
1778
|
+
label: `${avatar2.name} (${avatar2.source})`,
|
|
1779
|
+
value: `avatar-${String(index2)}`
|
|
1780
|
+
})),
|
|
1781
|
+
{
|
|
1782
|
+
hint: "Use an avatar ID not shown in the lists",
|
|
1783
|
+
label: "Enter an avatar ID manually",
|
|
1784
|
+
value: "manual-avatar"
|
|
1785
|
+
}
|
|
1786
|
+
];
|
|
1787
|
+
const selection = await prompts.choose(
|
|
1788
|
+
"Which avatar should the demo dispatch?",
|
|
1789
|
+
options,
|
|
1790
|
+
avatars.length > 0 ? "avatar-0" : "manual-avatar"
|
|
1791
|
+
);
|
|
1792
|
+
if (selection === "manual-avatar") {
|
|
1793
|
+
return {
|
|
1794
|
+
avatarId: assertResourceId(
|
|
1795
|
+
await prompts.input("Spatius avatar ID"),
|
|
1796
|
+
"SPATIUS_AVATAR_ID"
|
|
1797
|
+
)
|
|
1798
|
+
};
|
|
1799
|
+
}
|
|
1800
|
+
const index = Number.parseInt(selection.slice("avatar-".length), 10);
|
|
1801
|
+
const avatar = avatars[index];
|
|
1802
|
+
if (avatar === void 0) {
|
|
1803
|
+
throw new Error("The selected Spatius avatar is no longer available.");
|
|
1804
|
+
}
|
|
1805
|
+
return {
|
|
1806
|
+
avatarId: avatar.id,
|
|
1807
|
+
...avatar.backgroundUrl ? { backgroundUrl: avatar.backgroundUrl } : {}
|
|
1808
|
+
};
|
|
1809
|
+
}
|
|
1810
|
+
async function selectSpatiusResources({
|
|
1811
|
+
client,
|
|
1812
|
+
onWarning,
|
|
1813
|
+
projectName,
|
|
1814
|
+
prompts,
|
|
1815
|
+
redactor,
|
|
1816
|
+
session
|
|
1817
|
+
}) {
|
|
1818
|
+
const [apps, publicAvatars] = await session.authorized(
|
|
1819
|
+
(accessToken) => Promise.all([
|
|
1820
|
+
client.listApps(accessToken),
|
|
1821
|
+
client.listPublicAvatars(accessToken)
|
|
1822
|
+
])
|
|
1823
|
+
);
|
|
1824
|
+
const selected = await selectAppAndKey(
|
|
1825
|
+
session,
|
|
1826
|
+
client,
|
|
1827
|
+
apps,
|
|
1828
|
+
projectName,
|
|
1829
|
+
prompts,
|
|
1830
|
+
redactor
|
|
1831
|
+
);
|
|
1832
|
+
let personalAvatars = [];
|
|
1833
|
+
try {
|
|
1834
|
+
personalAvatars = await client.listPersonalAvatars(
|
|
1835
|
+
selected.app.appId,
|
|
1836
|
+
selected.apiKey
|
|
1837
|
+
);
|
|
1838
|
+
} catch {
|
|
1839
|
+
onWarning(
|
|
1840
|
+
"Personal avatars could not be loaded; continuing with the public avatar library."
|
|
1841
|
+
);
|
|
1842
|
+
}
|
|
1843
|
+
const avatar = await selectAvatar(publicAvatars, personalAvatars, prompts);
|
|
1844
|
+
return {
|
|
1845
|
+
apiKey: selected.apiKey,
|
|
1846
|
+
appId: selected.app.appId,
|
|
1847
|
+
...avatar
|
|
1848
|
+
};
|
|
1849
|
+
}
|
|
1850
|
+
var init_spatius_resources = __esm({
|
|
1851
|
+
"src/setup/spatius-resources.ts"() {
|
|
1852
|
+
"use strict";
|
|
1853
|
+
init_errors();
|
|
1854
|
+
init_environment();
|
|
1855
|
+
init_redaction();
|
|
1856
|
+
}
|
|
1857
|
+
});
|
|
1858
|
+
|
|
1859
|
+
// src/setup/wizard.ts
|
|
1860
|
+
var wizard_exports = {};
|
|
1861
|
+
__export(wizard_exports, {
|
|
1862
|
+
runCredentialSetup: () => runCredentialSetup
|
|
1863
|
+
});
|
|
1864
|
+
import { basename as basename2 } from "path";
|
|
1865
|
+
async function collectManualLiveKitCredentials(prompts) {
|
|
1866
|
+
const url = validateLiveKitUrl(
|
|
1867
|
+
await prompts.input("LiveKit URL", {
|
|
1868
|
+
placeholder: "wss://your-project.livekit.cloud"
|
|
1869
|
+
})
|
|
1870
|
+
);
|
|
1871
|
+
const apiKey = await prompts.password("LiveKit API key");
|
|
1872
|
+
const apiSecret = await prompts.password("LiveKit API secret");
|
|
1873
|
+
return validateCredentialBundle({
|
|
1874
|
+
liveKit: { apiKey, apiSecret, url },
|
|
1875
|
+
spatius: {
|
|
1876
|
+
apiKey: "validation-only",
|
|
1877
|
+
appId: "validation-only",
|
|
1878
|
+
avatarId: "validation-only"
|
|
1879
|
+
}
|
|
1880
|
+
}).liveKit;
|
|
1881
|
+
}
|
|
1882
|
+
async function collectLiveKitCredentials(prompts, dependencies, onStatus, onWarning) {
|
|
1883
|
+
let probe = await dependencies.probeLiveKit();
|
|
1884
|
+
let useCli = false;
|
|
1885
|
+
while (true) {
|
|
1886
|
+
if (!probe.available) {
|
|
1887
|
+
const update = probe.reason === "incompatible";
|
|
1888
|
+
onWarning(
|
|
1889
|
+
update ? "The installed LiveKit CLI needs an update to support `lk app env`." : "LiveKit CLI was not found on PATH. It is needed for browser-based setup."
|
|
1890
|
+
);
|
|
1891
|
+
const plan = await dependencies.planLiveKitInstall(update);
|
|
1892
|
+
onStatus(
|
|
1893
|
+
plan === void 0 ? `Install or update LiveKit CLI, then check again: ${LIVEKIT_INSTALL_GUIDE}` : `${update ? "Update" : "Install"} command: ${plan.displayCommand}`
|
|
1894
|
+
);
|
|
1895
|
+
const action = await prompts.choose(
|
|
1896
|
+
"Connect your LiveKit project",
|
|
1897
|
+
[
|
|
1898
|
+
...plan === void 0 ? [] : [
|
|
1899
|
+
{
|
|
1900
|
+
label: `${update ? "Update" : "Install"} LiveKit CLI and connect`,
|
|
1901
|
+
value: "install",
|
|
1902
|
+
hint: "Recommended; runs the command shown above"
|
|
1903
|
+
}
|
|
1904
|
+
],
|
|
1905
|
+
{ label: "I've installed it \u2014 check again", value: "retry" },
|
|
1906
|
+
{ label: "Enter credentials manually", value: "manual" },
|
|
1907
|
+
{ label: "Set up later", value: "skip" }
|
|
1908
|
+
],
|
|
1909
|
+
plan === void 0 ? "manual" : "install"
|
|
1910
|
+
);
|
|
1911
|
+
if (action === "skip") return void 0;
|
|
1912
|
+
if (action === "manual") return collectManualLiveKitCredentials(prompts);
|
|
1913
|
+
if (action === "install" && plan !== void 0) {
|
|
1914
|
+
try {
|
|
1915
|
+
onStatus(`${update ? "Updating" : "Installing"} LiveKit CLI\u2026`);
|
|
1916
|
+
await dependencies.installLiveKit(plan);
|
|
1917
|
+
} catch (error) {
|
|
1918
|
+
if (error instanceof PromptCancelledError) throw error;
|
|
1919
|
+
onWarning(
|
|
1920
|
+
"LiveKit CLI installation did not complete. Retry, enter credentials manually, or set up later."
|
|
1921
|
+
);
|
|
1922
|
+
continue;
|
|
1923
|
+
}
|
|
1924
|
+
}
|
|
1925
|
+
probe = await dependencies.probeLiveKit();
|
|
1926
|
+
if (!probe.available) {
|
|
1927
|
+
onWarning(
|
|
1928
|
+
"A compatible lk is still unavailable in this terminal. If installation changed PATH, open a new terminal and run create-spatius-app setup . --interactive from your project."
|
|
1929
|
+
);
|
|
1930
|
+
continue;
|
|
1931
|
+
}
|
|
1932
|
+
onStatus("LiveKit CLI is ready.");
|
|
1933
|
+
useCli = true;
|
|
1934
|
+
}
|
|
1935
|
+
const source = useCli ? "cli" : await prompts.choose(
|
|
1936
|
+
"How should LiveKit credentials be configured?",
|
|
1937
|
+
[
|
|
1938
|
+
{
|
|
1939
|
+
hint: probe.version === void 0 ? "Recommended; uses `lk app env`" : `Recommended; lk ${probe.version}`,
|
|
1940
|
+
label: "Use the LiveKit CLI",
|
|
1941
|
+
value: "cli"
|
|
1942
|
+
},
|
|
1943
|
+
{
|
|
1944
|
+
hint: "Opens LiveKit in your browser using `lk cloud auth`",
|
|
1945
|
+
label: "Connect another LiveKit project",
|
|
1946
|
+
value: "connect"
|
|
1947
|
+
},
|
|
1948
|
+
{
|
|
1949
|
+
hint: "Values are entered with secret masking",
|
|
1950
|
+
label: "Enter credentials manually",
|
|
1951
|
+
value: "manual"
|
|
1952
|
+
},
|
|
1953
|
+
{ label: "Set up later", value: "skip" }
|
|
1954
|
+
],
|
|
1955
|
+
"cli"
|
|
1956
|
+
);
|
|
1957
|
+
if (source === "skip") return void 0;
|
|
1958
|
+
if (source === "manual") return collectManualLiveKitCredentials(prompts);
|
|
1959
|
+
try {
|
|
1960
|
+
if (source === "connect") {
|
|
1961
|
+
onStatus(
|
|
1962
|
+
"Opening LiveKit in your browser. Sign in and select a project."
|
|
1963
|
+
);
|
|
1964
|
+
await dependencies.authenticateLiveKit();
|
|
1965
|
+
}
|
|
1966
|
+
onStatus(
|
|
1967
|
+
"Starting the LiveKit project credential flow\u2026 Follow the LiveKit prompts to select a linked project or sign in through your browser."
|
|
1968
|
+
);
|
|
1969
|
+
return await dependencies.loadLiveKitCredentials();
|
|
1970
|
+
} catch (error) {
|
|
1971
|
+
if (error instanceof PromptCancelledError) throw error;
|
|
1972
|
+
onWarning(
|
|
1973
|
+
"The LiveKit CLI flow did not complete or returned invalid credentials."
|
|
1974
|
+
);
|
|
1975
|
+
const action = await prompts.choose(
|
|
1976
|
+
"How should LiveKit setup continue?",
|
|
1977
|
+
[
|
|
1978
|
+
{ label: "Retry LiveKit connection", value: "retry" },
|
|
1979
|
+
{ label: "Enter credentials manually", value: "manual" },
|
|
1980
|
+
{ label: "Set up later", value: "skip" }
|
|
1981
|
+
],
|
|
1982
|
+
"manual"
|
|
1983
|
+
);
|
|
1984
|
+
if (action === "skip") return void 0;
|
|
1985
|
+
if (action === "manual") return collectManualLiveKitCredentials(prompts);
|
|
1986
|
+
useCli = false;
|
|
1987
|
+
}
|
|
1988
|
+
}
|
|
1989
|
+
}
|
|
1990
|
+
async function collectManualSpatiusCredentials(prompts) {
|
|
1991
|
+
return {
|
|
1992
|
+
apiKey: await prompts.password("Spatius API key"),
|
|
1993
|
+
appId: await prompts.password("Spatius app ID"),
|
|
1994
|
+
avatarId: await prompts.password("Spatius avatar ID")
|
|
1995
|
+
};
|
|
1996
|
+
}
|
|
1997
|
+
async function collectBrowserSpatiusCredentials(prompts, projectName, client, dependencies, redactor, onStatus, onWarning) {
|
|
1998
|
+
let session;
|
|
1999
|
+
try {
|
|
2000
|
+
session = await dependencies.loginToSpatius({
|
|
2001
|
+
client,
|
|
2002
|
+
onAuthorizationUrl: (url) => {
|
|
2003
|
+
onStatus(`Approve Spatius access in your browser:
|
|
2004
|
+
${url}`);
|
|
2005
|
+
},
|
|
2006
|
+
onBrowserOpenFailure: () => {
|
|
2007
|
+
onWarning(
|
|
2008
|
+
"The browser could not be opened automatically. Use the printed authorization URL."
|
|
2009
|
+
);
|
|
2010
|
+
},
|
|
2011
|
+
redactor
|
|
2012
|
+
});
|
|
2013
|
+
return await dependencies.selectSpatiusResources({
|
|
2014
|
+
client,
|
|
2015
|
+
onWarning,
|
|
2016
|
+
projectName,
|
|
2017
|
+
prompts,
|
|
2018
|
+
redactor,
|
|
2019
|
+
session
|
|
2020
|
+
});
|
|
2021
|
+
} finally {
|
|
2022
|
+
if (session !== void 0) {
|
|
2023
|
+
await session.revoke().catch(() => {
|
|
2024
|
+
onWarning(
|
|
2025
|
+
"The temporary Spatius CLI session could not be revoked automatically. No session token was stored locally."
|
|
2026
|
+
);
|
|
2027
|
+
});
|
|
2028
|
+
}
|
|
2029
|
+
}
|
|
2030
|
+
}
|
|
2031
|
+
async function collectSpatiusCredentials(prompts, projectName, client, dependencies, redactor, onStatus, onWarning) {
|
|
2032
|
+
const source = await prompts.choose(
|
|
2033
|
+
"How should Spatius credentials be configured?",
|
|
2034
|
+
[
|
|
2035
|
+
{
|
|
2036
|
+
hint: "Recommended; login is temporary and browser-approved",
|
|
2037
|
+
label: "Sign in with Spatius Studio",
|
|
2038
|
+
value: "browser"
|
|
2039
|
+
},
|
|
2040
|
+
{
|
|
2041
|
+
hint: "Values are entered with secret masking",
|
|
2042
|
+
label: "Enter credentials manually",
|
|
2043
|
+
value: "manual"
|
|
2044
|
+
}
|
|
2045
|
+
],
|
|
2046
|
+
"browser"
|
|
2047
|
+
);
|
|
2048
|
+
if (source === "manual") {
|
|
2049
|
+
return collectManualSpatiusCredentials(prompts);
|
|
2050
|
+
}
|
|
2051
|
+
try {
|
|
2052
|
+
return await collectBrowserSpatiusCredentials(
|
|
2053
|
+
prompts,
|
|
2054
|
+
projectName,
|
|
2055
|
+
client,
|
|
2056
|
+
dependencies,
|
|
2057
|
+
redactor,
|
|
2058
|
+
onStatus,
|
|
2059
|
+
onWarning
|
|
2060
|
+
);
|
|
2061
|
+
} catch (error) {
|
|
2062
|
+
if (error instanceof PromptCancelledError) {
|
|
2063
|
+
throw error;
|
|
2064
|
+
}
|
|
2065
|
+
const detail = redactor.redact(
|
|
2066
|
+
error instanceof Error ? error.message : "Unknown provider error."
|
|
2067
|
+
);
|
|
2068
|
+
onWarning(`Spatius browser setup did not complete: ${detail}`);
|
|
2069
|
+
const useManual = await prompts.confirm(
|
|
2070
|
+
"Enter Spatius credentials manually instead?",
|
|
2071
|
+
true
|
|
2072
|
+
);
|
|
2073
|
+
if (!useManual) {
|
|
2074
|
+
throw new PromptCancelledError("Credential setup was cancelled.");
|
|
2075
|
+
}
|
|
2076
|
+
return collectManualSpatiusCredentials(prompts);
|
|
2077
|
+
}
|
|
2078
|
+
}
|
|
2079
|
+
async function confirmReplacement(state, prompts, onWarning) {
|
|
2080
|
+
if (state.status === "complete") {
|
|
2081
|
+
const keep = await prompts.confirm(
|
|
2082
|
+
"A complete, consistent local credential configuration already exists. Keep it?",
|
|
2083
|
+
true
|
|
2084
|
+
);
|
|
2085
|
+
if (keep) return false;
|
|
2086
|
+
} else if (state.hasManagedValues) {
|
|
2087
|
+
onWarning(
|
|
2088
|
+
state.status === "inconsistent" ? "The Worker and agent credential files contain inconsistent managed values." : "The local credential files contain only part of the required configuration."
|
|
2089
|
+
);
|
|
2090
|
+
} else {
|
|
2091
|
+
return true;
|
|
2092
|
+
}
|
|
2093
|
+
return prompts.confirm(
|
|
2094
|
+
"Replace the existing managed credential values? Unrelated variables and comments will be preserved.",
|
|
2095
|
+
false
|
|
2096
|
+
);
|
|
2097
|
+
}
|
|
2098
|
+
function defaultClient(onDiagnostic) {
|
|
2099
|
+
const testBaseUrl = process.env.NODE_ENV === "test" ? process.env.CREATE_SPATIUS_APP_TEST_SPATIUS_BASE_URL : void 0;
|
|
2100
|
+
return new SpatiusApiClient(
|
|
2101
|
+
testBaseUrl ?? SPATIUS_CONSOLE_BASE_URL,
|
|
2102
|
+
fetch,
|
|
2103
|
+
onDiagnostic
|
|
2104
|
+
);
|
|
2105
|
+
}
|
|
2106
|
+
async function runCredentialSetup({
|
|
2107
|
+
dependencies = {},
|
|
2108
|
+
onDiagnostic,
|
|
2109
|
+
onStatus = () => void 0,
|
|
2110
|
+
onWarning = () => void 0,
|
|
2111
|
+
projectName,
|
|
2112
|
+
prompts,
|
|
2113
|
+
targetDirectory
|
|
2114
|
+
}) {
|
|
2115
|
+
const validateProject = dependencies.validateProject ?? assertSpatiusProject;
|
|
2116
|
+
const readState = dependencies.readState ?? readCredentialFileState;
|
|
2117
|
+
const readExamples = dependencies.readExamples ?? readCredentialExamples;
|
|
2118
|
+
const writeFiles = dependencies.writeFiles ?? writeCredentialFilesAtomically;
|
|
2119
|
+
const setupDependencies = {
|
|
2120
|
+
authenticateLiveKit: dependencies.authenticateLiveKit ?? authenticateLiveKitCli,
|
|
2121
|
+
installLiveKit: dependencies.installLiveKit ?? installLiveKitCli,
|
|
2122
|
+
planLiveKitInstall: dependencies.planLiveKitInstall ?? planLiveKitInstall,
|
|
2123
|
+
createSpatiusClient: dependencies.createSpatiusClient ?? (() => defaultClient(onDiagnostic)),
|
|
2124
|
+
loadLiveKitCredentials: dependencies.loadLiveKitCredentials ?? (() => loadLiveKitCredentialsWithCli()),
|
|
2125
|
+
loginToSpatius: dependencies.loginToSpatius ?? ((options) => loginToSpatius({
|
|
2126
|
+
...options,
|
|
2127
|
+
...process.env.NODE_ENV === "test" && process.env.CREATE_SPATIUS_APP_TEST_NO_BROWSER === "1" ? { openBrowser: () => Promise.resolve() } : {}
|
|
2128
|
+
})),
|
|
2129
|
+
probeLiveKit: dependencies.probeLiveKit ?? (() => probeLiveKitCli()),
|
|
2130
|
+
selectSpatiusResources: dependencies.selectSpatiusResources ?? selectSpatiusResources
|
|
2131
|
+
};
|
|
2132
|
+
await validateProject(targetDirectory);
|
|
2133
|
+
const state = await readState(targetDirectory);
|
|
2134
|
+
const shouldReplace = await confirmReplacement(state, prompts, onWarning);
|
|
2135
|
+
if (!shouldReplace) {
|
|
2136
|
+
onStatus("Existing local credentials were left unchanged.");
|
|
2137
|
+
return "unchanged";
|
|
2138
|
+
}
|
|
2139
|
+
const redactor = new SecretRedactor();
|
|
2140
|
+
const liveKit = await collectLiveKitCredentials(
|
|
2141
|
+
prompts,
|
|
2142
|
+
setupDependencies,
|
|
2143
|
+
onStatus,
|
|
2144
|
+
onWarning
|
|
2145
|
+
);
|
|
2146
|
+
if (liveKit === void 0) {
|
|
2147
|
+
onStatus(
|
|
2148
|
+
"Credential setup deferred. Run create-spatius-app setup . --interactive from your project when ready."
|
|
2149
|
+
);
|
|
2150
|
+
return "unchanged";
|
|
2151
|
+
}
|
|
2152
|
+
redactor.add(liveKit.apiKey, liveKit.apiSecret);
|
|
2153
|
+
const spatius = await collectSpatiusCredentials(
|
|
2154
|
+
prompts,
|
|
2155
|
+
projectName ?? basename2(targetDirectory),
|
|
2156
|
+
setupDependencies.createSpatiusClient(),
|
|
2157
|
+
setupDependencies,
|
|
2158
|
+
redactor,
|
|
2159
|
+
onStatus,
|
|
2160
|
+
onWarning
|
|
2161
|
+
);
|
|
2162
|
+
redactor.add(spatius.apiKey);
|
|
2163
|
+
const existingVoice = parseDotenv(state.worker ?? "").get(
|
|
2164
|
+
"CARTESIA_VOICE_ID"
|
|
2165
|
+
);
|
|
2166
|
+
const initialVoice = CHARACTER_VOICES.find((voice2) => voice2.voiceId === existingVoice) ?? CHARACTER_VOICES[0];
|
|
2167
|
+
const characterStyle = await prompts.choose(
|
|
2168
|
+
"Which character style should the voice match for your selected avatar?",
|
|
2169
|
+
CHARACTER_VOICES.map(({ value, label, hint }) => ({ value, label, hint })),
|
|
2170
|
+
initialVoice.value
|
|
2171
|
+
);
|
|
2172
|
+
const voice = CHARACTER_VOICES.find(
|
|
2173
|
+
(option) => option.value === characterStyle
|
|
2174
|
+
);
|
|
2175
|
+
if (!voice)
|
|
2176
|
+
throw new Error("Choose a feminine or masculine character voice.");
|
|
2177
|
+
const credentials = validateCredentialBundle({
|
|
2178
|
+
voiceId: voice.voiceId,
|
|
2179
|
+
liveKit,
|
|
2180
|
+
spatius
|
|
2181
|
+
});
|
|
2182
|
+
const examples = await readExamples(targetDirectory);
|
|
2183
|
+
const rendered = buildCredentialFileContents(state, examples, credentials);
|
|
2184
|
+
await writeFiles(targetDirectory, rendered);
|
|
2185
|
+
onStatus("Saved .dev.vars and agent/.env.local for local development only.");
|
|
2186
|
+
return "configured";
|
|
2187
|
+
}
|
|
2188
|
+
var init_wizard = __esm({
|
|
2189
|
+
"src/setup/wizard.ts"() {
|
|
2190
|
+
"use strict";
|
|
2191
|
+
init_errors();
|
|
2192
|
+
init_voices();
|
|
2193
|
+
init_environment();
|
|
2194
|
+
init_livekit();
|
|
2195
|
+
init_livekit_install();
|
|
2196
|
+
init_project();
|
|
2197
|
+
init_redaction();
|
|
2198
|
+
init_spatius_auth();
|
|
2199
|
+
init_spatius_api();
|
|
2200
|
+
init_spatius_resources();
|
|
2201
|
+
}
|
|
2202
|
+
});
|
|
2203
|
+
|
|
2204
|
+
// src/templates.ts
|
|
2205
|
+
init_errors();
|
|
2206
|
+
import { fileURLToPath } from "url";
|
|
2207
|
+
|
|
2208
|
+
// src/templates/cloudflare-livekit/index.ts
|
|
2209
|
+
init_errors();
|
|
2210
|
+
init_package_managers();
|
|
2211
|
+
init_project();
|
|
2212
|
+
|
|
2213
|
+
// src/templates/cloudflare-livekit/configure.ts
|
|
2214
|
+
init_package_managers();
|
|
2215
|
+
import { randomUUID } from "crypto";
|
|
2216
|
+
import { readFile, writeFile } from "fs/promises";
|
|
2217
|
+
import { basename, join as join2 } from "path";
|
|
2218
|
+
function toValidPackageName(value) {
|
|
2219
|
+
const normalized = value.trim().toLowerCase().replace(/\s+/gu, "-").replace(/^[._-]+/u, "").replace(/[^a-z\d~._-]+/gu, "-").replace(/-+/gu, "-").slice(0, 214).replace(/-$/u, "");
|
|
2220
|
+
return normalized === "" ? "spatius-app" : normalized;
|
|
2221
|
+
}
|
|
2222
|
+
function managerDescription(name, version) {
|
|
2223
|
+
return version === void 0 || version === "unknown" ? name : `${name} ${version}`;
|
|
2224
|
+
}
|
|
2225
|
+
function npmRequiresScriptAllowlist(version) {
|
|
2226
|
+
const major = Number.parseInt(version?.split(".")[0] ?? "", 10);
|
|
2227
|
+
return Number.isFinite(major) && major >= 12;
|
|
2228
|
+
}
|
|
2229
|
+
function createReplacements(packageManagers, platform) {
|
|
2230
|
+
const javascript = packageManagers.javascript;
|
|
2231
|
+
const python = packageManagers.python;
|
|
2232
|
+
return /* @__PURE__ */ new Map([
|
|
2233
|
+
[
|
|
2234
|
+
"{{SPATIUS_JAVASCRIPT_PACKAGE_MANAGER}}",
|
|
2235
|
+
managerDescription(javascript.name, javascript.version)
|
|
2236
|
+
],
|
|
2237
|
+
[
|
|
2238
|
+
"{{SPATIUS_PYTHON_PACKAGE_MANAGER}}",
|
|
2239
|
+
managerDescription(python.name, python.version)
|
|
2240
|
+
],
|
|
2241
|
+
[
|
|
2242
|
+
"__SPATIUS_JAVASCRIPT_INSTALL_COMMAND__",
|
|
2243
|
+
javascriptInstallCommand(javascript.name)
|
|
2244
|
+
],
|
|
2245
|
+
[
|
|
2246
|
+
"__SPATIUS_PYTHON_INSTALL_COMMANDS__",
|
|
2247
|
+
pythonInstallCommands(python, platform).join("\n")
|
|
2248
|
+
],
|
|
2249
|
+
[
|
|
2250
|
+
"__SPATIUS_PYTHON_INSTALL_SUMMARY__",
|
|
2251
|
+
pythonInstallCommands(python, platform).join(" && ")
|
|
2252
|
+
],
|
|
2253
|
+
["__SPATIUS_DEV_COMMAND__", javascriptRunCommand(javascript.name, "dev")],
|
|
2254
|
+
[
|
|
2255
|
+
"__SPATIUS_WEB_DEV_COMMAND__",
|
|
2256
|
+
javascriptRunCommand(javascript.name, "dev:web")
|
|
2257
|
+
],
|
|
2258
|
+
[
|
|
2259
|
+
"__SPATIUS_CHECK_COMMAND__",
|
|
2260
|
+
javascriptRunCommand(javascript.name, "check")
|
|
2261
|
+
],
|
|
2262
|
+
[
|
|
2263
|
+
"__SPATIUS_AGENT_CHECK_COMMAND__",
|
|
2264
|
+
javascriptRunCommand(javascript.name, "agent:check")
|
|
2265
|
+
],
|
|
2266
|
+
[
|
|
2267
|
+
"__SPATIUS_AGENT_DEV_COMMAND__",
|
|
2268
|
+
javascriptRunCommand(javascript.name, "agent:dev")
|
|
2269
|
+
],
|
|
2270
|
+
[
|
|
2271
|
+
"__SPATIUS_CF_TYPEGEN_COMMAND__",
|
|
2272
|
+
javascriptRunCommand(javascript.name, "cf-typegen")
|
|
2273
|
+
],
|
|
2274
|
+
[
|
|
2275
|
+
"__SPATIUS_DEPLOY_COMMAND__",
|
|
2276
|
+
javascriptRunCommand(javascript.name, "deploy")
|
|
2277
|
+
],
|
|
2278
|
+
[
|
|
2279
|
+
"__SPATIUS_WRANGLER_LOGIN_COMMAND__",
|
|
2280
|
+
javascriptRunCommand(javascript.name, "wrangler -- login")
|
|
2281
|
+
],
|
|
2282
|
+
[
|
|
2283
|
+
"__SPATIUS_WRANGLER_API_KEY_COMMAND__",
|
|
2284
|
+
javascriptRunCommand(
|
|
2285
|
+
javascript.name,
|
|
2286
|
+
"wrangler -- secret put LIVEKIT_API_KEY"
|
|
2287
|
+
)
|
|
2288
|
+
],
|
|
2289
|
+
[
|
|
2290
|
+
"__SPATIUS_WRANGLER_API_SECRET_COMMAND__",
|
|
2291
|
+
javascriptRunCommand(
|
|
2292
|
+
javascript.name,
|
|
2293
|
+
"wrangler -- secret put LIVEKIT_API_SECRET"
|
|
2294
|
+
)
|
|
2295
|
+
]
|
|
2296
|
+
]);
|
|
2297
|
+
}
|
|
2298
|
+
async function renderFile(path, replacements) {
|
|
2299
|
+
let contents = await readFile(path, "utf8");
|
|
2300
|
+
for (const [token, replacement] of replacements) {
|
|
2301
|
+
contents = contents.replaceAll(token, replacement);
|
|
2302
|
+
}
|
|
2303
|
+
await writeFile(path, contents);
|
|
2304
|
+
}
|
|
2305
|
+
async function configureGeneratedTemplate(targetDirectory, {
|
|
2306
|
+
packageManagers,
|
|
2307
|
+
platform = process.platform,
|
|
2308
|
+
projectName = basename(targetDirectory)
|
|
2309
|
+
}) {
|
|
2310
|
+
const packagePath = join2(targetDirectory, "package.json");
|
|
2311
|
+
const packageContents = await readFile(packagePath, "utf8");
|
|
2312
|
+
const metadata = JSON.parse(packageContents);
|
|
2313
|
+
const javascript = packageManagers.javascript;
|
|
2314
|
+
const run = (script) => javascriptRunCommand(javascript.name, script);
|
|
2315
|
+
metadata.name = toValidPackageName(projectName);
|
|
2316
|
+
if (javascript.version === void 0 || javascript.version === "unknown") {
|
|
2317
|
+
delete metadata.packageManager;
|
|
2318
|
+
} else {
|
|
2319
|
+
metadata.packageManager = `${javascript.name}@${javascript.version}`;
|
|
2320
|
+
}
|
|
2321
|
+
metadata.scripts.check = [
|
|
2322
|
+
"format:check",
|
|
2323
|
+
"lint",
|
|
2324
|
+
"typecheck",
|
|
2325
|
+
"test",
|
|
2326
|
+
"test:dev",
|
|
2327
|
+
"build"
|
|
2328
|
+
].map(run).join(" && ");
|
|
2329
|
+
metadata.scripts.deploy = `${run("build")} && wrangler deploy`;
|
|
2330
|
+
metadata.scripts.dev = `concurrently --kill-others --names web,agent "${run("dev:web")}" "${run("agent:dev")}"`;
|
|
2331
|
+
if (javascript.name === "npm" && npmRequiresScriptAllowlist(javascript.version)) {
|
|
2332
|
+
metadata.allowScripts = {
|
|
2333
|
+
"core-js": true,
|
|
2334
|
+
esbuild: true,
|
|
2335
|
+
workerd: true
|
|
2336
|
+
};
|
|
2337
|
+
delete metadata.trustedDependencies;
|
|
2338
|
+
} else if (javascript.name === "bun") {
|
|
2339
|
+
metadata.trustedDependencies = ["core-js", "esbuild", "workerd"];
|
|
2340
|
+
delete metadata.allowScripts;
|
|
2341
|
+
} else {
|
|
2342
|
+
delete metadata.allowScripts;
|
|
2343
|
+
delete metadata.trustedDependencies;
|
|
2344
|
+
}
|
|
2345
|
+
await writeFile(packagePath, `${JSON.stringify(metadata, void 0, 2)}
|
|
2346
|
+
`);
|
|
2347
|
+
if (javascript.name === "npm") {
|
|
2348
|
+
const packageLockPath = join2(targetDirectory, "package-lock.json");
|
|
2349
|
+
const packageLock = JSON.parse(
|
|
2350
|
+
await readFile(packageLockPath, "utf8")
|
|
2351
|
+
);
|
|
2352
|
+
packageLock.name = metadata.name;
|
|
2353
|
+
if (packageLock.packages?.[""] !== void 0) {
|
|
2354
|
+
packageLock.packages[""].name = metadata.name;
|
|
2355
|
+
}
|
|
2356
|
+
await writeFile(
|
|
2357
|
+
packageLockPath,
|
|
2358
|
+
`${JSON.stringify(packageLock, void 0, 2)}
|
|
2359
|
+
`
|
|
2360
|
+
);
|
|
2361
|
+
}
|
|
2362
|
+
const replacements = createReplacements(packageManagers, platform);
|
|
2363
|
+
const agentReplacements = /* @__PURE__ */ new Map([
|
|
2364
|
+
["spatius-agent", `spatius-agent-${randomUUID()}`]
|
|
2365
|
+
]);
|
|
2366
|
+
await Promise.all(
|
|
2367
|
+
[
|
|
2368
|
+
".dev.vars.example",
|
|
2369
|
+
"wrangler.jsonc",
|
|
2370
|
+
"agent/src/agent.py",
|
|
2371
|
+
"agent/README.md",
|
|
2372
|
+
"worker/index.test.ts"
|
|
2373
|
+
].map(
|
|
2374
|
+
async (relativePath) => renderFile(join2(targetDirectory, relativePath), agentReplacements)
|
|
2375
|
+
)
|
|
2376
|
+
);
|
|
2377
|
+
await Promise.all(
|
|
2378
|
+
["README.md", "AGENTS.md", "agent/README.md"].map(
|
|
2379
|
+
async (relativePath) => renderFile(join2(targetDirectory, relativePath), replacements)
|
|
2380
|
+
)
|
|
2381
|
+
);
|
|
2382
|
+
}
|
|
2383
|
+
|
|
2384
|
+
// src/templates/cloudflare-livekit/install.ts
|
|
2385
|
+
init_package_managers();
|
|
2386
|
+
import { posix, win32 } from "path";
|
|
2387
|
+
function commandStep(command, args, cwd, displayCommand, label) {
|
|
2388
|
+
return {
|
|
2389
|
+
args: [...command.argsPrefix, ...args],
|
|
2390
|
+
command: command.command,
|
|
2391
|
+
cwd,
|
|
2392
|
+
displayCommand,
|
|
2393
|
+
label
|
|
2394
|
+
};
|
|
2395
|
+
}
|
|
2396
|
+
function createInstallPlan(targetDirectory, packageManagers, platform = process.platform) {
|
|
2397
|
+
const path = platform === "win32" ? win32 : posix;
|
|
2398
|
+
const javascript = packageManagers.javascript;
|
|
2399
|
+
const python = packageManagers.python;
|
|
2400
|
+
const steps = [
|
|
2401
|
+
commandStep(
|
|
2402
|
+
javascript.command,
|
|
2403
|
+
["install"],
|
|
2404
|
+
targetDirectory,
|
|
2405
|
+
javascriptInstallCommand(javascript.name),
|
|
2406
|
+
`Installing JavaScript dependencies with ${javascript.name}`
|
|
2407
|
+
)
|
|
2408
|
+
];
|
|
2409
|
+
if (python.name === "uv") {
|
|
2410
|
+
steps.push(
|
|
2411
|
+
commandStep(
|
|
2412
|
+
python.command,
|
|
2413
|
+
["sync", "--directory", "agent", "--extra", "dev"],
|
|
2414
|
+
targetDirectory,
|
|
2415
|
+
pythonInstallCommands(python, platform)[0],
|
|
2416
|
+
"Installing Python dependencies with uv"
|
|
2417
|
+
)
|
|
2418
|
+
);
|
|
2419
|
+
return steps;
|
|
2420
|
+
}
|
|
2421
|
+
const runtime = python.pythonRuntime;
|
|
2422
|
+
if (runtime === void 0) {
|
|
2423
|
+
throw new Error("pip was selected without a Python runtime.");
|
|
2424
|
+
}
|
|
2425
|
+
const displayedCommands = pythonInstallCommands(python, platform);
|
|
2426
|
+
steps.push(
|
|
2427
|
+
commandStep(
|
|
2428
|
+
runtime,
|
|
2429
|
+
["-m", "venv", "agent/.venv"],
|
|
2430
|
+
targetDirectory,
|
|
2431
|
+
displayedCommands[0],
|
|
2432
|
+
"Creating the Python virtual environment"
|
|
2433
|
+
)
|
|
2434
|
+
);
|
|
2435
|
+
const virtualEnvironmentPython = platform === "win32" ? path.join(targetDirectory, "agent", ".venv", "Scripts", "python.exe") : path.join(targetDirectory, "agent", ".venv", "bin", "python");
|
|
2436
|
+
steps.push({
|
|
2437
|
+
args: ["-m", "pip", "install", "-e", ".[dev]"],
|
|
2438
|
+
command: virtualEnvironmentPython,
|
|
2439
|
+
cwd: path.join(targetDirectory, "agent"),
|
|
2440
|
+
displayCommand: displayedCommands[1],
|
|
2441
|
+
label: "Installing Python dependencies with pip"
|
|
2442
|
+
});
|
|
2443
|
+
return steps;
|
|
2444
|
+
}
|
|
2445
|
+
|
|
2446
|
+
// src/templates/cloudflare-livekit/index.ts
|
|
2447
|
+
var aliases = {
|
|
2448
|
+
"dev.vars.example": ".dev.vars.example",
|
|
2449
|
+
dockerignore: ".dockerignore",
|
|
2450
|
+
"env.example": ".env.example",
|
|
2451
|
+
gitignore: ".gitignore",
|
|
2452
|
+
npmrc: ".npmrc",
|
|
2453
|
+
prettierignore: ".prettierignore",
|
|
2454
|
+
"python-version": ".python-version",
|
|
2455
|
+
"Dockerfile.pip": "Dockerfile",
|
|
2456
|
+
"Dockerfile.uv": "Dockerfile"
|
|
2457
|
+
};
|
|
2458
|
+
var cloudflareLivekitTemplate = {
|
|
2459
|
+
id: "cloudflare-livekit",
|
|
2460
|
+
directory: "templates/cloudflare-livekit",
|
|
2461
|
+
description: "Create a React, Cloudflare Workers, and LiveKit Agents application.",
|
|
2462
|
+
components: [
|
|
2463
|
+
"- web/ React + Spatius AvatarKit",
|
|
2464
|
+
"- worker/ Cloudflare token and dispatch API",
|
|
2465
|
+
"- agent/ Python LiveKit agent"
|
|
2466
|
+
],
|
|
2467
|
+
includeFile(path, configuration) {
|
|
2468
|
+
const javascript = configuration?.packageManagers.javascript.name ?? "pnpm";
|
|
2469
|
+
const python = configuration?.packageManagers.python.name ?? "uv";
|
|
2470
|
+
if (path === "package-lock.json") return javascript === "npm";
|
|
2471
|
+
if (["npmrc", "pnpm-lock.yaml", "pnpm-workspace.yaml"].includes(path)) {
|
|
2472
|
+
return javascript === "pnpm";
|
|
2473
|
+
}
|
|
2474
|
+
if (path === "agent/uv.lock" || path === "agent/Dockerfile.uv") {
|
|
2475
|
+
return python === "uv";
|
|
2476
|
+
}
|
|
2477
|
+
if (path === "agent/Dockerfile.pip") return python === "pip";
|
|
2478
|
+
return true;
|
|
2479
|
+
},
|
|
2480
|
+
mapFile(path) {
|
|
2481
|
+
return path.split("/").map((segment) => aliases[segment] ?? segment).join("/");
|
|
2482
|
+
},
|
|
2483
|
+
configure: configureGeneratedTemplate,
|
|
2484
|
+
createInstallPlan,
|
|
2485
|
+
nextSteps({
|
|
2486
|
+
credentialsConfigured = false,
|
|
2487
|
+
dependenciesInstalled,
|
|
2488
|
+
javascriptPackageManager,
|
|
2489
|
+
platform,
|
|
2490
|
+
pythonPackageManager
|
|
2491
|
+
}) {
|
|
2492
|
+
return [
|
|
2493
|
+
...dependenciesInstalled ? [] : [
|
|
2494
|
+
javascriptInstallCommand(javascriptPackageManager),
|
|
2495
|
+
...pythonInstallCommands(pythonPackageManager, platform)
|
|
2496
|
+
],
|
|
2497
|
+
...credentialsConfigured ? [] : ["npx create-spatius-app setup . --interactive"],
|
|
2498
|
+
javascriptRunCommand(javascriptPackageManager, "dev")
|
|
2499
|
+
];
|
|
2500
|
+
},
|
|
2501
|
+
setup: {
|
|
2502
|
+
async recognizes(directory) {
|
|
2503
|
+
try {
|
|
2504
|
+
await assertSpatiusProject(directory);
|
|
2505
|
+
return true;
|
|
2506
|
+
} catch (error) {
|
|
2507
|
+
if (error instanceof CliError && error.code === "INVALID_ARGUMENT") {
|
|
2508
|
+
return false;
|
|
2509
|
+
}
|
|
2510
|
+
throw error;
|
|
2511
|
+
}
|
|
2512
|
+
},
|
|
2513
|
+
async run(options) {
|
|
2514
|
+
const { runCredentialSetup: runCredentialSetup2 } = await Promise.resolve().then(() => (init_wizard(), wizard_exports));
|
|
2515
|
+
return runCredentialSetup2(options);
|
|
2516
|
+
}
|
|
2517
|
+
},
|
|
2518
|
+
verification: [
|
|
2519
|
+
{ javascript: "pnpm", python: "uv", scripts: ["check", "agent:check"] },
|
|
2520
|
+
{ javascript: "npm", python: "pip", scripts: ["check", "agent:check"] }
|
|
2521
|
+
]
|
|
2522
|
+
};
|
|
2523
|
+
|
|
2524
|
+
// src/scaffold.ts
|
|
2525
|
+
init_errors();
|
|
2526
|
+
import { constants } from "fs";
|
|
2527
|
+
import { copyFile, lstat as lstat4, mkdir, readdir as readdir2, rm as rm3 } from "fs/promises";
|
|
2528
|
+
import { basename as basename3, dirname, isAbsolute, join as join5, win32 as win322 } from "path";
|
|
2529
|
+
|
|
2530
|
+
// src/project-directory.ts
|
|
2531
|
+
init_errors();
|
|
2532
|
+
import { homedir } from "os";
|
|
2533
|
+
import { lstat as lstat3, readdir } from "fs/promises";
|
|
2534
|
+
import { parse, resolve } from "path";
|
|
2535
|
+
function normalizeTargetDirectory(input, currentWorkingDirectory = process.cwd()) {
|
|
2536
|
+
return resolve(currentWorkingDirectory, input);
|
|
2537
|
+
}
|
|
2538
|
+
function assertSafeTargetDirectory(targetDirectory, homeDirectory = homedir()) {
|
|
2539
|
+
const normalizedTarget = resolve(targetDirectory);
|
|
2540
|
+
if (normalizedTarget === parse(normalizedTarget).root) {
|
|
2541
|
+
throw new CliError(
|
|
2542
|
+
"UNSAFE_TARGET",
|
|
2543
|
+
"Refusing to create a project at the filesystem root.",
|
|
2544
|
+
{
|
|
2545
|
+
exitCode: EXIT_CODES.invalidArgument,
|
|
2546
|
+
path: normalizedTarget,
|
|
2547
|
+
recovery: "Choose a dedicated project directory."
|
|
2548
|
+
}
|
|
2549
|
+
);
|
|
2550
|
+
}
|
|
2551
|
+
if (normalizedTarget === resolve(homeDirectory)) {
|
|
2552
|
+
throw new CliError(
|
|
2553
|
+
"UNSAFE_TARGET",
|
|
2554
|
+
"Refusing to create a project directly in the home directory.",
|
|
2555
|
+
{
|
|
2556
|
+
exitCode: EXIT_CODES.invalidArgument,
|
|
2557
|
+
path: normalizedTarget,
|
|
2558
|
+
recovery: "Choose a dedicated project directory inside your home directory."
|
|
2559
|
+
}
|
|
2560
|
+
);
|
|
2561
|
+
}
|
|
2562
|
+
}
|
|
2563
|
+
async function inspectTargetDirectory(targetDirectory) {
|
|
2564
|
+
try {
|
|
2565
|
+
const stats = await lstat3(targetDirectory);
|
|
2566
|
+
if (!stats.isDirectory() || stats.isSymbolicLink()) {
|
|
2567
|
+
return "not-a-directory";
|
|
2568
|
+
}
|
|
2569
|
+
const entries = await readdir(targetDirectory);
|
|
2570
|
+
return entries.length === 0 ? "empty-directory" : "non-empty-directory";
|
|
2571
|
+
} catch (error) {
|
|
2572
|
+
if (isNodeError(error) && error.code === "ENOENT") {
|
|
2573
|
+
return "missing";
|
|
2574
|
+
}
|
|
2575
|
+
throw error;
|
|
2576
|
+
}
|
|
2577
|
+
}
|
|
2578
|
+
async function assertTargetDirectoryAvailable(targetDirectory) {
|
|
2579
|
+
assertSafeTargetDirectory(targetDirectory);
|
|
2580
|
+
const state = await inspectTargetDirectory(targetDirectory);
|
|
2581
|
+
if (state === "not-a-directory") {
|
|
2582
|
+
throw new CliError(
|
|
2583
|
+
"TARGET_NOT_DIRECTORY",
|
|
2584
|
+
`The target path is not a directory: ${targetDirectory}`,
|
|
2585
|
+
{
|
|
2586
|
+
exitCode: EXIT_CODES.targetConflict,
|
|
2587
|
+
path: targetDirectory,
|
|
2588
|
+
recovery: "Choose a path that is missing or is an empty directory."
|
|
2589
|
+
}
|
|
2590
|
+
);
|
|
2591
|
+
}
|
|
2592
|
+
if (state === "non-empty-directory") {
|
|
2593
|
+
throw new CliError(
|
|
2594
|
+
"TARGET_NOT_EMPTY",
|
|
2595
|
+
`The target directory is not empty: ${targetDirectory}
|
|
2596
|
+
Choose a new directory or remove its contents first.`,
|
|
2597
|
+
{
|
|
2598
|
+
exitCode: EXIT_CODES.targetConflict,
|
|
2599
|
+
path: targetDirectory,
|
|
2600
|
+
recovery: "Choose an empty directory or remove its contents first."
|
|
2601
|
+
}
|
|
2602
|
+
);
|
|
2603
|
+
}
|
|
2604
|
+
return state;
|
|
2605
|
+
}
|
|
2606
|
+
function isNodeError(error) {
|
|
2607
|
+
return error instanceof Error && "code" in error;
|
|
2608
|
+
}
|
|
2609
|
+
|
|
2610
|
+
// src/scaffold.ts
|
|
2611
|
+
var defaultCopyEntry = async (source, destination) => {
|
|
2612
|
+
await copyFile(source, destination, constants.COPYFILE_EXCL);
|
|
2613
|
+
};
|
|
2614
|
+
var ignoredTemplateEntries = /* @__PURE__ */ new Set([
|
|
2615
|
+
".DS_Store",
|
|
2616
|
+
".npmignore",
|
|
2617
|
+
".pytest_cache",
|
|
2618
|
+
".ruff_cache",
|
|
2619
|
+
".venv",
|
|
2620
|
+
".wrangler",
|
|
2621
|
+
"__pycache__",
|
|
2622
|
+
"coverage",
|
|
2623
|
+
"dist",
|
|
2624
|
+
"node_modules",
|
|
2625
|
+
"test-results",
|
|
2626
|
+
"playwright-report"
|
|
2627
|
+
]);
|
|
2628
|
+
function shouldIgnoreTemplateEntry(entry) {
|
|
2629
|
+
return ignoredTemplateEntries.has(entry) || /\.py[cod]$/u.test(entry) || /^\.(?:env|dev\.vars)(?:\.|$)/u.test(entry) && !entry.endsWith(".example");
|
|
2630
|
+
}
|
|
2631
|
+
function assertRelativeDestination(path) {
|
|
2632
|
+
if (isAbsolute(path) || win322.isAbsolute(path) || path.includes("\\") || path.includes(":") || path.includes("\0") || path.split("/").some((segment) => ["", ".", ".."].includes(segment))) {
|
|
2633
|
+
throw new Error("Template file mappings must stay inside the project.");
|
|
2634
|
+
}
|
|
2635
|
+
}
|
|
2636
|
+
async function listTemplateFiles(templateDirectory, template, configuration, relativeDirectory = "") {
|
|
2637
|
+
const entries = await readdir2(join5(templateDirectory, relativeDirectory), {
|
|
2638
|
+
withFileTypes: true
|
|
2639
|
+
});
|
|
2640
|
+
const files = [];
|
|
2641
|
+
for (const entry of entries) {
|
|
2642
|
+
if (shouldIgnoreTemplateEntry(entry.name)) continue;
|
|
2643
|
+
const source = [relativeDirectory, entry.name].filter(Boolean).join("/");
|
|
2644
|
+
if (!template.includeFile(source, configuration)) continue;
|
|
2645
|
+
if (entry.isSymbolicLink()) {
|
|
2646
|
+
throw new Error("Bundled templates must not contain symbolic links.");
|
|
2647
|
+
}
|
|
2648
|
+
if (entry.isDirectory()) {
|
|
2649
|
+
files.push(
|
|
2650
|
+
...await listTemplateFiles(
|
|
2651
|
+
templateDirectory,
|
|
2652
|
+
template,
|
|
2653
|
+
configuration,
|
|
2654
|
+
source
|
|
2655
|
+
)
|
|
2656
|
+
);
|
|
2657
|
+
} else if (entry.isFile()) {
|
|
2658
|
+
const destination = template.mapFile(source);
|
|
2659
|
+
assertRelativeDestination(destination);
|
|
2660
|
+
files.push({ source, destination });
|
|
2661
|
+
} else {
|
|
2662
|
+
throw new Error("Bundled templates must contain regular files.");
|
|
2663
|
+
}
|
|
2664
|
+
}
|
|
2665
|
+
return files;
|
|
2666
|
+
}
|
|
2667
|
+
async function createScaffoldPlan({
|
|
2668
|
+
configuration,
|
|
2669
|
+
targetDirectory: targetDirectoryInput,
|
|
2670
|
+
template = getTemplate(),
|
|
2671
|
+
templateDirectory = resolveTemplateDirectory(template)
|
|
2672
|
+
}) {
|
|
2673
|
+
const targetDirectory = normalizeTargetDirectory(targetDirectoryInput);
|
|
2674
|
+
const targetState = await assertTargetDirectoryAvailable(targetDirectory);
|
|
2675
|
+
let entries;
|
|
2676
|
+
try {
|
|
2677
|
+
const root = await lstat4(templateDirectory);
|
|
2678
|
+
if (!root.isDirectory() || root.isSymbolicLink()) {
|
|
2679
|
+
throw new Error("The template must be a regular directory.");
|
|
2680
|
+
}
|
|
2681
|
+
entries = await listTemplateFiles(
|
|
2682
|
+
templateDirectory,
|
|
2683
|
+
template,
|
|
2684
|
+
configuration
|
|
2685
|
+
);
|
|
2686
|
+
const destinations = /* @__PURE__ */ new Set();
|
|
2687
|
+
const directories = /* @__PURE__ */ new Map();
|
|
2688
|
+
for (const { destination } of entries) {
|
|
2689
|
+
const key = destination.toLowerCase();
|
|
2690
|
+
if (destinations.has(key) || directories.has(key)) {
|
|
2691
|
+
throw new Error("Template file mappings collide.");
|
|
2692
|
+
}
|
|
2693
|
+
destinations.add(key);
|
|
2694
|
+
const segments = destination.split("/");
|
|
2695
|
+
for (let index = 1; index < segments.length; index++) {
|
|
2696
|
+
const parent = segments.slice(0, index).join("/");
|
|
2697
|
+
const parentKey = parent.toLowerCase();
|
|
2698
|
+
if (destinations.has(parentKey))
|
|
2699
|
+
throw new Error("Template file mappings collide.");
|
|
2700
|
+
const existingParent = directories.get(parentKey);
|
|
2701
|
+
if (existingParent !== void 0 && existingParent !== parent) {
|
|
2702
|
+
throw new Error(
|
|
2703
|
+
"Template directory mappings use inconsistent casing."
|
|
2704
|
+
);
|
|
2705
|
+
}
|
|
2706
|
+
directories.set(parentKey, parent);
|
|
2707
|
+
}
|
|
2708
|
+
}
|
|
2709
|
+
} catch (error) {
|
|
2710
|
+
throw new CliError(
|
|
2711
|
+
"TEMPLATE_NOT_FOUND",
|
|
2712
|
+
"The bundled project template could not be loaded at " + templateDirectory + ": " + (error instanceof Error ? error.message : "unknown filesystem error"),
|
|
2713
|
+
{
|
|
2714
|
+
exitCode: EXIT_CODES.filesystem,
|
|
2715
|
+
path: templateDirectory,
|
|
2716
|
+
recovery: "Reinstall create-spatius-app and try again."
|
|
2717
|
+
}
|
|
2718
|
+
);
|
|
2719
|
+
}
|
|
2720
|
+
if (entries.length === 0) {
|
|
2721
|
+
throw new CliError(
|
|
2722
|
+
"TEMPLATE_NOT_FOUND",
|
|
2723
|
+
"The bundled project template is empty.",
|
|
2724
|
+
{
|
|
2725
|
+
exitCode: EXIT_CODES.filesystem,
|
|
2726
|
+
path: templateDirectory,
|
|
2727
|
+
recovery: "Reinstall create-spatius-app and try again."
|
|
2728
|
+
}
|
|
2729
|
+
);
|
|
2730
|
+
}
|
|
2731
|
+
entries.sort(
|
|
2732
|
+
(left, right) => left.destination < right.destination ? -1 : left.destination > right.destination ? 1 : 0
|
|
2733
|
+
);
|
|
2734
|
+
return {
|
|
2735
|
+
entries,
|
|
2736
|
+
files: entries.map((entry) => entry.destination),
|
|
2737
|
+
targetDirectory,
|
|
2738
|
+
targetState,
|
|
2739
|
+
templateDirectory
|
|
2740
|
+
};
|
|
2741
|
+
}
|
|
2742
|
+
async function rollback(targetDirectory, targetCreated, createdPaths) {
|
|
2743
|
+
if (targetCreated) {
|
|
2744
|
+
await rm3(targetDirectory, { force: true, recursive: true });
|
|
2745
|
+
return;
|
|
2746
|
+
}
|
|
2747
|
+
for (const path of [...createdPaths].reverse()) {
|
|
2748
|
+
await rm3(path, { force: true, recursive: true });
|
|
2749
|
+
}
|
|
2750
|
+
}
|
|
2751
|
+
async function scaffoldProject({
|
|
2752
|
+
configuration,
|
|
2753
|
+
copyEntry = defaultCopyEntry,
|
|
2754
|
+
targetDirectory: targetDirectoryInput,
|
|
2755
|
+
template = getTemplate(),
|
|
2756
|
+
templateDirectory = resolveTemplateDirectory(template)
|
|
2757
|
+
}) {
|
|
2758
|
+
const plan = await createScaffoldPlan({
|
|
2759
|
+
configuration,
|
|
2760
|
+
targetDirectory: targetDirectoryInput,
|
|
2761
|
+
template,
|
|
2762
|
+
templateDirectory
|
|
2763
|
+
});
|
|
2764
|
+
const { targetDirectory } = plan;
|
|
2765
|
+
const createdPaths = [];
|
|
2766
|
+
const createdDirectories = /* @__PURE__ */ new Set([targetDirectory]);
|
|
2767
|
+
let targetCreated = false;
|
|
2768
|
+
async function createParent(directory) {
|
|
2769
|
+
if (createdDirectories.has(directory)) return;
|
|
2770
|
+
await createParent(dirname(directory));
|
|
2771
|
+
await mkdir(directory);
|
|
2772
|
+
createdPaths.push(directory);
|
|
2773
|
+
createdDirectories.add(directory);
|
|
2774
|
+
}
|
|
2775
|
+
try {
|
|
2776
|
+
const targetState = await assertTargetDirectoryAvailable(targetDirectory);
|
|
2777
|
+
if (targetState === "missing") {
|
|
2778
|
+
await mkdir(dirname(targetDirectory), { recursive: true });
|
|
2779
|
+
await mkdir(targetDirectory);
|
|
2780
|
+
targetCreated = true;
|
|
2781
|
+
}
|
|
2782
|
+
for (const entry of plan.entries) {
|
|
2783
|
+
const source = join5(templateDirectory, entry.source);
|
|
2784
|
+
const destination = join5(targetDirectory, entry.destination);
|
|
2785
|
+
const stats = await lstat4(source);
|
|
2786
|
+
if (!stats.isFile() || stats.isSymbolicLink()) {
|
|
2787
|
+
throw new Error("A template source is no longer a regular file.");
|
|
2788
|
+
}
|
|
2789
|
+
await createParent(dirname(destination));
|
|
2790
|
+
createdPaths.push(destination);
|
|
2791
|
+
await copyEntry(source, destination);
|
|
2792
|
+
}
|
|
2793
|
+
if (configuration !== void 0) {
|
|
2794
|
+
await template.configure(targetDirectory, configuration);
|
|
2795
|
+
}
|
|
2796
|
+
} catch (error) {
|
|
2797
|
+
await rollback(targetDirectory, targetCreated, createdPaths).catch(
|
|
2798
|
+
() => void 0
|
|
2799
|
+
);
|
|
2800
|
+
throw new CliError(
|
|
2801
|
+
"FILESYSTEM_ERROR",
|
|
2802
|
+
"Could not create " + basename3(targetDirectory) + ": " + (error instanceof Error ? error.message : "unknown filesystem error"),
|
|
2803
|
+
{
|
|
2804
|
+
exitCode: EXIT_CODES.filesystem,
|
|
2805
|
+
path: targetDirectory,
|
|
2806
|
+
recovery: "Check filesystem permissions and try again."
|
|
2807
|
+
}
|
|
2808
|
+
);
|
|
2809
|
+
}
|
|
2810
|
+
return { files: plan.files, targetDirectory };
|
|
2811
|
+
}
|
|
2812
|
+
|
|
2813
|
+
// src/templates.ts
|
|
2814
|
+
init_package_managers();
|
|
2815
|
+
var DEFAULT_TEMPLATE_ID = "cloudflare-livekit";
|
|
2816
|
+
var templateRegistry = {
|
|
2817
|
+
[DEFAULT_TEMPLATE_ID]: cloudflareLivekitTemplate
|
|
2818
|
+
};
|
|
2819
|
+
function getTemplate(id = DEFAULT_TEMPLATE_ID) {
|
|
2820
|
+
return templateRegistry[id];
|
|
2821
|
+
}
|
|
2822
|
+
function resolveTemplateDirectory(template = getTemplate()) {
|
|
2823
|
+
return fileURLToPath(new URL(`../${template.directory}/`, import.meta.url));
|
|
2824
|
+
}
|
|
2825
|
+
async function resolveProjectTemplate(targetDirectory, templates = Object.values(templateRegistry)) {
|
|
2826
|
+
const matches = [];
|
|
2827
|
+
for (const template of templates) {
|
|
2828
|
+
if (await template.setup.recognizes(targetDirectory))
|
|
2829
|
+
matches.push(template);
|
|
2830
|
+
}
|
|
2831
|
+
if (matches.length !== 1) {
|
|
2832
|
+
throw new CliError(
|
|
2833
|
+
"INVALID_ARGUMENT",
|
|
2834
|
+
matches.length === 0 ? "The target is not a compatible create-spatius-app project." : "The target matches more than one create-spatius-app template.",
|
|
2835
|
+
{
|
|
2836
|
+
exitCode: EXIT_CODES.invalidArgument,
|
|
2837
|
+
path: targetDirectory,
|
|
2838
|
+
recovery: "Run this command from a generated project, or pass its directory explicitly."
|
|
2839
|
+
}
|
|
2840
|
+
);
|
|
2841
|
+
}
|
|
2842
|
+
return matches[0];
|
|
2843
|
+
}
|
|
2844
|
+
export {
|
|
2845
|
+
DEFAULT_TEMPLATE_ID,
|
|
2846
|
+
createProcessInvocation,
|
|
2847
|
+
createScaffoldPlan,
|
|
2848
|
+
getTemplate,
|
|
2849
|
+
resolveProjectTemplate,
|
|
2850
|
+
resolveTemplateDirectory,
|
|
2851
|
+
scaffoldProject,
|
|
2852
|
+
selectJavaScriptPackageManager,
|
|
2853
|
+
selectPythonPackageManager,
|
|
2854
|
+
templateRegistry
|
|
2855
|
+
};
|
|
2856
|
+
//# sourceMappingURL=templates.js.map
|