@prasenjeet/shipli 1.0.0
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/LICENSE +21 -0
- package/README.md +143 -0
- package/package.json +42 -0
- package/src/auditor.js +524 -0
- package/src/config.js +42 -0
- package/src/guidelines.js +42 -0
- package/src/index.js +229 -0
- package/src/init.js +236 -0
- package/src/manifest-reader.js +33 -0
- package/src/plist-reader.js +30 -0
- package/src/pubspec-reader.js +30 -0
- package/src/reporter.js +170 -0
- package/src/rules/android-rules.md +3655 -0
- package/src/rules/appstore-rules.md +944 -0
- package/src/scanner.js +238 -0
package/src/auditor.js
ADDED
|
@@ -0,0 +1,524 @@
|
|
|
1
|
+
const GEMINI_API_URL = 'https://generativelanguage.googleapis.com/v1beta/models';
|
|
2
|
+
const CLAUDE_API_URL = 'https://api.anthropic.com/v1/messages';
|
|
3
|
+
|
|
4
|
+
// ── Response format (shared across all prompts) ──
|
|
5
|
+
|
|
6
|
+
const RESPONSE_FORMAT = `
|
|
7
|
+
RESPONSE FORMAT (strict JSON):
|
|
8
|
+
{
|
|
9
|
+
"summary": "One-paragraph overall assessment",
|
|
10
|
+
"score": "PASS" | "WARNING" | "FAIL",
|
|
11
|
+
"categories": [
|
|
12
|
+
{
|
|
13
|
+
"name": "Category Name",
|
|
14
|
+
"status": "PASS" | "WARNING" | "FAIL",
|
|
15
|
+
"findings": [
|
|
16
|
+
{
|
|
17
|
+
"severity": "pass" | "warning" | "fail",
|
|
18
|
+
"title": "Short finding title",
|
|
19
|
+
"detail": "Explanation with specific file/dependency references",
|
|
20
|
+
"guideline": "Relevant guideline or best practice reference"
|
|
21
|
+
}
|
|
22
|
+
]
|
|
23
|
+
}
|
|
24
|
+
],
|
|
25
|
+
"recommendations": ["Actionable recommendation 1", "Actionable recommendation 2"]
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
Be thorough but practical. Flag real risks, not theoretical ones. Reference specific file paths and dependency names in your findings.
|
|
29
|
+
Respond ONLY with the JSON object, no markdown fencing, no extra text.`;
|
|
30
|
+
|
|
31
|
+
// ── Store categories ──
|
|
32
|
+
|
|
33
|
+
const IOS_STORE_CATEGORIES = `
|
|
34
|
+
iOS APP STORE AUDIT CATEGORIES (evaluate each):
|
|
35
|
+
1. PRIVACY & PERMISSIONS — Are all used sensitive APIs declared in Info.plist with descriptive, specific usage strings? Are there undeclared permissions implied by dependencies?
|
|
36
|
+
2. DATA COLLECTION & TRACKING — Does the app collect user data? Is App Tracking Transparency needed? Are analytics/crash SDKs present?
|
|
37
|
+
3. CONTENT & DESIGN — Does the app meet minimum functionality? Any signs of webview-only wrapping? Responsive layout issues?
|
|
38
|
+
4. IN-APP PURCHASES — If payment/purchase code exists, is it using Apple's StoreKit/IAP? Signs of third-party payment for digital goods?
|
|
39
|
+
5. LEGAL & COMPLIANCE — Privacy policy requirements, export compliance (encryption), COPPA concerns, age ratings?
|
|
40
|
+
6. FORBIDDEN PATTERNS — Code push/dynamic code loading (not allowed by Apple), hot-patching, remote code execution?`;
|
|
41
|
+
|
|
42
|
+
const ANDROID_STORE_CATEGORIES = `
|
|
43
|
+
GOOGLE PLAY STORE AUDIT CATEGORIES (evaluate each):
|
|
44
|
+
1. PERMISSIONS & DATA SAFETY — Are all declared permissions in AndroidManifest.xml necessary? Does the app comply with Google Play's Data Safety requirements? Are runtime permissions handled correctly?
|
|
45
|
+
2. DATA COLLECTION & PRIVACY — Does the app collect personal/sensitive user data? Is a privacy policy provided? Does the app comply with Google Play's User Data policy?
|
|
46
|
+
3. CONTENT & BEHAVIOR — Does the app contain restricted content (gambling, financial services, health)? Any deceptive behavior, ad fraud, or misleading functionality?
|
|
47
|
+
4. BILLING & MONETIZATION — If the app sells digital goods, is it using Google Play Billing Library? Are subscriptions handled per Google Play policy?
|
|
48
|
+
5. TARGET API LEVEL & COMPATIBILITY — Does the app target the required minimum API level? Are there compatibility issues with recent Android versions?
|
|
49
|
+
6. MALWARE & ABUSE — Any signs of stalkerware, spyware, or abusive notifications? Does the app misuse device APIs or background services?`;
|
|
50
|
+
|
|
51
|
+
const CODE_CATEGORIES = `
|
|
52
|
+
CODE QUALITY AUDIT CATEGORIES (evaluate each):
|
|
53
|
+
1. SECURITY — Hardcoded API keys, insecure HTTP calls, SQL injection, command injection, known vulnerable patterns?
|
|
54
|
+
2. ARCHITECTURE — State management patterns, separation of concerns, proper layering, overly coupled components?
|
|
55
|
+
3. ERROR HANDLING — try/catch coverage, error boundaries, crash handling, graceful degradation?
|
|
56
|
+
4. PERFORMANCE — Heavy/problematic dependencies, unnecessary widget rebuilds, memory leaks, large synchronous operations on main thread?
|
|
57
|
+
5. BEST PRACTICES — Proper dispose/lifecycle handling, null safety compliance, deprecated API usage, resource cleanup?
|
|
58
|
+
6. DEPENDENCIES — Outdated/unmaintained packages, overly broad version constraints, misplaced dev dependencies?`;
|
|
59
|
+
|
|
60
|
+
// ── System prompt builders ──
|
|
61
|
+
|
|
62
|
+
function buildStorePrompt(platform) {
|
|
63
|
+
const isIos = platform === 'ios';
|
|
64
|
+
const isAndroid = platform === 'android';
|
|
65
|
+
const isBoth = platform === 'both';
|
|
66
|
+
|
|
67
|
+
const guidelinesRef = [];
|
|
68
|
+
if (isIos || isBoth) {
|
|
69
|
+
guidelinesRef.push('You have been provided the CURRENT Apple App Store Review Guidelines in the "APPLE_APP_STORE_GUIDELINES" section (if available). Cite specific Apple guideline section numbers (e.g., "5.1.1", "3.1.1").');
|
|
70
|
+
}
|
|
71
|
+
if (isAndroid || isBoth) {
|
|
72
|
+
guidelinesRef.push('You have been provided the CURRENT Google Play Developer Program Policies in the "GOOGLE_PLAY_GUIDELINES" section (if available). Cite specific Google Play policy names.');
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const knowledge = [];
|
|
76
|
+
if (isIos || isBoth) {
|
|
77
|
+
knowledge.push(
|
|
78
|
+
"- Apple's App Store Review Guidelines (all sections)",
|
|
79
|
+
'- iOS privacy requirements (Info.plist usage descriptions, App Tracking Transparency)',
|
|
80
|
+
'- In-app purchase requirements (StoreKit vs third-party payment)',
|
|
81
|
+
'- Common iOS rejection reasons for Flutter apps',
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
if (isAndroid || isBoth) {
|
|
85
|
+
knowledge.push(
|
|
86
|
+
"- Google Play Developer Program Policies (all sections)",
|
|
87
|
+
'- Android permissions model and Data Safety requirements',
|
|
88
|
+
'- Google Play Billing Library requirements for digital goods',
|
|
89
|
+
'- Common Android rejection reasons for Flutter apps',
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
knowledge.push(
|
|
93
|
+
'- Data safety and encryption export compliance',
|
|
94
|
+
'- Minimum functionality and content policy requirements',
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
const evidence = ['- "PUBSPEC_METADATA": App name, version, and list of pub.dev package dependencies'];
|
|
98
|
+
if (isIos || isBoth) evidence.push('- "INFO_PLIST_PERMISSIONS": NS*UsageDescription keys from Info.plist');
|
|
99
|
+
if (isAndroid || isBoth) evidence.push('- "ANDROID_MANIFEST_PERMISSIONS": uses-permission entries from AndroidManifest.xml');
|
|
100
|
+
evidence.push('- "DART_SKELETONS": Architectural skeleton of Dart files');
|
|
101
|
+
|
|
102
|
+
const categories = [];
|
|
103
|
+
if (isIos || isBoth) categories.push(IOS_STORE_CATEGORIES);
|
|
104
|
+
if (isAndroid || isBoth) categories.push(ANDROID_STORE_CATEGORIES);
|
|
105
|
+
|
|
106
|
+
const platformLabel = isBoth ? 'Apple App Store and Google Play' : isIos ? 'Apple App Store' : 'Google Play Store';
|
|
107
|
+
|
|
108
|
+
return `You are an experienced ${platformLabel} reviewer conducting a pre-submission compliance audit of a Flutter/Dart application.
|
|
109
|
+
|
|
110
|
+
${guidelinesRef.join('\n')}
|
|
111
|
+
|
|
112
|
+
You have deep knowledge of:
|
|
113
|
+
${knowledge.join('\n')}
|
|
114
|
+
|
|
115
|
+
Focus ONLY on store compliance — not code quality or architecture.
|
|
116
|
+
|
|
117
|
+
EVIDENCE FORMAT:
|
|
118
|
+
${evidence.join('\n')}
|
|
119
|
+
${categories.join('\n')}
|
|
120
|
+
${RESPONSE_FORMAT}`;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function buildCodePrompt() {
|
|
124
|
+
return `You are a senior Flutter/Dart software engineer conducting a code quality and security review. You have deep knowledge of:
|
|
125
|
+
|
|
126
|
+
- Flutter best practices, widget lifecycle, state management patterns
|
|
127
|
+
- Dart language features, null safety, async patterns
|
|
128
|
+
- Common security vulnerabilities in mobile apps (OWASP Mobile Top 10)
|
|
129
|
+
- Performance optimization for Flutter (build methods, rebuilds, isolates)
|
|
130
|
+
- Dependency management and pub.dev ecosystem health
|
|
131
|
+
|
|
132
|
+
Focus ONLY on code quality, security, and engineering best practices — not store compliance or legal requirements.
|
|
133
|
+
|
|
134
|
+
EVIDENCE FORMAT:
|
|
135
|
+
- "PUBSPEC_METADATA": App name, version, and list of pub.dev package dependencies
|
|
136
|
+
- "DART_SKELETONS": Architectural skeleton of Dart files
|
|
137
|
+
${CODE_CATEGORIES}
|
|
138
|
+
${RESPONSE_FORMAT}`;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function buildBothPrompt(platform) {
|
|
142
|
+
const isIos = platform === 'ios';
|
|
143
|
+
const isAndroid = platform === 'android';
|
|
144
|
+
const isBoth = platform === 'both';
|
|
145
|
+
|
|
146
|
+
const guidelinesRef = [];
|
|
147
|
+
if (isIos || isBoth) {
|
|
148
|
+
guidelinesRef.push('You have been provided the CURRENT Apple App Store Review Guidelines in the "APPLE_APP_STORE_GUIDELINES" section (if available). Cite specific Apple guideline numbers.');
|
|
149
|
+
}
|
|
150
|
+
if (isAndroid || isBoth) {
|
|
151
|
+
guidelinesRef.push('You have been provided the CURRENT Google Play Developer Program Policies in the "GOOGLE_PLAY_GUIDELINES" section (if available). Cite specific Google Play policy names.');
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const knowledge = [];
|
|
155
|
+
if (isIos || isBoth) {
|
|
156
|
+
knowledge.push(
|
|
157
|
+
"- Apple's App Store Review Guidelines (all sections)",
|
|
158
|
+
'- iOS privacy requirements (Info.plist usage descriptions, App Tracking Transparency)',
|
|
159
|
+
'- In-app purchase requirements (StoreKit vs third-party payment)',
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
if (isAndroid || isBoth) {
|
|
163
|
+
knowledge.push(
|
|
164
|
+
"- Google Play Developer Program Policies (all sections)",
|
|
165
|
+
'- Android permissions model and Data Safety requirements',
|
|
166
|
+
'- Google Play Billing Library requirements',
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
knowledge.push(
|
|
170
|
+
'- Flutter best practices, widget lifecycle, state management patterns',
|
|
171
|
+
'- Common security vulnerabilities in mobile apps (OWASP Mobile Top 10)',
|
|
172
|
+
'- Performance optimization and dependency management',
|
|
173
|
+
);
|
|
174
|
+
|
|
175
|
+
const evidence = ['- "PUBSPEC_METADATA": App name, version, and list of pub.dev package dependencies'];
|
|
176
|
+
if (isIos || isBoth) evidence.push('- "INFO_PLIST_PERMISSIONS": NS*UsageDescription keys from Info.plist');
|
|
177
|
+
if (isAndroid || isBoth) evidence.push('- "ANDROID_MANIFEST_PERMISSIONS": uses-permission entries from AndroidManifest.xml');
|
|
178
|
+
evidence.push('- "DART_SKELETONS": Architectural skeleton of Dart files');
|
|
179
|
+
|
|
180
|
+
const categories = [];
|
|
181
|
+
if (isIos || isBoth) categories.push(IOS_STORE_CATEGORIES);
|
|
182
|
+
if (isAndroid || isBoth) categories.push(ANDROID_STORE_CATEGORIES);
|
|
183
|
+
categories.push(CODE_CATEGORIES);
|
|
184
|
+
|
|
185
|
+
const platformLabel = isBoth ? 'Apple App Store and Google Play' : isIos ? 'Apple App Store' : 'Google Play Store';
|
|
186
|
+
|
|
187
|
+
return `You are an experienced ${platformLabel} reviewer AND senior Flutter/Dart engineer conducting a comprehensive audit.
|
|
188
|
+
|
|
189
|
+
${guidelinesRef.join('\n')}
|
|
190
|
+
|
|
191
|
+
You have deep knowledge of:
|
|
192
|
+
${knowledge.join('\n')}
|
|
193
|
+
|
|
194
|
+
Analyze the provided Flutter project evidence and produce a comprehensive audit report covering both store compliance and code quality.
|
|
195
|
+
|
|
196
|
+
EVIDENCE FORMAT:
|
|
197
|
+
${evidence.join('\n')}
|
|
198
|
+
${categories.join('\n')}
|
|
199
|
+
${RESPONSE_FORMAT}`;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// ── Package prompt builders ──
|
|
203
|
+
|
|
204
|
+
const PKG_STORE_CATEGORIES = `
|
|
205
|
+
PACKAGE STORE AUDIT CATEGORIES (evaluate each):
|
|
206
|
+
1. PLATFORM DECLARATIONS — Do declared platforms in pubspec match actual platform channel implementations? Are MethodChannel names consistent? Missing platforms?
|
|
207
|
+
2. CONSUMER PERMISSIONS GUIDANCE — Does the plugin use sensitive APIs (camera, location, contacts, etc.)? If so, are the required Info.plist keys / AndroidManifest permissions documented for host apps?
|
|
208
|
+
3. LEGAL & COMPLIANCE — License clarity, export compliance signals (encryption), data handling implications for consuming apps?`;
|
|
209
|
+
|
|
210
|
+
const PKG_CODE_CATEGORIES = `
|
|
211
|
+
PACKAGE CODE AUDIT CATEGORIES (evaluate each):
|
|
212
|
+
1. API SURFACE & DOCUMENTATION — Is the public API clean and well-structured? Are exported symbols intentional? Clear entry point?
|
|
213
|
+
2. DEPENDENCY HYGIENE — Are dependency constraints appropriate? Any problematic or heavy transitive dependencies? Should any deps be dev_dependencies?
|
|
214
|
+
3. SECURITY — Hardcoded API keys, insecure HTTP calls, unsafe platform channel data handling?
|
|
215
|
+
4. COMPATIBILITY & VERSIONING — Dart SDK constraints, Flutter SDK constraints, breaking change risks, deprecated API usage?
|
|
216
|
+
5. EXAMPLE APP QUALITY — Does the example/ app exist and demonstrate key features? Is it a good integration test?
|
|
217
|
+
6. FLUTTER-SPECIFIC — Proper dispose/lifecycle handling, memory leak risks, platform channel error handling, null safety compliance?`;
|
|
218
|
+
|
|
219
|
+
function buildPkgStorePrompt() {
|
|
220
|
+
return `You are an experienced Flutter plugin reviewer focused on compliance and platform integration. You have deep knowledge of:
|
|
221
|
+
|
|
222
|
+
- Flutter plugin architecture (platform channels, federated plugins)
|
|
223
|
+
- iOS and Android platform integration requirements for Flutter plugins
|
|
224
|
+
- Privacy and permission implications for consuming apps
|
|
225
|
+
- Apple App Store Review Guidelines and Google Play Developer Policies as they affect plugins
|
|
226
|
+
|
|
227
|
+
Focus ONLY on store compliance and consumer-facing requirements — not code quality.
|
|
228
|
+
|
|
229
|
+
EVIDENCE FORMAT:
|
|
230
|
+
- "PUBSPEC_METADATA": Package name, version, description, dependencies
|
|
231
|
+
- "PLUGIN_PLATFORMS": Supported platforms and native integration classes
|
|
232
|
+
- "DART_SKELETONS": Architectural skeleton of Dart files
|
|
233
|
+
- "EXAMPLE_APP" (if present): Skeleton of the example/ app
|
|
234
|
+
${PKG_STORE_CATEGORIES}
|
|
235
|
+
${RESPONSE_FORMAT}`;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function buildPkgCodePrompt() {
|
|
239
|
+
return `You are a senior Flutter/Dart package engineer conducting a code quality review. You have deep knowledge of:
|
|
240
|
+
|
|
241
|
+
- pub.dev scoring criteria (documentation, API design, maintenance, platform support)
|
|
242
|
+
- Dart package best practices (semantic versioning, dependency constraints, export hygiene)
|
|
243
|
+
- Flutter plugin architecture and platform channel patterns
|
|
244
|
+
- Performance and memory management in plugins
|
|
245
|
+
|
|
246
|
+
Focus ONLY on code quality, API design, and engineering best practices — not store compliance.
|
|
247
|
+
|
|
248
|
+
EVIDENCE FORMAT:
|
|
249
|
+
- "PUBSPEC_METADATA": Package name, version, description, dependencies
|
|
250
|
+
- "PLUGIN_PLATFORMS": Supported platforms and native integration classes
|
|
251
|
+
- "DART_SKELETONS": Architectural skeleton of Dart files
|
|
252
|
+
- "EXAMPLE_APP" (if present): Skeleton of the example/ app
|
|
253
|
+
${PKG_CODE_CATEGORIES}
|
|
254
|
+
${RESPONSE_FORMAT}`;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function buildPkgBothPrompt() {
|
|
258
|
+
return `You are an experienced Flutter/Dart package reviewer and pub.dev quality auditor. You have deep knowledge of:
|
|
259
|
+
|
|
260
|
+
- Flutter plugin architecture (platform channels, federated plugins, platform interface patterns)
|
|
261
|
+
- pub.dev scoring criteria (documentation, API design, maintenance, platform support)
|
|
262
|
+
- Dart package best practices (semantic versioning, dependency constraints, export hygiene)
|
|
263
|
+
- iOS and Android platform integration requirements
|
|
264
|
+
- Privacy and permission implications for consuming apps
|
|
265
|
+
|
|
266
|
+
Analyze the provided Flutter package/plugin evidence and produce a comprehensive audit report.
|
|
267
|
+
|
|
268
|
+
EVIDENCE FORMAT:
|
|
269
|
+
- "PUBSPEC_METADATA": Package name, version, description, dependencies
|
|
270
|
+
- "PLUGIN_PLATFORMS": Supported platforms and native integration classes
|
|
271
|
+
- "DART_SKELETONS": Architectural skeleton of Dart files
|
|
272
|
+
- "EXAMPLE_APP" (if present): Skeleton of the example/ app
|
|
273
|
+
${PKG_STORE_CATEGORIES}
|
|
274
|
+
${PKG_CODE_CATEGORIES}
|
|
275
|
+
${RESPONSE_FORMAT}`;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// ── Prompt selection ──
|
|
279
|
+
|
|
280
|
+
function selectPrompt(projectType, mode, platform) {
|
|
281
|
+
if (projectType === 'package') {
|
|
282
|
+
if (mode === 'store') return buildPkgStorePrompt();
|
|
283
|
+
if (mode === 'code') return buildPkgCodePrompt();
|
|
284
|
+
return buildPkgBothPrompt();
|
|
285
|
+
}
|
|
286
|
+
// App
|
|
287
|
+
if (mode === 'store') return buildStorePrompt(platform);
|
|
288
|
+
if (mode === 'code') return buildCodePrompt();
|
|
289
|
+
return buildBothPrompt(platform);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// ── Message builder ──
|
|
293
|
+
|
|
294
|
+
function buildUserMessage({ files, exampleFiles, permissions, androidPermissions, pubspec, plistFound, androidManifestFound, projectType, appleGuidelines, googleGuidelines }) {
|
|
295
|
+
const sections = [];
|
|
296
|
+
const isPackage = projectType === 'package';
|
|
297
|
+
|
|
298
|
+
// Inject live guidelines as grounding context
|
|
299
|
+
if (appleGuidelines?.content) {
|
|
300
|
+
sections.push('=== APPLE_APP_STORE_GUIDELINES ===');
|
|
301
|
+
sections.push('(Apple App Store Review Guidelines)');
|
|
302
|
+
sections.push(appleGuidelines.content);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
if (googleGuidelines?.content) {
|
|
306
|
+
sections.push('\n=== GOOGLE_PLAY_GUIDELINES ===');
|
|
307
|
+
sections.push('(Google Play Developer Program Policies)');
|
|
308
|
+
sections.push(googleGuidelines.content);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
sections.push('\n=== PUBSPEC_METADATA ===');
|
|
312
|
+
sections.push(`${isPackage ? 'Package' : 'App'}: ${pubspec.name}${pubspec.version ? ` v${pubspec.version}` : ''}`);
|
|
313
|
+
if (pubspec.description) {
|
|
314
|
+
sections.push(`Description: ${pubspec.description}`);
|
|
315
|
+
}
|
|
316
|
+
sections.push(`Dependencies: ${pubspec.dependencies.join(', ') || '(none)'}`);
|
|
317
|
+
sections.push(`Dev Dependencies: ${pubspec.devDependencies.join(', ') || '(none)'}`);
|
|
318
|
+
|
|
319
|
+
if (isPackage && pubspec.pluginPlatforms) {
|
|
320
|
+
sections.push('\n=== PLUGIN_PLATFORMS ===');
|
|
321
|
+
for (const [platform, config] of Object.entries(pubspec.pluginPlatforms)) {
|
|
322
|
+
sections.push(`${platform}: ${JSON.stringify(config)}`);
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// iOS permissions
|
|
327
|
+
if (!isPackage && plistFound !== undefined) {
|
|
328
|
+
sections.push('\n=== INFO_PLIST_PERMISSIONS ===');
|
|
329
|
+
if (!plistFound) {
|
|
330
|
+
sections.push('Info.plist NOT FOUND at ios/Runner/Info.plist');
|
|
331
|
+
} else if (Object.keys(permissions).length === 0) {
|
|
332
|
+
sections.push('No NS*UsageDescription keys found in Info.plist.');
|
|
333
|
+
} else {
|
|
334
|
+
for (const [key, value] of Object.entries(permissions)) {
|
|
335
|
+
sections.push(`${key}: "${value}"`);
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// Android permissions
|
|
341
|
+
if (!isPackage && androidManifestFound !== undefined) {
|
|
342
|
+
sections.push('\n=== ANDROID_MANIFEST_PERMISSIONS ===');
|
|
343
|
+
if (!androidManifestFound) {
|
|
344
|
+
sections.push('AndroidManifest.xml NOT FOUND at android/app/src/main/AndroidManifest.xml');
|
|
345
|
+
} else if (androidPermissions.length === 0) {
|
|
346
|
+
sections.push('No <uses-permission> entries found in AndroidManifest.xml.');
|
|
347
|
+
} else {
|
|
348
|
+
for (const perm of androidPermissions) {
|
|
349
|
+
sections.push(perm);
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
sections.push('\n=== DART_SKELETONS ===');
|
|
355
|
+
for (const file of files) {
|
|
356
|
+
sections.push(`\n--- ${file.relativePath} ---`);
|
|
357
|
+
sections.push(file.skeleton);
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
if (exampleFiles && exampleFiles.length > 0) {
|
|
361
|
+
sections.push('\n=== EXAMPLE_APP ===');
|
|
362
|
+
for (const file of exampleFiles) {
|
|
363
|
+
sections.push(`\n--- example/${file.relativePath} ---`);
|
|
364
|
+
sections.push(file.skeleton);
|
|
365
|
+
}
|
|
366
|
+
} else if (isPackage) {
|
|
367
|
+
sections.push('\n=== EXAMPLE_APP ===');
|
|
368
|
+
sections.push('No example/ app found in this package.');
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
return sections.join('\n');
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
// ── API callers ──
|
|
375
|
+
|
|
376
|
+
async function callGemini(userMessage, { apiKey, model, systemPrompt }) {
|
|
377
|
+
const url = `${GEMINI_API_URL}/${model}:generateContent`;
|
|
378
|
+
|
|
379
|
+
const body = {
|
|
380
|
+
system_instruction: { parts: [{ text: systemPrompt }] },
|
|
381
|
+
contents: [{ parts: [{ text: userMessage }] }],
|
|
382
|
+
generationConfig: {
|
|
383
|
+
temperature: 0.2,
|
|
384
|
+
responseMimeType: 'application/json',
|
|
385
|
+
},
|
|
386
|
+
};
|
|
387
|
+
|
|
388
|
+
const res = await fetch(url, {
|
|
389
|
+
method: 'POST',
|
|
390
|
+
headers: {
|
|
391
|
+
'Content-Type': 'application/json',
|
|
392
|
+
'x-goog-api-key': apiKey,
|
|
393
|
+
},
|
|
394
|
+
body: JSON.stringify(body),
|
|
395
|
+
});
|
|
396
|
+
|
|
397
|
+
if (!res.ok) {
|
|
398
|
+
const errBody = await res.text();
|
|
399
|
+
if (res.status === 400 && errBody.includes('API_KEY')) {
|
|
400
|
+
throw new Error('Invalid API key. Check your Gemini API key and try again.');
|
|
401
|
+
}
|
|
402
|
+
throw new Error(`Gemini API error (${res.status}): ${errBody}`);
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
const data = await res.json();
|
|
406
|
+
const text = data.candidates?.[0]?.content?.parts?.[0]?.text;
|
|
407
|
+
|
|
408
|
+
if (!text) {
|
|
409
|
+
const reason = data.candidates?.[0]?.finishReason;
|
|
410
|
+
throw new Error(`Gemini returned an empty response${reason ? ` (reason: ${reason})` : ''}`);
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
const usage = data.usageMetadata || {};
|
|
414
|
+
return {
|
|
415
|
+
text,
|
|
416
|
+
tokens: {
|
|
417
|
+
input: usage.promptTokenCount || null,
|
|
418
|
+
output: usage.candidatesTokenCount || null,
|
|
419
|
+
total: usage.totalTokenCount || null,
|
|
420
|
+
},
|
|
421
|
+
};
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
async function callClaude(userMessage, { apiKey, model, systemPrompt }) {
|
|
425
|
+
const body = {
|
|
426
|
+
model,
|
|
427
|
+
max_tokens: 8192,
|
|
428
|
+
system: systemPrompt,
|
|
429
|
+
messages: [{ role: 'user', content: userMessage }],
|
|
430
|
+
temperature: 0.2,
|
|
431
|
+
};
|
|
432
|
+
|
|
433
|
+
const res = await fetch(CLAUDE_API_URL, {
|
|
434
|
+
method: 'POST',
|
|
435
|
+
headers: {
|
|
436
|
+
'Content-Type': 'application/json',
|
|
437
|
+
'x-api-key': apiKey,
|
|
438
|
+
'anthropic-version': '2023-06-01',
|
|
439
|
+
},
|
|
440
|
+
body: JSON.stringify(body),
|
|
441
|
+
});
|
|
442
|
+
|
|
443
|
+
if (!res.ok) {
|
|
444
|
+
const errBody = await res.text();
|
|
445
|
+
if (res.status === 401) {
|
|
446
|
+
throw new Error('Invalid API key. Check your Anthropic API key and try again.');
|
|
447
|
+
}
|
|
448
|
+
throw new Error(`Claude API error (${res.status}): ${errBody}`);
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
const data = await res.json();
|
|
452
|
+
const text = data.content?.[0]?.text;
|
|
453
|
+
|
|
454
|
+
if (!text) {
|
|
455
|
+
throw new Error(`Claude returned an empty response (stop_reason: ${data.stop_reason || 'unknown'})`);
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
const usage = data.usage || {};
|
|
459
|
+
return {
|
|
460
|
+
text,
|
|
461
|
+
tokens: {
|
|
462
|
+
input: usage.input_tokens || null,
|
|
463
|
+
output: usage.output_tokens || null,
|
|
464
|
+
total: (usage.input_tokens || 0) + (usage.output_tokens || 0) || null,
|
|
465
|
+
},
|
|
466
|
+
};
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
// ── Main audit function ──
|
|
470
|
+
|
|
471
|
+
function estimateTokens(text) {
|
|
472
|
+
return Math.ceil(text.length / 4);
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
// Context window limits (input tokens) with buffer for output
|
|
476
|
+
const TOKEN_LIMITS = {
|
|
477
|
+
claude: 150_000, // 200K context, reserve 50K for output
|
|
478
|
+
gemini: 900_000, // 1M context, reserve 100K for output
|
|
479
|
+
};
|
|
480
|
+
|
|
481
|
+
export async function audit(evidence, { apiKey, model, provider, mode, platform }) {
|
|
482
|
+
const systemPrompt = selectPrompt(evidence.projectType, mode, platform);
|
|
483
|
+
const userMessage = buildUserMessage(evidence);
|
|
484
|
+
|
|
485
|
+
const estimated = {
|
|
486
|
+
system: estimateTokens(systemPrompt),
|
|
487
|
+
user: estimateTokens(userMessage),
|
|
488
|
+
total: estimateTokens(systemPrompt) + estimateTokens(userMessage),
|
|
489
|
+
};
|
|
490
|
+
|
|
491
|
+
// Safety check: warn if estimated tokens exceed provider limit
|
|
492
|
+
const limit = TOKEN_LIMITS[provider] || TOKEN_LIMITS.gemini;
|
|
493
|
+
if (estimated.total > limit) {
|
|
494
|
+
const pct = Math.round((estimated.total / limit) * 100);
|
|
495
|
+
throw new Error(
|
|
496
|
+
`Estimated input (~${estimated.total.toLocaleString()} tokens) exceeds ${provider} safe limit (~${limit.toLocaleString()} tokens) by ${pct - 100}%.\n` +
|
|
497
|
+
` Try: --platform ios or --platform android (instead of both), or --mode store or --mode code (instead of both).`
|
|
498
|
+
);
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
if (estimated.total > limit * 0.8) {
|
|
502
|
+
const pct = Math.round((estimated.total / limit) * 100);
|
|
503
|
+
process.stderr.write(
|
|
504
|
+
`\x1b[33m ⚠ Token usage is high (~${estimated.total.toLocaleString()} tokens, ${pct}% of ${provider} limit). Results may be truncated.\x1b[0m\n`
|
|
505
|
+
);
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
const response = provider === 'claude'
|
|
509
|
+
? await callClaude(userMessage, { apiKey, model, systemPrompt })
|
|
510
|
+
: await callGemini(userMessage, { apiKey, model, systemPrompt });
|
|
511
|
+
|
|
512
|
+
const cleaned = response.text.replace(/^```(?:json)?\s*\n?/i, '').replace(/\n?```\s*$/i, '').trim();
|
|
513
|
+
|
|
514
|
+
try {
|
|
515
|
+
const result = JSON.parse(cleaned);
|
|
516
|
+
result._tokens = {
|
|
517
|
+
estimated,
|
|
518
|
+
actual: response.tokens,
|
|
519
|
+
};
|
|
520
|
+
return result;
|
|
521
|
+
} catch {
|
|
522
|
+
throw new Error(`Failed to parse AI response as JSON. Raw output:\n${response.text.slice(0, 500)}`);
|
|
523
|
+
}
|
|
524
|
+
}
|
package/src/config.js
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { homedir } from 'node:os';
|
|
4
|
+
|
|
5
|
+
async function readConfigFile(filePath) {
|
|
6
|
+
try {
|
|
7
|
+
const content = await readFile(filePath, 'utf-8');
|
|
8
|
+
const parsed = JSON.parse(content);
|
|
9
|
+
if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
|
|
10
|
+
throw new Error('Config must be a JSON object');
|
|
11
|
+
}
|
|
12
|
+
return parsed;
|
|
13
|
+
} catch (err) {
|
|
14
|
+
if (err.code === 'ENOENT') return null;
|
|
15
|
+
if (err instanceof SyntaxError) {
|
|
16
|
+
throw new Error(`Invalid JSON in ${filePath}: ${err.message}`);
|
|
17
|
+
}
|
|
18
|
+
throw err;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export async function loadConfig(projectDir) {
|
|
23
|
+
// Load home-level config first, then project-level overrides
|
|
24
|
+
const homeConfig = await readConfigFile(join(homedir(), '.shipli'));
|
|
25
|
+
const projectConfig = projectDir
|
|
26
|
+
? await readConfigFile(join(projectDir, '.shipli'))
|
|
27
|
+
: null;
|
|
28
|
+
|
|
29
|
+
const merged = {
|
|
30
|
+
...(homeConfig || {}),
|
|
31
|
+
...(projectConfig || {}),
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
return {
|
|
35
|
+
provider: merged.provider || undefined,
|
|
36
|
+
model: merged.model || undefined,
|
|
37
|
+
key: merged.key || undefined,
|
|
38
|
+
type: merged.type || undefined,
|
|
39
|
+
mode: merged.mode || undefined,
|
|
40
|
+
platform: merged.platform || undefined,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises';
|
|
2
|
+
import { join, dirname } from 'node:path';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
|
|
5
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
6
|
+
|
|
7
|
+
const STORES = {
|
|
8
|
+
apple: {
|
|
9
|
+
file: join(__dirname, 'rules', 'appstore-rules.md'),
|
|
10
|
+
label: 'App Store',
|
|
11
|
+
},
|
|
12
|
+
google: {
|
|
13
|
+
file: join(__dirname, 'rules', 'android-rules.md'),
|
|
14
|
+
label: 'Play Store',
|
|
15
|
+
},
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Load guidelines from bundled .md files.
|
|
20
|
+
* @param {'apple' | 'google'} store
|
|
21
|
+
*/
|
|
22
|
+
export async function fetchGuidelines(store = 'apple') {
|
|
23
|
+
const config = STORES[store];
|
|
24
|
+
if (!config) throw new Error(`Unknown store: ${store}`);
|
|
25
|
+
|
|
26
|
+
try {
|
|
27
|
+
const content = await readFile(config.file, 'utf-8');
|
|
28
|
+
|
|
29
|
+
return {
|
|
30
|
+
content,
|
|
31
|
+
source: 'bundled',
|
|
32
|
+
store,
|
|
33
|
+
};
|
|
34
|
+
} catch (err) {
|
|
35
|
+
return {
|
|
36
|
+
content: null,
|
|
37
|
+
source: 'unavailable',
|
|
38
|
+
store,
|
|
39
|
+
warning: `Could not load ${config.label} guidelines (${err.message}). Audit will use the AI model's built-in knowledge.`,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
}
|