@hiyve/cli 1.0.18 → 1.0.19

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hiyve/cli",
3
- "version": "1.0.18",
3
+ "version": "1.0.19",
4
4
  "description": "Hiyve SDK CLI - Configure npm for private @hiyve packages",
5
5
  "type": "module",
6
6
  "bin": {
@@ -14,7 +14,10 @@ const TEMPLATES = {
14
14
  basic: {
15
15
  name: 'Basic',
16
16
  description: 'Video conferencing with controls and participant list',
17
- packages: ['@hiyve/react', '@hiyve/react-ui', '@hiyve/core', '@hiyve/rtc-client', '@hiyve/utilities'],
17
+ packages: [
18
+ '@hiyve/react', '@hiyve/react-ui', '@hiyve/core', '@hiyve/rtc-client', '@hiyve/utilities',
19
+ '@hiyve/admin',
20
+ ],
18
21
  features: { intelligence: false, collaboration: false, capture: false },
19
22
  },
20
23
  ai: {
@@ -23,21 +26,65 @@ const TEMPLATES = {
23
26
  packages: [
24
27
  '@hiyve/react', '@hiyve/react-ui', '@hiyve/react-intelligence', '@hiyve/react-capture',
25
28
  '@hiyve/core', '@hiyve/cloud', '@hiyve/rtc-client', '@hiyve/utilities',
29
+ '@hiyve/admin',
26
30
  ],
27
31
  features: { intelligence: true, collaboration: false, capture: true },
28
32
  },
29
33
  full: {
30
34
  name: 'Full Suite',
31
35
  description: 'Video + AI + chat, polls, Q&A, file sharing, notes',
36
+ // react-room's REQUIRED peers are all here: react-intelligence and
37
+ // react-semantic-relay are imported statically, not optionally.
32
38
  packages: [
33
- '@hiyve/react', '@hiyve/react-ui', '@hiyve/react-intelligence', '@hiyve/react-capture',
34
- '@hiyve/react-collaboration', '@hiyve/react-notes', '@hiyve/react-room',
39
+ '@hiyve/react', '@hiyve/react-ui', '@hiyve/react-intelligence', '@hiyve/react-semantic-relay',
40
+ '@hiyve/react-capture', '@hiyve/react-collaboration', '@hiyve/react-notes', '@hiyve/react-room',
35
41
  '@hiyve/core', '@hiyve/cloud', '@hiyve/rtc-client', '@hiyve/utilities',
42
+ '@hiyve/admin',
36
43
  ],
37
44
  features: { intelligence: true, collaboration: true, capture: true },
38
45
  },
39
46
  };
40
47
 
48
+ /**
49
+ * The signaling region every generated project pins. It appears in BOTH
50
+ * `.env.example` (SERVER_REGION — what the token server mints for) and the
51
+ * client (`region` on HiyveProvider / HiyveRoom). They must agree: a token
52
+ * presented to a stack that did not sign it is rejected with a generic
53
+ * "Internal server error".
54
+ */
55
+ export const DEFAULT_REGION = 'us-east-2';
56
+
57
+ /** Port the generated token server listens on; vite proxies /api to it. */
58
+ const SERVER_PORT = 3001;
59
+
60
+ /**
61
+ * Pinned versions. Taken from what `npm create vite@latest -- --template
62
+ * react-ts` emits (2026-09-01) plus the peer ranges the published @hiyve
63
+ * packages declare: React 18 or 19, MUI ^9. Bumping MUI below 9 or React
64
+ * below 18 makes `npm install` fail with ERESOLVE against the registry.
65
+ */
66
+ export const PINS = {
67
+ react: '^19.2.8',
68
+ 'react-dom': '^19.2.8',
69
+ '@mui/material': '^9.4.0',
70
+ '@mui/icons-material': '^9.4.0',
71
+ '@emotion/react': '^11.14.0',
72
+ '@emotion/styled': '^11.14.1',
73
+ express: '^5.2.1',
74
+ cors: '^2.8.6',
75
+ dotenv: '^17.4.2',
76
+ '@types/react': '^19.2.18',
77
+ '@types/react-dom': '^19.2.4',
78
+ '@types/express': '^5.0.6',
79
+ '@types/cors': '^2.8.19',
80
+ '@types/node': '^24.13.3',
81
+ '@vitejs/plugin-react': '^6.1.0',
82
+ concurrently: '^10.0.5',
83
+ tsx: '^4.23.13',
84
+ typescript: '~6.0.2',
85
+ vite: '^8.2.2',
86
+ };
87
+
41
88
  export async function init(projectName, options) {
42
89
  console.log('');
43
90
  console.log(chalk.cyan(' Hiyve Project Scaffolding'));
@@ -102,8 +149,11 @@ export async function init(projectName, options) {
102
149
  writeFile(projectDir, 'vite.config.ts', generateViteConfig());
103
150
  writeFile(projectDir, '.env.example', generateEnvExample(template));
104
151
  writeFile(projectDir, '.gitignore', generateGitignore());
105
- writeFile(projectDir, 'src/main.tsx', generateMain());
152
+ writeFile(projectDir, 'src/main.tsx', generateMain(template));
106
153
  writeFile(projectDir, 'src/App.tsx', generateApp(template));
154
+ if (!usesPrebuiltRoom(template)) {
155
+ writeFile(projectDir, 'src/VideoRoom.tsx', generateVideoRoom());
156
+ }
107
157
  writeFile(projectDir, 'index.html', generateIndexHtml(projectName));
108
158
  writeFile(projectDir, 'server/index.ts', generateServer(template));
109
159
 
@@ -113,7 +163,7 @@ export async function init(projectName, options) {
113
163
  console.log(chalk.gray(' Next steps:'));
114
164
  console.log('');
115
165
  console.log(` ${chalk.cyan('cd')} ${projectName}`);
116
- console.log(` ${chalk.cyan('cp')} .env.example .env ${chalk.gray('# Add your API key')}`);
166
+ console.log(` ${chalk.cyan('cp')} .env.example .env ${chalk.gray('# Add your API key + client secret (console.hiyve.dev)')}`);
117
167
  console.log(` ${chalk.cyan('npm install')}`);
118
168
  console.log(` ${chalk.cyan('npm run dev')}`);
119
169
  console.log('');
@@ -136,17 +186,23 @@ function writeFile(dir, filePath, content) {
136
186
  writeFileSync(fullPath, content, 'utf-8');
137
187
  }
138
188
 
139
- function generatePackageJson(name, template) {
189
+ function usesPrebuiltRoom(template) {
190
+ return template.features.intelligence && template.features.collaboration;
191
+ }
192
+
193
+ export function generatePackageJson(name, template) {
140
194
  const deps = {};
141
195
  for (const pkg of template.packages) {
142
196
  deps[pkg] = 'latest';
143
197
  }
144
- deps['react'] = '^18.2.0';
145
- deps['react-dom'] = '^18.2.0';
146
- deps['@mui/material'] = '^5.15.0';
147
- deps['@mui/icons-material'] = '^5.15.0';
148
- deps['@emotion/react'] = '^11.11.0';
149
- deps['@emotion/styled'] = '^11.11.0';
198
+ for (const k of ['react', 'react-dom', '@mui/material', '@mui/icons-material', '@emotion/react', '@emotion/styled', 'express', 'cors', 'dotenv']) {
199
+ deps[k] = PINS[k];
200
+ }
201
+
202
+ const devDependencies = {};
203
+ for (const k of ['@types/react', '@types/react-dom', '@types/express', '@types/cors', '@types/node', '@vitejs/plugin-react', 'concurrently', 'tsx', 'typescript', 'vite']) {
204
+ devDependencies[k] = PINS[k];
205
+ }
150
206
 
151
207
  const pkg = {
152
208
  name,
@@ -154,39 +210,32 @@ function generatePackageJson(name, template) {
154
210
  private: true,
155
211
  type: 'module',
156
212
  scripts: {
157
- dev: 'concurrently "vite" "tsx watch server/index.ts"',
213
+ dev: 'concurrently "tsx watch server/index.ts" "vite"',
158
214
  build: 'vite build',
159
215
  preview: 'vite preview',
160
- 'server': 'tsx server/index.ts',
216
+ server: 'tsx server/index.ts',
161
217
  },
162
218
  dependencies: deps,
163
- devDependencies: {
164
- '@types/react': '^18.2.0',
165
- '@types/react-dom': '^18.2.0',
166
- '@vitejs/plugin-react': '^4.2.0',
167
- 'concurrently': '^8.2.0',
168
- 'dotenv': '^16.3.0',
169
- 'express': '^4.18.0',
170
- 'tsx': '^4.7.0',
171
- 'typescript': '^5.3.0',
172
- 'vite': '^5.0.0',
173
- },
219
+ devDependencies,
174
220
  };
175
221
 
176
222
  return JSON.stringify(pkg, null, 2) + '\n';
177
223
  }
178
224
 
179
- function generateTsConfig() {
225
+ export function generateTsConfig() {
226
+ // Mirrors the react-ts template `npm create vite@latest` emits, with one
227
+ // tsconfig covering both src/ (browser) and server/ (tsx).
180
228
  const config = {
181
229
  compilerOptions: {
182
- target: 'ES2020',
183
- useDefineForClassFields: true,
184
- lib: ['ES2020', 'DOM', 'DOM.Iterable'],
230
+ target: 'ES2023',
231
+ lib: ['ES2023', 'DOM', 'DOM.Iterable'],
185
232
  module: 'ESNext',
186
- skipLibCheck: true,
187
233
  moduleResolution: 'bundler',
234
+ types: ['vite/client', 'node'],
235
+ skipLibCheck: true,
188
236
  allowImportingTsExtensions: true,
189
- resolveJsonModule: true,
237
+ verbatimModuleSyntax: true,
238
+ moduleDetection: 'force',
190
239
  isolatedModules: true,
191
240
  noEmit: true,
192
241
  jsx: 'react-jsx',
@@ -195,46 +244,55 @@ function generateTsConfig() {
195
244
  noUnusedParameters: true,
196
245
  noFallthroughCasesInSwitch: true,
197
246
  },
198
- include: ['src'],
247
+ include: ['src', 'server', 'vite.config.ts'],
199
248
  };
200
249
  return JSON.stringify(config, null, 2) + '\n';
201
250
  }
202
251
 
203
- function generateViteConfig() {
252
+ export function generateViteConfig() {
204
253
  return `import { defineConfig } from 'vite';
205
254
  import react from '@vitejs/plugin-react';
206
255
 
207
256
  export default defineConfig({
208
257
  plugins: [react()],
209
258
  server: {
210
- port: 3000,
211
259
  proxy: {
212
- '/api': 'http://localhost:4000',
260
+ // The token server (server/index.ts). @hiyve/react fetches
261
+ // /api/generate-room-token and /api/generate-cloud-token from here.
262
+ '/api': 'http://localhost:${SERVER_PORT}',
213
263
  },
214
264
  },
215
265
  });
216
266
  `;
217
267
  }
218
268
 
219
- function generateEnvExample(template) {
220
- let env = `# Hiyve API Key (from cloud.hiyve.io)
221
- HIYVE_API_KEY=pk_live_your_api_key_here
269
+ export function generateEnvExample(template) {
270
+ let env = `# Both keys come from https://console.hiyve.dev (API Keys)
271
+
272
+ # API Key (required — identifies your app)
273
+ APIKEY=pk_live_your_api_key_here
274
+
275
+ # Client Secret (required for video conferencing — mints room tokens; server-side only)
276
+ CLIENT_SECRET=sk_live_your_client_secret_here
222
277
 
223
- # Room configuration
224
- HIYVE_SIGNALING_URL=https://signal.hiyve.dev
278
+ # Signaling region the token server mints for. MUST match the \`region\`
279
+ # passed to HiyveProvider / HiyveRoom in src/ (${DEFAULT_REGION} in both).
280
+ SERVER_REGION=${DEFAULT_REGION}
281
+ SERVER_REGION_URL=.rtc.muziemedia.com
282
+ ENVIRONMENT=development
225
283
  `;
226
284
 
227
285
  if (template.features.intelligence) {
228
286
  env += `
229
- # Cloud API (for AI features)
230
- HIYVE_CLOUD_URL=https://cloud.hiyve.io
287
+ # Cloud API for AI features. Defaults to https://cloud.hiyve.io — leave unset.
288
+ # HIYVE_CLOUD_API_URL=https://cloud.hiyve.io
231
289
  `;
232
290
  }
233
291
 
234
292
  return env;
235
293
  }
236
294
 
237
- function generateGitignore() {
295
+ export function generateGitignore() {
238
296
  return `node_modules
239
297
  dist
240
298
  .env
@@ -242,53 +300,70 @@ dist
242
300
  `;
243
301
  }
244
302
 
245
- function generateMain() {
246
- return `import React from 'react';
247
- import ReactDOM from 'react-dom/client';
303
+ export function generateMain(template) {
304
+ if (usesPrebuiltRoom(template)) {
305
+ // HiyveRoom (in App.tsx) provides the HiyveProvider, theme and region.
306
+ return `import ReactDOM from 'react-dom/client';
248
307
  import App from './App';
249
308
 
309
+ ReactDOM.createRoot(document.getElementById('root')!).render(<App />);
310
+ `;
311
+ }
312
+
313
+ return `import ReactDOM from 'react-dom/client';
314
+ import { HiyveProvider } from '@hiyve/react';
315
+ import App from './App';
316
+
317
+ // region MUST match SERVER_REGION in .env — see .env.example.
250
318
  ReactDOM.createRoot(document.getElementById('root')!).render(
251
- <React.StrictMode>
319
+ <HiyveProvider region="${DEFAULT_REGION}">
252
320
  <App />
253
- </React.StrictMode>,
321
+ </HiyveProvider>,
254
322
  );
255
323
  `;
256
324
  }
257
325
 
258
- function generateApp(template) {
259
- if (template.features.intelligence && template.features.collaboration) {
260
- // Full template use @hiyve/react-room PrebuiltRoom
261
- return `import React, { useState, useCallback } from 'react';
326
+ export function generateApp(template) {
327
+ if (usesPrebuiltRoom(template)) {
328
+ return `import { useState, useCallback } from 'react';
262
329
  import { HiyveRoom, PrebuiltRoom, PrebuiltLobby } from '@hiyve/react-room';
263
330
 
331
+ // Must match SERVER_REGION in .env — see .env.example.
332
+ const REGION = '${DEFAULT_REGION}';
333
+
334
+ /** Fetch a room token from server/index.ts (mountHiyveRoutes). */
335
+ async function fetchRoomToken(): Promise<string> {
336
+ const res = await fetch('/api/generate-room-token', { method: 'POST' });
337
+ const data = await res.json().catch(() => ({}));
338
+ if (!res.ok) throw new Error(data.message || 'Failed to generate room token');
339
+ return data.roomToken;
340
+ }
341
+
342
+ /** Fetch a cloud token for AI features from the same server. */
343
+ async function fetchCloudToken(): Promise<string> {
344
+ const res = await fetch('/api/generate-cloud-token', { method: 'POST' });
345
+ const data = await res.json().catch(() => ({}));
346
+ if (!res.ok) throw new Error(data.message || 'Failed to generate cloud token');
347
+ return data.cloudToken;
348
+ }
349
+
264
350
  export default function App() {
265
351
  const [roomToken, setRoomToken] = useState<string | null>(null);
266
- const [cloudToken, setCloudToken] = useState<string | null>(null);
267
352
  const [displayName, setDisplayName] = useState('');
268
353
 
269
354
  const handleJoin = useCallback(async (name: string) => {
270
355
  setDisplayName(name);
271
- const res = await fetch('/api/token', {
272
- method: 'POST',
273
- headers: { 'Content-Type': 'application/json' },
274
- body: JSON.stringify({ roomName: 'my-room', userId: name, displayName: name }),
275
- });
276
- const data = await res.json();
277
- setRoomToken(data.roomToken);
278
- if (data.cloudToken) setCloudToken(data.cloudToken);
356
+ setRoomToken(await fetchRoomToken());
279
357
  }, []);
280
358
 
281
- const handleLeave = useCallback(() => {
282
- setRoomToken(null);
283
- setCloudToken(null);
284
- }, []);
359
+ const handleLeave = useCallback(() => setRoomToken(null), []);
285
360
 
286
361
  if (!roomToken) {
287
362
  return <PrebuiltLobby onJoin={handleJoin} />;
288
363
  }
289
364
 
290
365
  return (
291
- <HiyveRoom roomToken={roomToken} cloudToken={cloudToken} intelligence>
366
+ <HiyveRoom roomToken={roomToken} region={REGION} generateToken={fetchCloudToken} userId={displayName} intelligence>
292
367
  <PrebuiltRoom userId={displayName} onLeave={handleLeave} />
293
368
  </HiyveRoom>
294
369
  );
@@ -296,67 +371,62 @@ export default function App() {
296
371
  `;
297
372
  }
298
373
 
299
- // Basic / AI template compose directly
300
- const imports = [`import { HiyveProvider, useRoom, useConnection } from '@hiyve/react';`];
301
- imports.push(`import { VideoGrid, ControlBar } from '@hiyve/react-ui';`);
302
-
303
- let providerOpen = ' <HiyveProvider generateRoomToken={generateRoomToken}>';
304
- let providerClose = ' </HiyveProvider>';
305
- let extraComponents = '';
306
-
307
- if (template.features.intelligence) {
308
- extraComponents += `\n {/* AI features available via @hiyve/react-intelligence hooks */}`;
374
+ // Basic / AI — the getting-started guide's App, verbatim. HiyveProvider
375
+ // (src/main.tsx) fetches the room token itself; JoinForm drives the flow.
376
+ const aiNote = template.features.intelligence
377
+ ? `\n// AI: add hooks from @hiyve/react-intelligence inside VideoRoom (HiyveProvider\n// already fetches the cloud token from /api/generate-cloud-token).\n`
378
+ : '';
379
+ return `import { useRoomFlow } from '@hiyve/react';
380
+ import { JoinForm, ConnectingScreen } from '@hiyve/react-ui';
381
+ import VideoRoom from './VideoRoom';
382
+ ${aiNote}
383
+ function App() {
384
+ const { screen } = useRoomFlow();
385
+
386
+ switch (screen) {
387
+ case 'connecting':
388
+ return <ConnectingScreen />;
389
+ case 'in-room':
390
+ return <VideoRoom />;
391
+ default:
392
+ return <JoinForm autoConnect devicePreviewMode="inline" />;
309
393
  }
310
-
311
- return `import React, { useState, useCallback } from 'react';
312
- ${imports.join('\n')}
313
-
314
- function Room({ onLeave }: { onLeave: () => void }) {
315
- return (
316
- <div style={{ display: 'flex', flexDirection: 'column', height: '100vh', background: '#121212' }}>
317
- <div style={{ flex: 1, overflow: 'hidden' }}>
318
- <VideoGrid localVideoElementId="local-video" />
319
- </div>${extraComponents}
320
- <ControlBar onLeave={onLeave} />
321
- </div>
322
- );
323
394
  }
324
395
 
325
- export default function App() {
326
- const [roomToken, setRoomToken] = useState<string | null>(null);
327
-
328
- const handleJoin = useCallback(async () => {
329
- const res = await fetch('/api/token', {
330
- method: 'POST',
331
- headers: { 'Content-Type': 'application/json' },
332
- body: JSON.stringify({ roomName: 'my-room', userId: 'user-' + Date.now() }),
333
- });
334
- const data = await res.json();
335
- setRoomToken(data.roomToken);
336
- }, []);
396
+ export default App;
397
+ `;
398
+ }
337
399
 
338
- const generateRoomToken = useCallback(() => Promise.resolve(roomToken!), [roomToken]);
400
+ export function generateVideoRoom() {
401
+ return `import { useState } from 'react';
402
+ import { useRoom, useConnection } from '@hiyve/react';
403
+ import { VideoGrid, ControlBar } from '@hiyve/react-ui';
339
404
 
340
- if (!roomToken) {
341
- return (
342
- <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', height: '100vh', background: '#121212' }}>
343
- <button onClick={handleJoin} style={{ padding: '12px 32px', fontSize: 18, borderRadius: 8, border: 'none', background: '#6c63ff', color: '#fff', cursor: 'pointer' }}>
344
- Join Room
345
- </button>
346
- </div>
347
- );
348
- }
405
+ function VideoRoom() {
406
+ const { room } = useRoom();
407
+ const { leaveRoom } = useConnection();
408
+ const [layout, setLayout] = useState('grid');
349
409
 
350
410
  return (
351
- ${providerOpen}
352
- <Room onLeave={() => setRoomToken(null)} />
353
- ${providerClose}
411
+ <div style={{ height: '100vh', display: 'flex', flexDirection: 'column' }}>
412
+ {room?.name && <h2 style={{ margin: '8px 16px' }}>{room.name}</h2>}
413
+ <VideoGrid localVideoElementId="local-video" layout={layout} showNames showLocalFlip />
414
+ <ControlBar
415
+ onLeave={leaveRoom}
416
+ showScreenShare
417
+ showLayoutSelector
418
+ layout={layout}
419
+ onLayoutChange={setLayout}
420
+ />
421
+ </div>
354
422
  );
355
423
  }
424
+
425
+ export default VideoRoom;
356
426
  `;
357
427
  }
358
428
 
359
- function generateIndexHtml(name) {
429
+ export function generateIndexHtml(name) {
360
430
  return `<!DOCTYPE html>
361
431
  <html lang="en">
362
432
  <head>
@@ -372,71 +442,25 @@ function generateIndexHtml(name) {
372
442
  `;
373
443
  }
374
444
 
375
- function generateServer(template) {
376
- const hasCloud = template.features.intelligence;
377
-
378
- return `import express from 'express';
379
- import 'dotenv/config';
445
+ export function generateServer() {
446
+ // The getting-started guide's server. mountHiyveRoutes exposes
447
+ // /generate-room-token, /generate-cloud-token, /create-join-token and
448
+ // /health under /api; loadHiyveConfig reads APIKEY, CLIENT_SECRET and
449
+ // SERVER_REGION from .env. Nothing here talks to signaling directly.
450
+ return `import 'dotenv/config';
451
+ import express from 'express';
452
+ import cors from 'cors';
453
+ import { mountHiyveRoutes, loadHiyveConfig } from '@hiyve/admin';
380
454
 
381
455
  const app = express();
456
+ app.use(cors({ origin: ['http://localhost:5173'] }));
382
457
  app.use(express.json());
383
458
 
384
- const API_KEY = process.env.HIYVE_API_KEY;
385
- const SIGNALING_URL = process.env.HIYVE_SIGNALING_URL || 'https://signal.hiyve.dev';
386
- ${hasCloud ? `const CLOUD_URL = process.env.HIYVE_CLOUD_URL || 'https://cloud.hiyve.io';\n` : ''}
387
- /**
388
- * Generate a room token for the client.
389
- * In production, add your own authentication before issuing tokens.
390
- */
391
- app.post('/api/token', async (req, res) => {
392
- const { roomName, userId, displayName } = req.body;
393
-
394
- try {
395
- const response = await fetch(\`\${SIGNALING_URL}/api/rooms/token\`, {
396
- method: 'POST',
397
- headers: {
398
- 'Content-Type': 'application/json',
399
- 'Authorization': \`Bearer \${API_KEY}\`,
400
- },
401
- body: JSON.stringify({ roomName, userId, displayName: displayName || userId }),
402
- });
403
-
404
- if (!response.ok) {
405
- throw new Error(\`Token request failed: \${response.status}\`);
406
- }
407
-
408
- const data = await response.json();
409
- ${hasCloud ? `
410
- // Also generate a cloud token for AI features
411
- let cloudToken: string | undefined;
412
- try {
413
- const cloudRes = await fetch(\`\${CLOUD_URL}/auth/tokens/cloud\`, {
414
- method: 'POST',
415
- headers: {
416
- 'Content-Type': 'application/json',
417
- 'x-api-key': API_KEY!,
418
- },
419
- body: JSON.stringify({ userId }),
420
- });
421
- if (cloudRes.ok) {
422
- const cloudData = await cloudRes.json();
423
- cloudToken = cloudData.token;
424
- }
425
- } catch {
426
- // Cloud token optional — continue without it
427
- }
428
-
429
- res.json({ roomToken: data.token, cloudToken });
430
- ` : ` res.json({ roomToken: data.token });
431
- `} } catch (err: any) {
432
- console.error('Token error:', err.message);
433
- res.status(500).json({ error: 'Failed to generate token' });
434
- }
435
- });
459
+ const apiRouter = express.Router();
460
+ mountHiyveRoutes(apiRouter, loadHiyveConfig());
461
+ app.use('/api', apiRouter);
436
462
 
437
- const PORT = process.env.PORT || 4000;
438
- app.listen(PORT, () => {
439
- console.log(\`Server running on http://localhost:\${PORT}\`);
440
- });
463
+ const PORT = Number(process.env.PORT) || ${SERVER_PORT};
464
+ app.listen(PORT, () => console.log(\`Server running on http://localhost:\${PORT}\`));
441
465
  `;
442
466
  }
@@ -0,0 +1,151 @@
1
+ /**
2
+ * The scaffolder must produce a project that installs and runs against the
3
+ * PUBLISHED packages and matches the getting-started guide. It drifted
4
+ * badly once (2026-09-01): MUI ^5 against a ^9 peer (ERESOLVE on install), a
5
+ * hand-rolled token route posting to a host with no DNS record, no region
6
+ * pin, and env var names the SDK does not read. These tests check the
7
+ * generated output against the real peer ranges in this workspace so the
8
+ * next drift fails here, not on a developer's first `npm install`.
9
+ */
10
+ import { describe, it, expect } from 'vitest';
11
+ import { readFileSync } from 'node:fs';
12
+ import { fileURLToPath } from 'node:url';
13
+ import { dirname, join } from 'node:path';
14
+ import {
15
+ PINS,
16
+ DEFAULT_REGION,
17
+ generatePackageJson,
18
+ generateEnvExample,
19
+ generateMain,
20
+ generateApp,
21
+ generateVideoRoom,
22
+ generateServer,
23
+ generateViteConfig,
24
+ generateTsConfig,
25
+ } from './init.js';
26
+
27
+ const here = dirname(fileURLToPath(import.meta.url));
28
+ const workspacePkg = (name) =>
29
+ JSON.parse(readFileSync(join(here, '../../../..', name, 'package.json'), 'utf8'));
30
+
31
+ const TEMPLATES = {
32
+ basic: { packages: ['@hiyve/react', '@hiyve/react-ui', '@hiyve/admin'], features: { intelligence: false, collaboration: false } },
33
+ ai: { packages: ['@hiyve/react', '@hiyve/react-ui', '@hiyve/react-intelligence', '@hiyve/admin'], features: { intelligence: true, collaboration: false } },
34
+ full: { packages: ['@hiyve/react-room', '@hiyve/react-intelligence', '@hiyve/react-semantic-relay', '@hiyve/admin'], features: { intelligence: true, collaboration: true } },
35
+ };
36
+
37
+ /** Major of a ^x.y.z / ~x.y.z pin. */
38
+ const majorOf = (range) => Number.parseInt(range.replace(/^[\^~]/, ''), 10);
39
+ /** Majors a caret-only peer range admits, e.g. "^18.0.0 || ^19.0.0" -> [18, 19]. */
40
+ const peerMajors = (peer) => peer.split('||').map((r) => majorOf(r.trim()));
41
+ /** Does a caret pin fall inside a caret-only peer range? */
42
+ const rangeFitsPeer = (pin, peer) => peerMajors(peer).includes(majorOf(pin));
43
+
44
+ describe('generated package.json', () => {
45
+ const pkg = JSON.parse(generatePackageJson('demo', TEMPLATES.basic));
46
+
47
+ it('pins MUI inside the range @hiyve/react-ui and @hiyve/react-room actually require', () => {
48
+ for (const p of ['react-ui', 'react-room']) {
49
+ const peer = workspacePkg(`packages/${p}`).peerDependencies['@mui/material'];
50
+ expect(rangeFitsPeer(pkg.dependencies['@mui/material'], peer), `${p} wants ${peer}`).toBe(true);
51
+ }
52
+ expect(pkg.dependencies['@mui/icons-material']).toBe(pkg.dependencies['@mui/material']);
53
+ });
54
+
55
+ it('pins React inside the range @hiyve/react requires', () => {
56
+ const peer = workspacePkg('packages/react').peerDependencies.react;
57
+ expect(rangeFitsPeer(pkg.dependencies.react, peer)).toBe(true);
58
+ expect(pkg.dependencies['react-dom']).toBe(pkg.dependencies.react);
59
+ });
60
+
61
+ it('never pins MUI 5 or React 18 — the drift that broke install', () => {
62
+ expect(majorOf(pkg.dependencies['@mui/material'])).toBeGreaterThanOrEqual(9);
63
+ expect(majorOf(pkg.dependencies.react)).toBeGreaterThanOrEqual(19);
64
+ });
65
+
66
+ it('includes @hiyve/admin — the token server is the SDK, not hand-rolled', () => {
67
+ expect(pkg.dependencies['@hiyve/admin']).toBeDefined();
68
+ expect(pkg.dependencies.express).toBeDefined();
69
+ expect(pkg.dependencies.cors).toBeDefined();
70
+ });
71
+
72
+ it('every dev dependency the tsconfig relies on is present', () => {
73
+ for (const k of ['@types/node', 'typescript', 'vite', '@vitejs/plugin-react', 'tsx', 'concurrently']) {
74
+ expect(pkg.devDependencies[k], k).toBe(PINS[k]);
75
+ }
76
+ });
77
+ });
78
+
79
+ describe('generated server', () => {
80
+ const server = generateServer();
81
+
82
+ it('mounts the SDK routes instead of posting to a signaling host by hand', () => {
83
+ expect(server).toContain("from '@hiyve/admin'");
84
+ expect(server).toContain('mountHiyveRoutes(apiRouter, loadHiyveConfig())');
85
+ expect(server).toContain("app.use('/api', apiRouter)");
86
+ expect(server).not.toContain('signal.hiyve.dev');
87
+ expect(server).not.toContain('/api/rooms/token');
88
+ expect(server).not.toContain('HIYVE_API_KEY');
89
+ });
90
+
91
+ it('listens where vite proxies /api to', () => {
92
+ const port = server.match(/\|\| (\d+);/)[1];
93
+ expect(generateViteConfig()).toContain(`http://localhost:${port}`);
94
+ });
95
+ });
96
+
97
+ describe('generated .env.example', () => {
98
+ it('uses the variable names loadHiyveConfig reads, and the region the client pins', () => {
99
+ const env = generateEnvExample(TEMPLATES.basic);
100
+ expect(env).toMatch(/^APIKEY=pk_/m);
101
+ expect(env).toMatch(/^CLIENT_SECRET=sk_/m);
102
+ expect(env).toContain(`SERVER_REGION=${DEFAULT_REGION}`);
103
+ expect(env).not.toContain('HIYVE_API_KEY');
104
+ expect(env).not.toContain('signal.hiyve.dev');
105
+ expect(env).toContain('console.hiyve.dev');
106
+ });
107
+ });
108
+
109
+ describe('region pin', () => {
110
+ it('basic/ai: HiyveProvider in main.tsx carries the region', () => {
111
+ expect(generateMain(TEMPLATES.basic)).toContain(`<HiyveProvider region="${DEFAULT_REGION}">`);
112
+ expect(generateMain(TEMPLATES.ai)).toContain(`<HiyveProvider region="${DEFAULT_REGION}">`);
113
+ });
114
+
115
+ it('full: HiyveRoom carries the region and main.tsx does not double-wrap', () => {
116
+ expect(generateApp(TEMPLATES.full)).toContain(`region={REGION}`);
117
+ expect(generateApp(TEMPLATES.full)).toContain(`const REGION = '${DEFAULT_REGION}'`);
118
+ expect(generateMain(TEMPLATES.full)).not.toContain('HiyveProvider');
119
+ });
120
+
121
+ it('client and server agree on the region', () => {
122
+ const env = generateEnvExample(TEMPLATES.full);
123
+ const serverRegion = env.match(/^SERVER_REGION=(\S+)/m)[1];
124
+ expect(serverRegion).toBe(DEFAULT_REGION);
125
+ });
126
+ });
127
+
128
+ describe('generated client', () => {
129
+ it('basic/ai is the guide verbatim: useRoomFlow + JoinForm, SDK fetches the token', () => {
130
+ const app = generateApp(TEMPLATES.basic);
131
+ expect(app).toContain("import { useRoomFlow } from '@hiyve/react'");
132
+ expect(app).toContain('<JoinForm autoConnect devicePreviewMode="inline" />');
133
+ expect(app).not.toContain('fetch(');
134
+ expect(generateVideoRoom()).toContain('<VideoGrid localVideoElementId="local-video"');
135
+ });
136
+
137
+ it('full fetches tokens from the SDK routes and reads the fields they return', () => {
138
+ const app = generateApp(TEMPLATES.full);
139
+ expect(app).toContain("fetch('/api/generate-room-token'");
140
+ expect(app).toContain('return data.roomToken');
141
+ expect(app).toContain("fetch('/api/generate-cloud-token'");
142
+ expect(app).toContain('return data.cloudToken');
143
+ expect(app).not.toContain('/api/token');
144
+ });
145
+
146
+ it('tsconfig covers both the browser and the server sources', () => {
147
+ const ts = JSON.parse(generateTsConfig());
148
+ expect(ts.include).toEqual(expect.arrayContaining(['src', 'server']));
149
+ expect(ts.compilerOptions.types).toEqual(expect.arrayContaining(['vite/client', 'node']));
150
+ });
151
+ });