@forvibe/cli 1.0.9 → 1.1.1

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.
@@ -26,7 +26,7 @@ Your task is to generate a complete, ASO-optimized store listing for a mobile ap
26
26
  6. Write in English (en-US locale)
27
27
 
28
28
  Respond ONLY with a valid JSON object, no markdown, no explanation.`;
29
- function buildASOPrompt(report) {
29
+ function buildASOPrompt(report, selectedStores) {
30
30
  const parts = [];
31
31
  parts.push(`## App Information
32
32
  - App Name: ${report.app_name}
@@ -50,23 +50,31 @@ ${report.unique_selling_points.map((u) => `- ${u}`).join("\n")}`);
50
50
  parts.push(`## README (for additional context)
51
51
  ${report.readme_content.substring(0, 3e3)}`);
52
52
  }
53
- const includePlayStore = report.platforms.includes("android");
54
- parts.push(`## Required JSON Response Format
55
- {
56
- "appstore": {
53
+ const includePlayStore = selectedStores.length > 0 ? selectedStores.includes("playstore") : report.platforms.includes("android");
54
+ const includeAppStore = selectedStores.length > 0 ? selectedStores.includes("appstore") : true;
55
+ const jsonParts = [];
56
+ if (includeAppStore) {
57
+ jsonParts.push(` "appstore": {
57
58
  "app_name": "max 30 chars, keyword-rich app name",
58
59
  "subtitle": "max 30 chars, complementary keywords",
59
60
  "description": "max 4000 chars, ASO-optimized with bullets and line breaks",
60
61
  "keywords": "keyword1,keyword2,keyword3 (max 100 chars, NO spaces after commas)",
61
62
  "promotional_text": "max 170 chars, engaging hook",
62
63
  "whats_new": "max 170 chars, launch highlights"
63
- }${includePlayStore ? `,
64
- "playstore": {
64
+ }`);
65
+ }
66
+ if (includePlayStore) {
67
+ jsonParts.push(` "playstore": {
65
68
  "title": "max 30 chars, Play Store optimized title",
66
69
  "short_description": "max 80 chars, concise value proposition",
67
70
  "description": "max 4000 chars, HTML formatting allowed",
68
71
  "whats_new": "max 500 chars, launch release notes"
69
- }` : ""}
72
+ }`);
73
+ }
74
+ parts.push(`## Required JSON Response Format
75
+ {
76
+ ${jsonParts.join(",\n")}
77
+ }
70
78
  }`);
71
79
  return parts.join("\n\n");
72
80
  }
@@ -82,16 +90,17 @@ function truncateKeywords(keywords, maxLength) {
82
90
  return result || keywords.substring(0, maxLength);
83
91
  }
84
92
  function enforceCharLimits(raw) {
85
- const result = {
86
- appstore: {
93
+ const result = {};
94
+ if (raw.appstore) {
95
+ result.appstore = {
87
96
  app_name: (raw.appstore.app_name || "").substring(0, 30),
88
97
  subtitle: (raw.appstore.subtitle || "").substring(0, 30),
89
98
  description: (raw.appstore.description || "").substring(0, 4e3),
90
99
  keywords: truncateKeywords(raw.appstore.keywords || "", 100),
91
100
  promotional_text: (raw.appstore.promotional_text || "").substring(0, 170),
92
101
  whats_new: (raw.appstore.whats_new || "").substring(0, 170)
93
- }
94
- };
102
+ };
103
+ }
95
104
  if (raw.playstore) {
96
105
  result.playstore = {
97
106
  title: (raw.playstore.title || "").substring(0, 30),
@@ -102,8 +111,8 @@ function enforceCharLimits(raw) {
102
111
  }
103
112
  return result;
104
113
  }
105
- async function generateASOContent(report, provider) {
106
- const userPrompt = buildASOPrompt(report);
114
+ async function generateASOContent(report, provider, selectedStores = []) {
115
+ const userPrompt = buildASOPrompt(report, selectedStores);
107
116
  const responseText = await provider.generateJSON(ASO_SYSTEM_PROMPT, userPrompt, 0.5);
108
117
  let parsed;
109
118
  try {
@@ -121,9 +130,14 @@ async function generateASOContent(report, provider) {
121
130
  throw new Error("Failed to parse ASO AI response as JSON");
122
131
  }
123
132
  }
124
- if (!parsed.appstore) {
133
+ const wantsAppStore = selectedStores.length === 0 || selectedStores.includes("appstore");
134
+ if (wantsAppStore && !parsed.appstore) {
125
135
  throw new Error("AI response missing appstore field");
126
136
  }
137
+ const wantsPlayStore = selectedStores.includes("playstore") || selectedStores.length === 0 && report.platforms.includes("android");
138
+ if (wantsPlayStore && !parsed.playstore) {
139
+ console.warn("AI response missing playstore field \u2014 will be generated server-side");
140
+ }
127
141
  return enforceCharLimits(parsed);
128
142
  }
129
143
  export {
package/dist/index.js CHANGED
@@ -5,6 +5,8 @@ import { Command } from "commander";
5
5
 
6
6
  // src/commands/analyze.ts
7
7
  import { createInterface } from "readline";
8
+ import { readFileSync as readFileSync5, statSync as statSync6 } from "fs";
9
+ import { basename as basename3, extname as extname4, relative as relative4 } from "path";
8
10
  import chalk from "chalk";
9
11
  import ora from "ora";
10
12
 
@@ -1885,8 +1887,7 @@ function generateProjectTree(rootDir, maxDepth = 5, maxEntries = 300) {
1885
1887
  // src/analyzers/asset-scanner.ts
1886
1888
  import { readFileSync as readFileSync4, existsSync as existsSync5, readdirSync as readdirSync7, statSync as statSync5 } from "fs";
1887
1889
  import { join as join8, extname as extname3, relative as relative3, basename as basename2 } from "path";
1888
- var MAX_ASSET_SIZE = 1 * 1024 * 1024;
1889
- var MAX_TOTAL_BASE64_SIZE = 3 * 1024 * 1024;
1890
+ var MAX_ASSET_SIZE = 5 * 1024 * 1024;
1890
1891
  var MAX_TOTAL_ASSETS = 10;
1891
1892
  var MAX_SCREENSHOTS = 5;
1892
1893
  var MIN_ASSET_SIZE = 500;
@@ -2090,9 +2091,6 @@ function getPromotionalPaths(rootDir, techStack) {
2090
2091
  }
2091
2092
  return paths;
2092
2093
  }
2093
- function totalBase64Size(assets) {
2094
- return assets.reduce((sum, a) => sum + a.base64_data.length, 0);
2095
- }
2096
2094
  function scanAppAssets(rootDir, techStack) {
2097
2095
  const assets = [];
2098
2096
  const screenshotDirs = getScreenshotDirs(rootDir, techStack);
@@ -2131,17 +2129,7 @@ function scanAppAssets(rootDir, techStack) {
2131
2129
  break;
2132
2130
  }
2133
2131
  }
2134
- const result = assets.slice(0, MAX_TOTAL_ASSETS);
2135
- while (result.length > 0 && totalBase64Size(result) > MAX_TOTAL_BASE64_SIZE) {
2136
- let largestIdx = 0;
2137
- for (let i = 1; i < result.length; i++) {
2138
- if (result[i].base64_data.length > result[largestIdx].base64_data.length) {
2139
- largestIdx = i;
2140
- }
2141
- }
2142
- result.splice(largestIdx, 1);
2143
- }
2144
- return result;
2132
+ return assets.slice(0, MAX_TOTAL_ASSETS);
2145
2133
  }
2146
2134
 
2147
2135
  // src/api/forvibe-client.ts
@@ -2163,6 +2151,7 @@ var ForvibeClient = class {
2163
2151
  baseUrl;
2164
2152
  sessionToken = null;
2165
2153
  sessionId = null;
2154
+ storeConfig = null;
2166
2155
  constructor(baseUrl) {
2167
2156
  this.baseUrl = (baseUrl || process.env.FORVIBE_API_URL || DEFAULT_API_URL).replace(/\/$/, "");
2168
2157
  }
@@ -2192,6 +2181,7 @@ var ForvibeClient = class {
2192
2181
  const data = await response.json();
2193
2182
  this.sessionToken = data.session_token;
2194
2183
  this.sessionId = data.session_id;
2184
+ this.storeConfig = data.store_config || null;
2195
2185
  return data;
2196
2186
  }
2197
2187
  /**
@@ -2237,6 +2227,59 @@ var ForvibeClient = class {
2237
2227
  }
2238
2228
  return await response.json();
2239
2229
  }
2230
+ /**
2231
+ * Request signed upload URLs for app assets. CLI uploads the binaries
2232
+ * directly to Supabase Storage, avoiding Vercel's 4.5MB body limit on
2233
+ * the report endpoint.
2234
+ */
2235
+ async getAssetUploadUrls(assets) {
2236
+ if (!this.sessionToken) {
2237
+ throw new Error("Not connected. Please validate OTC code first.");
2238
+ }
2239
+ const response = await fetchWithAuth(
2240
+ `${this.baseUrl}/api/agent/assets/presign`,
2241
+ {
2242
+ method: "POST",
2243
+ headers: {
2244
+ "Content-Type": "application/json",
2245
+ Authorization: `Bearer ${this.sessionToken}`
2246
+ },
2247
+ body: JSON.stringify({ assets })
2248
+ }
2249
+ );
2250
+ if (!response.ok) {
2251
+ const bodyText = await response.text().catch(() => "");
2252
+ let errorMessage;
2253
+ try {
2254
+ errorMessage = JSON.parse(bodyText).error;
2255
+ } catch {
2256
+ }
2257
+ throw new Error(
2258
+ errorMessage || `Failed to get upload URLs (HTTP ${response.status}: ${bodyText.substring(0, 200)})`
2259
+ );
2260
+ }
2261
+ return await response.json();
2262
+ }
2263
+ /**
2264
+ * PUT a binary asset directly to a Supabase Storage signed upload URL.
2265
+ * Do NOT send our Bearer token — the signed URL carries its own auth.
2266
+ */
2267
+ async uploadAssetToSignedUrl(signedUrl, buffer, mimeType) {
2268
+ const response = await fetch(signedUrl, {
2269
+ method: "PUT",
2270
+ headers: {
2271
+ "Content-Type": mimeType,
2272
+ "x-upsert": "true"
2273
+ },
2274
+ body: new Uint8Array(buffer)
2275
+ });
2276
+ if (!response.ok) {
2277
+ const bodyText = await response.text().catch(() => "");
2278
+ throw new Error(
2279
+ `Supabase Storage upload failed (HTTP ${response.status}: ${bodyText.substring(0, 200)})`
2280
+ );
2281
+ }
2282
+ }
2240
2283
  /**
2241
2284
  * Submit codebase review results for combined analysis
2242
2285
  */
@@ -2408,6 +2451,79 @@ function getAvailableProviders(deep = false) {
2408
2451
  }
2409
2452
 
2410
2453
  // src/commands/analyze.ts
2454
+ function mimeForPath(p) {
2455
+ const ext = extname4(p).toLowerCase();
2456
+ if (ext === ".jpg" || ext === ".jpeg") return "image/jpeg";
2457
+ if (ext === ".webp") return "image/webp";
2458
+ return "image/png";
2459
+ }
2460
+ async function uploadAssetsToStorage(client, rootDir, assets, appIconPath) {
2461
+ const pending = [];
2462
+ for (const asset of assets) {
2463
+ if (!asset.base64_data) continue;
2464
+ pending.push({
2465
+ asset,
2466
+ buffer: Buffer.from(asset.base64_data, "base64")
2467
+ });
2468
+ }
2469
+ if (appIconPath) {
2470
+ try {
2471
+ const stat = statSync6(appIconPath);
2472
+ if (stat.isFile() && stat.size <= 5 * 1024 * 1024) {
2473
+ const buffer = readFileSync5(appIconPath);
2474
+ pending.push({
2475
+ asset: {
2476
+ asset_type: "app_icon",
2477
+ file_name: basename3(appIconPath),
2478
+ mime_type: mimeForPath(appIconPath),
2479
+ width: null,
2480
+ height: null,
2481
+ source_path: relative4(rootDir, appIconPath)
2482
+ },
2483
+ buffer
2484
+ });
2485
+ }
2486
+ } catch {
2487
+ }
2488
+ }
2489
+ if (pending.length === 0) return [];
2490
+ const { uploads } = await client.getAssetUploadUrls(
2491
+ pending.map((p) => ({
2492
+ asset_type: p.asset.asset_type,
2493
+ file_name: p.asset.file_name,
2494
+ mime_type: p.asset.mime_type
2495
+ }))
2496
+ );
2497
+ if (uploads.length !== pending.length) {
2498
+ throw new Error(
2499
+ `Upload slot count mismatch: requested ${pending.length}, got ${uploads.length}`
2500
+ );
2501
+ }
2502
+ const concurrency = 3;
2503
+ for (let i = 0; i < pending.length; i += concurrency) {
2504
+ const chunk = pending.slice(i, i + concurrency);
2505
+ const slots = uploads.slice(i, i + concurrency);
2506
+ await Promise.all(
2507
+ chunk.map(
2508
+ (p, idx) => client.uploadAssetToSignedUrl(
2509
+ slots[idx].signed_url,
2510
+ p.buffer,
2511
+ p.asset.mime_type
2512
+ )
2513
+ )
2514
+ );
2515
+ }
2516
+ return pending.map((p, i) => ({
2517
+ asset_type: p.asset.asset_type,
2518
+ file_name: uploads[i].file_name,
2519
+ mime_type: p.asset.mime_type,
2520
+ width: p.asset.width,
2521
+ height: p.asset.height,
2522
+ source_path: p.asset.source_path,
2523
+ storage_bucket: uploads[i].bucket,
2524
+ storage_path: uploads[i].storage_path
2525
+ }));
2526
+ }
2411
2527
  function askQuestion(question) {
2412
2528
  const rl = createInterface({
2413
2529
  input: process.stdin,
@@ -2598,8 +2714,9 @@ async function analyzeCommand(options) {
2598
2714
  prefixText: " "
2599
2715
  }).start();
2600
2716
  try {
2601
- const { generateASOContent } = await import("./aso-generator-5I3OEF5J.js");
2602
- const asoContent = await generateASOContent(report, provider);
2717
+ const { generateASOContent } = await import("./aso-generator-PF4SDBTL.js");
2718
+ const stores = client.storeConfig?.stores || [];
2719
+ const asoContent = await generateASOContent(report, provider, stores);
2603
2720
  report.aso_content = asoContent;
2604
2721
  asoSpinner.succeed(chalk.green("Store listing content generated!"));
2605
2722
  } catch (error) {
@@ -2628,16 +2745,19 @@ async function analyzeCommand(options) {
2628
2745
  console.log(` Assets: ${report.app_assets?.length ? chalk.green(`${report.app_assets.length} found`) : chalk.gray("none")}`);
2629
2746
  console.log(chalk.gray(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
2630
2747
  if (report.aso_content) {
2631
- console.log();
2632
- console.log(chalk.bold(" \u{1F4F1} App Store Listing Preview"));
2633
- console.log(chalk.gray(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
2634
- const aso = report.aso_content.appstore;
2635
- console.log(` Title: ${chalk.white(aso.app_name)} ${chalk.gray(`(${aso.app_name.length}/30)`)}`);
2636
- console.log(` Subtitle: ${chalk.white(aso.subtitle)} ${chalk.gray(`(${aso.subtitle.length}/30)`)}`);
2637
- console.log(` Keywords: ${chalk.white(aso.keywords)} ${chalk.gray(`(${aso.keywords.length}/100)`)}`);
2638
- console.log(` Promo: ${chalk.white(aso.promotional_text)} ${chalk.gray(`(${aso.promotional_text.length}/170)`)}`);
2639
- const descPreview = aso.description.substring(0, 150).replace(/\n/g, " ");
2640
- console.log(` Description: ${chalk.white(descPreview)}${aso.description.length > 150 ? chalk.gray("...") : ""} ${chalk.gray(`(${aso.description.length}/4000)`)}`);
2748
+ if (report.aso_content.appstore) {
2749
+ console.log();
2750
+ console.log(chalk.bold(" \u{1F4F1} App Store Listing Preview"));
2751
+ console.log(chalk.gray(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
2752
+ const aso = report.aso_content.appstore;
2753
+ console.log(` Title: ${chalk.white(aso.app_name)} ${chalk.gray(`(${aso.app_name.length}/30)`)}`);
2754
+ console.log(` Subtitle: ${chalk.white(aso.subtitle)} ${chalk.gray(`(${aso.subtitle.length}/30)`)}`);
2755
+ console.log(` Keywords: ${chalk.white(aso.keywords)} ${chalk.gray(`(${aso.keywords.length}/100)`)}`);
2756
+ console.log(` Promo: ${chalk.white(aso.promotional_text)} ${chalk.gray(`(${aso.promotional_text.length}/170)`)}`);
2757
+ const descPreview = aso.description.substring(0, 150).replace(/\n/g, " ");
2758
+ console.log(` Description: ${chalk.white(descPreview)}${aso.description.length > 150 ? chalk.gray("...") : ""} ${chalk.gray(`(${aso.description.length}/4000)`)}`);
2759
+ console.log(chalk.gray(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
2760
+ }
2641
2761
  if (report.aso_content.playstore) {
2642
2762
  console.log();
2643
2763
  console.log(chalk.bold(" \u{1F916} Play Store Listing Preview"));
@@ -2647,57 +2767,62 @@ async function analyzeCommand(options) {
2647
2767
  console.log(` Short Desc: ${chalk.white(ps.short_description)} ${chalk.gray(`(${ps.short_description.length}/80)`)}`);
2648
2768
  const psDescPreview = ps.description.substring(0, 150).replace(/\n/g, " ");
2649
2769
  console.log(` Description: ${chalk.white(psDescPreview)}${ps.description.length > 150 ? chalk.gray("...") : ""} ${chalk.gray(`(${ps.description.length}/4000)`)}`);
2770
+ console.log(chalk.gray(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
2650
2771
  }
2651
- console.log(chalk.gray(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
2652
2772
  }
2653
- console.log();
2654
- console.log(chalk.bold(" \u{1F4DD} App Review Information"));
2655
- console.log(chalk.gray(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
2656
- console.log(chalk.gray(" Apple's review team may need to sign in to your app."));
2657
- console.log(chalk.gray(" You can also provide this later in the Forvibe dashboard."));
2658
- console.log();
2659
- const hasAuthSDKs = (sdkScan.third_party_services.auth?.length ?? 0) > 0;
2660
- if (hasAuthSDKs) {
2661
- const signInAnswer = await askQuestion(
2662
- chalk.cyan(" Does your app require sign-in to use? (y/n): ")
2663
- );
2664
- const signInRequired = signInAnswer.toLowerCase().startsWith("y");
2665
- report.sign_in_required = signInRequired;
2666
- if (signInRequired) {
2667
- console.log();
2668
- console.log(chalk.yellow(" \u26A0 Please create a test account in your app for Apple's review team."));
2669
- console.log(chalk.gray(" This account should have full access to your app's features."));
2670
- console.log();
2671
- const testEmail = await askQuestion(chalk.cyan(" Test account email: "));
2672
- const testPassword = await askQuestion(chalk.cyan(" Test account password: "));
2673
- if (testEmail && testPassword) {
2674
- report.test_account = { email: testEmail, password: testPassword };
2675
- console.log(chalk.green(" \u2713 Test account saved"));
2773
+ const selectedStores = client.storeConfig?.stores || [];
2774
+ const hasASC = selectedStores.includes("appstore") || selectedStores.length === 0;
2775
+ const hasPS = selectedStores.includes("playstore") || selectedStores.length === 0;
2776
+ if (hasASC) {
2777
+ console.log();
2778
+ console.log(chalk.bold(" \u{1F4DD} App Review Information"));
2779
+ console.log(chalk.gray(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
2780
+ console.log(chalk.gray(" Apple's review team may need to sign in to your app."));
2781
+ console.log(chalk.gray(" You can also provide this later in the Forvibe dashboard."));
2782
+ console.log();
2783
+ const hasAuthSDKs = (sdkScan.third_party_services.auth?.length ?? 0) > 0;
2784
+ if (hasAuthSDKs) {
2785
+ const signInAnswer = await askQuestion(
2786
+ chalk.cyan(" Does your app require sign-in to use? (y/n): ")
2787
+ );
2788
+ const signInRequired = signInAnswer.toLowerCase().startsWith("y");
2789
+ report.sign_in_required = signInRequired;
2790
+ if (signInRequired) {
2791
+ console.log();
2792
+ console.log(chalk.yellow(" \u26A0 Please create a test account in your app for Apple's review team."));
2793
+ console.log(chalk.gray(" This account should have full access to your app's features."));
2794
+ console.log();
2795
+ const testEmail = await askQuestion(chalk.cyan(" Test account email: "));
2796
+ const testPassword = await askQuestion(chalk.cyan(" Test account password: "));
2797
+ if (testEmail && testPassword) {
2798
+ report.test_account = { email: testEmail, password: testPassword };
2799
+ console.log(chalk.green(" \u2713 Test account saved"));
2800
+ }
2676
2801
  }
2802
+ } else {
2803
+ report.sign_in_required = false;
2677
2804
  }
2678
- } else {
2679
- report.sign_in_required = false;
2680
- }
2681
- console.log();
2682
- console.log(chalk.gray(" Contact information for Apple's review team (press Enter to skip):"));
2683
- console.log();
2684
- const firstName = await askQuestion(chalk.cyan(" First name: "));
2685
- const lastName = await askQuestion(chalk.cyan(" Last name: "));
2686
- const contactEmail = await askQuestion(chalk.cyan(" Email: "));
2687
- const contactPhone = await askQuestion(chalk.cyan(" Phone (e.g. +1 555 123 4567): "));
2688
- if (firstName && lastName && contactEmail) {
2689
- report.review_contact = {
2690
- first_name: firstName,
2691
- last_name: lastName,
2692
- email: contactEmail,
2693
- phone: contactPhone || ""
2694
- };
2695
- console.log(chalk.green(" \u2713 Contact info saved"));
2696
- } else {
2697
- console.log(chalk.gray(" \u2139 Skipped \u2014 you can add this later in the dashboard."));
2805
+ console.log();
2806
+ console.log(chalk.gray(" Contact information for Apple's review team (press Enter to skip):"));
2807
+ console.log();
2808
+ const firstName = await askQuestion(chalk.cyan(" First name: "));
2809
+ const lastName = await askQuestion(chalk.cyan(" Last name: "));
2810
+ const contactEmail = await askQuestion(chalk.cyan(" Email: "));
2811
+ const contactPhone = await askQuestion(chalk.cyan(" Phone (e.g. +1 555 123 4567): "));
2812
+ if (firstName && lastName && contactEmail) {
2813
+ report.review_contact = {
2814
+ first_name: firstName,
2815
+ last_name: lastName,
2816
+ email: contactEmail,
2817
+ phone: contactPhone || ""
2818
+ };
2819
+ console.log(chalk.green(" \u2713 Contact info saved"));
2820
+ } else {
2821
+ console.log(chalk.gray(" \u2139 Skipped \u2014 you can add this later in the dashboard."));
2822
+ }
2823
+ console.log(chalk.gray(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
2698
2824
  }
2699
- console.log(chalk.gray(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
2700
- if (techStack.platforms.includes("android")) {
2825
+ if (hasPS) {
2701
2826
  console.log();
2702
2827
  console.log(chalk.bold(" \u{1F916} Play Store Declarations"));
2703
2828
  console.log(chalk.gray(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
@@ -2837,6 +2962,36 @@ async function analyzeCommand(options) {
2837
2962
  console.log(chalk.gray(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
2838
2963
  }
2839
2964
  console.log();
2965
+ try {
2966
+ const uploadCount = (report.app_assets?.length ?? 0) + (branding.app_icon_path ? 1 : 0);
2967
+ if (uploadCount > 0) {
2968
+ const uploadSpinner = ora({
2969
+ text: `Uploading ${uploadCount} asset${uploadCount === 1 ? "" : "s"}...`,
2970
+ prefixText: " "
2971
+ }).start();
2972
+ try {
2973
+ const uploaded = await uploadAssetsToStorage(
2974
+ client,
2975
+ rootDir,
2976
+ report.app_assets ?? [],
2977
+ branding.app_icon_path
2978
+ );
2979
+ report.app_assets = uploaded;
2980
+ report.app_icon_base64 = null;
2981
+ uploadSpinner.succeed(chalk.green(`Uploaded ${uploaded.length} asset${uploaded.length === 1 ? "" : "s"}`));
2982
+ } catch (uploadError) {
2983
+ uploadSpinner.fail(
2984
+ chalk.red(
2985
+ `Asset upload failed: ${uploadError instanceof Error ? uploadError.message : "Unknown error"}`
2986
+ )
2987
+ );
2988
+ process.exit(1);
2989
+ }
2990
+ }
2991
+ } catch (e) {
2992
+ console.error(chalk.red(`Asset upload error: ${e instanceof Error ? e.message : String(e)}`));
2993
+ process.exit(1);
2994
+ }
2840
2995
  const sendSpinner = ora({
2841
2996
  text: "Sending report to Forvibe...",
2842
2997
  prefixText: " "
@@ -2945,7 +3100,7 @@ function parseFeatureAnalysis(response) {
2945
3100
  }
2946
3101
 
2947
3102
  // src/review/rag-selector.ts
2948
- import { readFileSync as readFileSync5 } from "fs";
3103
+ import { readFileSync as readFileSync6 } from "fs";
2949
3104
  import { fileURLToPath } from "url";
2950
3105
  import path from "path";
2951
3106
  var DATA_FILES = [
@@ -2968,7 +3123,7 @@ function getDataDir() {
2968
3123
  ];
2969
3124
  for (const dir of candidates) {
2970
3125
  try {
2971
- readFileSync5(path.join(dir, "safety.json"));
3126
+ readFileSync6(path.join(dir, "safety.json"));
2972
3127
  return dir;
2973
3128
  } catch {
2974
3129
  }
@@ -2981,7 +3136,7 @@ function loadAllStories() {
2981
3136
  const stories = [];
2982
3137
  for (const file of DATA_FILES) {
2983
3138
  try {
2984
- const content = readFileSync5(path.join(dataDir, file), "utf-8");
3139
+ const content = readFileSync6(path.join(dataDir, file), "utf-8");
2985
3140
  const parsed = JSON.parse(content);
2986
3141
  stories.push(...parsed);
2987
3142
  } catch {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@forvibe/cli",
3
- "version": "1.0.9",
3
+ "version": "1.1.1",
4
4
  "description": "Forvibe CLI - AI-powered project analyzer for App Store automation",
5
5
  "type": "module",
6
6
  "bin": {