@brezel/installer 1.0.0

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/install.sh ADDED
@@ -0,0 +1,41 @@
1
+ #!/bin/sh
2
+ set -e
3
+
4
+ # Colors for output
5
+ RED='\033[0;31m'
6
+ GREEN='\033[0;32m'
7
+ BLUE='\033[0;34m'
8
+ NC='\033[0m' # No Color
9
+
10
+ echo "${BLUE}🄨 Welcome to the Brezel Installer!${NC}"
11
+
12
+ # Check for Node.js
13
+ if ! command -v node >/dev/null 2>&1; then
14
+ echo "${RED}āŒ Node.js is required but not found.${NC}"
15
+ echo "Please install Node.js 18+ and try again."
16
+ exit 1
17
+ fi
18
+
19
+ # Check for npm
20
+ if ! command -v npm >/dev/null 2>&1; then
21
+ echo "${RED}āŒ npm is required but not found.${NC}"
22
+ exit 1
23
+ fi
24
+
25
+ # In production, we would use npx -y @kibro/brezel-installer@latest
26
+ # For development/demo purposes in this repo:
27
+ if [ -f "package.json" ] && [ -d "src" ]; then
28
+ echo "šŸ“¦ Preparing installer..."
29
+ npm install --silent
30
+ npm run build --silent
31
+ fi
32
+
33
+ if [ -f "./dist/index.js" ]; then
34
+ echo "šŸš€ Launching installer..."
35
+ # Pass all arguments to the TS installer
36
+ node ./dist/index.js "$@"
37
+ else
38
+ # Fallback to npx if no local build is found
39
+ echo "šŸš€ Fetching and launching latest installer..."
40
+ npx -y @brezel/installer@latest "$@"
41
+ fi
package/package.json ADDED
@@ -0,0 +1,43 @@
1
+ {
2
+ "name": "@brezel/installer",
3
+ "version": "1.0.0",
4
+ "description": "Installer for Brezel",
5
+ "main": "dist/index.js",
6
+ "bin": {
7
+ "brezel-installer": "./dist/index.js"
8
+ },
9
+ "scripts": {
10
+ "build": "tsup src/index.ts --format cjs --clean --minify",
11
+ "start": "node dist/index.js",
12
+ "dev": "ts-node src/index.ts"
13
+ },
14
+ "keywords": [],
15
+ "author": "flynamic",
16
+ "license": "MIT",
17
+ "repository": {
18
+ "type": "git",
19
+ "url": "git+https://github.com/brezelio/installer.git"
20
+ },
21
+ "type": "commonjs",
22
+ "devDependencies": {
23
+ "@semantic-release/commit-analyzer": "^13.0.1",
24
+ "@semantic-release/git": "^10.0.1",
25
+ "@semantic-release/github": "^12.0.6",
26
+ "@semantic-release/npm": "^13.1.5",
27
+ "@semantic-release/release-notes-generator": "^14.1.0",
28
+ "@types/fs-extra": "^11.0.4",
29
+ "@types/node": "^25.2.2",
30
+ "@types/prompts": "^2.4.9",
31
+ "chalk": "^5.6.2",
32
+ "commander": "^14.0.3",
33
+ "execa": "^9.6.1",
34
+ "ora": "^9.3.0",
35
+ "prompts": "^2.4.2",
36
+ "semantic-release": "^25.0.3",
37
+ "tsup": "^8.5.1",
38
+ "typescript": "^5.9.3"
39
+ },
40
+ "publishConfig": {
41
+ "access": "public"
42
+ }
43
+ }
package/src/index.ts ADDED
@@ -0,0 +1,538 @@
1
+ #!/usr/bin/env node
2
+ import { Command } from 'commander';
3
+ import chalk from 'chalk';
4
+ import prompts from 'prompts';
5
+ import { execa } from 'execa';
6
+ import fs from 'fs';
7
+ import path from 'path';
8
+ import ora from 'ora';
9
+
10
+ const program = new Command();
11
+
12
+ async function runTask(title: string, cmd: string, args: string[], options: any) {
13
+ const spinner = ora(title).start();
14
+ const subprocess = execa(cmd, args, { ...options, all: true });
15
+
16
+ subprocess.stdout?.on('data', (data) => {
17
+ const line = data.toString().trim().split('\n').pop();
18
+ if (line) {
19
+ spinner.text = `${title} ${chalk.dim(`(${line.substring(0, 60).trim()}...)`)}`;
20
+ }
21
+ });
22
+
23
+ subprocess.stderr?.on('data', (data) => {
24
+ const line = data.toString().trim().split('\n').pop();
25
+ if (line) {
26
+ spinner.text = `${title} ${chalk.dim(`(${line.substring(0, 60).trim()}...)`)}`;
27
+ }
28
+ });
29
+
30
+ try {
31
+ await subprocess;
32
+ spinner.succeed(title);
33
+ } catch (e: any) {
34
+ spinner.fail(`${title} failed.`);
35
+ if (e.all) console.error(chalk.red(e.all));
36
+ process.exit(1);
37
+ }
38
+ }
39
+
40
+ program
41
+ .name('brezel-installer')
42
+ .description('Installer for Brezel (SPA + API)')
43
+ .version('1.0.0')
44
+ .option('-d, --dir <directory>', 'Installation directory', './brezel')
45
+ .option('-m, --mode <mode>', 'Installation mode (native, docker)', 'native')
46
+ .option('-s, --system <name>', 'System name', 'example')
47
+ .option('-u, --url <url>', 'API URL', 'http://brezel-api.test')
48
+ .option('--spa-url <url>', 'SPA URL', 'http://localhost:5173')
49
+ .option('--db-host <host>', 'Database host', '127.0.0.1')
50
+ .option('--db-port <port>', 'Database port', '3306')
51
+ .option('--db-name <name>', 'Database name', 'brezel_meta')
52
+ .option('--db-user <user>', 'Database user', 'root')
53
+ .option('--db-password <password>', 'Database password', '')
54
+ .option('--php-path <path>', 'Path to PHP executable', 'php')
55
+ .option('--gitlab-token <token>', 'GitLab Personal Access Token')
56
+ .option('--no-interactive', 'Run in non-interactive mode')
57
+ .option('--source-mode <mode>', 'Source control mode (clone, fork)', 'clone')
58
+ .option('--components <list>', 'Optional components to install (mariadb, nginx, ssl, cron)', '');
59
+
60
+ const REPO_SKELETON = 'https://github.com/brezelio/brezel.git';
61
+
62
+ program.action(async (options) => {
63
+ console.log(chalk.bold.blue('\n🄨 Welcome to the Brezel Installer!\n'));
64
+
65
+ const checkPhp = async (phpPath: string) => {
66
+ try {
67
+ const { stdout } = await execa(phpPath, ['-r', 'echo PHP_VERSION;']);
68
+ // Use regex to find the version string (e.g. 8.3.1 or 8.4.14) in case of warnings
69
+ const match = stdout.match(/(\d+)\.(\d+)\.(\d+)/);
70
+ if (!match) return null;
71
+
72
+ const major = parseInt(match[1], 10);
73
+ const minor = parseInt(match[2], 10);
74
+ const version = match[0];
75
+
76
+ return {
77
+ version,
78
+ valid: (major > 8 || (major === 8 && minor >= 3))
79
+ };
80
+ } catch (e) {
81
+ return null;
82
+ }
83
+ };
84
+
85
+ // 1. Prerequisites Check
86
+ const spinner = ora('Checking prerequisites...').start();
87
+ const checks: any = {};
88
+
89
+ try {
90
+ // Critical Deps
91
+ for (const dep of ['git', 'node', 'npm']) {
92
+ await execa(dep, ['--version']);
93
+ checks[dep] = true;
94
+ }
95
+
96
+ // PHP version check
97
+ const phpResult = await checkPhp(options.phpPath || 'php');
98
+ if (phpResult) {
99
+ checks.php = phpResult.version;
100
+ checks.phpValid = phpResult.valid;
101
+ } else {
102
+ checks.php = false;
103
+ checks.phpValid = false;
104
+ }
105
+
106
+ // Composer check
107
+ try {
108
+ await execa('composer', ['--version']);
109
+ checks.composer = true;
110
+ } catch (e) {
111
+ checks.composer = false;
112
+ }
113
+
114
+ // Docker check
115
+ try {
116
+ await execa('docker', ['--version']);
117
+ checks.docker = true;
118
+ } catch (e) {
119
+ checks.docker = false;
120
+ }
121
+
122
+ // Valet check (macOS only)
123
+ if (process.platform === 'darwin') {
124
+ try {
125
+ await execa('valet', ['--version']);
126
+ checks.valet = true;
127
+ } catch (e) {
128
+ checks.valet = false;
129
+ }
130
+ }
131
+
132
+ spinner.succeed('System check complete.');
133
+ } catch (error) {
134
+ spinner.fail('Missing critical prerequisites (git, node, or npm).');
135
+ process.exit(1);
136
+ }
137
+
138
+ let responses = options;
139
+
140
+ if (options.interactive !== false) {
141
+ const interactiveResponses = await prompts([
142
+ {
143
+ type: 'select',
144
+ name: 'mode',
145
+ message: 'How do you want to install Brezel?',
146
+ choices: [
147
+ {
148
+ title: `Native (Bare metal) ${!checks.phpValid && checks.php ? chalk.yellow('(PHP 8.3+ required, current: ' + checks.php + ')') : (!checks.php ? chalk.red('(PHP not found)') : '')}`,
149
+ value: 'native',
150
+ disabled: false
151
+ },
152
+ ...(checks.valet ? [{
153
+ title: `Valet (macOS Magic) ${chalk.dim('- handles domain mapping automatically')}`,
154
+ value: 'valet'
155
+ }] : []),
156
+ {
157
+ title: `Docker (Containerized) ${!checks.docker ? chalk.red('(Docker required)') : ''}`,
158
+ value: 'docker',
159
+ disabled: !checks.docker
160
+ }
161
+ ],
162
+ initial: (options.mode === 'valet' && checks.valet) ? 1 : ((options.mode === 'docker' && checks.docker) ? (checks.valet ? 2 : 1) : 0)
163
+ },
164
+ {
165
+ type: (prev, values) => (values.mode === 'native' || values.mode === 'valet') ? 'text' : null,
166
+ name: 'phpPath',
167
+ message: 'Path to PHP 8.3+ executable:',
168
+ initial: options.phpPath || 'php',
169
+ validate: async (val) => {
170
+ const res = await checkPhp(val);
171
+ if (!res) return 'PHP not found at this path.';
172
+ if (!res.valid) return `PHP version ${res.version} is too old. 8.3+ required.`;
173
+ return true;
174
+ }
175
+ },
176
+ {
177
+ type: 'select',
178
+ name: 'sourceMode',
179
+ message: 'Source control mode:',
180
+ choices: [
181
+ { title: 'Clone without history', value: 'clone' },
182
+ { title: 'Fork and clone', value: 'fork' }
183
+ ],
184
+ initial: options.sourceMode === 'fork' ? 1 : 0
185
+ },
186
+ {
187
+ type: (prev) => prev === 'fork' ? 'text' : null,
188
+ name: 'forkUrl',
189
+ message: 'Enter your fork URL (git@...):',
190
+ validate: (v) => v.length > 0 ? true : 'Fork URL is required'
191
+ },
192
+ {
193
+ type: 'text',
194
+ name: 'gitlabToken',
195
+ message: 'GitLab Personal Access Token (for @kibro packages, scope: read_api, read_registry)',
196
+ initial: options.gitlabToken,
197
+ validate: (v) => (v && v.length > 0) ? true : 'Token is required'
198
+ },
199
+ {
200
+ type: 'text',
201
+ name: 'dir',
202
+ message: 'Installation directory:',
203
+ initial: options.dir
204
+ },
205
+ {
206
+ type: 'text',
207
+ name: 'system',
208
+ message: 'System name:',
209
+ initial: options.system
210
+ },
211
+ {
212
+ type: 'text',
213
+ name: 'url',
214
+ message: 'API URL:',
215
+ initial: (prev: any, values: any) => options.url !== 'http://brezel-api.test' ? options.url : `http://${values.system}.test`
216
+ },
217
+ {
218
+ type: 'text',
219
+ name: 'spaUrl',
220
+ message: 'SPA URL:',
221
+ initial: (prev: any, values: any) => {
222
+ if (options.spaUrl !== 'http://localhost:5173') return options.spaUrl;
223
+ if (values.mode === 'valet') return `http://${values.system}.test:5173`;
224
+ return `http://localhost:5173`;
225
+ }
226
+ },
227
+ {
228
+ type: 'multiselect',
229
+ name: 'components',
230
+ message: 'Select optional components to install:',
231
+ choices: [
232
+ { title: 'MariaDB', value: 'mariadb' },
233
+ { title: 'Nginx', value: 'nginx' },
234
+ { title: 'SSL (Certbot)', value: 'ssl' },
235
+ { title: 'Cron jobs', value: 'cron' }
236
+ ],
237
+ initial: (options.components && typeof options.components === 'string')
238
+ ? options.components.split(',').map(c => ['mariadb', 'nginx', 'ssl', 'cron'].indexOf(c))
239
+ : undefined
240
+ },
241
+ {
242
+ type: 'text',
243
+ name: 'dbHost',
244
+ message: 'Database Host',
245
+ initial: options.dbHost
246
+ },
247
+ {
248
+ type: 'text',
249
+ name: 'dbPort',
250
+ message: 'Database Port',
251
+ initial: options.dbPort
252
+ },
253
+ {
254
+ type: 'text',
255
+ name: 'dbName',
256
+ message: 'Database Name',
257
+ initial: options.dbName
258
+ },
259
+ {
260
+ type: 'text',
261
+ name: 'dbUser',
262
+ message: 'Database User',
263
+ initial: options.dbUser
264
+ },
265
+ {
266
+ type: 'password',
267
+ name: 'dbPassword',
268
+ message: 'Database Password',
269
+ initial: options.dbPassword
270
+ }
271
+ ]);
272
+
273
+ responses = { ...options, ...interactiveResponses };
274
+ } else {
275
+ // In non-interactive mode, parse components string into array
276
+ if (typeof responses.components === 'string') {
277
+ responses.components = responses.components.split(',').filter(Boolean);
278
+ }
279
+ }
280
+
281
+ if (!responses.dir) process.exit(1);
282
+
283
+ // Validation after response
284
+ const isNative = responses.mode === 'native' || responses.mode === 'valet';
285
+
286
+ if (isNative) {
287
+ const phpRes = await checkPhp(responses.phpPath);
288
+ if (!phpRes || !phpRes.valid) {
289
+ ora(`Native/Valet mode requires PHP 8.3+, but version ${phpRes?.version || 'none'} was found at ${responses.phpPath}.`).fail();
290
+ process.exit(1);
291
+ }
292
+ if (!checks.composer) {
293
+ ora('Native/Valet mode requires Composer, but it was not found.').fail();
294
+ process.exit(1);
295
+ }
296
+ } else if (responses.mode === 'docker' && !checks.docker) {
297
+ ora('Docker mode requires Docker, but it was not found.').fail();
298
+ process.exit(1);
299
+ }
300
+
301
+ const rootDir = path.resolve(responses.dir);
302
+
303
+ if (!fs.existsSync(rootDir)) {
304
+ const s = ora(`Cloning Brezel skeleton (${responses.sourceMode === 'clone' ? 'no history' : 'fork'})...`).start();
305
+ try {
306
+ const cloneUrl = responses.sourceMode === 'fork' ? responses.forkUrl : REPO_SKELETON;
307
+ const cloneArgs = ['clone'];
308
+ if (responses.sourceMode === 'clone') {
309
+ cloneArgs.push('--depth', '1');
310
+ }
311
+ cloneArgs.push(cloneUrl, rootDir);
312
+
313
+ await execa('git', cloneArgs);
314
+
315
+ if (responses.sourceMode === 'clone') {
316
+ fs.rmSync(path.join(rootDir, '.git'), { recursive: true, force: true });
317
+ await execa('git', ['init'], { cwd: rootDir });
318
+ await execa('git', ['add', '.'], { cwd: rootDir });
319
+ await execa('git', ['commit', '-m', 'Initial commit from Brezel Installer'], { cwd: rootDir });
320
+ }
321
+
322
+ s.succeed('Brezel skeleton cloned.');
323
+ } catch (e) {
324
+ s.fail('Failed to clone repository.');
325
+ console.error(e);
326
+ process.exit(1);
327
+ }
328
+ }
329
+
330
+ // 2. Optional Components (Bare metal only)
331
+ if (responses.mode === 'native' && responses.components && responses.components.length > 0) {
332
+ console.log(chalk.bold.cyan('\nšŸ›  Installing optional components...'));
333
+ for (const component of responses.components) {
334
+ const compSpinner = ora(`Installing ${component}...`).start();
335
+ try {
336
+ if (process.platform === 'linux') {
337
+ if (component === 'mariadb') {
338
+ await execa('sudo', ['apt-get', 'update']);
339
+ await execa('sudo', ['apt-get', 'install', '-y', 'mariadb-server']);
340
+ compSpinner.succeed('MariaDB installed.');
341
+ } else if (component === 'nginx') {
342
+ await execa('sudo', ['apt-get', 'install', '-y', 'nginx']);
343
+ compSpinner.succeed('Nginx installed.');
344
+ } else if (component === 'ssl') {
345
+ await execa('sudo', ['apt-get', 'install', '-y', 'certbot', 'python3-certbot-nginx']);
346
+ compSpinner.succeed('Certbot installed.');
347
+ } else if (component === 'cron') {
348
+ const cronJob = `* * * * * cd ${rootDir} && ${responses.phpPath} bakery schedule:run >> /dev/null 2>&1`;
349
+ compSpinner.info('Cron job suggested: ' + cronJob);
350
+ }
351
+ } else if (process.platform === 'darwin') {
352
+ if (component === 'mariadb') {
353
+ await execa('brew', ['install', 'mariadb']);
354
+ compSpinner.succeed('MariaDB installed via Homebrew.');
355
+ } else if (component === 'nginx') {
356
+ await execa('brew', ['install', 'nginx']);
357
+ compSpinner.succeed('Nginx installed via Homebrew.');
358
+ } else {
359
+ compSpinner.warn(`${component} installation not fully automated for macOS.`);
360
+ }
361
+ } else {
362
+ compSpinner.warn(`${component} installation not supported on this OS.`);
363
+ }
364
+ } catch (e) {
365
+ compSpinner.fail(`Failed to install ${component}.`);
366
+ }
367
+ }
368
+ }
369
+
370
+ if (responses.mode === 'docker') {
371
+ console.log(chalk.bold.cyan('\n🐳 Setting up Docker environment...'));
372
+
373
+ const envExamplePath = path.join(rootDir, '.env.example');
374
+ const envPath = path.join(rootDir, '.env');
375
+
376
+ if (fs.existsSync(envExamplePath)) {
377
+ let envContent = fs.readFileSync(envExamplePath, 'utf-8');
378
+
379
+ envContent = envContent.replace(/APP_URL=.*/, `APP_URL=${responses.url}`);
380
+ envContent = envContent.replace(/VITE_APP_API_URL=.*/, `VITE_APP_API_URL=${responses.url}`);
381
+ envContent = envContent.replace(/VITE_APP_SYSTEM=.*/, `VITE_APP_SYSTEM=${responses.system}`);
382
+ envContent = envContent.replace(/TENANCY_HOST=.*/, `TENANCY_HOST=db`);
383
+ envContent = envContent.replace(/TENANCY_PASSWORD=.*/, `TENANCY_PASSWORD=password`);
384
+
385
+ if (!envContent.includes('APP_SYSTEM=')) {
386
+ envContent += `\nAPP_SYSTEM=${responses.system}\n`;
387
+ } else {
388
+ envContent = envContent.replace(/APP_SYSTEM=.*/, `APP_SYSTEM=${responses.system}`);
389
+ }
390
+
391
+ fs.writeFileSync(envPath, envContent);
392
+ ora('Configured .env for Docker').succeed();
393
+ }
394
+
395
+ await runTask('Building and starting Docker containers', 'docker', ['compose', 'up', '-d', '--build'], {
396
+ cwd: rootDir,
397
+ env: {
398
+ ...process.env,
399
+ COMPOSER_TOKEN: responses.gitlabToken,
400
+ NPM_TOKEN: responses.gitlabToken,
401
+ APP_SYSTEM: responses.system,
402
+ APP_URL: responses.url
403
+ }
404
+ });
405
+
406
+ console.log(chalk.bold.cyan('\n🄐 Initializing Brezel in Docker...'));
407
+ const initSpinner = ora('Waiting for database...').start();
408
+
409
+ await new Promise(r => setTimeout(r, 10000));
410
+
411
+ const runDockerCmd = async (cmd: string[]) => {
412
+ await execa('docker', ['compose', 'exec', 'api', ...cmd], { cwd: rootDir, stdio: 'inherit' });
413
+ };
414
+
415
+ try {
416
+ initSpinner.text = 'Running bakery init...';
417
+ await runDockerCmd(['php', 'bakery', 'init']);
418
+
419
+ console.log(chalk.dim(`Creating system "${responses.system}"...`));
420
+ await runDockerCmd(['php', 'bakery', 'system', 'create', responses.system]);
421
+
422
+ console.log(chalk.dim('Applying system config...'));
423
+ await runDockerCmd(['php', 'bakery', 'apply']);
424
+
425
+ initSpinner.succeed('Brezel initialized in Docker.');
426
+
427
+ } catch (e) {
428
+ initSpinner.fail('Initialization in Docker failed.');
429
+ console.error(e);
430
+ }
431
+
432
+ console.log(chalk.bold.green('\nāœ… Docker Installation complete!'));
433
+ console.log(chalk.white(`
434
+ Services are running:
435
+ API: ${responses.url} (mapped to localhost:8081)
436
+ SPA: ${responses.spaUrl} (mapped to localhost:3000)
437
+
438
+ To stop:
439
+ cd ${rootDir}
440
+ docker compose down
441
+ `));
442
+
443
+ return;
444
+ }
445
+
446
+ // 3. Install Dependencies (Native/Valet Mode)
447
+ if (isNative) {
448
+ console.log(chalk.bold.cyan('\nšŸ“¦ Installing Dependencies...'));
449
+
450
+ await execa('composer', ['config', 'gitlab-token.gitlab.kiwis-and-brownies.de', responses.gitlabToken], { cwd: rootDir });
451
+
452
+ await runTask('Installing PHP dependencies (Composer)', 'composer', ['install', '--no-interaction'], { cwd: rootDir });
453
+
454
+ const npmrcPath = path.join(rootDir, '.npmrc');
455
+ const npmrcContent = `
456
+ @kibro:registry=https://gitlab.kiwis-and-brownies.de/api/v4/packages/npm/
457
+ //gitlab.kiwis-and-brownies.de/api/v4/packages/npm/:_authToken=${responses.gitlabToken}
458
+ `;
459
+ fs.writeFileSync(npmrcPath, npmrcContent);
460
+
461
+ await runTask('Installing Node dependencies (npm)', 'npm', ['install'], { cwd: rootDir });
462
+
463
+ const envExamplePath = path.join(rootDir, '.env.example');
464
+ const envPath = path.join(rootDir, '.env');
465
+ if (fs.existsSync(envExamplePath)) {
466
+ let envContent = fs.readFileSync(envExamplePath, 'utf-8');
467
+
468
+ envContent = envContent.replace(/APP_URL=.*/, `APP_URL=${responses.url}`);
469
+ envContent = envContent.replace(/VITE_APP_API_URL=.*/, `VITE_APP_API_URL=${responses.url}`);
470
+ envContent = envContent.replace(/VITE_APP_SYSTEM=.*/, `VITE_APP_SYSTEM=${responses.system}`);
471
+
472
+ envContent = envContent.replace(/TENANCY_HOST=.*/, `TENANCY_HOST=${responses.dbHost}`);
473
+ envContent = envContent.replace(/TENANCY_PORT=.*/, `TENANCY_PORT=${responses.dbPort}`);
474
+ envContent = envContent.replace(/TENANCY_DATABASE=.*/, `TENANCY_DATABASE=${responses.dbName}`);
475
+ envContent = envContent.replace(/TENANCY_USERNAME=.*/, `TENANCY_USERNAME=${responses.dbUser}`);
476
+ envContent = envContent.replace(/TENANCY_PASSWORD=.*/, `TENANCY_PASSWORD=${responses.dbPassword}`);
477
+
478
+ fs.writeFileSync(envPath, envContent);
479
+ ora('Configured .env').succeed();
480
+ }
481
+
482
+ console.log(chalk.bold.cyan('\n🄐 Initializing Brezel...'));
483
+
484
+ const runBakery = async (args: string[]) => {
485
+ await execa(responses.phpPath, ['bakery', ...args], { cwd: rootDir, stdio: 'inherit' });
486
+ };
487
+
488
+ try {
489
+ console.log(chalk.dim('Running initialization...'));
490
+ await runBakery(['init']);
491
+ console.log(chalk.dim(`Creating system "${responses.system}"...`));
492
+ await runBakery(['system', 'create', responses.system]);
493
+ console.log(chalk.dim('Applying system config...'));
494
+ await runBakery(['apply']);
495
+ } catch (e) {
496
+ console.error(chalk.red('Initialization failed.'));
497
+ console.error(e);
498
+ }
499
+
500
+ await runTask('Building SPA', 'npm', ['run', 'build'], { cwd: rootDir });
501
+
502
+ // Valet specific setup
503
+ if (responses.mode === 'valet') {
504
+ console.log(chalk.bold.cyan('\nšŸŽ© Valet Setup...'));
505
+ try {
506
+ await execa('valet', ['link', responses.system], { cwd: rootDir });
507
+ ora(`Linked ${responses.system}.test to Valet.`).succeed();
508
+
509
+ // Try to secure it
510
+ if (responses.url.startsWith('https://')) {
511
+ await execa('valet', ['secure', responses.system], { cwd: rootDir });
512
+ ora(`Secured ${responses.system}.test with SSL.`).succeed();
513
+ }
514
+ } catch (e) {
515
+ console.warn(chalk.yellow('Valet link/secure failed. You might need to run it manually.'));
516
+ }
517
+ }
518
+ }
519
+
520
+ console.log(chalk.bold.cyan('\nšŸ“¦ Installing Export Services...'));
521
+ try {
522
+ await execa('npx', ['@kibro/export-installer@latest'], { stdio: 'inherit' });
523
+ } catch (e) {
524
+ console.warn(chalk.yellow('Export services installer finished with warning or error.'));
525
+ }
526
+
527
+ console.log(chalk.bold.green('\nāœ… Installation complete!'));
528
+ console.log(chalk.white(`
529
+ To start the server (API + SPA dev):
530
+ cd ${responses.dir}
531
+ npm run dev
532
+
533
+ For Windows users:
534
+ bin\\serve_on_windows.ps1
535
+ `));
536
+ });
537
+
538
+ program.parse();