@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.
- package/dist/index.js +163 -19
- 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 =
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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 =
|
|
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 {
|