clawfire 0.6.8 → 0.6.9

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/dist/cli.js CHANGED
@@ -269,7 +269,7 @@ async function runDevServer() {
269
269
  const port = portArg ? parseInt(portArg.split("=")[1], 10) : 3e3;
270
270
  const apiPort = apiPortArg ? parseInt(apiPortArg.split("=")[1], 10) : 3456;
271
271
  const noHotReload = args.includes("--no-hot-reload");
272
- const { startDevServer } = await import("./dev-server-ESMIOA6C.js");
272
+ const { startDevServer } = await import("./dev-server-6BALG4FC.js");
273
273
  await startDevServer({
274
274
  projectDir,
275
275
  port,
@@ -2956,7 +2956,7 @@ var FirebaseSetup = class {
2956
2956
  return { success: true, message: `Web app "${displayName}" selected.` };
2957
2957
  }
2958
2958
  // ─── Deploy ─────────────────────────────────────────────────────────
2959
- async deployHosting() {
2959
+ async deployHosting(targets = "hosting") {
2960
2960
  const firebaseJsonPath = resolve4(this.projectDir, "firebase.json");
2961
2961
  if (!existsSync5(firebaseJsonPath)) {
2962
2962
  return { success: false, message: "firebase.json not found. Enable hosting first." };
@@ -2969,12 +2969,12 @@ var FirebaseSetup = class {
2969
2969
  } catch {
2970
2970
  return { success: false, message: "Invalid firebase.json." };
2971
2971
  }
2972
+ const timeoutMs = targets.includes("functions") ? 3e5 : 12e4;
2972
2973
  try {
2973
2974
  const output = await this.execTimeout(
2974
2975
  "firebase",
2975
- ["deploy", "--only", "hosting", "--json"],
2976
- 12e4
2977
- // 2 min timeout
2976
+ ["deploy", "--only", targets, "--json"],
2977
+ timeoutMs
2978
2978
  );
2979
2979
  let url = "";
2980
2980
  try {
@@ -2997,12 +2997,111 @@ var FirebaseSetup = class {
2997
2997
  url = `https://${state.projectId}.web.app`;
2998
2998
  }
2999
2999
  }
3000
- return { success: true, url: url || void 0, message: "Hosting deployed successfully!" };
3000
+ const label = targets.includes("functions") ? "Hosting + Functions" : "Hosting";
3001
+ return { success: true, url: url || void 0, message: `${label} deployed successfully!` };
3001
3002
  } catch (err) {
3002
3003
  const msg = err instanceof Error ? err.message : "Unknown error";
3003
3004
  return { success: false, message: `Deploy failed: ${msg}` };
3004
3005
  }
3005
3006
  }
3007
+ // ─── Full Deploy Pipeline ────────────────────────────────────────────
3008
+ /**
3009
+ * Sync environment variables to functions/.env for production.
3010
+ * Copies project root .env vars + injects CLAWFIRE_FIREBASE_CONFIG.
3011
+ */
3012
+ syncEnvToFunctions(firebaseConfig) {
3013
+ const functionsDir = resolve4(this.projectDir, "functions");
3014
+ if (!existsSync5(functionsDir)) {
3015
+ return { success: false, message: "functions/ directory not found.", count: 0 };
3016
+ }
3017
+ const lines = [];
3018
+ const rootEnvPath = resolve4(this.projectDir, ".env");
3019
+ if (existsSync5(rootEnvPath)) {
3020
+ const content = readFileSync4(rootEnvPath, "utf-8");
3021
+ for (const rawLine of content.split("\n")) {
3022
+ let trimmed = rawLine.trim();
3023
+ if (!trimmed || trimmed.startsWith("#")) continue;
3024
+ if (trimmed.startsWith("export ")) {
3025
+ trimmed = trimmed.slice(7);
3026
+ }
3027
+ const eqIdx = trimmed.indexOf("=");
3028
+ if (eqIdx === -1) continue;
3029
+ const key = trimmed.slice(0, eqIdx).trim();
3030
+ if (key === "NPM_TOKEN" || key === "NODE_ENV") continue;
3031
+ lines.push(trimmed);
3032
+ }
3033
+ }
3034
+ if (firebaseConfig && Object.keys(firebaseConfig).length > 0) {
3035
+ const filtered = lines.filter((l) => !l.startsWith("CLAWFIRE_FIREBASE_CONFIG="));
3036
+ filtered.push(`CLAWFIRE_FIREBASE_CONFIG=${JSON.stringify(firebaseConfig)}`);
3037
+ lines.length = 0;
3038
+ lines.push(...filtered);
3039
+ }
3040
+ const functionsEnvPath = resolve4(functionsDir, ".env");
3041
+ writeFileSync3(functionsEnvPath, lines.join("\n") + "\n", "utf-8");
3042
+ return { success: true, message: `Synced ${lines.length} env vars to functions/.env`, count: lines.length };
3043
+ }
3044
+ /**
3045
+ * Ensure firebase.json has functions configuration.
3046
+ */
3047
+ ensureFunctionsConfig() {
3048
+ const firebaseJsonPath = resolve4(this.projectDir, "firebase.json");
3049
+ if (!existsSync5(firebaseJsonPath)) {
3050
+ return { success: false, message: "firebase.json not found." };
3051
+ }
3052
+ try {
3053
+ const config = JSON.parse(readFileSync4(firebaseJsonPath, "utf-8"));
3054
+ if (!config.functions) {
3055
+ config.functions = {
3056
+ source: "functions",
3057
+ runtime: "nodejs20",
3058
+ codebase: "clawfire"
3059
+ };
3060
+ writeFileSync3(firebaseJsonPath, JSON.stringify(config, null, 2) + "\n", "utf-8");
3061
+ return { success: true, message: "Functions config added to firebase.json." };
3062
+ }
3063
+ return { success: true, message: "Functions config already present." };
3064
+ } catch {
3065
+ return { success: false, message: "Invalid firebase.json." };
3066
+ }
3067
+ }
3068
+ /**
3069
+ * Install dependencies and build functions for deployment.
3070
+ */
3071
+ async buildFunctions() {
3072
+ const functionsDir = resolve4(this.projectDir, "functions");
3073
+ if (!existsSync5(functionsDir)) {
3074
+ return { success: false, message: "functions/ directory not found." };
3075
+ }
3076
+ const pkgPath = resolve4(functionsDir, "package.json");
3077
+ if (!existsSync5(pkgPath)) {
3078
+ return { success: false, message: "functions/package.json not found." };
3079
+ }
3080
+ const nodeModulesPath = resolve4(functionsDir, "node_modules");
3081
+ if (!existsSync5(nodeModulesPath)) {
3082
+ try {
3083
+ await this.execTimeout("npm", ["install"], 12e4, functionsDir);
3084
+ } catch (err) {
3085
+ const msg = err instanceof Error ? err.message : "Unknown error";
3086
+ return { success: false, message: `npm install in functions/ failed: ${msg}` };
3087
+ }
3088
+ }
3089
+ try {
3090
+ const pkg = JSON.parse(readFileSync4(pkgPath, "utf-8"));
3091
+ if (pkg.scripts?.build) {
3092
+ await this.execTimeout("npm", ["run", "build"], 6e4, functionsDir);
3093
+ } else {
3094
+ const tsconfigPath = resolve4(functionsDir, "tsconfig.json");
3095
+ if (existsSync5(tsconfigPath)) {
3096
+ await this.execTimeout("npx", ["tsc"], 6e4, functionsDir);
3097
+ }
3098
+ }
3099
+ return { success: true, message: "Functions built successfully." };
3100
+ } catch (err) {
3101
+ const msg = err instanceof Error ? err.message : "Unknown error";
3102
+ return { success: false, message: `Functions build failed: ${msg}` };
3103
+ }
3104
+ }
3006
3105
  // ─── Service Enable ────────────────────────────────────────────────
3007
3106
  enableService(service) {
3008
3107
  const firebaseJsonPath = resolve4(this.projectDir, "firebase.json");
@@ -3251,9 +3350,9 @@ https://console.developers.google.com/apis/api/firestore.googleapis.com/overview
3251
3350
  }
3252
3351
  }
3253
3352
  // ─── Helpers ───────────────────────────────────────────────────────
3254
- execTimeout(command, args, timeoutMs) {
3353
+ execTimeout(command, args, timeoutMs, cwd) {
3255
3354
  return new Promise((resolve6, reject) => {
3256
- const proc = execFile2(command, args, { cwd: this.projectDir, timeout: timeoutMs }, (err, stdout, stderr) => {
3355
+ const proc = execFile2(command, args, { cwd: cwd || this.projectDir, timeout: timeoutMs }, (err, stdout, stderr) => {
3257
3356
  if (err) {
3258
3357
  const detail = stderr?.trim() || stdout?.trim() || "";
3259
3358
  const enriched = new Error(`${err.message}${detail ? "\n" + detail : ""}`);
@@ -4416,22 +4515,27 @@ ${liveReloadScript}
4416
4515
  if (url.pathname === "/__dev/deploy/hosting" && req.method === "POST") {
4417
4516
  (async () => {
4418
4517
  try {
4419
- if (this.pageCompiler.isActive()) {
4420
- const projectConfig = this.readProjectConfig();
4421
- const firebaseConfig = {};
4422
- for (const field of projectConfig.fields) {
4423
- if (!field.isPlaceholder) {
4424
- firebaseConfig[field.key] = field.value;
4425
- }
4518
+ const steps = [];
4519
+ const functionsDir = resolve5(this.options.projectDir, "functions");
4520
+ const hasFunctions = existsSync6(functionsDir);
4521
+ const projectConfig = this.readProjectConfig();
4522
+ const firebaseConfig = {};
4523
+ for (const field of projectConfig.fields) {
4524
+ if (!field.isPlaceholder) {
4525
+ firebaseConfig[field.key] = field.value;
4426
4526
  }
4527
+ }
4528
+ if (this.pageCompiler.isActive()) {
4427
4529
  const configScript = Object.keys(firebaseConfig).length > 0 ? `
4428
4530
  <script>window.__CLAWFIRE_CONFIG__=${JSON.stringify(firebaseConfig)};</script>` : "";
4429
4531
  const routerScript = generateProductionRouterScript();
4430
4532
  const scriptToInject = configScript + routerScript;
4431
4533
  const buildResult = this.pageCompiler.buildForProduction(this.publicDir, scriptToInject);
4432
4534
  console.log(` \x1B[32m\u2713\x1B[0m Built ${buildResult.pages.length} pages for production`);
4535
+ steps.push(`${buildResult.pages.length} pages built`);
4433
4536
  if (configScript) {
4434
4537
  console.log(` \x1B[32m\u2713\x1B[0m Firebase config injected (projectId: ${firebaseConfig.projectId || "?"})`);
4538
+ steps.push("Firebase config injected");
4435
4539
  }
4436
4540
  if (buildResult.errors.length > 0) {
4437
4541
  for (const err of buildResult.errors) {
@@ -4439,9 +4543,39 @@ ${liveReloadScript}
4439
4543
  }
4440
4544
  }
4441
4545
  }
4442
- const result = await this.firebaseSetup.deployHosting();
4546
+ let deployFunctions = false;
4547
+ if (hasFunctions) {
4548
+ const syncResult = this.firebaseSetup.syncEnvToFunctions(firebaseConfig);
4549
+ if (syncResult.success) {
4550
+ console.log(` \x1B[32m\u2713\x1B[0m ${syncResult.message}`);
4551
+ steps.push(`${syncResult.count} env vars synced`);
4552
+ } else {
4553
+ console.log(` \x1B[33m\u26A0\x1B[0m Env sync: ${syncResult.message}`);
4554
+ }
4555
+ const configResult = this.firebaseSetup.ensureFunctionsConfig();
4556
+ if (configResult.success) {
4557
+ console.log(` \x1B[32m\u2713\x1B[0m ${configResult.message}`);
4558
+ }
4559
+ console.log(" Building functions...");
4560
+ const buildResult = await this.firebaseSetup.buildFunctions();
4561
+ if (buildResult.success) {
4562
+ console.log(` \x1B[32m\u2713\x1B[0m ${buildResult.message}`);
4563
+ steps.push("Functions built");
4564
+ deployFunctions = true;
4565
+ } else {
4566
+ console.log(` \x1B[31m\u2717\x1B[0m ${buildResult.message}`);
4567
+ console.log(" \x1B[33m\u26A0\x1B[0m Deploying hosting only (functions build failed)");
4568
+ steps.push("Functions build failed \u2014 hosting only");
4569
+ }
4570
+ }
4571
+ const targets = deployFunctions ? "hosting,functions" : "hosting";
4572
+ console.log(` Deploying ${targets}...`);
4573
+ const result = await this.firebaseSetup.deployHosting(targets);
4443
4574
  clearFirebaseStatusCache();
4444
- sendJson(result);
4575
+ sendJson({
4576
+ ...result,
4577
+ message: result.message + (steps.length > 0 ? ` (${steps.join(", ")})` : "")
4578
+ });
4445
4579
  } catch (err) {
4446
4580
  sendJson({ success: false, message: err instanceof Error ? err.message : "Failed" }, 500);
4447
4581
  }
package/dist/dev.cjs CHANGED
@@ -3368,7 +3368,7 @@ var FirebaseSetup = class {
3368
3368
  return { success: true, message: `Web app "${displayName}" selected.` };
3369
3369
  }
3370
3370
  // ─── Deploy ─────────────────────────────────────────────────────────
3371
- async deployHosting() {
3371
+ async deployHosting(targets = "hosting") {
3372
3372
  const firebaseJsonPath = (0, import_node_path4.resolve)(this.projectDir, "firebase.json");
3373
3373
  if (!(0, import_node_fs4.existsSync)(firebaseJsonPath)) {
3374
3374
  return { success: false, message: "firebase.json not found. Enable hosting first." };
@@ -3381,12 +3381,12 @@ var FirebaseSetup = class {
3381
3381
  } catch {
3382
3382
  return { success: false, message: "Invalid firebase.json." };
3383
3383
  }
3384
+ const timeoutMs = targets.includes("functions") ? 3e5 : 12e4;
3384
3385
  try {
3385
3386
  const output = await this.execTimeout(
3386
3387
  "firebase",
3387
- ["deploy", "--only", "hosting", "--json"],
3388
- 12e4
3389
- // 2 min timeout
3388
+ ["deploy", "--only", targets, "--json"],
3389
+ timeoutMs
3390
3390
  );
3391
3391
  let url = "";
3392
3392
  try {
@@ -3409,12 +3409,111 @@ var FirebaseSetup = class {
3409
3409
  url = `https://${state.projectId}.web.app`;
3410
3410
  }
3411
3411
  }
3412
- return { success: true, url: url || void 0, message: "Hosting deployed successfully!" };
3412
+ const label = targets.includes("functions") ? "Hosting + Functions" : "Hosting";
3413
+ return { success: true, url: url || void 0, message: `${label} deployed successfully!` };
3413
3414
  } catch (err) {
3414
3415
  const msg = err instanceof Error ? err.message : "Unknown error";
3415
3416
  return { success: false, message: `Deploy failed: ${msg}` };
3416
3417
  }
3417
3418
  }
3419
+ // ─── Full Deploy Pipeline ────────────────────────────────────────────
3420
+ /**
3421
+ * Sync environment variables to functions/.env for production.
3422
+ * Copies project root .env vars + injects CLAWFIRE_FIREBASE_CONFIG.
3423
+ */
3424
+ syncEnvToFunctions(firebaseConfig) {
3425
+ const functionsDir = (0, import_node_path4.resolve)(this.projectDir, "functions");
3426
+ if (!(0, import_node_fs4.existsSync)(functionsDir)) {
3427
+ return { success: false, message: "functions/ directory not found.", count: 0 };
3428
+ }
3429
+ const lines = [];
3430
+ const rootEnvPath = (0, import_node_path4.resolve)(this.projectDir, ".env");
3431
+ if ((0, import_node_fs4.existsSync)(rootEnvPath)) {
3432
+ const content = (0, import_node_fs4.readFileSync)(rootEnvPath, "utf-8");
3433
+ for (const rawLine of content.split("\n")) {
3434
+ let trimmed = rawLine.trim();
3435
+ if (!trimmed || trimmed.startsWith("#")) continue;
3436
+ if (trimmed.startsWith("export ")) {
3437
+ trimmed = trimmed.slice(7);
3438
+ }
3439
+ const eqIdx = trimmed.indexOf("=");
3440
+ if (eqIdx === -1) continue;
3441
+ const key = trimmed.slice(0, eqIdx).trim();
3442
+ if (key === "NPM_TOKEN" || key === "NODE_ENV") continue;
3443
+ lines.push(trimmed);
3444
+ }
3445
+ }
3446
+ if (firebaseConfig && Object.keys(firebaseConfig).length > 0) {
3447
+ const filtered = lines.filter((l) => !l.startsWith("CLAWFIRE_FIREBASE_CONFIG="));
3448
+ filtered.push(`CLAWFIRE_FIREBASE_CONFIG=${JSON.stringify(firebaseConfig)}`);
3449
+ lines.length = 0;
3450
+ lines.push(...filtered);
3451
+ }
3452
+ const functionsEnvPath = (0, import_node_path4.resolve)(functionsDir, ".env");
3453
+ (0, import_node_fs4.writeFileSync)(functionsEnvPath, lines.join("\n") + "\n", "utf-8");
3454
+ return { success: true, message: `Synced ${lines.length} env vars to functions/.env`, count: lines.length };
3455
+ }
3456
+ /**
3457
+ * Ensure firebase.json has functions configuration.
3458
+ */
3459
+ ensureFunctionsConfig() {
3460
+ const firebaseJsonPath = (0, import_node_path4.resolve)(this.projectDir, "firebase.json");
3461
+ if (!(0, import_node_fs4.existsSync)(firebaseJsonPath)) {
3462
+ return { success: false, message: "firebase.json not found." };
3463
+ }
3464
+ try {
3465
+ const config = JSON.parse((0, import_node_fs4.readFileSync)(firebaseJsonPath, "utf-8"));
3466
+ if (!config.functions) {
3467
+ config.functions = {
3468
+ source: "functions",
3469
+ runtime: "nodejs20",
3470
+ codebase: "clawfire"
3471
+ };
3472
+ (0, import_node_fs4.writeFileSync)(firebaseJsonPath, JSON.stringify(config, null, 2) + "\n", "utf-8");
3473
+ return { success: true, message: "Functions config added to firebase.json." };
3474
+ }
3475
+ return { success: true, message: "Functions config already present." };
3476
+ } catch {
3477
+ return { success: false, message: "Invalid firebase.json." };
3478
+ }
3479
+ }
3480
+ /**
3481
+ * Install dependencies and build functions for deployment.
3482
+ */
3483
+ async buildFunctions() {
3484
+ const functionsDir = (0, import_node_path4.resolve)(this.projectDir, "functions");
3485
+ if (!(0, import_node_fs4.existsSync)(functionsDir)) {
3486
+ return { success: false, message: "functions/ directory not found." };
3487
+ }
3488
+ const pkgPath = (0, import_node_path4.resolve)(functionsDir, "package.json");
3489
+ if (!(0, import_node_fs4.existsSync)(pkgPath)) {
3490
+ return { success: false, message: "functions/package.json not found." };
3491
+ }
3492
+ const nodeModulesPath = (0, import_node_path4.resolve)(functionsDir, "node_modules");
3493
+ if (!(0, import_node_fs4.existsSync)(nodeModulesPath)) {
3494
+ try {
3495
+ await this.execTimeout("npm", ["install"], 12e4, functionsDir);
3496
+ } catch (err) {
3497
+ const msg = err instanceof Error ? err.message : "Unknown error";
3498
+ return { success: false, message: `npm install in functions/ failed: ${msg}` };
3499
+ }
3500
+ }
3501
+ try {
3502
+ const pkg = JSON.parse((0, import_node_fs4.readFileSync)(pkgPath, "utf-8"));
3503
+ if (pkg.scripts?.build) {
3504
+ await this.execTimeout("npm", ["run", "build"], 6e4, functionsDir);
3505
+ } else {
3506
+ const tsconfigPath = (0, import_node_path4.resolve)(functionsDir, "tsconfig.json");
3507
+ if ((0, import_node_fs4.existsSync)(tsconfigPath)) {
3508
+ await this.execTimeout("npx", ["tsc"], 6e4, functionsDir);
3509
+ }
3510
+ }
3511
+ return { success: true, message: "Functions built successfully." };
3512
+ } catch (err) {
3513
+ const msg = err instanceof Error ? err.message : "Unknown error";
3514
+ return { success: false, message: `Functions build failed: ${msg}` };
3515
+ }
3516
+ }
3418
3517
  // ─── Service Enable ────────────────────────────────────────────────
3419
3518
  enableService(service) {
3420
3519
  const firebaseJsonPath = (0, import_node_path4.resolve)(this.projectDir, "firebase.json");
@@ -3663,9 +3762,9 @@ https://console.developers.google.com/apis/api/firestore.googleapis.com/overview
3663
3762
  }
3664
3763
  }
3665
3764
  // ─── Helpers ───────────────────────────────────────────────────────
3666
- execTimeout(command, args, timeoutMs) {
3765
+ execTimeout(command, args, timeoutMs, cwd) {
3667
3766
  return new Promise((resolve7, reject) => {
3668
- const proc = (0, import_node_child_process2.execFile)(command, args, { cwd: this.projectDir, timeout: timeoutMs }, (err, stdout, stderr) => {
3767
+ const proc = (0, import_node_child_process2.execFile)(command, args, { cwd: cwd || this.projectDir, timeout: timeoutMs }, (err, stdout, stderr) => {
3669
3768
  if (err) {
3670
3769
  const detail = stderr?.trim() || stdout?.trim() || "";
3671
3770
  const enriched = new Error(`${err.message}${detail ? "\n" + detail : ""}`);
@@ -4828,22 +4927,27 @@ ${liveReloadScript}
4828
4927
  if (url.pathname === "/__dev/deploy/hosting" && req.method === "POST") {
4829
4928
  (async () => {
4830
4929
  try {
4831
- if (this.pageCompiler.isActive()) {
4832
- const projectConfig = this.readProjectConfig();
4833
- const firebaseConfig = {};
4834
- for (const field of projectConfig.fields) {
4835
- if (!field.isPlaceholder) {
4836
- firebaseConfig[field.key] = field.value;
4837
- }
4930
+ const steps = [];
4931
+ const functionsDir = (0, import_node_path5.resolve)(this.options.projectDir, "functions");
4932
+ const hasFunctions = (0, import_node_fs5.existsSync)(functionsDir);
4933
+ const projectConfig = this.readProjectConfig();
4934
+ const firebaseConfig = {};
4935
+ for (const field of projectConfig.fields) {
4936
+ if (!field.isPlaceholder) {
4937
+ firebaseConfig[field.key] = field.value;
4838
4938
  }
4939
+ }
4940
+ if (this.pageCompiler.isActive()) {
4839
4941
  const configScript = Object.keys(firebaseConfig).length > 0 ? `
4840
4942
  <script>window.__CLAWFIRE_CONFIG__=${JSON.stringify(firebaseConfig)};</script>` : "";
4841
4943
  const routerScript = generateProductionRouterScript();
4842
4944
  const scriptToInject = configScript + routerScript;
4843
4945
  const buildResult = this.pageCompiler.buildForProduction(this.publicDir, scriptToInject);
4844
4946
  console.log(` \x1B[32m\u2713\x1B[0m Built ${buildResult.pages.length} pages for production`);
4947
+ steps.push(`${buildResult.pages.length} pages built`);
4845
4948
  if (configScript) {
4846
4949
  console.log(` \x1B[32m\u2713\x1B[0m Firebase config injected (projectId: ${firebaseConfig.projectId || "?"})`);
4950
+ steps.push("Firebase config injected");
4847
4951
  }
4848
4952
  if (buildResult.errors.length > 0) {
4849
4953
  for (const err of buildResult.errors) {
@@ -4851,9 +4955,39 @@ ${liveReloadScript}
4851
4955
  }
4852
4956
  }
4853
4957
  }
4854
- const result = await this.firebaseSetup.deployHosting();
4958
+ let deployFunctions = false;
4959
+ if (hasFunctions) {
4960
+ const syncResult = this.firebaseSetup.syncEnvToFunctions(firebaseConfig);
4961
+ if (syncResult.success) {
4962
+ console.log(` \x1B[32m\u2713\x1B[0m ${syncResult.message}`);
4963
+ steps.push(`${syncResult.count} env vars synced`);
4964
+ } else {
4965
+ console.log(` \x1B[33m\u26A0\x1B[0m Env sync: ${syncResult.message}`);
4966
+ }
4967
+ const configResult = this.firebaseSetup.ensureFunctionsConfig();
4968
+ if (configResult.success) {
4969
+ console.log(` \x1B[32m\u2713\x1B[0m ${configResult.message}`);
4970
+ }
4971
+ console.log(" Building functions...");
4972
+ const buildResult = await this.firebaseSetup.buildFunctions();
4973
+ if (buildResult.success) {
4974
+ console.log(` \x1B[32m\u2713\x1B[0m ${buildResult.message}`);
4975
+ steps.push("Functions built");
4976
+ deployFunctions = true;
4977
+ } else {
4978
+ console.log(` \x1B[31m\u2717\x1B[0m ${buildResult.message}`);
4979
+ console.log(" \x1B[33m\u26A0\x1B[0m Deploying hosting only (functions build failed)");
4980
+ steps.push("Functions build failed \u2014 hosting only");
4981
+ }
4982
+ }
4983
+ const targets = deployFunctions ? "hosting,functions" : "hosting";
4984
+ console.log(` Deploying ${targets}...`);
4985
+ const result = await this.firebaseSetup.deployHosting(targets);
4855
4986
  clearFirebaseStatusCache();
4856
- sendJson(result);
4987
+ sendJson({
4988
+ ...result,
4989
+ message: result.message + (steps.length > 0 ? ` (${steps.join(", ")})` : "")
4990
+ });
4857
4991
  } catch (err) {
4858
4992
  sendJson({ success: false, message: err instanceof Error ? err.message : "Failed" }, 500);
4859
4993
  }