@hiyve/cli 1.0.5 → 1.0.8
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 +6 -8
- package/bin/hiyve.js +15 -1
- package/package.json +9 -5
- package/src/commands/init.js +442 -0
- package/src/commands/list.js +2 -2
- package/src/commands/login.js +15 -10
- package/src/commands/whoami.js +2 -2
- package/src/config.js +37 -2
- package/src/config.test.js +32 -0
- package/src/index.js +2 -1
- package/src/utils/npmrc.js +19 -13
- package/src/utils/npmrc.test.js +331 -0
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/
|
|
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
|
|
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
|
|
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://
|
|
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
|
|
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 `
|
|
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.
|
|
3
|
+
"version": "1.0.8",
|
|
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": "
|
|
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.
|
|
41
|
+
"chalk": "^5.6.2",
|
|
41
42
|
"commander": "^12.1.0",
|
|
42
|
-
"ora": "^8.
|
|
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
|
+
}
|
package/src/commands/list.js
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
import chalk from 'chalk';
|
|
8
8
|
import ora from 'ora';
|
|
9
|
-
import {
|
|
9
|
+
import { getApiUrl } 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(`${
|
|
33
|
+
const response = await fetch(`${getApiUrl()}packages`, {
|
|
34
34
|
headers: {
|
|
35
35
|
Authorization: `Bearer ${config.apiKey}`,
|
|
36
36
|
},
|
package/src/commands/login.js
CHANGED
|
@@ -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 {
|
|
11
|
+
import { getApiUrl, 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('
|
|
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
|
-
|
|
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('
|
|
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(`${
|
|
66
|
+
const response = await fetch(`${getApiUrl()}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(
|
|
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(`${
|
|
114
|
+
const packagesResponse = await fetch(`${getApiUrl()}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/
|
|
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/
|
|
150
|
+
console.log(chalk.cyan(' npm install @hiyve/react'));
|
|
146
151
|
console.log('');
|
|
147
152
|
}
|
|
148
153
|
}
|
package/src/commands/whoami.js
CHANGED
|
@@ -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 {
|
|
10
|
+
import { getApiUrl } 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(`${
|
|
42
|
+
const response = await fetch(`${getApiUrl()}verify`, {
|
|
43
43
|
headers: {
|
|
44
44
|
Authorization: `Bearer ${config.apiKey}`,
|
|
45
45
|
},
|
package/src/config.js
CHANGED
|
@@ -1,10 +1,45 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Hiyve CLI Configuration
|
|
3
|
+
*
|
|
4
|
+
* Two URLs matter:
|
|
5
|
+
* - API URL: where the CLI talks to (verify, list, packages). Switches with --dev.
|
|
6
|
+
* - Registry URL: what goes into ~/.npmrc for npm to fetch tarballs. Always prod.
|
|
3
7
|
*/
|
|
4
8
|
|
|
5
|
-
|
|
6
|
-
|
|
9
|
+
const PROD_API_URL = 'https://api.hiyve.dev/registry/';
|
|
10
|
+
const DEV_API_URL = 'https://api.muziemedia.com/registry/';
|
|
11
|
+
|
|
12
|
+
let _devMode = false;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Enable dev mode (CLI API calls go to api.muziemedia.com)
|
|
16
|
+
*/
|
|
17
|
+
export function setDevMode(enabled) {
|
|
18
|
+
_devMode = enabled;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Get the API URL for CLI operations (verify, list, packages).
|
|
23
|
+
* Switches between dev and prod based on --dev flag.
|
|
24
|
+
*/
|
|
25
|
+
export function getApiUrl() {
|
|
26
|
+
return _devMode ? DEV_API_URL : PROD_API_URL;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Get the registry URL for ~/.npmrc (where npm fetches tarballs).
|
|
31
|
+
* Always prod — there is one S3 bucket, both APIs point to the same packages.
|
|
32
|
+
*/
|
|
33
|
+
export function getRegistryUrl() {
|
|
34
|
+
return PROD_API_URL;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Backwards-compatible named export
|
|
38
|
+
export const REGISTRY_URL = PROD_API_URL;
|
|
7
39
|
|
|
8
40
|
export default {
|
|
9
41
|
REGISTRY_URL,
|
|
42
|
+
getApiUrl,
|
|
43
|
+
getRegistryUrl,
|
|
44
|
+
setDevMode,
|
|
10
45
|
};
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import config, { REGISTRY_URL, getApiUrl, 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('getApiUrl returns prod URL by default', () => {
|
|
15
|
+
setDevMode(false);
|
|
16
|
+
expect(getApiUrl()).toBe('https://api.hiyve.dev/registry/');
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it('getApiUrl returns dev URL when dev mode is enabled', () => {
|
|
20
|
+
setDevMode(true);
|
|
21
|
+
expect(getApiUrl()).toBe('https://api.muziemedia.com/registry/');
|
|
22
|
+
setDevMode(false);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('getRegistryUrl always returns prod URL regardless of dev mode', () => {
|
|
26
|
+
setDevMode(false);
|
|
27
|
+
expect(getRegistryUrl()).toBe('https://api.hiyve.dev/registry/');
|
|
28
|
+
setDevMode(true);
|
|
29
|
+
expect(getRegistryUrl()).toBe('https://api.hiyve.dev/registry/');
|
|
30
|
+
setDevMode(false);
|
|
31
|
+
});
|
|
32
|
+
});
|
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,
|
|
12
|
+
export { REGISTRY_URL, getApiUrl, getRegistryUrl, setDevMode } from './config.js';
|
package/src/utils/npmrc.js
CHANGED
|
@@ -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://
|
|
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('
|
|
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
|
|
39
|
+
// Parse the registry URL to derive the auth token path
|
|
37
40
|
const url = new URL(registryUrl);
|
|
38
|
-
const
|
|
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(
|
|
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('
|
|
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-
|
|
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,
|
|
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
|
+
});
|