@husar.ai/cli 0.4.1 → 0.4.2
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/AGENTS.md +835 -0
- package/dist/auth/api.d.ts +15 -0
- package/dist/auth/api.js +86 -0
- package/dist/auth/api.js.map +1 -0
- package/dist/auth/config.d.ts +32 -0
- package/dist/auth/config.js +95 -0
- package/dist/auth/config.js.map +1 -0
- package/dist/auth/login.d.ts +30 -0
- package/dist/auth/login.js +450 -0
- package/dist/auth/login.js.map +1 -0
- package/dist/cli.js +83 -3
- package/dist/cli.js.map +1 -1
- package/dist/functions/create.d.ts +6 -0
- package/dist/functions/create.js +311 -0
- package/dist/functions/create.js.map +1 -0
- package/dist/mcp.js +20 -14
- package/dist/mcp.js.map +1 -1
- package/dist/types/config.d.ts +3 -1
- package/dist/types/config.js +12 -1
- package/dist/types/config.js.map +1 -1
- package/dist/zeus/const.js +635 -289
- package/dist/zeus/const.js.map +1 -1
- package/dist/zeus/index.d.ts +3079 -1601
- package/dist/zeus/index.js +150 -2
- package/dist/zeus/index.js.map +1 -1
- package/package.json +3 -3
- package/src/auth/api.ts +133 -0
- package/src/auth/config.ts +198 -0
- package/src/auth/login.ts +631 -0
- package/src/cli.ts +96 -4
- package/src/functions/create.ts +489 -0
- package/src/mcp.ts +47 -27
- package/src/types/config.ts +32 -1
- package/src/zeus/const.ts +641 -289
- package/src/zeus/index.ts +2996 -1465
package/src/cli.ts
CHANGED
|
@@ -7,7 +7,10 @@ import { ConfigMaker } from 'config-maker';
|
|
|
7
7
|
import { parser } from './functions/parser.js';
|
|
8
8
|
import { generateCms } from './functions/generate.js';
|
|
9
9
|
import { startMcpServer } from './mcp.js';
|
|
10
|
-
import { HusarConfigType } from '@/types/config.js';
|
|
10
|
+
import { HusarConfigType, getAdminToken } from '@/types/config.js';
|
|
11
|
+
import { startLoginFlow } from './auth/login.js';
|
|
12
|
+
import { clearAuth, getCurrentUser, getConfigPath, isLoggedIn, readAuthConfig } from './auth/config.js';
|
|
13
|
+
import { runCreateCommand } from './functions/create.js';
|
|
11
14
|
|
|
12
15
|
const config = new ConfigMaker<HusarConfigType>('husar', { decoders: {} });
|
|
13
16
|
|
|
@@ -40,13 +43,14 @@ program
|
|
|
40
43
|
if (!conf.host) {
|
|
41
44
|
throw new Error('Host is not configured. Please run "husar generate" first.');
|
|
42
45
|
}
|
|
43
|
-
|
|
44
|
-
|
|
46
|
+
const adminToken = getAdminToken(conf);
|
|
47
|
+
if (!adminToken) {
|
|
48
|
+
throw new Error('Admin token not configured. Set HUSAR_ADMIN_TOKEN env var, or adminTokenEnv in husar.json');
|
|
45
49
|
}
|
|
46
50
|
const result = await parser({
|
|
47
51
|
inputFile,
|
|
48
52
|
opts: { name, type: opts?.type },
|
|
49
|
-
authentication: { HUSAR_MCP_HOST: conf.host, HUSAR_MCP_ADMIN_TOKEN:
|
|
53
|
+
authentication: { HUSAR_MCP_HOST: conf.host, HUSAR_MCP_ADMIN_TOKEN: adminToken },
|
|
50
54
|
});
|
|
51
55
|
console.log(result ? 'File parsed and upserted successfully.' : 'Failed to parse and upsert file.');
|
|
52
56
|
});
|
|
@@ -61,4 +65,92 @@ program
|
|
|
61
65
|
await new Promise<void>(() => {});
|
|
62
66
|
});
|
|
63
67
|
|
|
68
|
+
// ============ Auth Commands ============
|
|
69
|
+
|
|
70
|
+
program
|
|
71
|
+
.command('login')
|
|
72
|
+
.description('Log in to Husar.ai via browser authentication')
|
|
73
|
+
.action(async () => {
|
|
74
|
+
const loggedIn = await isLoggedIn();
|
|
75
|
+
if (loggedIn) {
|
|
76
|
+
const user = await getCurrentUser();
|
|
77
|
+
console.log(`Already logged in as ${user?.email}`);
|
|
78
|
+
console.log('Run "husar logout" to switch accounts.');
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
console.log('Starting Husar.ai authentication...');
|
|
83
|
+
const result = await startLoginFlow();
|
|
84
|
+
|
|
85
|
+
if (result.success) {
|
|
86
|
+
console.log(`\n✅ Successfully logged in as ${result.email}`);
|
|
87
|
+
if (result.project) {
|
|
88
|
+
console.log(` Connected to project: ${result.project.projectName}`);
|
|
89
|
+
console.log(` Host: ${result.project.host}`);
|
|
90
|
+
}
|
|
91
|
+
} else {
|
|
92
|
+
console.error(`\n❌ Login failed: ${result.error}`);
|
|
93
|
+
process.exit(1);
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
program
|
|
98
|
+
.command('logout')
|
|
99
|
+
.description('Log out and clear stored credentials')
|
|
100
|
+
.action(async () => {
|
|
101
|
+
const loggedIn = await isLoggedIn();
|
|
102
|
+
if (!loggedIn) {
|
|
103
|
+
console.log('Not logged in.');
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const user = await getCurrentUser();
|
|
108
|
+
await clearAuth();
|
|
109
|
+
console.log(`✅ Logged out${user?.email ? ` (was: ${user.email})` : ''}`);
|
|
110
|
+
console.log(` Cleared credentials from ${getConfigPath()}`);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
program
|
|
114
|
+
.command('whoami')
|
|
115
|
+
.description('Display current logged-in user and projects')
|
|
116
|
+
.action(async () => {
|
|
117
|
+
const loggedIn = await isLoggedIn();
|
|
118
|
+
if (!loggedIn) {
|
|
119
|
+
console.log('Not logged in. Run "husar login" to authenticate.');
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const user = await getCurrentUser();
|
|
124
|
+
const authConfig = await readAuthConfig();
|
|
125
|
+
|
|
126
|
+
console.log(`\n👤 Logged in as: ${user?.email}`);
|
|
127
|
+
|
|
128
|
+
if (authConfig.projects && Object.keys(authConfig.projects).length > 0) {
|
|
129
|
+
console.log('\n📦 Connected projects:');
|
|
130
|
+
for (const [name, project] of Object.entries(authConfig.projects)) {
|
|
131
|
+
console.log(` • ${name}`);
|
|
132
|
+
console.log(` Host: ${project.host}`);
|
|
133
|
+
}
|
|
134
|
+
} else {
|
|
135
|
+
console.log('\nNo projects connected. Run "husar create" to set up a new project.');
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
console.log(`\n📁 Config: ${getConfigPath()}`);
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
// ============ Create Command ============
|
|
142
|
+
|
|
143
|
+
program
|
|
144
|
+
.command('create [projectName]')
|
|
145
|
+
.description('Create a new project with Husar CMS integration')
|
|
146
|
+
.option('-f, --framework <framework>', 'Framework to use: nextjs or vite', 'nextjs')
|
|
147
|
+
.option('--skip-install', 'Skip package installation after scaffolding')
|
|
148
|
+
.action(async (projectName: string | undefined, opts: { framework: string; skipInstall?: boolean }) => {
|
|
149
|
+
await runCreateCommand({
|
|
150
|
+
projectName,
|
|
151
|
+
framework: opts.framework as 'nextjs' | 'vite',
|
|
152
|
+
skipInstall: opts.skipInstall,
|
|
153
|
+
});
|
|
154
|
+
});
|
|
155
|
+
|
|
64
156
|
program.parse(process.argv);
|
|
@@ -0,0 +1,489 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Create command for Husar CLI
|
|
3
|
+
*
|
|
4
|
+
* Onboarding flow for new users:
|
|
5
|
+
* 1. Choose connection method (manual URL or cloud login)
|
|
6
|
+
* 2. If cloud login: Get JWT → List projects → Select one
|
|
7
|
+
* 3. Panel CMS login: Get adminToken via browser auth
|
|
8
|
+
* 4. Delegate to framework CLI (create-next-app / create-vite)
|
|
9
|
+
* 5. Add Husar configuration and install packages
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { spawn } from 'node:child_process';
|
|
13
|
+
import { promises as fs } from 'node:fs';
|
|
14
|
+
import { join, resolve } from 'node:path';
|
|
15
|
+
import * as readline from 'node:readline';
|
|
16
|
+
|
|
17
|
+
import { getCloudAuth, ProjectAuth } from '../auth/config.js';
|
|
18
|
+
import { startCloudLoginFlow, startPanelLoginFlow } from '../auth/login.js';
|
|
19
|
+
import { getProjects, CloudProject, verifyAccessToken } from '../auth/api.js';
|
|
20
|
+
|
|
21
|
+
export interface CreateOptions {
|
|
22
|
+
/** Project directory name */
|
|
23
|
+
projectName?: string;
|
|
24
|
+
/** Framework to use: nextjs or vite */
|
|
25
|
+
framework: 'nextjs' | 'vite';
|
|
26
|
+
/** Skip npm install after scaffolding */
|
|
27
|
+
skipInstall?: boolean;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
31
|
+
// MAIN CREATE COMMAND
|
|
32
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Run the create command
|
|
36
|
+
*/
|
|
37
|
+
export async function runCreateCommand(options: CreateOptions): Promise<void> {
|
|
38
|
+
console.log('\n🚀 Husar.ai Project Creator\n');
|
|
39
|
+
|
|
40
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
41
|
+
// STEP 1: How to connect to CMS?
|
|
42
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
43
|
+
|
|
44
|
+
const connectionMethod = await promptConnectionMethod();
|
|
45
|
+
let panelHost: string;
|
|
46
|
+
|
|
47
|
+
if (connectionMethod === 'manual') {
|
|
48
|
+
// User enters URL manually
|
|
49
|
+
panelHost = await promptManualUrl();
|
|
50
|
+
} else {
|
|
51
|
+
// Cloud login flow → select project
|
|
52
|
+
const cloudAuth = await ensureCloudLogin();
|
|
53
|
+
if (!cloudAuth) {
|
|
54
|
+
console.error('\n❌ Cloud authentication failed. Please try again.');
|
|
55
|
+
process.exit(1);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Fetch user's projects
|
|
59
|
+
console.log('\n📦 Fetching your projects...\n');
|
|
60
|
+
const projects = await getProjects(cloudAuth.accessToken);
|
|
61
|
+
|
|
62
|
+
if (projects.length === 0) {
|
|
63
|
+
console.log('❌ No projects found in your account.');
|
|
64
|
+
console.log(' Create a project at https://admin.husar.ai first.\n');
|
|
65
|
+
process.exit(1);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Let user select a project
|
|
69
|
+
const selectedProject = await promptSelectProject(projects);
|
|
70
|
+
panelHost = selectedProject.adminURL;
|
|
71
|
+
console.log(`\n📌 Selected: ${selectedProject.name}`);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
75
|
+
// STEP 2: Panel CMS login (get adminToken)
|
|
76
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
77
|
+
|
|
78
|
+
console.log('\n🔐 Opening browser for CMS authentication...');
|
|
79
|
+
console.log(" You'll need your SUPERADMIN credentials (from the welcome email)\n");
|
|
80
|
+
|
|
81
|
+
const panelAuth = await startPanelLoginFlow(panelHost);
|
|
82
|
+
|
|
83
|
+
if (!panelAuth.success || !panelAuth.project) {
|
|
84
|
+
console.error('\n❌ CMS authentication failed:', panelAuth.error ?? 'Unknown error');
|
|
85
|
+
process.exit(1);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const project = panelAuth.project;
|
|
89
|
+
console.log(`\n✅ Connected to: ${project.projectName}`);
|
|
90
|
+
console.log(` Host: ${project.host}\n`);
|
|
91
|
+
|
|
92
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
93
|
+
// STEP 3: Get project name (directory)
|
|
94
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
95
|
+
|
|
96
|
+
const projectName = options.projectName || (await promptProjectName(project.projectName));
|
|
97
|
+
if (!projectName) {
|
|
98
|
+
console.error('❌ Project name is required.');
|
|
99
|
+
process.exit(1);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const projectPath = resolve(process.cwd(), projectName);
|
|
103
|
+
|
|
104
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
105
|
+
// STEP 4: Run framework CLI
|
|
106
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
107
|
+
|
|
108
|
+
console.log(`\n📦 Creating ${options.framework === 'nextjs' ? 'Next.js' : 'Vite + React'} project...\n`);
|
|
109
|
+
|
|
110
|
+
const frameworkSuccess = await runFrameworkCli(options.framework, projectName);
|
|
111
|
+
if (!frameworkSuccess) {
|
|
112
|
+
console.error('\n❌ Framework CLI failed. Please check the output above.');
|
|
113
|
+
process.exit(1);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
117
|
+
// STEP 5: Add Husar configuration (with hybrid storage)
|
|
118
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
119
|
+
|
|
120
|
+
console.log('\n⚙️ Adding Husar configuration...\n');
|
|
121
|
+
await addHusarConfig(projectPath, project, options.framework);
|
|
122
|
+
|
|
123
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
124
|
+
// STEP 6: Install Husar packages
|
|
125
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
126
|
+
|
|
127
|
+
if (!options.skipInstall) {
|
|
128
|
+
console.log('\n📥 Installing Husar packages...\n');
|
|
129
|
+
await installHusarPackages(projectPath);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
133
|
+
// STEP 7: Generate CMS files
|
|
134
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
135
|
+
|
|
136
|
+
console.log('\n🔧 Generating CMS client...\n');
|
|
137
|
+
await generateCmsClient(projectPath, options.framework);
|
|
138
|
+
|
|
139
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
140
|
+
// SUCCESS!
|
|
141
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
142
|
+
|
|
143
|
+
console.log('\n' + '═'.repeat(50));
|
|
144
|
+
console.log('✨ Project created successfully!');
|
|
145
|
+
console.log('═'.repeat(50));
|
|
146
|
+
console.log(`\n📁 Project location: ${projectPath}`);
|
|
147
|
+
console.log('\n🎯 Next steps:');
|
|
148
|
+
console.log(` cd ${projectName}`);
|
|
149
|
+
console.log(' npm run dev\n');
|
|
150
|
+
console.log('📚 Documentation: https://docs.husar.ai');
|
|
151
|
+
console.log('💬 Need help? Join our Discord: https://discord.gg/husar\n');
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
155
|
+
// PROMPTS
|
|
156
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Prompt for connection method
|
|
160
|
+
*/
|
|
161
|
+
async function promptConnectionMethod(): Promise<'manual' | 'cloud'> {
|
|
162
|
+
console.log('How do you want to connect to your CMS?\n');
|
|
163
|
+
console.log(' 1. Enter project URL manually');
|
|
164
|
+
console.log(' 2. Login to husar.ai and select from list\n');
|
|
165
|
+
|
|
166
|
+
const choice = await promptChoice(2);
|
|
167
|
+
return choice === 1 ? 'manual' : 'cloud';
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Prompt for manual URL entry
|
|
172
|
+
*/
|
|
173
|
+
async function promptManualUrl(): Promise<string> {
|
|
174
|
+
return new Promise((resolve) => {
|
|
175
|
+
const rl = readline.createInterface({
|
|
176
|
+
input: process.stdin,
|
|
177
|
+
output: process.stdout,
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
const ask = () => {
|
|
181
|
+
rl.question('\n🌐 Enter your CMS panel URL: ', (answer) => {
|
|
182
|
+
const url = answer.trim();
|
|
183
|
+
|
|
184
|
+
// Basic validation
|
|
185
|
+
if (!url) {
|
|
186
|
+
console.log(' URL is required.');
|
|
187
|
+
ask();
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
if (!url.startsWith('http://') && !url.startsWith('https://')) {
|
|
192
|
+
console.log(' URL must start with http:// or https://');
|
|
193
|
+
ask();
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
rl.close();
|
|
198
|
+
resolve(url);
|
|
199
|
+
});
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
ask();
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Prompt for project selection from list
|
|
208
|
+
*/
|
|
209
|
+
async function promptSelectProject(projects: CloudProject[]): Promise<CloudProject> {
|
|
210
|
+
console.log('Select a project:\n');
|
|
211
|
+
projects.forEach((p, i) => {
|
|
212
|
+
console.log(` ${i + 1}. ${p.name}`);
|
|
213
|
+
console.log(` ${p.adminURL}\n`);
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
const choice = await promptChoice(projects.length);
|
|
217
|
+
return projects[choice - 1]!;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Prompt for project directory name
|
|
222
|
+
*/
|
|
223
|
+
async function promptProjectName(suggestedName?: string): Promise<string> {
|
|
224
|
+
return new Promise((resolve) => {
|
|
225
|
+
const rl = readline.createInterface({
|
|
226
|
+
input: process.stdin,
|
|
227
|
+
output: process.stdout,
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
const defaultName = suggestedName ? suggestedName.replace(/[^a-zA-Z0-9-_]/g, '-') : '';
|
|
231
|
+
const prompt = defaultName
|
|
232
|
+
? `📝 Enter project directory name [${defaultName}]: `
|
|
233
|
+
: '📝 Enter project directory name: ';
|
|
234
|
+
|
|
235
|
+
rl.question(prompt, (answer) => {
|
|
236
|
+
rl.close();
|
|
237
|
+
const name = answer.trim() || defaultName;
|
|
238
|
+
resolve(name);
|
|
239
|
+
});
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Prompt for numeric choice
|
|
245
|
+
*/
|
|
246
|
+
async function promptChoice(max: number): Promise<number> {
|
|
247
|
+
return new Promise((resolvePrompt) => {
|
|
248
|
+
const rl = readline.createInterface({
|
|
249
|
+
input: process.stdin,
|
|
250
|
+
output: process.stdout,
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
const ask = () => {
|
|
254
|
+
rl.question(`Enter choice (1-${max}): `, (answer) => {
|
|
255
|
+
const num = parseInt(answer, 10);
|
|
256
|
+
if (num >= 1 && num <= max) {
|
|
257
|
+
rl.close();
|
|
258
|
+
resolvePrompt(num);
|
|
259
|
+
} else {
|
|
260
|
+
console.log(`Please enter a number between 1 and ${max}`);
|
|
261
|
+
ask();
|
|
262
|
+
}
|
|
263
|
+
});
|
|
264
|
+
};
|
|
265
|
+
ask();
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
270
|
+
// CLOUD LOGIN
|
|
271
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Ensure user has valid cloud login (JWT)
|
|
275
|
+
*/
|
|
276
|
+
async function ensureCloudLogin(): Promise<{ accessToken: string; email?: string } | null> {
|
|
277
|
+
// Check if we already have a valid cloud auth
|
|
278
|
+
const existingAuth = await getCloudAuth();
|
|
279
|
+
|
|
280
|
+
if (existingAuth) {
|
|
281
|
+
// Verify the token is still valid
|
|
282
|
+
console.log('🔍 Checking existing cloud session...');
|
|
283
|
+
const isValid = await verifyAccessToken(existingAuth.accessToken);
|
|
284
|
+
|
|
285
|
+
if (isValid) {
|
|
286
|
+
console.log(`✅ Logged in as: ${existingAuth.email ?? 'user'}`);
|
|
287
|
+
return existingAuth;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
console.log('⚠️ Session expired, need to re-authenticate...');
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// Need to login
|
|
294
|
+
console.log('\n🔐 Please log in to Husar.ai...\n');
|
|
295
|
+
const result = await startCloudLoginFlow();
|
|
296
|
+
|
|
297
|
+
if (result.success && result.accessToken) {
|
|
298
|
+
console.log(`\n✅ Logged in as: ${result.email ?? 'user'}`);
|
|
299
|
+
return {
|
|
300
|
+
accessToken: result.accessToken,
|
|
301
|
+
email: result.email,
|
|
302
|
+
};
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
return null;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
309
|
+
// FRAMEWORK CLI
|
|
310
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* Run the framework-specific CLI
|
|
314
|
+
*/
|
|
315
|
+
async function runFrameworkCli(framework: 'nextjs' | 'vite', projectName: string): Promise<boolean> {
|
|
316
|
+
return new Promise((resolve) => {
|
|
317
|
+
let command: string;
|
|
318
|
+
let args: string[];
|
|
319
|
+
|
|
320
|
+
if (framework === 'nextjs') {
|
|
321
|
+
// Using npx create-next-app with recommended options
|
|
322
|
+
command = 'npx';
|
|
323
|
+
args = [
|
|
324
|
+
'create-next-app@latest',
|
|
325
|
+
projectName,
|
|
326
|
+
'--typescript',
|
|
327
|
+
'--tailwind',
|
|
328
|
+
'--eslint',
|
|
329
|
+
'--app',
|
|
330
|
+
'--src-dir',
|
|
331
|
+
'--import-alias',
|
|
332
|
+
'@/*',
|
|
333
|
+
];
|
|
334
|
+
} else {
|
|
335
|
+
// Using npm create vite with React + TypeScript
|
|
336
|
+
command = 'npm';
|
|
337
|
+
args = ['create', 'vite@latest', projectName, '--', '--template', 'react-ts'];
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
console.log(`Running: ${command} ${args.join(' ')}\n`);
|
|
341
|
+
|
|
342
|
+
const child = spawn(command, args, {
|
|
343
|
+
stdio: 'inherit',
|
|
344
|
+
shell: true,
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
child.on('close', (code) => {
|
|
348
|
+
resolve(code === 0);
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
child.on('error', (err) => {
|
|
352
|
+
console.error('Failed to start framework CLI:', err.message);
|
|
353
|
+
resolve(false);
|
|
354
|
+
});
|
|
355
|
+
});
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
359
|
+
// CONFIGURATION
|
|
360
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
361
|
+
|
|
362
|
+
/**
|
|
363
|
+
* Add Husar configuration files to the project
|
|
364
|
+
* Uses HYBRID storage: husar.json (committal) + .env.local (secrets)
|
|
365
|
+
*/
|
|
366
|
+
async function addHusarConfig(projectPath: string, project: ProjectAuth, framework: 'nextjs' | 'vite'): Promise<void> {
|
|
367
|
+
const hostEnvVar = framework === 'nextjs' ? 'NEXT_PUBLIC_HUSAR_HOST' : 'VITE_HUSAR_HOST';
|
|
368
|
+
|
|
369
|
+
// 1. Create husar.json (committal - NO adminToken!)
|
|
370
|
+
const husarConfig = {
|
|
371
|
+
host: project.host,
|
|
372
|
+
hostEnvironmentVariable: hostEnvVar,
|
|
373
|
+
authenticationEnvironmentVariable: 'HUSAR_API_KEY',
|
|
374
|
+
adminTokenEnv: 'HUSAR_ADMIN_TOKEN', // Points to env var, not actual token
|
|
375
|
+
overrideHost: false,
|
|
376
|
+
};
|
|
377
|
+
await fs.writeFile(join(projectPath, 'husar.json'), JSON.stringify(husarConfig, null, 2));
|
|
378
|
+
console.log(' ✓ Created husar.json');
|
|
379
|
+
|
|
380
|
+
// 2. Create .env.local (secrets - NOT committed)
|
|
381
|
+
const envContent = `# Husar CMS Configuration
|
|
382
|
+
# This file contains secrets - DO NOT commit to version control!
|
|
383
|
+
|
|
384
|
+
# CMS Host URL
|
|
385
|
+
${hostEnvVar}=${project.host}
|
|
386
|
+
|
|
387
|
+
# Content API Key (for public content queries)
|
|
388
|
+
HUSAR_API_KEY=
|
|
389
|
+
|
|
390
|
+
# Admin Token (for CLI/MCP operations)
|
|
391
|
+
HUSAR_ADMIN_TOKEN=${project.adminToken}
|
|
392
|
+
`;
|
|
393
|
+
await fs.writeFile(join(projectPath, '.env.local'), envContent);
|
|
394
|
+
console.log(' ✓ Created .env.local (contains admin credentials)');
|
|
395
|
+
|
|
396
|
+
// 3. Create opencode.jsonc for MCP integration
|
|
397
|
+
const opencodeConfig = {
|
|
398
|
+
$schema: 'https://opencode.ai/schemas/opencode.json',
|
|
399
|
+
mcpServers: {
|
|
400
|
+
husar: {
|
|
401
|
+
command: 'npx',
|
|
402
|
+
args: ['@husar.ai/cli', 'mcp'],
|
|
403
|
+
cwd: '.',
|
|
404
|
+
},
|
|
405
|
+
},
|
|
406
|
+
};
|
|
407
|
+
await fs.writeFile(join(projectPath, 'opencode.jsonc'), JSON.stringify(opencodeConfig, null, 2));
|
|
408
|
+
console.log(' ✓ Created opencode.jsonc (MCP configuration)');
|
|
409
|
+
|
|
410
|
+
// 4. Update .gitignore
|
|
411
|
+
const gitignorePath = join(projectPath, '.gitignore');
|
|
412
|
+
try {
|
|
413
|
+
let gitignore = await fs.readFile(gitignorePath, 'utf-8');
|
|
414
|
+
const additions: string[] = [];
|
|
415
|
+
|
|
416
|
+
if (!gitignore.includes('.env.local')) {
|
|
417
|
+
additions.push('.env.local');
|
|
418
|
+
}
|
|
419
|
+
if (!gitignore.includes('.env*.local')) {
|
|
420
|
+
additions.push('.env*.local');
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
if (additions.length > 0) {
|
|
424
|
+
gitignore += '\n# Local environment files (contain secrets)\n' + additions.join('\n') + '\n';
|
|
425
|
+
await fs.writeFile(gitignorePath, gitignore);
|
|
426
|
+
console.log(' ✓ Updated .gitignore');
|
|
427
|
+
}
|
|
428
|
+
} catch {
|
|
429
|
+
// .gitignore doesn't exist, create it
|
|
430
|
+
await fs.writeFile(gitignorePath, '# Local environment files (contain secrets)\n.env.local\n.env*.local\n');
|
|
431
|
+
console.log(' ✓ Created .gitignore');
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
/**
|
|
436
|
+
* Install Husar packages
|
|
437
|
+
*/
|
|
438
|
+
async function installHusarPackages(projectPath: string): Promise<void> {
|
|
439
|
+
return new Promise((resolve) => {
|
|
440
|
+
const packages = ['@husar.ai/ssr', '@husar.ai/render'];
|
|
441
|
+
|
|
442
|
+
const child = spawn('npm', ['install', ...packages], {
|
|
443
|
+
cwd: projectPath,
|
|
444
|
+
stdio: 'inherit',
|
|
445
|
+
shell: true,
|
|
446
|
+
});
|
|
447
|
+
|
|
448
|
+
child.on('close', () => {
|
|
449
|
+
console.log(' ✓ Installed @husar.ai/ssr and @husar.ai/render');
|
|
450
|
+
resolve();
|
|
451
|
+
});
|
|
452
|
+
|
|
453
|
+
child.on('error', (err) => {
|
|
454
|
+
console.error('Warning: Failed to install packages:', err.message);
|
|
455
|
+
console.log(' Run manually: npm install @husar.ai/ssr @husar.ai/render');
|
|
456
|
+
resolve();
|
|
457
|
+
});
|
|
458
|
+
});
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
/**
|
|
462
|
+
* Generate CMS client files using husar generate
|
|
463
|
+
*/
|
|
464
|
+
async function generateCmsClient(projectPath: string, framework: 'nextjs' | 'vite'): Promise<void> {
|
|
465
|
+
// Determine src folder based on framework
|
|
466
|
+
const srcFolder = framework === 'nextjs' ? './src' : './src';
|
|
467
|
+
|
|
468
|
+
return new Promise((resolve) => {
|
|
469
|
+
const child = spawn('npx', ['@husar.ai/cli', 'generate', srcFolder], {
|
|
470
|
+
cwd: projectPath,
|
|
471
|
+
stdio: 'inherit',
|
|
472
|
+
shell: true,
|
|
473
|
+
});
|
|
474
|
+
|
|
475
|
+
child.on('close', (code) => {
|
|
476
|
+
if (code === 0) {
|
|
477
|
+
console.log(' ✓ Generated cms/ folder with Zeus client');
|
|
478
|
+
} else {
|
|
479
|
+
console.log(' ⚠ CMS generation may have failed - you can run "husar generate" later');
|
|
480
|
+
}
|
|
481
|
+
resolve();
|
|
482
|
+
});
|
|
483
|
+
|
|
484
|
+
child.on('error', () => {
|
|
485
|
+
console.log(' ⚠ Could not generate CMS client - run "husar generate" manually');
|
|
486
|
+
resolve();
|
|
487
|
+
});
|
|
488
|
+
});
|
|
489
|
+
}
|