@forvibe/cli 1.1.0 → 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.
Files changed (2) hide show
  1. package/dist/index.js +163 -19
  2. package/package.json +1 -1
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
@@ -2239,6 +2227,59 @@ var ForvibeClient = class {
2239
2227
  }
2240
2228
  return await response.json();
2241
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
+ }
2242
2283
  /**
2243
2284
  * Submit codebase review results for combined analysis
2244
2285
  */
@@ -2410,6 +2451,79 @@ function getAvailableProviders(deep = false) {
2410
2451
  }
2411
2452
 
2412
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
+ }
2413
2527
  function askQuestion(question) {
2414
2528
  const rl = createInterface({
2415
2529
  input: process.stdin,
@@ -2848,6 +2962,36 @@ async function analyzeCommand(options) {
2848
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"));
2849
2963
  }
2850
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
+ }
2851
2995
  const sendSpinner = ora({
2852
2996
  text: "Sending report to Forvibe...",
2853
2997
  prefixText: " "
@@ -2956,7 +3100,7 @@ function parseFeatureAnalysis(response) {
2956
3100
  }
2957
3101
 
2958
3102
  // src/review/rag-selector.ts
2959
- import { readFileSync as readFileSync5 } from "fs";
3103
+ import { readFileSync as readFileSync6 } from "fs";
2960
3104
  import { fileURLToPath } from "url";
2961
3105
  import path from "path";
2962
3106
  var DATA_FILES = [
@@ -2979,7 +3123,7 @@ function getDataDir() {
2979
3123
  ];
2980
3124
  for (const dir of candidates) {
2981
3125
  try {
2982
- readFileSync5(path.join(dir, "safety.json"));
3126
+ readFileSync6(path.join(dir, "safety.json"));
2983
3127
  return dir;
2984
3128
  } catch {
2985
3129
  }
@@ -2992,7 +3136,7 @@ function loadAllStories() {
2992
3136
  const stories = [];
2993
3137
  for (const file of DATA_FILES) {
2994
3138
  try {
2995
- const content = readFileSync5(path.join(dataDir, file), "utf-8");
3139
+ const content = readFileSync6(path.join(dataDir, file), "utf-8");
2996
3140
  const parsed = JSON.parse(content);
2997
3141
  stories.push(...parsed);
2998
3142
  } catch {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@forvibe/cli",
3
- "version": "1.1.0",
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": {