create-spatius-app 0.1.3-beta.1 → 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +12 -10
- package/dist/cli.js +199 -207
- package/dist/templates.js +183 -199
- package/package.json +3 -3
- package/templates/cloudflare-livekit/AGENTS.md +2 -0
- package/templates/cloudflare-livekit/README.md +10 -0
- package/templates/cloudflare-livekit/agent/Dockerfile.pip +8 -0
- package/templates/cloudflare-livekit/agent/Dockerfile.uv +8 -0
- package/templates/cloudflare-livekit/agent/README.md +16 -1
- package/templates/cloudflare-livekit/agent/pyproject.toml +1 -1
- package/templates/cloudflare-livekit/agent/src/agent.py +2 -1
- package/templates/cloudflare-livekit/agent/tests/test_agent_startup.py +49 -0
- package/templates/cloudflare-livekit/agent/uv.lock +14 -14
- package/templates/cloudflare-livekit/gitignore +1 -0
- package/templates/cloudflare-livekit/package-lock.json +511 -411
- package/templates/cloudflare-livekit/package.json +17 -16
- package/templates/cloudflare-livekit/pnpm-lock.yaml +572 -480
- package/templates/cloudflare-livekit/tests/dev-fixtures.mjs +7 -2
- package/templates/cloudflare-livekit/tests/dev-supervision.test.mjs +82 -20
- package/templates/layers/agora/AGENTS.md +9 -4
- package/templates/layers/agora/README.md +14 -8
- package/templates/layers/agora/package-lock.json +6307 -4129
- package/templates/layers/agora/package.json +24 -18
- package/templates/layers/agora/pnpm-lock.yaml +1765 -563
- package/templates/layers/agora/pnpm-workspace.yaml +8 -0
- package/templates/layers/agora/scripts/check-worker.mjs +98 -0
- package/templates/layers/agora/tsconfig.worker.json +23 -0
- package/templates/layers/agora/worker/agora.test.ts +2 -0
- package/templates/layers/agora/worker/agora.ts +1 -10
- package/templates/layers/agora/worker/index.test.ts +66 -0
- package/templates/layers/agora/worker/index.ts +4 -0
- package/templates/layers/agora/wrangler.jsonc +24 -0
- package/templates/cloudflare-livekit/worker-configuration.d.ts +0 -17184
- package/templates/layers/agora/tsconfig.json +0 -16
- package/templates/layers/node/Dockerfile +0 -12
- package/templates/layers/node/dockerignore +0 -10
- package/templates/layers/node/package-lock.json +0 -5569
- package/templates/layers/node/pnpm-lock.yaml +0 -3470
- package/templates/layers/node/railway.json +0 -8
- package/templates/layers/node/scripts/dev-service.mjs +0 -14
- package/templates/layers/node/scripts/dev-web.mjs +0 -22
- package/templates/layers/node/server/index.ts +0 -92
- package/templates/layers/node/vite.config.ts +0 -17
- package/templates/layers/railway-agent/agent/railway.json +0 -5
- /package/templates/layers/agora/{env.example → dev.vars.example} +0 -0
|
@@ -219,12 +219,17 @@ export async function readEvents(path) {
|
|
|
219
219
|
|
|
220
220
|
export async function waitForEvents(path, predicate) {
|
|
221
221
|
const deadline = Date.now() + 8000;
|
|
222
|
+
let events = [];
|
|
222
223
|
while (Date.now() < deadline) {
|
|
223
|
-
|
|
224
|
+
events = await readEvents(path);
|
|
224
225
|
if (predicate(events)) return events;
|
|
225
226
|
await delay(25);
|
|
226
227
|
}
|
|
227
|
-
|
|
228
|
+
// Include progress without logging the fixture's environment or credentials.
|
|
229
|
+
const progress = events.map(({ role, type }) => `${role}:${type}`).join(', ');
|
|
230
|
+
assert.fail(
|
|
231
|
+
`Timed out waiting for fake processes to become ready. Events: ${progress || 'none'}`,
|
|
232
|
+
);
|
|
228
233
|
}
|
|
229
234
|
|
|
230
235
|
export async function assertStopped(pids) {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import assert from 'node:assert/strict';
|
|
2
2
|
import { spawn } from 'node:child_process';
|
|
3
|
+
import { existsSync } from 'node:fs';
|
|
3
4
|
import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
4
5
|
import { createRequire } from 'node:module';
|
|
5
6
|
import { delimiter, dirname, join } from 'node:path';
|
|
@@ -27,7 +28,7 @@ const concurrentlyBin = join(
|
|
|
27
28
|
require(concurrentlyPackage).bin.concurrently,
|
|
28
29
|
);
|
|
29
30
|
|
|
30
|
-
async function supervise(f, extraEnv) {
|
|
31
|
+
async function supervise(f, extraEnv, manager = 'npm', executable = manager) {
|
|
31
32
|
const viteBin = join(f.root, 'node_modules', 'vite', 'bin');
|
|
32
33
|
await mkdir(viteBin, { recursive: true });
|
|
33
34
|
await writeFile(
|
|
@@ -37,14 +38,25 @@ async function supervise(f, extraEnv) {
|
|
|
37
38
|
const sourcePackage = JSON.parse(
|
|
38
39
|
await readFile(join(templateDirectory, 'package.json'), 'utf8'),
|
|
39
40
|
);
|
|
40
|
-
//
|
|
41
|
-
//
|
|
41
|
+
// npm is available alongside Node. The pnpm regression also exercises the
|
|
42
|
+
// package manager running this suite, without downloading another version.
|
|
42
43
|
const scripts = Object.fromEntries(
|
|
43
44
|
['dev', 'dev:web', 'agent:dev'].map((name) => [
|
|
44
45
|
name,
|
|
45
|
-
sourcePackage.scripts[name].replaceAll('pnpm run ',
|
|
46
|
+
sourcePackage.scripts[name].replaceAll('pnpm run ', `${manager} run `),
|
|
46
47
|
]),
|
|
47
48
|
);
|
|
49
|
+
// Observe completed production cleanup independently of pipe closure. A package
|
|
50
|
+
// manager can exit early while descendants still hold its output pipes open.
|
|
51
|
+
const finished = join(f.root, 'dev-finished');
|
|
52
|
+
await writeFile(
|
|
53
|
+
join(f.root, 'scripts', 'dev-finished.mjs'),
|
|
54
|
+
`import { writeFileSync } from 'node:fs'; await import('./dev.mjs'); writeFileSync(${JSON.stringify(finished)}, '');`,
|
|
55
|
+
);
|
|
56
|
+
scripts.dev = scripts.dev.replace(
|
|
57
|
+
'scripts/dev.mjs',
|
|
58
|
+
'scripts/dev-finished.mjs',
|
|
59
|
+
);
|
|
48
60
|
await writeFile(
|
|
49
61
|
join(f.root, 'package.json'),
|
|
50
62
|
JSON.stringify({
|
|
@@ -72,7 +84,7 @@ async function supervise(f, extraEnv) {
|
|
|
72
84
|
);
|
|
73
85
|
const env = { ...f.env, ...extraEnv };
|
|
74
86
|
env.PATH += `${delimiter}${process.env.PATH ?? process.env.Path ?? ''}`;
|
|
75
|
-
const spec = commandSpec(
|
|
87
|
+
const spec = commandSpec(executable, ['run', 'dev'], env);
|
|
76
88
|
const child = spawn(spec.command, spec.arguments_, {
|
|
77
89
|
cwd: f.root,
|
|
78
90
|
env,
|
|
@@ -92,23 +104,30 @@ async function supervise(f, extraEnv) {
|
|
|
92
104
|
child.once('error', reject);
|
|
93
105
|
child.once('close', (code, signal) => resolve({ code, signal, output }));
|
|
94
106
|
});
|
|
95
|
-
|
|
107
|
+
const exited = new Promise((resolve) => {
|
|
108
|
+
child.once('exit', () => resolve({ finished: existsSync(finished) }));
|
|
109
|
+
});
|
|
110
|
+
return { child, done, exited };
|
|
96
111
|
}
|
|
97
112
|
|
|
98
|
-
for (const [name, side, code] of [
|
|
113
|
+
for (const [name, side, code, probe] of [
|
|
99
114
|
['web exits zero', 'web', '0'],
|
|
100
115
|
['web fails', 'web', '7'],
|
|
101
116
|
['agent exits zero', 'agent', '0'],
|
|
102
117
|
['agent fails', 'agent', '29'],
|
|
118
|
+
['web exits zero after a timed-out CLI probe', 'web', '0', 'version-hang'],
|
|
103
119
|
]) {
|
|
104
120
|
test(
|
|
105
121
|
`root dev supervisor: ${name} stops its sibling and all grandchildren`,
|
|
106
122
|
{ timeout: 15000 },
|
|
107
123
|
async (t) => {
|
|
108
|
-
|
|
124
|
+
// Windows has only a preflight python.exe stub. Use the runnable uv shim
|
|
125
|
+
// so a slow CLI probe can fall back without exiting before registration.
|
|
126
|
+
const f = await fixture(t, { uv: windows || Boolean(probe) });
|
|
109
127
|
const run = await supervise(f, {
|
|
110
128
|
DEV_TEST_RUNTIME_HOLD: '1',
|
|
111
129
|
DEV_TEST_GRANDCHILD: '1',
|
|
130
|
+
...(probe ? { DEV_TEST_PROBE: probe } : {}),
|
|
112
131
|
...(side === 'web'
|
|
113
132
|
? { DEV_TEST_WEB_EXIT: code }
|
|
114
133
|
: { DEV_TEST_EXIT_WHEN_RELEASED: '1', DEV_TEST_RUNTIME_EXIT: code }),
|
|
@@ -130,15 +149,14 @@ for (const [name, side, code] of [
|
|
|
130
149
|
await assertStopped(events.map(({ pid }) => pid));
|
|
131
150
|
// A bounded CLI probe may select Python fallback on a busy machine.
|
|
132
151
|
// Supervision must launch exactly one runtime through either supported path.
|
|
133
|
-
|
|
134
|
-
(
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
args.includes('src/agent.py'),
|
|
139
|
-
).length,
|
|
140
|
-
1,
|
|
152
|
+
const runtimes = (await readEvents(f.events)).filter(
|
|
153
|
+
({ role, args, type }) =>
|
|
154
|
+
['lk', 'uv', 'python'].includes(role) &&
|
|
155
|
+
type === 'invoked' &&
|
|
156
|
+
args.includes('src/agent.py'),
|
|
141
157
|
);
|
|
158
|
+
assert.equal(runtimes.length, 1);
|
|
159
|
+
if (probe) assert.equal(runtimes[0].role, 'uv');
|
|
142
160
|
},
|
|
143
161
|
);
|
|
144
162
|
}
|
|
@@ -152,7 +170,7 @@ test(
|
|
|
152
170
|
'Node cannot send console Ctrl+C events on Windows; taskkill tree cleanup is tested separately.',
|
|
153
171
|
},
|
|
154
172
|
async (t) => {
|
|
155
|
-
const f = await fixture(t);
|
|
173
|
+
const f = await fixture(t, { uv: windows });
|
|
156
174
|
const run = await supervise(f, {
|
|
157
175
|
DEV_TEST_RUNTIME_HOLD: '1',
|
|
158
176
|
DEV_TEST_GRANDCHILD: '1',
|
|
@@ -180,6 +198,50 @@ test(
|
|
|
180
198
|
},
|
|
181
199
|
);
|
|
182
200
|
|
|
201
|
+
// Generated npm/bun projects need not install pnpm just to run their checks.
|
|
202
|
+
// Root CI and generated pnpm checks supply the exact invoking pnpm executable.
|
|
203
|
+
const pnpmExecutable = process.env.npm_execpath;
|
|
204
|
+
test(
|
|
205
|
+
'pnpm waits for dev cleanup before exiting after one Ctrl+C',
|
|
206
|
+
{
|
|
207
|
+
timeout: 15000,
|
|
208
|
+
skip:
|
|
209
|
+
windows ||
|
|
210
|
+
!pnpmExecutable ||
|
|
211
|
+
!/pnpm(?:\.[cm]?js)?$/i.test(pnpmExecutable),
|
|
212
|
+
},
|
|
213
|
+
async (t) => {
|
|
214
|
+
const f = await fixture(t);
|
|
215
|
+
const run = await supervise(
|
|
216
|
+
f,
|
|
217
|
+
{
|
|
218
|
+
DEV_TEST_RUNTIME_HOLD: '1',
|
|
219
|
+
DEV_TEST_GRANDCHILD: '1',
|
|
220
|
+
DEV_TEST_DETACHED: '1',
|
|
221
|
+
},
|
|
222
|
+
'pnpm',
|
|
223
|
+
pnpmExecutable,
|
|
224
|
+
);
|
|
225
|
+
const events = await waitForEvents(
|
|
226
|
+
f.events,
|
|
227
|
+
(events) =>
|
|
228
|
+
events.filter(
|
|
229
|
+
({ role, type }) => role === 'grandchild' && type === 'ready',
|
|
230
|
+
).length === 2,
|
|
231
|
+
);
|
|
232
|
+
await delay(250);
|
|
233
|
+
process.kill(-run.child.pid, 'SIGINT');
|
|
234
|
+
assert.equal(
|
|
235
|
+
(await run.exited).finished,
|
|
236
|
+
true,
|
|
237
|
+
'pnpm returned before dev cleanup finished; use a pnpm launcher >=12.4.1 when using pnpm 12.',
|
|
238
|
+
);
|
|
239
|
+
const result = await run.done;
|
|
240
|
+
assertNoCredentials(result.output);
|
|
241
|
+
await assertStopped(events.map(({ pid }) => pid));
|
|
242
|
+
},
|
|
243
|
+
);
|
|
244
|
+
|
|
183
245
|
test(
|
|
184
246
|
'a timed-out CLI probe reaps its descendants before Python fallback, including Windows cmd wrappers',
|
|
185
247
|
{ timeout: 15000 },
|
|
@@ -252,7 +314,7 @@ test(
|
|
|
252
314
|
'agent failure before registration never starts web',
|
|
253
315
|
{ timeout: 15000 },
|
|
254
316
|
async (t) => {
|
|
255
|
-
const f = await fixture(t);
|
|
317
|
+
const f = await fixture(t, { uv: windows });
|
|
256
318
|
const run = await supervise(f, { DEV_TEST_RUNTIME_EXIT: '29' });
|
|
257
319
|
const result = await run.done;
|
|
258
320
|
assert.notEqual(result.code, 0);
|
|
@@ -265,7 +327,7 @@ test(
|
|
|
265
327
|
'registration timeout stops the agent without starting web',
|
|
266
328
|
{ timeout: 15000 },
|
|
267
329
|
async (t) => {
|
|
268
|
-
const f = await fixture(t);
|
|
330
|
+
const f = await fixture(t, { uv: windows });
|
|
269
331
|
// Accelerate only the copied fixture's deadline; exercise the real web gate.
|
|
270
332
|
const path = join(f.root, 'scripts', 'wait-for-agent.mjs');
|
|
271
333
|
await writeFile(
|
|
@@ -290,7 +352,7 @@ test(
|
|
|
290
352
|
'Ctrl+C while waiting for registration stops the agent without starting web',
|
|
291
353
|
{ timeout: 15000, skip: windows },
|
|
292
354
|
async (t) => {
|
|
293
|
-
const f = await fixture(t);
|
|
355
|
+
const f = await fixture(t, { uv: windows });
|
|
294
356
|
const run = await supervise(f, {
|
|
295
357
|
DEV_TEST_RUNTIME_HOLD: '1',
|
|
296
358
|
DEV_TEST_REGISTER: join(f.root, 'never-register'),
|
|
@@ -1,8 +1,13 @@
|
|
|
1
1
|
# Generated Agora application
|
|
2
2
|
|
|
3
3
|
Use `__DEV__` for local development and `__CHECK__` for verification.
|
|
4
|
-
The
|
|
5
|
-
|
|
4
|
+
The Cloudflare Worker API calls hosted Conversational AI over HTTP. Agora owns
|
|
5
|
+
the conversational agent; this application has no Python worker or Node server.
|
|
6
|
+
Browser audio uses Agora RTC, and text input and transcripts use the official
|
|
6
7
|
Agora client toolkit. Spatius renders the avatar locally.
|
|
7
|
-
|
|
8
|
-
|
|
8
|
+
|
|
9
|
+
Keep secrets in `.dev.vars` locally and Cloudflare Worker secrets in production.
|
|
10
|
+
Wrangler generates the ignored `worker-configuration.d.ts` during installation
|
|
11
|
+
and before lint, type checking, and builds. Update `wrangler.jsonc` to change bindings.
|
|
12
|
+
Preserve session cleanup on disconnect and failed startup.
|
|
13
|
+
Use mocks for provider calls in tests. See DEPLOYMENT.md for Cloudflare setup.
|
|
@@ -1,6 +1,9 @@
|
|
|
1
|
-
# Spatius voice assistant —
|
|
1
|
+
# Spatius voice assistant — Cloudflare + Agora
|
|
2
|
+
|
|
3
|
+
A minimal React avatar conversation with a Cloudflare Worker API and Agora
|
|
4
|
+
Conversational AI. The frontend and API deploy together on Cloudflare Workers;
|
|
5
|
+
Agora hosts the conversational agent.
|
|
2
6
|
|
|
3
|
-
A minimal React avatar conversation with an HTTP backend and Agora Conversational AI.
|
|
4
7
|
Requires Node.js 22.12+, an Agora project with Conversational AI and RTM enabled,
|
|
5
8
|
a published English assistant pipeline, and Spatius credentials. No Python is needed.
|
|
6
9
|
|
|
@@ -10,10 +13,13 @@ npx create-spatius-app setup . --interactive
|
|
|
10
13
|
__DEV__
|
|
11
14
|
```
|
|
12
15
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
16
|
+
Local setup saves credentials in `.dev.vars`. The Vite Cloudflare plugin runs
|
|
17
|
+
both the frontend and Worker API locally. Open the printed URL, start a
|
|
18
|
+
conversation, and speak or type. Microphone permission is optional: typed
|
|
19
|
+
messages use Agora RTM. The transcript stays visible after disconnecting.
|
|
20
|
+
|
|
21
|
+
Configure the assistant persona, speech recognition language, model, and voice
|
|
22
|
+
in the published Agora pipeline. Match `AGORA_AVATAR_SAMPLE_RATE` to its TTS output.
|
|
18
23
|
|
|
19
|
-
|
|
24
|
+
Run `__CHECK__` to verify the application. See [DEPLOYMENT.md](DEPLOYMENT.md)
|
|
25
|
+
to deploy the frontend and API together to Cloudflare Workers.
|