@hiyve/cli 1.0.5 → 1.0.7

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 CHANGED
@@ -13,8 +13,7 @@ After login, you can install Hiyve packages normally:
13
13
 
14
14
  ```bash
15
15
  npm install @hiyve/rtc-client
16
- npm install @hiyve/auth
17
- npm install @hiyve/auth-react
16
+ npm install @hiyve/react @hiyve/react-ui
18
17
  ```
19
18
 
20
19
  ## Commands
@@ -28,7 +27,7 @@ Authenticate with Hiyve and configure npm for @hiyve packages.
28
27
  npx hiyve-cli login
29
28
 
30
29
  # With API key as argument
31
- npx hiyve-cli login --key mk_your_api_key_here
30
+ npx hiyve-cli login --key sk_live_your_secret_key_here
32
31
  ```
33
32
 
34
33
  ### `hiyve logout`
@@ -51,7 +50,7 @@ npx hiyve-cli whoami
51
50
 
52
51
  1. Log in to the [Hiyve SDK Admin Portal](https://console.hiyve.dev)
53
52
  2. Navigate to **API Keys** in the sidebar
54
- 3. Copy your API key (starts with `mk_`)
53
+ 3. Copy your secret key (starts with `sk_test_` or `sk_live_`)
55
54
 
56
55
  ## What Does Login Do?
57
56
 
@@ -59,7 +58,7 @@ The `login` command:
59
58
 
60
59
  1. Validates your API key with the Hiyve registry
61
60
  2. Adds two lines to your `~/.npmrc` file:
62
- - `@hiyve:registry=https://console.hiyve.dev/api/registry/`
61
+ - `@hiyve:registry=https://api.hiyve.dev/registry/`
63
62
  - `//:_authToken=your_api_key`
64
63
 
65
64
  This tells npm to fetch `@hiyve/*` packages from the private Hiyve registry instead of the public npm registry.
@@ -68,8 +67,7 @@ This tells npm to fetch `@hiyve/*` packages from the private Hiyve registry inst
68
67
 
69
68
  ### "Invalid API key" error
70
69
 
71
- - Make sure your API key starts with `mk_`
72
- - Check that your API key is 35 characters long
70
+ - Make sure your key starts with `sk_test_` or `sk_live_`
73
71
  - Verify your account is active in the admin portal
74
72
 
75
73
  ### "Connection failed" error
@@ -94,7 +92,7 @@ cat ~/.npmrc | grep hiyve
94
92
  ## Security
95
93
 
96
94
  - Your API key is stored in `~/.npmrc` (standard npm token storage)
97
- - The API key is sent only to `console.hiyve.dev`
95
+ - The API key is sent only to `api.hiyve.dev`
98
96
  - Run `hiyve logout` to remove your credentials
99
97
 
100
98
  ## Support
package/bin/hiyve.js CHANGED
@@ -11,11 +11,19 @@ import { login } from '../src/commands/login.js';
11
11
  import { logout } from '../src/commands/logout.js';
12
12
  import { whoami } from '../src/commands/whoami.js';
13
13
  import { list } from '../src/commands/list.js';
14
+ import { init } from '../src/commands/init.js';
15
+ import { setDevMode } from '../src/config.js';
14
16
 
15
17
  program
16
18
  .name('hiyve')
17
19
  .description('Hiyve SDK CLI - Configure npm for private @hiyve packages')
18
- .version('1.0.0');
20
+ .version('1.0.0')
21
+ .option('--dev', 'Use dev registry (api.muziemedia.com)')
22
+ .hook('preAction', () => {
23
+ if (program.opts().dev) {
24
+ setDevMode(true);
25
+ }
26
+ });
19
27
 
20
28
  program
21
29
  .command('login')
@@ -39,6 +47,12 @@ program
39
47
  .description('List all available @hiyve packages')
40
48
  .action(list);
41
49
 
50
+ program
51
+ .command('init [project-name]')
52
+ .description('Scaffold a new Hiyve project')
53
+ .option('-t, --template <name>', 'Template: basic, ai, or full')
54
+ .action(init);
55
+
42
56
  // Show help if no command provided
43
57
  program.parse();
44
58
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hiyve/cli",
3
- "version": "1.0.5",
3
+ "version": "1.0.7",
4
4
  "description": "Hiyve SDK CLI - Configure npm for private @hiyve packages",
5
5
  "type": "module",
6
6
  "bin": {
@@ -12,7 +12,8 @@
12
12
  "src"
13
13
  ],
14
14
  "scripts": {
15
- "test": "echo \"No tests yet\"",
15
+ "test": "vitest run",
16
+ "test:watch": "vitest",
16
17
  "deploy": "./deploy.sh"
17
18
  },
18
19
  "keywords": [
@@ -37,9 +38,12 @@
37
38
  "access": "public"
38
39
  },
39
40
  "dependencies": {
40
- "chalk": "^5.3.0",
41
+ "chalk": "^5.6.2",
41
42
  "commander": "^12.1.0",
42
- "ora": "^8.1.0",
43
+ "ora": "^8.2.0",
43
44
  "prompts": "^2.4.2"
45
+ },
46
+ "devDependencies": {
47
+ "vitest": "^2.1.9"
44
48
  }
45
- }
49
+ }
@@ -0,0 +1,442 @@
1
+ /**
2
+ * Hiyve CLI - init command
3
+ *
4
+ * Scaffold a new Hiyve project from a template.
5
+ */
6
+
7
+ import { writeFileSync, mkdirSync, existsSync } from 'node:fs';
8
+ import { join, resolve } from 'node:path';
9
+ import chalk from 'chalk';
10
+ import ora from 'ora';
11
+ import prompts from 'prompts';
12
+
13
+ const TEMPLATES = {
14
+ basic: {
15
+ name: 'Basic',
16
+ description: 'Video conferencing with controls and participant list',
17
+ packages: ['@hiyve/react', '@hiyve/react-ui', '@hiyve/core', '@hiyve/rtc-client', '@hiyve/utilities'],
18
+ features: { intelligence: false, collaboration: false, capture: false },
19
+ },
20
+ ai: {
21
+ name: 'AI-Powered',
22
+ description: 'Video + search, transcription, meeting intelligence',
23
+ packages: [
24
+ '@hiyve/react', '@hiyve/react-ui', '@hiyve/react-intelligence', '@hiyve/react-capture',
25
+ '@hiyve/core', '@hiyve/cloud', '@hiyve/rtc-client', '@hiyve/utilities',
26
+ ],
27
+ features: { intelligence: true, collaboration: false, capture: true },
28
+ },
29
+ full: {
30
+ name: 'Full Suite',
31
+ description: 'Video + AI + chat, polls, Q&A, file sharing, notes',
32
+ packages: [
33
+ '@hiyve/react', '@hiyve/react-ui', '@hiyve/react-intelligence', '@hiyve/react-capture',
34
+ '@hiyve/react-collaboration', '@hiyve/react-notes', '@hiyve/react-room',
35
+ '@hiyve/core', '@hiyve/cloud', '@hiyve/rtc-client', '@hiyve/utilities',
36
+ ],
37
+ features: { intelligence: true, collaboration: true, capture: true },
38
+ },
39
+ };
40
+
41
+ export async function init(projectName, options) {
42
+ console.log('');
43
+ console.log(chalk.cyan(' Hiyve Project Scaffolding'));
44
+ console.log(chalk.gray(' ─'.repeat(20)));
45
+ console.log('');
46
+
47
+ // Resolve project name
48
+ if (!projectName) {
49
+ const response = await prompts({
50
+ type: 'text',
51
+ name: 'name',
52
+ message: 'Project name:',
53
+ initial: 'my-hiyve-app',
54
+ validate: (val) => (val.trim() ? true : 'Project name is required'),
55
+ });
56
+ if (!response.name) {
57
+ console.log(chalk.yellow(' Cancelled.'));
58
+ process.exit(0);
59
+ }
60
+ projectName = response.name.trim();
61
+ }
62
+
63
+ // Resolve template
64
+ let templateKey = options.template;
65
+ if (!templateKey || !TEMPLATES[templateKey]) {
66
+ const response = await prompts({
67
+ type: 'select',
68
+ name: 'template',
69
+ message: 'Choose a template:',
70
+ choices: Object.entries(TEMPLATES).map(([key, t]) => ({
71
+ title: `${t.name} ${chalk.gray(`— ${t.description}`)}`,
72
+ value: key,
73
+ })),
74
+ initial: 0,
75
+ });
76
+ if (!response.template) {
77
+ console.log(chalk.yellow(' Cancelled.'));
78
+ process.exit(0);
79
+ }
80
+ templateKey = response.template;
81
+ }
82
+
83
+ const template = TEMPLATES[templateKey];
84
+ const projectDir = resolve(process.cwd(), projectName);
85
+
86
+ if (existsSync(projectDir)) {
87
+ console.log(chalk.red(` Directory "${projectName}" already exists.`));
88
+ process.exit(1);
89
+ }
90
+
91
+ const spinner = ora(`Creating ${chalk.cyan(projectName)} with ${template.name} template...`).start();
92
+
93
+ try {
94
+ // Create directory structure
95
+ mkdirSync(projectDir, { recursive: true });
96
+ mkdirSync(join(projectDir, 'src'), { recursive: true });
97
+ mkdirSync(join(projectDir, 'server'), { recursive: true });
98
+
99
+ // Generate files
100
+ writeFile(projectDir, 'package.json', generatePackageJson(projectName, template));
101
+ writeFile(projectDir, 'tsconfig.json', generateTsConfig());
102
+ writeFile(projectDir, 'vite.config.ts', generateViteConfig());
103
+ writeFile(projectDir, '.env.example', generateEnvExample(template));
104
+ writeFile(projectDir, '.gitignore', generateGitignore());
105
+ writeFile(projectDir, 'src/main.tsx', generateMain());
106
+ writeFile(projectDir, 'src/App.tsx', generateApp(template));
107
+ writeFile(projectDir, 'index.html', generateIndexHtml(projectName));
108
+ writeFile(projectDir, 'server/index.ts', generateServer(template));
109
+
110
+ spinner.succeed(`Created ${chalk.cyan(projectName)}`);
111
+
112
+ console.log('');
113
+ console.log(chalk.gray(' Next steps:'));
114
+ console.log('');
115
+ console.log(` ${chalk.cyan('cd')} ${projectName}`);
116
+ console.log(` ${chalk.cyan('cp')} .env.example .env ${chalk.gray('# Add your API key')}`);
117
+ console.log(` ${chalk.cyan('npm install')}`);
118
+ console.log(` ${chalk.cyan('npm run dev')}`);
119
+ console.log('');
120
+ console.log(chalk.gray(` Template: ${template.name}`));
121
+ console.log(chalk.gray(` Packages: ${template.packages.length}`));
122
+ console.log('');
123
+ } catch (err) {
124
+ spinner.fail('Failed to create project');
125
+ console.error(chalk.red(` ${err.message}`));
126
+ process.exit(1);
127
+ }
128
+ }
129
+
130
+ // ─── File Generators ──────────────────────────────────────
131
+
132
+ function writeFile(dir, filePath, content) {
133
+ const fullPath = join(dir, filePath);
134
+ const parentDir = join(fullPath, '..');
135
+ mkdirSync(parentDir, { recursive: true });
136
+ writeFileSync(fullPath, content, 'utf-8');
137
+ }
138
+
139
+ function generatePackageJson(name, template) {
140
+ const deps = {};
141
+ for (const pkg of template.packages) {
142
+ deps[pkg] = 'latest';
143
+ }
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';
150
+
151
+ const pkg = {
152
+ name,
153
+ version: '0.1.0',
154
+ private: true,
155
+ type: 'module',
156
+ scripts: {
157
+ dev: 'concurrently "vite" "tsx watch server/index.ts"',
158
+ build: 'vite build',
159
+ preview: 'vite preview',
160
+ 'server': 'tsx server/index.ts',
161
+ },
162
+ 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
+ },
174
+ };
175
+
176
+ return JSON.stringify(pkg, null, 2) + '\n';
177
+ }
178
+
179
+ function generateTsConfig() {
180
+ const config = {
181
+ compilerOptions: {
182
+ target: 'ES2020',
183
+ useDefineForClassFields: true,
184
+ lib: ['ES2020', 'DOM', 'DOM.Iterable'],
185
+ module: 'ESNext',
186
+ skipLibCheck: true,
187
+ moduleResolution: 'bundler',
188
+ allowImportingTsExtensions: true,
189
+ resolveJsonModule: true,
190
+ isolatedModules: true,
191
+ noEmit: true,
192
+ jsx: 'react-jsx',
193
+ strict: true,
194
+ noUnusedLocals: true,
195
+ noUnusedParameters: true,
196
+ noFallthroughCasesInSwitch: true,
197
+ },
198
+ include: ['src'],
199
+ };
200
+ return JSON.stringify(config, null, 2) + '\n';
201
+ }
202
+
203
+ function generateViteConfig() {
204
+ return `import { defineConfig } from 'vite';
205
+ import react from '@vitejs/plugin-react';
206
+
207
+ export default defineConfig({
208
+ plugins: [react()],
209
+ server: {
210
+ port: 3000,
211
+ proxy: {
212
+ '/api': 'http://localhost:4000',
213
+ },
214
+ },
215
+ });
216
+ `;
217
+ }
218
+
219
+ function generateEnvExample(template) {
220
+ let env = `# Hiyve API Key (from console.hiyve.dev)
221
+ HIYVE_API_KEY=sk_live_your_secret_key_here
222
+
223
+ # Room configuration
224
+ HIYVE_SIGNALING_URL=https://signal.hiyve.dev
225
+ `;
226
+
227
+ if (template.features.intelligence) {
228
+ env += `
229
+ # Cloud API (for AI features)
230
+ HIYVE_CLOUD_URL=https://api.hiyve.dev
231
+ `;
232
+ }
233
+
234
+ return env;
235
+ }
236
+
237
+ function generateGitignore() {
238
+ return `node_modules
239
+ dist
240
+ .env
241
+ *.local
242
+ `;
243
+ }
244
+
245
+ function generateMain() {
246
+ return `import React from 'react';
247
+ import ReactDOM from 'react-dom/client';
248
+ import App from './App';
249
+
250
+ ReactDOM.createRoot(document.getElementById('root')!).render(
251
+ <React.StrictMode>
252
+ <App />
253
+ </React.StrictMode>,
254
+ );
255
+ `;
256
+ }
257
+
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';
262
+ import { HiyveRoom, PrebuiltRoom, PrebuiltLobby } from '@hiyve/react-room';
263
+
264
+ export default function App() {
265
+ const [roomToken, setRoomToken] = useState<string | null>(null);
266
+ const [cloudToken, setCloudToken] = useState<string | null>(null);
267
+ const [displayName, setDisplayName] = useState('');
268
+
269
+ const handleJoin = useCallback(async (name: string) => {
270
+ 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);
279
+ }, []);
280
+
281
+ const handleLeave = useCallback(() => {
282
+ setRoomToken(null);
283
+ setCloudToken(null);
284
+ }, []);
285
+
286
+ if (!roomToken) {
287
+ return <PrebuiltLobby onJoin={handleJoin} />;
288
+ }
289
+
290
+ return (
291
+ <HiyveRoom roomToken={roomToken} cloudToken={cloudToken} intelligence>
292
+ <PrebuiltRoom userId={displayName} onLeave={handleLeave} />
293
+ </HiyveRoom>
294
+ );
295
+ }
296
+ `;
297
+ }
298
+
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 */}`;
309
+ }
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
+ }
324
+
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
+ }, []);
337
+
338
+ const generateRoomToken = useCallback(() => Promise.resolve(roomToken!), [roomToken]);
339
+
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
+ }
349
+
350
+ return (
351
+ ${providerOpen}
352
+ <Room onLeave={() => setRoomToken(null)} />
353
+ ${providerClose}
354
+ );
355
+ }
356
+ `;
357
+ }
358
+
359
+ function generateIndexHtml(name) {
360
+ return `<!DOCTYPE html>
361
+ <html lang="en">
362
+ <head>
363
+ <meta charset="UTF-8" />
364
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
365
+ <title>${name}</title>
366
+ </head>
367
+ <body>
368
+ <div id="root"></div>
369
+ <script type="module" src="/src/main.tsx"></script>
370
+ </body>
371
+ </html>
372
+ `;
373
+ }
374
+
375
+ function generateServer(template) {
376
+ const hasCloud = template.features.intelligence;
377
+
378
+ return `import express from 'express';
379
+ import 'dotenv/config';
380
+
381
+ const app = express();
382
+ app.use(express.json());
383
+
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://api.hiyve.dev';\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
+ });
436
+
437
+ const PORT = process.env.PORT || 4000;
438
+ app.listen(PORT, () => {
439
+ console.log(\`Server running on http://localhost:\${PORT}\`);
440
+ });
441
+ `;
442
+ }
@@ -6,7 +6,7 @@
6
6
 
7
7
  import chalk from 'chalk';
8
8
  import ora from 'ora';
9
- import { REGISTRY_URL } from '../config.js';
9
+ import { getRegistryUrl } from '../config.js';
10
10
  import { getCurrentConfig } from '../utils/npmrc.js';
11
11
 
12
12
  /**
@@ -30,7 +30,7 @@ export async function list() {
30
30
  const spinner = ora('Fetching packages...').start();
31
31
 
32
32
  try {
33
- const response = await fetch(`${REGISTRY_URL}packages`, {
33
+ const response = await fetch(`${getRegistryUrl()}packages`, {
34
34
  headers: {
35
35
  Authorization: `Bearer ${config.apiKey}`,
36
36
  },
@@ -8,7 +8,7 @@ import prompts from 'prompts';
8
8
  import chalk from 'chalk';
9
9
  import ora from 'ora';
10
10
  import { configureNpmrc } from '../utils/npmrc.js';
11
- import { REGISTRY_URL } from '../config.js';
11
+ import { getRegistryUrl } from '../config.js';
12
12
 
13
13
  /**
14
14
  * Login to Hiyve and configure npm
@@ -31,7 +31,9 @@ export async function login(options) {
31
31
  message: 'Enter your Hiyve API key:',
32
32
  validate: (value) => {
33
33
  if (!value) return 'API key is required';
34
- if (!value.startsWith('mk_')) return 'API key should start with mk_';
34
+ if (!value.startsWith('sk_') && !value.startsWith('mk_')) {
35
+ return 'API key should start with sk_test_, sk_live_, or mk_';
36
+ }
35
37
  if (value.length < 35) return 'API key appears to be too short';
36
38
  return true;
37
39
  },
@@ -46,11 +48,14 @@ export async function login(options) {
46
48
  apiKey = response.apiKey;
47
49
  }
48
50
 
49
- // Validate API key format
50
- if (!apiKey.match(/^mk_[a-fA-F0-9]{32}$/)) {
51
+ // Validate API key format (sk_test_*, sk_live_*, or legacy mk_*)
52
+ const isSecretKey = apiKey.match(/^sk_(test|live)_[a-fA-F0-9]+$/);
53
+ const isManagementKey = apiKey.match(/^mk_[a-fA-F0-9]{32}$/);
54
+
55
+ if (!isSecretKey && !isManagementKey) {
51
56
  console.log('');
52
57
  console.log(chalk.red('✗ Invalid API key format'));
53
- console.log(chalk.gray(' API key should be 35 characters: mk_ followed by 32 hex characters'));
58
+ console.log(chalk.gray(' Expected: sk_test_* or sk_live_* (from console.hiyve.dev)'));
54
59
  process.exit(1);
55
60
  }
56
61
 
@@ -58,7 +63,7 @@ export async function login(options) {
58
63
  const spinner = ora('Verifying API key...').start();
59
64
 
60
65
  try {
61
- const response = await fetch(`${REGISTRY_URL}verify`, {
66
+ const response = await fetch(`${getRegistryUrl()}verify`, {
62
67
  headers: {
63
68
  Authorization: `Bearer ${apiKey}`,
64
69
  },
@@ -90,7 +95,7 @@ export async function login(options) {
90
95
  const spinner2 = ora('Configuring npm...').start();
91
96
 
92
97
  try {
93
- await configureNpmrc(REGISTRY_URL, apiKey);
98
+ await configureNpmrc(getRegistryUrl(), apiKey);
94
99
  spinner2.succeed('npm configured for @hiyve packages');
95
100
  } catch (err) {
96
101
  spinner2.fail('Failed to configure npm');
@@ -106,7 +111,7 @@ export async function login(options) {
106
111
 
107
112
  // Fetch available packages from registry
108
113
  try {
109
- const packagesResponse = await fetch(`${REGISTRY_URL}packages`, {
114
+ const packagesResponse = await fetch(`${getRegistryUrl()}packages`, {
110
115
  headers: { Authorization: `Bearer ${apiKey}` },
111
116
  });
112
117
 
@@ -135,14 +140,14 @@ export async function login(options) {
135
140
  // Fallback if packages endpoint fails
136
141
  console.log('You can now install Hiyve packages:');
137
142
  console.log(chalk.cyan(' npm install @hiyve/rtc-client'));
138
- console.log(chalk.cyan(' npm install @hiyve/client-provider'));
143
+ console.log(chalk.cyan(' npm install @hiyve/react'));
139
144
  console.log('');
140
145
  }
141
146
  } catch {
142
147
  // Fallback on network error
143
148
  console.log('You can now install Hiyve packages:');
144
149
  console.log(chalk.cyan(' npm install @hiyve/rtc-client'));
145
- console.log(chalk.cyan(' npm install @hiyve/client-provider'));
150
+ console.log(chalk.cyan(' npm install @hiyve/react'));
146
151
  console.log('');
147
152
  }
148
153
  }
@@ -7,7 +7,7 @@
7
7
  import chalk from 'chalk';
8
8
  import ora from 'ora';
9
9
  import { getCurrentConfig } from '../utils/npmrc.js';
10
- import { REGISTRY_URL } from '../config.js';
10
+ import { getRegistryUrl } from '../config.js';
11
11
 
12
12
  /**
13
13
  * Show current authentication status
@@ -39,7 +39,7 @@ export async function whoami() {
39
39
  const spinner = ora('Verifying with server...').start();
40
40
 
41
41
  try {
42
- const response = await fetch(`${REGISTRY_URL}verify`, {
42
+ const response = await fetch(`${getRegistryUrl()}verify`, {
43
43
  headers: {
44
44
  Authorization: `Bearer ${config.apiKey}`,
45
45
  },
package/src/config.js CHANGED
@@ -2,9 +2,30 @@
2
2
  * Hiyve CLI Configuration
3
3
  */
4
4
 
5
- // Registry URL - the private npm registry endpoint
6
- export const REGISTRY_URL = 'https://console.hiyve.dev/api/registry/';
5
+ const PROD_REGISTRY_URL = 'https://api.hiyve.dev/registry/';
6
+ const DEV_REGISTRY_URL = 'https://api.muziemedia.com/registry/';
7
+
8
+ let _devMode = false;
9
+
10
+ /**
11
+ * Enable dev mode (use api.muziemedia.com instead of api.hiyve.dev)
12
+ */
13
+ export function setDevMode(enabled) {
14
+ _devMode = enabled;
15
+ }
16
+
17
+ /**
18
+ * Get the current registry URL based on dev mode
19
+ */
20
+ export function getRegistryUrl() {
21
+ return _devMode ? DEV_REGISTRY_URL : PROD_REGISTRY_URL;
22
+ }
23
+
24
+ // Backwards-compatible named export (reads live value)
25
+ export const REGISTRY_URL = PROD_REGISTRY_URL;
7
26
 
8
27
  export default {
9
28
  REGISTRY_URL,
29
+ getRegistryUrl,
30
+ setDevMode,
10
31
  };
@@ -0,0 +1,25 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import config, { REGISTRY_URL, getRegistryUrl, setDevMode } from './config.js';
3
+
4
+ describe('config', () => {
5
+ it('exports REGISTRY_URL as a named export', () => {
6
+ expect(REGISTRY_URL).toBe('https://api.hiyve.dev/registry/');
7
+ });
8
+
9
+ it('default export contains REGISTRY_URL', () => {
10
+ expect(config).toHaveProperty('REGISTRY_URL');
11
+ expect(config.REGISTRY_URL).toBe('https://api.hiyve.dev/registry/');
12
+ });
13
+
14
+ it('getRegistryUrl returns prod URL by default', () => {
15
+ setDevMode(false);
16
+ expect(getRegistryUrl()).toBe('https://api.hiyve.dev/registry/');
17
+ });
18
+
19
+ it('getRegistryUrl returns dev URL when dev mode is enabled', () => {
20
+ setDevMode(true);
21
+ expect(getRegistryUrl()).toBe('https://api.muziemedia.com/registry/');
22
+ // Reset
23
+ setDevMode(false);
24
+ });
25
+ });
package/src/index.js CHANGED
@@ -7,5 +7,6 @@
7
7
  export { login } from './commands/login.js';
8
8
  export { logout } from './commands/logout.js';
9
9
  export { whoami } from './commands/whoami.js';
10
+ export { init } from './commands/init.js';
10
11
  export { configureNpmrc, removeNpmrc, getCurrentConfig } from './utils/npmrc.js';
11
- export { REGISTRY_URL, SCOPED_PACKAGES } from './config.js';
12
+ export { REGISTRY_URL, getRegistryUrl, setDevMode } from './config.js';
@@ -12,7 +12,7 @@ const NPMRC_PATH = path.join(os.homedir(), '.npmrc');
12
12
 
13
13
  /**
14
14
  * Configure ~/.npmrc for Hiyve registry
15
- * @param {string} registryUrl - The registry URL (e.g., https://console.hiyve.dev/api/registry/)
15
+ * @param {string} registryUrl - The registry URL (e.g., https://api.hiyve.dev/registry/)
16
16
  * @param {string} apiKey - The API key for authentication
17
17
  */
18
18
  export async function configureNpmrc(registryUrl, apiKey) {
@@ -23,23 +23,26 @@ export async function configureNpmrc(registryUrl, apiKey) {
23
23
  content = fs.readFileSync(NPMRC_PATH, 'utf8');
24
24
  }
25
25
 
26
- // Remove any existing @hiyve config lines
26
+ // Remove any existing @hiyve config lines (both old and new registry URLs)
27
27
  const lines = content.split('\n').filter((line) => {
28
28
  const trimmed = line.trim();
29
29
  return (
30
30
  !trimmed.startsWith('@hiyve:registry') &&
31
31
  !trimmed.includes('//console.hiyve.dev/') &&
32
- !trimmed.includes(':_authToken=mk_')
32
+ !trimmed.includes('//api.hiyve.dev/') &&
33
+ !trimmed.includes('//api.muziemedia.com/') &&
34
+ !trimmed.includes(':_authToken=mk_') &&
35
+ !trimmed.includes(':_authToken=sk_')
33
36
  );
34
37
  });
35
38
 
36
- // Parse the registry URL to extract host
39
+ // Parse the registry URL to derive the auth token path
37
40
  const url = new URL(registryUrl);
38
- const registryHost = url.host;
41
+ const authPath = `//${url.host}${url.pathname}`;
39
42
 
40
43
  // Add new config lines
41
44
  lines.push(`@hiyve:registry=${registryUrl}`);
42
- lines.push(`//${registryHost}/api/registry/:_authToken=${apiKey}`);
45
+ lines.push(`${authPath}:_authToken=${apiKey}`);
43
46
 
44
47
  // Write back, removing empty lines at start/end
45
48
  const finalContent = lines.filter((line) => line.trim()).join('\n') + '\n';
@@ -56,13 +59,16 @@ export async function removeNpmrc() {
56
59
 
57
60
  const content = fs.readFileSync(NPMRC_PATH, 'utf8');
58
61
 
59
- // Remove @hiyve config lines
62
+ // Remove @hiyve config lines (both old and new registry URLs)
60
63
  const lines = content.split('\n').filter((line) => {
61
64
  const trimmed = line.trim();
62
65
  return (
63
66
  !trimmed.startsWith('@hiyve:registry') &&
64
67
  !trimmed.includes('//console.hiyve.dev/') &&
65
- !trimmed.includes(':_authToken=mk_')
68
+ !trimmed.includes('//api.hiyve.dev/') &&
69
+ !trimmed.includes('//api.muziemedia.com/') &&
70
+ !trimmed.includes(':_authToken=mk_') &&
71
+ !trimmed.includes(':_authToken=sk_')
66
72
  );
67
73
  });
68
74
 
@@ -86,22 +92,22 @@ export function getCurrentConfig() {
86
92
  // Find registry line
87
93
  const registryLine = lines.find((line) => line.trim().startsWith('@hiyve:registry'));
88
94
 
89
- // Find token line
95
+ // Find token line (sk_* or legacy mk_*)
90
96
  const tokenLine = lines.find((line) => {
91
97
  const trimmed = line.trim();
92
- return trimmed.includes(':_authToken=mk_');
98
+ return trimmed.includes(':_authToken=sk_') || trimmed.includes(':_authToken=mk_');
93
99
  });
94
100
 
95
101
  if (!registryLine || !tokenLine) {
96
102
  return null;
97
103
  }
98
104
 
99
- // Extract API key
100
- const tokenMatch = tokenLine.match(/:_authToken=(mk_[a-fA-F0-9]+)/);
105
+ // Extract API key (sk_* or legacy mk_*)
106
+ const tokenMatch = tokenLine.match(/:_authToken=((?:sk_|mk_)[a-zA-Z0-9_]+)/);
101
107
  const apiKey = tokenMatch ? tokenMatch[1] : null;
102
108
 
103
109
  // Mask API key for display
104
- const maskedApiKey = apiKey ? `${apiKey.slice(0, 6)}...${apiKey.slice(-4)}` : null;
110
+ const maskedApiKey = apiKey ? `${apiKey.slice(0, 8)}...${apiKey.slice(-4)}` : null;
105
111
 
106
112
  return {
107
113
  apiKey,
@@ -0,0 +1,331 @@
1
+ import { vi, describe, it, expect, beforeEach } from 'vitest';
2
+ import fs from 'fs';
3
+ import os from 'os';
4
+
5
+ vi.mock('fs');
6
+ vi.mock('os');
7
+
8
+ // os.homedir() is called at module scope to build NPMRC_PATH,
9
+ // so we must set the return value before the module loads.
10
+ os.homedir.mockReturnValue('/mock-home');
11
+
12
+ const { configureNpmrc, removeNpmrc, getCurrentConfig } = await import('./npmrc.js');
13
+
14
+ const NPMRC_PATH = '/mock-home/.npmrc';
15
+
16
+ describe('configureNpmrc', () => {
17
+ beforeEach(() => {
18
+ vi.clearAllMocks();
19
+ os.homedir.mockReturnValue('/mock-home');
20
+ });
21
+
22
+ it('creates .npmrc when it does not exist', async () => {
23
+ fs.existsSync.mockReturnValue(false);
24
+ fs.writeFileSync.mockImplementation(() => {});
25
+
26
+ await configureNpmrc('https://api.hiyve.dev/registry/', 'sk_live_abc123def456');
27
+
28
+ expect(fs.writeFileSync).toHaveBeenCalledOnce();
29
+ const [writePath, content] = fs.writeFileSync.mock.calls[0];
30
+ expect(writePath).toBe(NPMRC_PATH);
31
+ expect(content).toContain('@hiyve:registry=https://api.hiyve.dev/registry/');
32
+ expect(content).toContain(':_authToken=sk_live_abc123def456');
33
+ });
34
+
35
+ it('replaces existing @hiyve lines when .npmrc already has them', async () => {
36
+ const existingContent = [
37
+ '@hiyve:registry=https://old-registry.example.com/',
38
+ '//console.hiyve.dev/api/registry/:_authToken=sk_live_oldkey1234',
39
+ 'other-config=value',
40
+ ].join('\n');
41
+
42
+ fs.existsSync.mockReturnValue(true);
43
+ fs.readFileSync.mockReturnValue(existingContent);
44
+ fs.writeFileSync.mockImplementation(() => {});
45
+
46
+ await configureNpmrc('https://api.hiyve.dev/registry/', 'sk_live_newkey5678');
47
+
48
+ const [, content] = fs.writeFileSync.mock.calls[0];
49
+ // Old lines should be gone
50
+ expect(content).not.toContain('old-registry.example.com');
51
+ expect(content).not.toContain('sk_live_oldkey1234');
52
+ // New lines should be present
53
+ expect(content).toContain('@hiyve:registry=https://api.hiyve.dev/registry/');
54
+ expect(content).toContain(':_authToken=sk_live_newkey5678');
55
+ });
56
+
57
+ it('replaces legacy mk_ tokens', async () => {
58
+ const existingContent = [
59
+ '@hiyve:registry=https://console.hiyve.dev/api/registry/',
60
+ '//console.hiyve.dev/api/registry/:_authToken=mk_oldlegacykey1234',
61
+ 'other-config=value',
62
+ ].join('\n');
63
+
64
+ fs.existsSync.mockReturnValue(true);
65
+ fs.readFileSync.mockReturnValue(existingContent);
66
+ fs.writeFileSync.mockImplementation(() => {});
67
+
68
+ await configureNpmrc('https://api.hiyve.dev/registry/', 'sk_live_newkey5678');
69
+
70
+ const [, content] = fs.writeFileSync.mock.calls[0];
71
+ expect(content).not.toContain('mk_oldlegacykey1234');
72
+ expect(content).toContain(':_authToken=sk_live_newkey5678');
73
+ });
74
+
75
+ it('removes old console.hiyve.dev lines when switching to new registry', async () => {
76
+ const existingContent = [
77
+ '@hiyve:registry=https://console.hiyve.dev/api/registry/',
78
+ '//console.hiyve.dev/api/registry/:_authToken=sk_live_oldkey1234',
79
+ 'other-config=value',
80
+ ].join('\n');
81
+
82
+ fs.existsSync.mockReturnValue(true);
83
+ fs.readFileSync.mockReturnValue(existingContent);
84
+ fs.writeFileSync.mockImplementation(() => {});
85
+
86
+ await configureNpmrc('https://api.hiyve.dev/registry/', 'sk_live_newkey5678');
87
+
88
+ const [, content] = fs.writeFileSync.mock.calls[0];
89
+ expect(content).not.toContain('console.hiyve.dev');
90
+ expect(content).toContain('api.hiyve.dev');
91
+ });
92
+
93
+ it('preserves non-hiyve lines', async () => {
94
+ const existingContent = [
95
+ 'registry=https://registry.npmjs.org/',
96
+ '//npm.pkg.github.com/:_authToken=ghp_abc123',
97
+ '@hiyve:registry=https://old.example.com/',
98
+ ].join('\n');
99
+
100
+ fs.existsSync.mockReturnValue(true);
101
+ fs.readFileSync.mockReturnValue(existingContent);
102
+ fs.writeFileSync.mockImplementation(() => {});
103
+
104
+ await configureNpmrc('https://api.hiyve.dev/registry/', 'sk_test_abcdef000000');
105
+
106
+ const [, content] = fs.writeFileSync.mock.calls[0];
107
+ expect(content).toContain('registry=https://registry.npmjs.org/');
108
+ expect(content).toContain('//npm.pkg.github.com/:_authToken=ghp_abc123');
109
+ });
110
+
111
+ it('writes correct @hiyve:registry and authToken lines', async () => {
112
+ fs.existsSync.mockReturnValue(false);
113
+ fs.writeFileSync.mockImplementation(() => {});
114
+
115
+ await configureNpmrc('https://api.hiyve.dev/registry/', 'sk_live_1a2b3c4d5e6f');
116
+
117
+ const [, content, encoding] = fs.writeFileSync.mock.calls[0];
118
+ expect(encoding).toBe('utf8');
119
+
120
+ const lines = content.split('\n').filter((l) => l.trim());
121
+ const registryLine = lines.find((l) => l.startsWith('@hiyve:registry='));
122
+ const tokenLine = lines.find((l) => l.includes(':_authToken='));
123
+
124
+ expect(registryLine).toBe('@hiyve:registry=https://api.hiyve.dev/registry/');
125
+ expect(tokenLine).toBe('//api.hiyve.dev/registry/:_authToken=sk_live_1a2b3c4d5e6f');
126
+ });
127
+ });
128
+
129
+ describe('removeNpmrc', () => {
130
+ beforeEach(() => {
131
+ vi.clearAllMocks();
132
+ os.homedir.mockReturnValue('/mock-home');
133
+ });
134
+
135
+ it('removes @hiyve lines with sk_ tokens (new registry)', async () => {
136
+ const existingContent = [
137
+ 'registry=https://registry.npmjs.org/',
138
+ '@hiyve:registry=https://api.hiyve.dev/registry/',
139
+ '//api.hiyve.dev/registry/:_authToken=sk_live_abc123def456',
140
+ 'other-config=true',
141
+ ].join('\n');
142
+
143
+ fs.existsSync.mockReturnValue(true);
144
+ fs.readFileSync.mockReturnValue(existingContent);
145
+ fs.writeFileSync.mockImplementation(() => {});
146
+
147
+ await removeNpmrc();
148
+
149
+ const [, content] = fs.writeFileSync.mock.calls[0];
150
+ expect(content).not.toContain('@hiyve:registry');
151
+ expect(content).not.toContain('api.hiyve.dev');
152
+ expect(content).not.toContain('sk_live_abc123def456');
153
+ });
154
+
155
+ it('removes @hiyve lines with sk_ tokens (old registry)', async () => {
156
+ const existingContent = [
157
+ 'registry=https://registry.npmjs.org/',
158
+ '@hiyve:registry=https://console.hiyve.dev/api/registry/',
159
+ '//console.hiyve.dev/api/registry/:_authToken=sk_live_abc123def456',
160
+ 'other-config=true',
161
+ ].join('\n');
162
+
163
+ fs.existsSync.mockReturnValue(true);
164
+ fs.readFileSync.mockReturnValue(existingContent);
165
+ fs.writeFileSync.mockImplementation(() => {});
166
+
167
+ await removeNpmrc();
168
+
169
+ const [, content] = fs.writeFileSync.mock.calls[0];
170
+ expect(content).not.toContain('@hiyve:registry');
171
+ expect(content).not.toContain('console.hiyve.dev');
172
+ expect(content).not.toContain('sk_live_abc123def456');
173
+ });
174
+
175
+ it('removes @hiyve lines with legacy mk_ tokens', async () => {
176
+ const existingContent = [
177
+ 'registry=https://registry.npmjs.org/',
178
+ '@hiyve:registry=https://console.hiyve.dev/api/registry/',
179
+ '//console.hiyve.dev/api/registry/:_authToken=mk_abc123def456789012345678901234',
180
+ 'other-config=true',
181
+ ].join('\n');
182
+
183
+ fs.existsSync.mockReturnValue(true);
184
+ fs.readFileSync.mockReturnValue(existingContent);
185
+ fs.writeFileSync.mockImplementation(() => {});
186
+
187
+ await removeNpmrc();
188
+
189
+ const [, content] = fs.writeFileSync.mock.calls[0];
190
+ expect(content).not.toContain('@hiyve:registry');
191
+ expect(content).not.toContain('mk_abc123');
192
+ });
193
+
194
+ it('preserves non-hiyve lines', async () => {
195
+ const existingContent = [
196
+ 'registry=https://registry.npmjs.org/',
197
+ '@hiyve:registry=https://api.hiyve.dev/registry/',
198
+ '//api.hiyve.dev/registry/:_authToken=sk_test_abc123',
199
+ '@myorg:registry=https://myorg.example.com/',
200
+ ].join('\n');
201
+
202
+ fs.existsSync.mockReturnValue(true);
203
+ fs.readFileSync.mockReturnValue(existingContent);
204
+ fs.writeFileSync.mockImplementation(() => {});
205
+
206
+ await removeNpmrc();
207
+
208
+ const [, content] = fs.writeFileSync.mock.calls[0];
209
+ expect(content).toContain('registry=https://registry.npmjs.org/');
210
+ expect(content).toContain('@myorg:registry=https://myorg.example.com/');
211
+ });
212
+
213
+ it('handles case when .npmrc does not exist', async () => {
214
+ fs.existsSync.mockReturnValue(false);
215
+
216
+ await expect(removeNpmrc()).resolves.toBeUndefined();
217
+ expect(fs.readFileSync).not.toHaveBeenCalled();
218
+ expect(fs.writeFileSync).not.toHaveBeenCalled();
219
+ });
220
+ });
221
+
222
+ describe('getCurrentConfig', () => {
223
+ beforeEach(() => {
224
+ vi.clearAllMocks();
225
+ os.homedir.mockReturnValue('/mock-home');
226
+ });
227
+
228
+ it('returns null when .npmrc does not exist', () => {
229
+ fs.existsSync.mockReturnValue(false);
230
+
231
+ expect(getCurrentConfig()).toBeNull();
232
+ });
233
+
234
+ it('returns null when .npmrc has no @hiyve config', () => {
235
+ const content = [
236
+ 'registry=https://registry.npmjs.org/',
237
+ '//npm.pkg.github.com/:_authToken=ghp_abc123',
238
+ ].join('\n');
239
+
240
+ fs.existsSync.mockReturnValue(true);
241
+ fs.readFileSync.mockReturnValue(content);
242
+
243
+ expect(getCurrentConfig()).toBeNull();
244
+ });
245
+
246
+ it('returns config for sk_ keys (new registry)', () => {
247
+ const content = [
248
+ '@hiyve:registry=https://api.hiyve.dev/registry/',
249
+ '//api.hiyve.dev/registry/:_authToken=sk_live_1a2b3c4d5e6f7890abcd',
250
+ ].join('\n');
251
+
252
+ fs.existsSync.mockReturnValue(true);
253
+ fs.readFileSync.mockReturnValue(content);
254
+
255
+ const result = getCurrentConfig();
256
+ expect(result).toEqual({
257
+ apiKey: 'sk_live_1a2b3c4d5e6f7890abcd',
258
+ maskedApiKey: 'sk_live_...abcd',
259
+ registryUrl: 'https://api.hiyve.dev/registry/',
260
+ });
261
+ });
262
+
263
+ it('returns config for sk_ keys (old registry — backward compat)', () => {
264
+ const content = [
265
+ '@hiyve:registry=https://console.hiyve.dev/api/registry/',
266
+ '//console.hiyve.dev/api/registry/:_authToken=sk_live_1a2b3c4d5e6f7890abcd',
267
+ ].join('\n');
268
+
269
+ fs.existsSync.mockReturnValue(true);
270
+ fs.readFileSync.mockReturnValue(content);
271
+
272
+ const result = getCurrentConfig();
273
+ expect(result).toEqual({
274
+ apiKey: 'sk_live_1a2b3c4d5e6f7890abcd',
275
+ maskedApiKey: 'sk_live_...abcd',
276
+ registryUrl: 'https://console.hiyve.dev/api/registry/',
277
+ });
278
+ });
279
+
280
+ it('returns config for legacy mk_ keys', () => {
281
+ const content = [
282
+ '@hiyve:registry=https://api.hiyve.dev/registry/',
283
+ '//api.hiyve.dev/registry/:_authToken=mk_1a2b3c4d5e6f7890abcd',
284
+ ].join('\n');
285
+
286
+ fs.existsSync.mockReturnValue(true);
287
+ fs.readFileSync.mockReturnValue(content);
288
+
289
+ const result = getCurrentConfig();
290
+ expect(result).toEqual({
291
+ apiKey: 'mk_1a2b3c4d5e6f7890abcd',
292
+ maskedApiKey: 'mk_1a2b3...abcd',
293
+ registryUrl: 'https://api.hiyve.dev/registry/',
294
+ });
295
+ });
296
+
297
+ it('maskedApiKey shows first 8 and last 4 chars', () => {
298
+ const apiKey = 'sk_test_aabbccdd11223344eeff';
299
+ const content = [
300
+ '@hiyve:registry=https://api.hiyve.dev/registry/',
301
+ `//api.hiyve.dev/registry/:_authToken=${apiKey}`,
302
+ ].join('\n');
303
+
304
+ fs.existsSync.mockReturnValue(true);
305
+ fs.readFileSync.mockReturnValue(content);
306
+
307
+ const result = getCurrentConfig();
308
+ expect(result.apiKey).toBe(apiKey);
309
+ expect(result.maskedApiKey).toBe('sk_test_...eeff');
310
+ });
311
+
312
+ it('returns null when only registry line is present but no token', () => {
313
+ const content = '@hiyve:registry=https://api.hiyve.dev/registry/\n';
314
+
315
+ fs.existsSync.mockReturnValue(true);
316
+ fs.readFileSync.mockReturnValue(content);
317
+
318
+ expect(getCurrentConfig()).toBeNull();
319
+ });
320
+
321
+ it('returns null when only token line is present but no registry', () => {
322
+ const content = '//api.hiyve.dev/registry/:_authToken=sk_live_abc123def456\n';
323
+
324
+ fs.existsSync.mockReturnValue(true);
325
+ fs.readFileSync.mockReturnValue(content);
326
+
327
+ // The token line contains "//api.hiyve.dev/" so it would be found,
328
+ // but there is no @hiyve:registry line, so result should be null
329
+ expect(getCurrentConfig()).toBeNull();
330
+ });
331
+ });