chadstart 1.0.4 → 1.0.6

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/admin/index.html CHANGED
@@ -261,6 +261,7 @@
261
261
  <button onclick="showConfigTab('files')" id="cfg-tab-files" class="cfg-tab px-3 py-1.5 text-xs rounded border border-cs-border text-cs-muted hover:text-cs-text" style="transition:color 150ms,background 150ms;" data-i18n="config.tabs.files">Files &amp; Uploads</button>
262
262
  <button onclick="showConfigTab('settings')" id="cfg-tab-settings" class="cfg-tab px-3 py-1.5 text-xs rounded border border-cs-border text-cs-muted hover:text-cs-text" style="transition:color 150ms,background 150ms;" data-i18n="config.tabs.settings">Settings</button>
263
263
  <button onclick="showConfigTab('all')" id="cfg-tab-all" class="cfg-tab px-3 py-1.5 text-xs rounded border border-cs-border text-cs-muted hover:text-cs-text" style="transition:color 150ms,background 150ms;" data-i18n="config.tabs.all">All</button>
264
+ <span id="cfg-format-badge" class="ml-auto px-2 py-1 text-xs rounded font-mono" style="background:var(--color-cs-border);color:var(--color-cs-muted);" title="Config format">YAML</span>
264
265
  </div>
265
266
  <p id="cfg-section-desc" class="text-xs mb-3" style="color:var(--color-cs-muted);"></p>
266
267
  <div id="cfg-form" class="hidden mb-6"></div>
@@ -674,6 +675,8 @@
674
675
  // Config editor
675
676
  let configMode = false;
676
677
  let configData = null;
678
+ let configFormat = 'yaml';
679
+ let configWritable = true;
677
680
  let configActiveTab = 'general';
678
681
  let _cfgEntityId = 0, _cfgEndpointId = 0, _cfgFileId = 0, _cfgRlId = 0, _cfgTriggerId = 0;
679
682
 
@@ -1706,13 +1709,36 @@ const _LBL = 'block text-xs mb-1';
1706
1709
 
1707
1710
  async function loadConfig() {
1708
1711
  try {
1709
- const r = await apiFetch('/admin/config');
1712
+ const [r, infoRes] = await Promise.all([
1713
+ apiFetch('/admin/config'),
1714
+ apiFetch('/admin/config-info'),
1715
+ ]);
1710
1716
  if (!r.ok) throw new Error('Failed to load config');
1711
1717
  configData = await r.json();
1718
+ if (infoRes.ok) {
1719
+ const info = await infoRes.json();
1720
+ configFormat = info.format || 'yaml';
1721
+ configWritable = info.writable !== false;
1722
+ }
1723
+ updateConfigFormatBadge();
1712
1724
  showConfigTab('general');
1713
1725
  } catch (e) { toast(e.message, 'error'); }
1714
1726
  }
1715
1727
 
1728
+ function updateConfigFormatBadge() {
1729
+ const badge = document.getElementById('cfg-format-badge');
1730
+ if (!badge) return;
1731
+ badge.textContent = configFormat.toUpperCase();
1732
+ badge.title = configWritable
1733
+ ? 'Config format: ' + configFormat
1734
+ : 'Config format: ' + configFormat + ' (read-only)';
1735
+ const saveBtn = document.getElementById('cfg-save-btn');
1736
+ if (saveBtn) {
1737
+ saveBtn.disabled = !configWritable;
1738
+ saveBtn.title = configWritable ? '' : 'Saving is not supported for ' + configFormat + ' configs. Convert to YAML, JSON, or JSON5 to enable saving.';
1739
+ }
1740
+ }
1741
+
1716
1742
  function showConfigTab(tab) {
1717
1743
  configActiveTab = tab;
1718
1744
  document.querySelectorAll('.cfg-tab').forEach(b => b.classList.remove('active'));
@@ -1,4 +1,4 @@
1
- # chadstart.example.yml
1
+ # chadstart.example.yaml
2
2
  # ─────────────────────────────────────────────────────────────────────────────
3
3
  # Reference file that lists every available option in a ChadStart YAML config.
4
4
  # Copy this file to chadstart.yaml and remove / adjust the sections you need.
@@ -415,6 +415,28 @@ sentry:
415
415
  tracesSampleRate: 1.0 # Fraction of transactions to sample (0.0–1.0)
416
416
  debug: false # Enable Sentry SDK debug logging
417
417
 
418
+ # ── Email / SMTP ──────────────────────────────────────────────────────────────
419
+ # Configure SMTP for transactional emails (verification, password reset, etc.).
420
+ # The SMTP password is a secret and must be provided via the SMTP_PASS env var.
421
+ # All fields can be overridden by environment variables:
422
+ # SMTP_HOST, SMTP_PORT, SMTP_USER, SMTP_PASS, SMTP_FROM
423
+
424
+ email:
425
+ host: smtp.example.com # SMTP server hostname. Overridable via SMTP_HOST
426
+ port: 587 # SMTP port (587=STARTTLS, 465=SSL, 25=plain). Overridable via SMTP_PORT
427
+ username: noreply@example.com # SMTP login. Overridable via SMTP_USER
428
+ from: "My App <noreply@example.com>" # Default sender. Overridable via SMTP_FROM
429
+ secure: false # Use TLS (auto-detected from port if omitted)
430
+ templates:
431
+ verification:
432
+ subject: "Verify your email for {{appName}}"
433
+ text: "Hi {{name}},\n\nPlease verify your email by visiting:\n{{link}}\n\nThanks,\n{{appName}}"
434
+ html: "<h2>Verify your email</h2><p>Hi {{name}},</p><p>Please verify your email by clicking the link below:</p><p><a href=\"{{link}}\">Verify Email</a></p><p>Thanks,<br>{{appName}}</p>"
435
+ passwordReset:
436
+ subject: "Reset your password for {{appName}}"
437
+ text: "Hi {{name}},\n\nYou requested a password reset. Visit:\n{{link}}\n\nIf you didn't request this, ignore this email.\n\nThanks,\n{{appName}}"
438
+ html: "<h2>Reset your password</h2><p>Hi {{name}},</p><p>You requested a password reset. Click the link below:</p><p><a href=\"{{link}}\">Reset Password</a></p><p>If you didn't request this, ignore this email.</p><p>Thanks,<br>{{appName}}</p>"
439
+
418
440
  # ── OAuth / Social Login ─────────────────────────────────────────────────────
419
441
  # Powered by the "grant" library — supports 200+ OAuth providers.
420
442
  # Secrets (client keys / secrets) MUST be set via environment variables:
@@ -125,6 +125,78 @@
125
125
  }
126
126
  }
127
127
  },
128
+ "logs": {
129
+ "type": "object",
130
+ "description": "Request logging configuration. Logs are stored in a _cs_logs system table and accessible via GET /admin/logs.",
131
+ "additionalProperties": false,
132
+ "properties": {
133
+ "retention": {
134
+ "type": "integer",
135
+ "default": 30,
136
+ "description": "Number of days to keep log entries. 0 = keep forever. Default: 30."
137
+ },
138
+ "exclude": {
139
+ "type": "array",
140
+ "items": { "type": "string" },
141
+ "description": "URL path prefixes to exclude from logging (e.g. ['/health', '/admin/vendor'])."
142
+ }
143
+ }
144
+ },
145
+ "backup": {
146
+ "type": "object",
147
+ "description": "Database backup configuration. Backups are created via POST /admin/backup or `npx chadstart backup`.",
148
+ "additionalProperties": false,
149
+ "properties": {
150
+ "dir": {
151
+ "type": "string",
152
+ "default": "backups",
153
+ "description": "Directory to store backup files. Overridable via BACKUP_DIR env var. Default: backups."
154
+ }
155
+ }
156
+ },
157
+ "email": {
158
+ "type": "object",
159
+ "description": "Email / SMTP configuration for sending transactional emails (verification, password reset, notifications). The SMTP password must be supplied via the SMTP_PASS environment variable — never put it here. All fields can be overridden by SMTP_HOST, SMTP_PORT, SMTP_USER, SMTP_FROM env vars.",
160
+ "additionalProperties": false,
161
+ "properties": {
162
+ "host": {
163
+ "type": "string",
164
+ "description": "SMTP server hostname (e.g. smtp.gmail.com). Overridable via SMTP_HOST env var."
165
+ },
166
+ "port": {
167
+ "type": "integer",
168
+ "default": 587,
169
+ "description": "SMTP server port. Common values: 587 (STARTTLS), 465 (SSL/TLS), 25 (unencrypted). Overridable via SMTP_PORT env var."
170
+ },
171
+ "username": {
172
+ "type": "string",
173
+ "description": "SMTP login username. Overridable via SMTP_USER env var."
174
+ },
175
+ "from": {
176
+ "type": "string",
177
+ "description": "Default sender address (e.g. 'My App <noreply@example.com>'). Overridable via SMTP_FROM env var."
178
+ },
179
+ "secure": {
180
+ "type": "boolean",
181
+ "description": "Use TLS when connecting to the server. Defaults to true for port 465, false otherwise."
182
+ },
183
+ "templates": {
184
+ "type": "object",
185
+ "description": "Email templates with {{variable}} placeholders. Used by built-in flows (verification, password reset).",
186
+ "additionalProperties": false,
187
+ "properties": {
188
+ "verification": {
189
+ "$ref": "#/$defs/emailTemplate",
190
+ "description": "Template for email verification messages."
191
+ },
192
+ "passwordReset": {
193
+ "$ref": "#/$defs/emailTemplate",
194
+ "description": "Template for password reset messages."
195
+ }
196
+ }
197
+ }
198
+ }
199
+ },
128
200
  "oauth": {
129
201
  "type": "object",
130
202
  "description": "OAuth / social login configuration powered by the grant library. Secrets (client keys and secrets) must be supplied via OAUTH_<PROVIDER>_KEY and OAUTH_<PROVIDER>_SECRET environment variables.",
@@ -292,6 +364,7 @@
292
364
  "type": "object",
293
365
  "properties": {
294
366
  "authenticable": { "type": "boolean", "default": false, "description": "Makes this entity authenticable (adds email + password fields, enables login/signup)." },
367
+ "requireEmailVerification": { "type": "boolean", "default": false, "description": "When true, login is blocked until the user verifies their email address." },
295
368
  "single": { "type": "boolean", "default": false, "description": "Single entity — only one record exists (no create/delete)." },
296
369
  "mainProp": { "type": "string", "description": "Identifier property used in the admin panel." },
297
370
  "nameSingular": { "type": "string" },
@@ -400,6 +473,16 @@
400
473
  "ttl": { "type": "integer", "description": "Time window in milliseconds." }
401
474
  }
402
475
  },
476
+ "emailTemplate": {
477
+ "type": "object",
478
+ "description": "An email template with subject, text body, and optional HTML body. Supports {{variable}} placeholders.",
479
+ "additionalProperties": false,
480
+ "properties": {
481
+ "subject": { "type": "string", "description": "Email subject line. Supports {{appName}}, {{name}}, {{link}} placeholders." },
482
+ "text": { "type": "string", "description": "Plain-text email body. Supports {{appName}}, {{name}}, {{link}} placeholders." },
483
+ "html": { "type": "string", "description": "HTML email body. Supports {{appName}}, {{name}}, {{link}} placeholders." }
484
+ }
485
+ },
403
486
  "oauthProvider": {
404
487
  "type": "object",
405
488
  "description": "Configuration for a single OAuth provider. The key and secret should be set via OAUTH_<PROVIDER>_KEY and OAUTH_<PROVIDER>_SECRET environment variables.",
package/cli/cli.js CHANGED
@@ -7,23 +7,26 @@ const fs = require('fs');
7
7
 
8
8
  const args = process.argv.slice(2);
9
9
  const command = args[0];
10
- const DEFAULT_YAML = 'chadstart.yaml';
10
+ const DEFAULT_CONFIG = 'chadstart.yaml';
11
11
 
12
12
  function printUsage() {
13
13
  console.log(`
14
- ChadStart - YAML-first Backend as a Service
14
+ ChadStart - Config-driven Backend as a Service
15
15
 
16
16
  Usage:
17
- npx chadstart dev Start server with hot-reload on YAML changes
17
+ npx chadstart dev Start server with hot-reload on config changes
18
18
  npx chadstart start Start server (production mode)
19
- npx chadstart build Validate YAML config and print schema summary
19
+ npx chadstart build Validate config and print schema summary
20
20
  npx chadstart seed Seed the database with dummy data
21
21
  npx chadstart migrate Run pending database migrations
22
- npx chadstart migrate:generate Generate migration from YAML diff (git-based)
22
+ npx chadstart migrate:generate Generate migration from config diff (git-based)
23
23
  npx chadstart migrate:status Show current migration status
24
+ npx chadstart backup Create a database backup
25
+ npx chadstart restore <file> Restore database from a backup file
24
26
 
25
27
  Options:
26
- --config <file> Path to YAML config (default: chadstart.yaml)
28
+ --config <file> Path to config file (default: auto-discover)
29
+ Supported formats: yaml, json, json5, jsonnet, js
27
30
  --port <number> Override port from config
28
31
  --migrations-dir <dir> Path to migrations directory (default: migrations)
29
32
  --description <text> Description for generated migration
@@ -31,6 +34,7 @@ Options:
31
34
  Examples:
32
35
  npx chadstart dev
33
36
  npx chadstart dev --config my-backend.yaml
37
+ npx chadstart dev --config chadstart.json
34
38
  npx chadstart start --port 8080
35
39
  npx chadstart migrate:generate --description add-posts-table
36
40
  npx chadstart migrate
@@ -42,7 +46,16 @@ function getOption(flag) {
42
46
  return idx !== -1 ? args[idx + 1] : null;
43
47
  }
44
48
 
45
- const yamlPath = path.resolve(getOption('--config') || process.env.CHADSTART_FILE_PATH || DEFAULT_YAML);
49
+ function resolveConfigPath() {
50
+ const explicit = getOption('--config') || process.env.CHADSTART_FILE_PATH;
51
+ if (explicit) return path.resolve(explicit);
52
+ // Auto-discover: try each supported filename in priority order
53
+ const { discoverConfigFile } = require('../core/config-loader');
54
+ const found = discoverConfigFile(process.cwd());
55
+ return found || path.resolve(DEFAULT_CONFIG); // fall back to default for error messages
56
+ }
57
+
58
+ const configPath = resolveConfigPath();
46
59
  const portOverride = getOption('--port');
47
60
 
48
61
  if (!command || command === 'help' || command === '--help' || command === '-h') {
@@ -66,6 +79,10 @@ if (command === 'create') {
66
79
  runMigrateGenerate();
67
80
  } else if (command === 'migrate:status') {
68
81
  runMigrateStatus();
82
+ } else if (command === 'backup') {
83
+ runBackup();
84
+ } else if (command === 'restore') {
85
+ runRestore();
69
86
  } else {
70
87
  console.error(`Unknown command: ${command}`);
71
88
  printUsage();
@@ -119,19 +136,19 @@ async function runCreate() {
119
136
  }
120
137
 
121
138
  async function runSeed() {
122
- if (!fs.existsSync(yamlPath)) {
123
- console.error(`Config not found: ${yamlPath}`);
139
+ if (!fs.existsSync(configPath)) {
140
+ console.error(`Config not found: ${configPath}`);
124
141
  process.exit(1);
125
142
  }
126
143
 
127
144
  try {
128
- const { loadYaml } = require('../core/yaml-loader');
145
+ const { loadConfig } = require('../core/config-loader');
129
146
  const { validateSchema } = require('../core/schema-validator');
130
147
  const { buildCore } = require('../core/entity-engine');
131
148
  const { initDb } = require('../core/db');
132
149
  const { seedAll } = require('../core/seeder');
133
150
 
134
- const config = loadYaml(yamlPath);
151
+ const config = loadConfig(configPath);
135
152
  validateSchema(config);
136
153
  const core = buildCore(config);
137
154
  initDb(core);
@@ -158,19 +175,19 @@ async function runSeed() {
158
175
  }
159
176
 
160
177
  async function runStart() {
161
- if (!fs.existsSync(yamlPath)) {
162
- console.error(`Config not found: ${yamlPath}`);
178
+ if (!fs.existsSync(configPath)) {
179
+ console.error(`Config not found: ${configPath}`);
163
180
  process.exit(1);
164
181
  }
165
182
 
166
183
  applyPortOverride();
167
184
  const { startServer } = require('../server/express-server');
168
- await startServer(yamlPath);
185
+ await startServer(configPath);
169
186
  }
170
187
 
171
188
  async function runDev() {
172
- if (!fs.existsSync(yamlPath)) {
173
- console.error(`Config not found: ${yamlPath}`);
189
+ if (!fs.existsSync(configPath)) {
190
+ console.error(`Config not found: ${configPath}`);
174
191
  process.exit(1);
175
192
  }
176
193
 
@@ -186,7 +203,7 @@ async function runDev() {
186
203
  // Re-require fresh server module on each reload
187
204
  clearRequireCache();
188
205
  const { startServer } = require('../server/express-server');
189
- const result = await startServer(yamlPath);
206
+ const result = await startServer(configPath);
190
207
  currentServer = result.server;
191
208
  } catch (err) {
192
209
  console.error('[dev] Failed to start server:', err.message);
@@ -198,12 +215,12 @@ async function runDev() {
198
215
  try {
199
216
  const chokidar = require('chokidar');
200
217
  // Watch YAML config
201
- const watcher = chokidar.watch(yamlPath, { ignoreInitial: true });
218
+ const watcher = chokidar.watch(configPath, { ignoreInitial: true });
202
219
  watcher.on('change', async () => {
203
- console.log(`\n[dev] ${path.basename(yamlPath)} changed — restarting...\n`);
220
+ console.log(`\n[dev] ${path.basename(configPath)} changed — restarting...\n`);
204
221
  await boot();
205
222
  });
206
- console.log(`[dev] Watching ${yamlPath} for changes...\n`);
223
+ console.log(`[dev] Watching ${configPath} for changes...\n`);
207
224
 
208
225
  // Watch the functions folder for hot reload of function files
209
226
  const functionsDir = path.resolve(process.env.CHADSTART_FUNCTIONS_FOLDER || 'functions');
@@ -228,17 +245,17 @@ async function runDev() {
228
245
  }
229
246
 
230
247
  function runBuild() {
231
- if (!fs.existsSync(yamlPath)) {
232
- console.error(`Config not found: ${yamlPath}`);
248
+ if (!fs.existsSync(configPath)) {
249
+ console.error(`Config not found: ${configPath}`);
233
250
  process.exit(1);
234
251
  }
235
252
 
236
253
  try {
237
- const { loadYaml } = require('../core/yaml-loader');
254
+ const { loadConfig } = require('../core/config-loader');
238
255
  const { validateSchema } = require('../core/schema-validator');
239
256
  const { buildCore } = require('../core/entity-engine');
240
257
 
241
- const config = loadYaml(yamlPath);
258
+ const config = loadConfig(configPath);
242
259
  validateSchema(config);
243
260
  const core = buildCore(config);
244
261
 
@@ -290,20 +307,20 @@ const migrationsDir = path.resolve(getOption('--migrations-dir') || 'migrations'
290
307
  const migrationDescription = getOption('--description') || null;
291
308
 
292
309
  async function runMigrate() {
293
- if (!fs.existsSync(yamlPath)) {
294
- console.error(`Config not found: ${yamlPath}`);
310
+ if (!fs.existsSync(configPath)) {
311
+ console.error(`Config not found: ${configPath}`);
295
312
  process.exit(1);
296
313
  }
297
314
 
298
315
  try {
299
- const { loadYaml } = require('../core/yaml-loader');
316
+ const { loadConfig } = require('../core/config-loader');
300
317
  const { validateSchema } = require('../core/schema-validator');
301
318
  const { buildCore } = require('../core/entity-engine');
302
319
  const { initDb, closeDb } = require('../core/db');
303
320
  const { runMigrations, buildExecQueryFn } = require('../core/migrations');
304
321
  const dbModule = require('../core/db');
305
322
 
306
- const config = loadYaml(yamlPath);
323
+ const config = loadConfig(configPath);
307
324
  validateSchema(config);
308
325
  const core = buildCore(config);
309
326
  await initDb(core);
@@ -331,8 +348,8 @@ async function runMigrate() {
331
348
  }
332
349
 
333
350
  async function runMigrateGenerate() {
334
- if (!fs.existsSync(yamlPath)) {
335
- console.error(`Config not found: ${yamlPath}`);
351
+ if (!fs.existsSync(configPath)) {
352
+ console.error(`Config not found: ${configPath}`);
336
353
  process.exit(1);
337
354
  }
338
355
 
@@ -341,7 +358,7 @@ async function runMigrateGenerate() {
341
358
 
342
359
  console.log('\n📝 Generating migration from YAML diff...\n');
343
360
 
344
- const result = generateMigration(yamlPath, migrationsDir, migrationDescription);
361
+ const result = generateMigration(configPath, migrationsDir, migrationDescription);
345
362
 
346
363
  if (result.isEmpty) {
347
364
  console.log(' ℹ️ No schema changes detected — nothing to generate.\n');
@@ -358,20 +375,20 @@ async function runMigrateGenerate() {
358
375
  }
359
376
 
360
377
  async function runMigrateStatus() {
361
- if (!fs.existsSync(yamlPath)) {
362
- console.error(`Config not found: ${yamlPath}`);
378
+ if (!fs.existsSync(configPath)) {
379
+ console.error(`Config not found: ${configPath}`);
363
380
  process.exit(1);
364
381
  }
365
382
 
366
383
  try {
367
- const { loadYaml } = require('../core/yaml-loader');
384
+ const { loadConfig } = require('../core/config-loader');
368
385
  const { validateSchema } = require('../core/schema-validator');
369
386
  const { buildCore } = require('../core/entity-engine');
370
387
  const { initDb, closeDb } = require('../core/db');
371
388
  const { getMigrationStatus, buildExecQueryFn } = require('../core/migrations');
372
389
  const dbModule = require('../core/db');
373
390
 
374
- const config = loadYaml(yamlPath);
391
+ const config = loadConfig(configPath);
375
392
  validateSchema(config);
376
393
  const core = buildCore(config);
377
394
  await initDb(core);
@@ -402,6 +419,78 @@ async function runMigrateStatus() {
402
419
 
403
420
  // ─── Other helpers ───────────────────────────────────────────────────────────
404
421
 
422
+ async function runBackup() {
423
+ if (!fs.existsSync(configPath)) {
424
+ console.error(`Config not found: ${configPath}`);
425
+ process.exit(1);
426
+ }
427
+
428
+ try {
429
+ const { loadConfig } = require('../core/config-loader');
430
+ const { validateSchema } = require('../core/schema-validator');
431
+ const { buildCore } = require('../core/entity-engine');
432
+ const { initDb, closeDb } = require('../core/db');
433
+ const { createBackup } = require('../core/backup');
434
+
435
+ const config = loadConfig(configPath);
436
+ validateSchema(config);
437
+ const core = buildCore(config);
438
+ await initDb(core);
439
+
440
+ console.log('\n💾 Creating backup...\n');
441
+ const result = await createBackup(core.backup);
442
+ console.log(` ✅ Backup created: ${result.file}`);
443
+ console.log(` Path: ${result.path}`);
444
+ console.log(` Size: ${(result.size / 1024).toFixed(1)} KB`);
445
+ console.log(` Engine: ${result.engine}\n`);
446
+
447
+ await closeDb();
448
+ } catch (err) {
449
+ console.error(`\n❌ ${err.message}\n`);
450
+ process.exit(1);
451
+ }
452
+ }
453
+
454
+ async function runRestore() {
455
+ const backupFile = args[1];
456
+ if (!backupFile) {
457
+ console.error('Error: backup file name is required. Usage: npx chadstart restore <file>');
458
+ process.exit(1);
459
+ }
460
+
461
+ if (!fs.existsSync(configPath)) {
462
+ console.error(`Config not found: ${configPath}`);
463
+ process.exit(1);
464
+ }
465
+
466
+ try {
467
+ const { loadConfig } = require('../core/config-loader');
468
+ const { validateSchema } = require('../core/schema-validator');
469
+ const { buildCore } = require('../core/entity-engine');
470
+ const { initDb } = require('../core/db');
471
+ const { restoreBackup } = require('../core/backup');
472
+
473
+ const config = loadConfig(configPath);
474
+ validateSchema(config);
475
+ const core = buildCore(config);
476
+ await initDb(core);
477
+
478
+ console.log(`\n🔄 Restoring from ${backupFile}...\n`);
479
+ const result = await restoreBackup(backupFile, core.backup);
480
+ if (result.success) {
481
+ console.log(` ✅ ${result.message}\n`);
482
+ } else {
483
+ console.error(` ❌ ${result.message}\n`);
484
+ process.exit(1);
485
+ }
486
+ } catch (err) {
487
+ console.error(`\n❌ ${err.message}\n`);
488
+ process.exit(1);
489
+ }
490
+ }
491
+
492
+ // ─── Other helpers ───────────────────────────────────────────────────────────
493
+
405
494
  function applyPortOverride() {
406
495
  if (portOverride) {
407
496
  process.env.CHADSTART_PORT = portOverride;