@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
|
-
|
|
55
|
-
|
|
56
|
-
|
|
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
|
-
}
|
|
64
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
|
@@ -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-
|
|
2602
|
-
const
|
|
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
|
-
|
|
2632
|
-
|
|
2633
|
-
|
|
2634
|
-
|
|
2635
|
-
|
|
2636
|
-
|
|
2637
|
-
|
|
2638
|
-
|
|
2639
|
-
|
|
2640
|
-
|
|
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
|
-
|
|
2654
|
-
|
|
2655
|
-
|
|
2656
|
-
|
|
2657
|
-
|
|
2658
|
-
|
|
2659
|
-
|
|
2660
|
-
|
|
2661
|
-
|
|
2662
|
-
|
|
2663
|
-
);
|
|
2664
|
-
|
|
2665
|
-
|
|
2666
|
-
|
|
2667
|
-
|
|
2668
|
-
|
|
2669
|
-
|
|
2670
|
-
|
|
2671
|
-
|
|
2672
|
-
|
|
2673
|
-
|
|
2674
|
-
|
|
2675
|
-
|
|
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
|
-
|
|
2679
|
-
|
|
2680
|
-
|
|
2681
|
-
|
|
2682
|
-
|
|
2683
|
-
|
|
2684
|
-
|
|
2685
|
-
|
|
2686
|
-
|
|
2687
|
-
|
|
2688
|
-
|
|
2689
|
-
|
|
2690
|
-
|
|
2691
|
-
|
|
2692
|
-
|
|
2693
|
-
|
|
2694
|
-
|
|
2695
|
-
|
|
2696
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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 =
|
|
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 {
|