runcloud 0.1.21 → 0.1.22

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.
@@ -0,0 +1,162 @@
1
+ import { readFileSync } from 'node:fs';
2
+ import { evaluateCompliance, renderEvaluation } from './appStoreEvaluate.js';
3
+ const STATUSES = new Set(['pass', 'warning', 'fail', 'info']);
4
+ const EVIDENCE_REQUIRED = new Set(['pass', 'warning', 'fail']);
5
+ function activeItems(manifest) {
6
+ return manifest.items.filter((item) => !item.deferred && item.resolvedBy !== 'card');
7
+ }
8
+ export function loadComplianceManifest(path) {
9
+ const manifestPath = path ?? process.env.NEWLY_APP_STORE_COMPLIANCE_MANIFEST;
10
+ if (!manifestPath) {
11
+ throw new Error('Missing compliance manifest. Pass --manifest or set NEWLY_APP_STORE_COMPLIANCE_MANIFEST.');
12
+ }
13
+ return JSON.parse(readFileSync(manifestPath, 'utf8'));
14
+ }
15
+ export function renderComplianceCheck(manifest) {
16
+ const lines = activeItems(manifest).map((item) => {
17
+ const bits = [item.category, item.platform.join('+'), item.check];
18
+ if (item.guideline)
19
+ bits.push(`§${item.guideline}`);
20
+ if (item.critical)
21
+ bits.push('CRITICAL');
22
+ if (item.limit)
23
+ bits.push(`limit ${item.limit}`);
24
+ if (item.source)
25
+ bits.push(item.source);
26
+ if (item.authorable)
27
+ bits.push('authorable');
28
+ if (item.autofill)
29
+ bits.push(`autofill:${item.autofill}`);
30
+ return `[${item.priority}] ${item.id} (${bits.join(', ')}) - ${item.label}`;
31
+ });
32
+ return (`Compliance manifest v${manifest.manifest_version}. The item list and completeness gate are ` +
33
+ `deterministic manifest contracts. Start local deterministic rows with: newly app-store ` +
34
+ `compliance evaluate --manifest <compliance_manifest.json> --project . --json. Use Markdown ` +
35
+ `skills only for unresolved judgment/live-store rows and listing authoring. Determine a status ` +
36
+ `(pass/warning/fail/info) for EVERY item below and submit them ALL in one report_compliance ` +
37
+ `call, each with a short message and evidence (a file:line or command output). Carry each ` +
38
+ `item's guideline / critical / authorable / autofill flags through into the report.\n\n${lines.join('\n')}`);
39
+ }
40
+ export function validateComplianceReport(rawItems, manifest) {
41
+ const list = Array.isArray(rawItems) ? rawItems : [];
42
+ const byId = new Map();
43
+ for (const item of list) {
44
+ if (item && typeof item === 'object' && typeof item.id === 'string') {
45
+ byId.set(item.id, item);
46
+ }
47
+ }
48
+ const missing = [];
49
+ const invalid = [];
50
+ for (const item of activeItems(manifest)) {
51
+ const reported = byId.get(item.id);
52
+ if (!reported) {
53
+ missing.push(item.id);
54
+ continue;
55
+ }
56
+ const status = reported.status;
57
+ if (typeof status !== 'string' || !STATUSES.has(status)) {
58
+ invalid.push(`${item.id} (status must be pass/warning/fail/info)`);
59
+ continue;
60
+ }
61
+ const evidence = reported.evidence;
62
+ if (EVIDENCE_REQUIRED.has(status) &&
63
+ (typeof evidence !== 'string' || evidence.trim() === '')) {
64
+ invalid.push(`${item.id} (a ${status} verdict needs evidence)`);
65
+ }
66
+ }
67
+ if (missing.length || invalid.length) {
68
+ const parts = ['report_compliance rejected: the report is incomplete or invalid.'];
69
+ if (missing.length)
70
+ parts.push(`Missing ${missing.length} item(s): ${missing.join(', ')}.`);
71
+ if (invalid.length)
72
+ parts.push(`Invalid item(s): ${invalid.join('; ')}.`);
73
+ parts.push('Re-call report_compliance with the complete, corrected item list.');
74
+ return { ok: false, error: parts.join(' '), missing, invalid };
75
+ }
76
+ return {
77
+ ok: true,
78
+ items: activeItems(manifest).map((item) => {
79
+ const reported = byId.get(item.id);
80
+ return {
81
+ id: item.id,
82
+ label: typeof reported.label === 'string' ? reported.label : item.label,
83
+ status: reported.status,
84
+ message: typeof reported.message === 'string' ? reported.message : '',
85
+ evidence: typeof reported.evidence === 'string' ? reported.evidence : undefined,
86
+ category: typeof reported.category === 'string' ? reported.category : item.category,
87
+ priority: typeof reported.priority === 'string' ? reported.priority : item.priority,
88
+ platform: item.platform,
89
+ authorable: item.authorable === true || undefined,
90
+ autofill: item.autofill,
91
+ guideline: item.guideline,
92
+ critical: item.critical === true || undefined,
93
+ };
94
+ }),
95
+ };
96
+ }
97
+ export function registerAppStore(program) {
98
+ const appStore = program.command('app-store').description('App Store / Google Play workflows');
99
+ const compliance = appStore
100
+ .command('compliance')
101
+ .description('App Store / Google Play readiness manifest contracts');
102
+ const printContract = (opts) => {
103
+ console.log(renderComplianceCheck(loadComplianceManifest(opts.manifest)));
104
+ };
105
+ const validateReport = (opts) => {
106
+ const manifest = loadComplianceManifest(opts.manifest);
107
+ const report = JSON.parse(readFileSync(opts.report, 'utf8'));
108
+ const items = Array.isArray(report) ? report : report.items;
109
+ const result = validateComplianceReport(items, manifest);
110
+ if (!result.ok) {
111
+ console.error(result.error);
112
+ process.exitCode = 1;
113
+ return;
114
+ }
115
+ console.log(JSON.stringify({ items: result.items }, null, 2));
116
+ };
117
+ const evaluateProject = (opts) => {
118
+ const manifest = loadComplianceManifest(opts.manifest);
119
+ const result = evaluateCompliance(manifest, opts.project ?? process.cwd());
120
+ if (opts.json) {
121
+ console.log(JSON.stringify(result, null, 2));
122
+ return;
123
+ }
124
+ console.log(renderEvaluation(result));
125
+ };
126
+ compliance
127
+ .command('contract')
128
+ .description('Print the canonical manifest contract; this does not evaluate the app')
129
+ .requiredOption('--manifest <path>', 'path to compliance_manifest.json')
130
+ .action(printContract);
131
+ compliance
132
+ .command('check')
133
+ .description('Alias for contract')
134
+ .requiredOption('--manifest <path>', 'path to compliance_manifest.json')
135
+ .action(printContract);
136
+ compliance
137
+ .command('validate-report')
138
+ .description('Validate a report_compliance JSON payload against the manifest')
139
+ .requiredOption('--manifest <path>', 'path to compliance_manifest.json')
140
+ .requiredOption('--report <path>', 'path to JSON report: either an array or { "items": [...] }')
141
+ .action(validateReport);
142
+ compliance
143
+ .command('validate')
144
+ .description('Alias for validate-report')
145
+ .requiredOption('--manifest <path>', 'path to compliance_manifest.json')
146
+ .requiredOption('--report <path>', 'path to JSON report: either an array or { "items": [...] }')
147
+ .action(validateReport);
148
+ compliance
149
+ .command('evaluate')
150
+ .description('Evaluate deterministic local checks and list unresolved manual/live-store rows')
151
+ .requiredOption('--manifest <path>', 'path to compliance_manifest.json')
152
+ .option('--project <path>', 'project directory to evaluate', process.cwd())
153
+ .option('--json', 'emit machine-readable JSON')
154
+ .action(evaluateProject);
155
+ compliance
156
+ .command('run')
157
+ .description('Alias for evaluate')
158
+ .requiredOption('--manifest <path>', 'path to compliance_manifest.json')
159
+ .option('--project <path>', 'project directory to evaluate', process.cwd())
160
+ .option('--json', 'emit machine-readable JSON')
161
+ .action(evaluateProject);
162
+ }
@@ -0,0 +1,515 @@
1
+ import { existsSync, readFileSync, readdirSync, statSync, } from 'node:fs';
2
+ import path from 'node:path';
3
+ const CONFIG_RULES = {
4
+ app_name: { path: ['expo', 'name'], absent: 'fail' },
5
+ slug: { path: ['expo', 'slug'], absent: 'fail' },
6
+ ios_bundle_id: { path: ['expo', 'ios', 'bundleIdentifier'], absent: 'fail' },
7
+ android_package: { path: ['expo', 'android', 'package'], absent: 'fail' },
8
+ version: { path: ['expo', 'version'], absent: 'warning' },
9
+ scheme: { path: ['expo', 'scheme'], absent: 'info' },
10
+ ios_encryption: { path: ['expo', 'ios', 'infoPlist', 'ITSAppUsesNonExemptEncryption'], absent: 'warning' },
11
+ user_interface_style: { path: ['expo', 'userInterfaceStyle'], absent: 'info' },
12
+ orientation: { path: ['expo', 'orientation'], absent: 'info' },
13
+ ios_supports_tablet: { path: ['expo', 'ios', 'supportsTablet'], absent: 'info' },
14
+ };
15
+ const JUDGMENT_IDS = new Set([
16
+ 'apple_sign_in_order',
17
+ 'permission_string_quality',
18
+ 'account_deletion',
19
+ 'iap_screen_compliance',
20
+ 'background_mode_justification',
21
+ 'push_notification_setup',
22
+ 'deep_link_config',
23
+ ]);
24
+ const LIVE_STORE_IDS = new Set([
25
+ 'apple_privacy_policy',
26
+ 'apple_support_url',
27
+ 'apple_description',
28
+ 'apple_store_name',
29
+ 'apple_subtitle',
30
+ 'apple_keywords',
31
+ 'apple_promotional_text',
32
+ 'apple_whats_new',
33
+ 'apple_marketing_url',
34
+ 'apple_copyright',
35
+ 'apple_app_category',
36
+ 'apple_secondary_category',
37
+ 'apple_content_rights',
38
+ 'apple_age_rating',
39
+ 'apple_privacy_label',
40
+ 'google_play_title',
41
+ 'google_play_short_description',
42
+ 'google_play_description',
43
+ 'google_play_contact_email',
44
+ 'google_play_contact_website',
45
+ 'apple_screenshots',
46
+ 'google_play_screenshots',
47
+ 'google_play_feature_graphic',
48
+ ]);
49
+ const STORE_REMINDER_IDS = new Set([
50
+ 'android_privacy_policy',
51
+ 'android_content_rating',
52
+ 'android_app_category',
53
+ 'google_data_safety',
54
+ 'google_app_access',
55
+ 'google_ads_declaration',
56
+ 'google_target_audience',
57
+ 'google_government_apps',
58
+ 'google_financial_features',
59
+ 'google_health',
60
+ ]);
61
+ const TEXT_EXTENSIONS = new Set([
62
+ '.c',
63
+ '.cc',
64
+ '.cpp',
65
+ '.cxx',
66
+ '.h',
67
+ '.hpp',
68
+ '.java',
69
+ '.js',
70
+ '.jsx',
71
+ '.json',
72
+ '.kt',
73
+ '.m',
74
+ '.md',
75
+ '.mm',
76
+ '.mjs',
77
+ '.plist',
78
+ '.swift',
79
+ '.ts',
80
+ '.tsx',
81
+ '.xml',
82
+ ]);
83
+ const SKIP_DIRS = new Set([
84
+ '.expo',
85
+ '.git',
86
+ '.next',
87
+ 'build',
88
+ 'dist',
89
+ 'node_modules',
90
+ 'Pods',
91
+ ]);
92
+ export function evaluateCompliance(manifest, projectDir) {
93
+ const root = path.resolve(projectDir);
94
+ const appJson = readJson(path.join(root, 'app.json'));
95
+ const targetPlatforms = targetPlatformsFromAppJson(appJson);
96
+ const items = [];
97
+ const unresolved = [];
98
+ for (const item of activeItems(manifest)) {
99
+ if (!targetsItem(item, targetPlatforms)) {
100
+ items.push(makeItem(item, 'info', `Not targeting ${platformNames(item.platform)}.`, 'app.json target platform detection'));
101
+ continue;
102
+ }
103
+ const evaluated = evaluateLocalItem(root, appJson, item);
104
+ if (evaluated) {
105
+ items.push(evaluated);
106
+ continue;
107
+ }
108
+ unresolved.push({
109
+ id: item.id,
110
+ label: item.label,
111
+ category: item.category,
112
+ priority: item.priority,
113
+ platform: item.platform,
114
+ check: item.check,
115
+ reason: unresolvedReason(item),
116
+ });
117
+ }
118
+ return {
119
+ manifestVersion: manifest.manifest_version,
120
+ projectDir: root,
121
+ targetPlatforms,
122
+ items,
123
+ unresolved,
124
+ };
125
+ }
126
+ export function renderEvaluation(result) {
127
+ const lines = [
128
+ `Compliance evaluation draft for ${result.projectDir}`,
129
+ `Manifest v${result.manifestVersion}; target platforms: ${result.targetPlatforms.join(', ')}`,
130
+ '',
131
+ `Deterministic items (${result.items.length}):`,
132
+ ];
133
+ for (const item of result.items) {
134
+ lines.push(`- ${item.status.toUpperCase()} ${item.id}: ${item.message ?? ''}`);
135
+ if (item.evidence)
136
+ lines.push(` evidence: ${item.evidence}`);
137
+ }
138
+ lines.push('', `Unresolved items (${result.unresolved.length}):`);
139
+ for (const item of result.unresolved) {
140
+ lines.push(`- ${item.id}: ${item.reason}`);
141
+ }
142
+ lines.push('', 'JSON shape: { "items": [...deterministic report_compliance rows...], "unresolved": [...] }');
143
+ return lines.join('\n');
144
+ }
145
+ function activeItems(manifest) {
146
+ return manifest.items.filter((item) => !item.deferred && item.resolvedBy !== 'card');
147
+ }
148
+ function evaluateLocalItem(root, appJson, item) {
149
+ if (CONFIG_RULES[item.id])
150
+ return evaluateConfigItem(appJson, item, CONFIG_RULES[item.id]);
151
+ switch (item.id) {
152
+ case 'hardcoded_secrets':
153
+ return patternItem(root, item, /sk-[A-Za-z0-9]{20,}|AIza[0-9A-Za-z_-]{30,}|-----BEGIN.*PRIVATE KEY|api[_-]?key\s*[:=]\s*['"][A-Za-z0-9]{16,}/i, 'fail', 'Hardcoded secret-like value found.', 'No secret-like values found.');
154
+ case 'http_endpoints':
155
+ return patternItem(root, item, /http:\/\/(?!localhost|127\.0\.0\.1)/i, 'warning', 'Non-local HTTP endpoint found.', 'No non-local HTTP endpoints found.');
156
+ case 'placeholder_content':
157
+ return patternItem(root, item, /lorem ipsum|example\.com|\bTBD\b|\bTODO\b|Coming soon|placeholder/i, 'warning', 'Placeholder text found.', 'No placeholder text found.');
158
+ case 'competitor_reference':
159
+ return patternItem(root, item, /google play|\.apk\b|samsung|android app/i, 'warning', 'Possible platform/competitor reference found in user-facing copy.', 'No competitor platform references found.', { dirs: ['app', 'src', 'components'] });
160
+ case 'private_api_usage':
161
+ return patternItem(root, item, /dlopen|dlsym|NSSelectorFromString|performSelector|valueForKey.*private|_UI[A-Z]/, 'fail', 'Private API or dynamic lookup pattern found.', 'No private API patterns found.', { dirs: ['ios', 'app', 'src'] });
162
+ case 'dynamic_code_execution':
163
+ return patternItem(root, item, /JSPatch|rollout\.io|CodePush.*eval|eval\(.*fetch|new Function\(.*http/i, 'fail', 'Dynamic code execution pattern found.', 'No dynamic code execution patterns found.');
164
+ case 'uiwebview':
165
+ return patternItem(root, item, /\bUIWebView\b/, 'fail', 'UIWebView reference found.', 'No UIWebView references found.', { dirs: ['ios', 'app', 'src'] });
166
+ case 'crypto_mining':
167
+ return patternItem(root, item, /coinhive|cryptonight|miner|monero|stratum\+tcp/i, 'fail', 'Cryptocurrency mining pattern found.', 'No cryptocurrency mining patterns found.');
168
+ case 'hardcoded_ipv4':
169
+ return patternItem(root, item, /\b((25[0-5]|2[0-4][0-9]|1?[0-9]?[0-9])\.){3}(25[0-5]|2[0-4][0-9]|1?[0-9]?[0-9])\b/, 'warning', 'Non-local hardcoded IPv4 address found.', 'No non-local hardcoded IPv4 addresses found.', {
170
+ dirs: ['app', 'src'],
171
+ filter: (match) => !/\b(127\.0\.0\.1|0\.0\.0\.0)\b/.test(match.text),
172
+ });
173
+ case 'restore_purchases':
174
+ return evaluateRestorePurchases(root, item);
175
+ case 'tracking_without_att':
176
+ return evaluateTrackingWithoutAtt(root, item);
177
+ case 'external_payment_digital':
178
+ return evaluateExternalPayment(root, item);
179
+ case 'expo_layout_missing':
180
+ return evaluateExpoLayout(root, item);
181
+ case 'expo_duplicate_route':
182
+ return evaluateDuplicateRoutes(root, item);
183
+ case 'plugin_permission_missing':
184
+ return evaluatePluginPermissions(root, appJson, item);
185
+ case 'permission_descriptions':
186
+ return evaluatePermissionDescriptions(root, appJson, item);
187
+ case 'privacy_manifest':
188
+ return evaluatePrivacyManifest(root, item);
189
+ default:
190
+ if (STORE_REMINDER_IDS.has(item.id)) {
191
+ return makeItem(item, 'info', 'Complete this declaration in Google Play Console.', 'Play Console questionnaire');
192
+ }
193
+ if (JUDGMENT_IDS.has(item.id) || LIVE_STORE_IDS.has(item.id))
194
+ return null;
195
+ return null;
196
+ }
197
+ }
198
+ function evaluateConfigItem(appJson, item, rule) {
199
+ const value = getPath(appJson, rule.path);
200
+ const present = hasValue(value);
201
+ const evidence = `app.json ${rule.path.join('.')} = ${value === undefined ? 'undefined' : JSON.stringify(value)}`;
202
+ if (present)
203
+ return makeItem(item, 'pass', `${item.label} is set.`, evidence);
204
+ return makeItem(item, rule.absent, `${item.label} is not set.`, evidence);
205
+ }
206
+ function evaluateRestorePurchases(root, item) {
207
+ const iap = scan(root, /react-native-purchases|expo-in-app-purchases|StoreKit/i);
208
+ if (!iap.length)
209
+ return makeItem(item, 'pass', 'No in-app purchase APIs found.', 'scan: no IAP APIs found');
210
+ const restore = scan(root, /restorePurchases|restoreTransactions/i);
211
+ if (restore.length)
212
+ return makeItem(item, 'pass', 'Restore purchases flow appears in source.', evidence(restore));
213
+ return makeItem(item, 'fail', 'IAP APIs found but no restore purchases flow was found.', evidence(iap));
214
+ }
215
+ function evaluateTrackingWithoutAtt(root, item) {
216
+ const tracking = scan(root, /react-native-firebase\/analytics|facebook|appsflyer|amplitude|segment/i);
217
+ if (!tracking.length)
218
+ return makeItem(item, 'pass', 'No tracking SDK patterns found.', 'scan: no tracking SDK patterns found');
219
+ const att = scan(root, /expo-tracking-transparency|requestTrackingPermission/i);
220
+ if (att.length)
221
+ return makeItem(item, 'pass', 'Tracking SDK and ATT permission request were both found.', evidence(att));
222
+ return makeItem(item, 'fail', 'Tracking SDK found without ATT permission request.', evidence(tracking));
223
+ }
224
+ function evaluateExternalPayment(root, item) {
225
+ const matches = scan(root, /@stripe\/stripe-react-native|paypal|braintree/i);
226
+ if (!matches.length)
227
+ return makeItem(item, 'pass', 'No external payment SDK patterns found.', 'scan: no Stripe/PayPal/Braintree patterns found');
228
+ return makeItem(item, 'warning', 'External payment SDK found; verify it is not used for digital goods on iOS.', evidence(matches));
229
+ }
230
+ function evaluateExpoLayout(root, item) {
231
+ const appDir = path.join(root, 'app');
232
+ if (!existsSync(appDir))
233
+ return makeItem(item, 'pass', 'No Expo Router app directory found.', 'scan: app/ directory absent');
234
+ const dirs = walkDirs(appDir).map((dir) => path.relative(root, dir) || 'app');
235
+ const missing = dirs.filter((dir) => !hasLayoutFile(path.join(root, dir)));
236
+ if (!missing.length)
237
+ return makeItem(item, 'pass', 'Every app route directory has a layout file.', 'scan: app route directories');
238
+ return makeItem(item, 'fail', 'Expo Router directories without _layout files found.', missing.slice(0, 8).join(', '));
239
+ }
240
+ function evaluateDuplicateRoutes(root, item) {
241
+ const appDir = path.join(root, 'app');
242
+ if (!existsSync(appDir))
243
+ return makeItem(item, 'pass', 'No Expo Router app directory found.', 'scan: app/ directory absent');
244
+ const routes = new Map();
245
+ for (const file of walkFiles(appDir)) {
246
+ if (!/\.(tsx|ts|jsx|js)$/.test(file) || /(^|\/)_layout\.(tsx|ts|jsx|js)$/.test(toRel(root, file)))
247
+ continue;
248
+ const route = routePath(root, file);
249
+ const current = routes.get(route) ?? [];
250
+ current.push(toRel(root, file));
251
+ routes.set(route, current);
252
+ }
253
+ const duplicates = [...routes.entries()].filter(([, files]) => files.length > 1);
254
+ if (!duplicates.length)
255
+ return makeItem(item, 'pass', 'No duplicate Expo routes found.', 'scan: app route files');
256
+ return makeItem(item, 'fail', 'Duplicate Expo route files found.', duplicates.slice(0, 5).map(([route, files]) => `${route}: ${files.join(', ')}`).join('\n'));
257
+ }
258
+ function evaluatePluginPermissions(root, appJson, item) {
259
+ const missing = missingPermissionRequirements(root, appJson);
260
+ if (!missing.length)
261
+ return makeItem(item, 'pass', 'Used native capabilities have matching plugin/permission declarations.', 'app.json permission cross-check');
262
+ return makeItem(item, 'fail', 'Native capability usage is missing a matching permission declaration.', missing.join('; '));
263
+ }
264
+ function evaluatePermissionDescriptions(root, appJson, item) {
265
+ const missing = missingIosUsageDescriptions(root, appJson);
266
+ if (!missing.length)
267
+ return makeItem(item, 'pass', 'Used iOS capabilities have usage descriptions.', 'app.json ios.infoPlist cross-check');
268
+ return makeItem(item, 'fail', 'Used iOS capability is missing an NS*UsageDescription.', missing.join('; '));
269
+ }
270
+ function evaluatePrivacyManifest(root, item) {
271
+ const hasManifest = findByName(root, 'PrivacyInfo.xcprivacy').length > 0;
272
+ const requiredReasonApi = scan(root, /NSUserDefaults|systemUptime|fileModificationDate|diskSpace|activeInputModes/i);
273
+ const tracking = scan(root, /react-native-firebase\/analytics|facebook|appsflyer|amplitude|segment/i);
274
+ if (hasManifest)
275
+ return makeItem(item, 'pass', 'PrivacyInfo.xcprivacy is present.', 'scan: PrivacyInfo.xcprivacy found');
276
+ if (tracking.length)
277
+ return makeItem(item, 'fail', 'Tracking SDK pattern found but no PrivacyInfo.xcprivacy file was found.', evidence(tracking));
278
+ if (requiredReasonApi.length)
279
+ return makeItem(item, 'warning', 'Required-reason API pattern found but no PrivacyInfo.xcprivacy file was found.', evidence(requiredReasonApi));
280
+ return makeItem(item, 'pass', 'No required-reason API patterns found and no tracking SDK patterns found.', 'scan: no required privacy manifest signals found');
281
+ }
282
+ function patternItem(root, item, regex, statusOnHit, messageOnHit, messageOnMiss, options = {}) {
283
+ const matches = scan(root, regex, options);
284
+ if (matches.length)
285
+ return makeItem(item, statusOnHit, messageOnHit, evidence(matches));
286
+ return makeItem(item, 'pass', messageOnMiss, `scan: no matches for ${regex}`);
287
+ }
288
+ function missingPermissionRequirements(root, appJson) {
289
+ const missing = [...missingIosUsageDescriptions(root, appJson)];
290
+ const permissions = arrayOfStrings(getPath(appJson, ['expo', 'android', 'permissions']));
291
+ const source = sourceText(root, ['app', 'src', 'components']);
292
+ const androidChecks = [
293
+ { name: 'camera', used: /expo-camera|CameraView|requestCameraPermissions/i, permissions: ['CAMERA'] },
294
+ { name: 'location', used: /expo-location|getCurrentPositionAsync|watchPositionAsync/i, permissions: ['ACCESS_FINE_LOCATION', 'ACCESS_COARSE_LOCATION'] },
295
+ { name: 'photos', used: /expo-image-picker|expo-media-library|MediaLibrary/i, permissions: ['READ_MEDIA_IMAGES', 'READ_EXTERNAL_STORAGE'] },
296
+ { name: 'microphone', used: /expo-av|expo-audio|Audio\.|requestMicrophonePermissions/i, permissions: ['RECORD_AUDIO'] },
297
+ { name: 'notifications', used: /expo-notifications/i, permissions: ['POST_NOTIFICATIONS'] },
298
+ ];
299
+ for (const check of androidChecks) {
300
+ if (!check.used.test(source))
301
+ continue;
302
+ if (!check.permissions.some((permission) => permissions.includes(permission))) {
303
+ missing.push(`android ${check.name} permission (${check.permissions.join(' or ')})`);
304
+ }
305
+ }
306
+ return missing;
307
+ }
308
+ function missingIosUsageDescriptions(root, appJson) {
309
+ const infoPlist = objectValue(getPath(appJson, ['expo', 'ios', 'infoPlist']));
310
+ const source = sourceText(root, ['app', 'src', 'components']);
311
+ const checks = [
312
+ { name: 'camera', used: /expo-camera|CameraView|requestCameraPermissions/i, keys: ['NSCameraUsageDescription'] },
313
+ { name: 'location', used: /expo-location|getCurrentPositionAsync|watchPositionAsync/i, keys: ['NSLocationWhenInUseUsageDescription', 'NSLocationAlwaysAndWhenInUseUsageDescription'] },
314
+ { name: 'photos', used: /expo-image-picker|expo-media-library|MediaLibrary/i, keys: ['NSPhotoLibraryUsageDescription'] },
315
+ { name: 'microphone', used: /expo-av|expo-audio|Audio\.|requestMicrophonePermissions/i, keys: ['NSMicrophoneUsageDescription'] },
316
+ ];
317
+ const missing = [];
318
+ for (const check of checks) {
319
+ if (!check.used.test(source))
320
+ continue;
321
+ if (!check.keys.some((key) => hasValue(infoPlist[key]))) {
322
+ missing.push(`ios ${check.name} usage description (${check.keys.join(' or ')})`);
323
+ }
324
+ }
325
+ return missing;
326
+ }
327
+ function unresolvedReason(item) {
328
+ if (JUDGMENT_IDS.has(item.id))
329
+ return 'requires app-specific judgment over UI/copy behavior';
330
+ if (LIVE_STORE_IDS.has(item.id))
331
+ return 'requires live store metadata/media state or listing authoring workflow';
332
+ return `no deterministic evaluator is registered for ${item.check} item`;
333
+ }
334
+ function targetPlatformsFromAppJson(appJson) {
335
+ const ios = stringValue(getPath(appJson, ['expo', 'ios', 'bundleIdentifier']));
336
+ const android = stringValue(getPath(appJson, ['expo', 'android', 'package']));
337
+ const platforms = [];
338
+ if (ios)
339
+ platforms.push('ios');
340
+ if (android)
341
+ platforms.push('android');
342
+ return platforms.length ? platforms : ['ios', 'android'];
343
+ }
344
+ function targetsItem(item, targetPlatforms) {
345
+ return item.platform.some((platform) => targetPlatforms.includes(platform));
346
+ }
347
+ function makeItem(manifestItem, status, message, evidenceText) {
348
+ return {
349
+ id: manifestItem.id,
350
+ label: manifestItem.label,
351
+ status,
352
+ message,
353
+ evidence: evidenceText,
354
+ category: manifestItem.category,
355
+ priority: manifestItem.priority,
356
+ platform: manifestItem.platform,
357
+ authorable: manifestItem.authorable === true || undefined,
358
+ autofill: manifestItem.autofill,
359
+ guideline: manifestItem.guideline,
360
+ critical: manifestItem.critical === true || undefined,
361
+ };
362
+ }
363
+ function scan(root, regex, options = {}) {
364
+ const files = collectScanFiles(root, options.dirs);
365
+ const matches = [];
366
+ for (const file of files) {
367
+ const content = readText(file);
368
+ if (content == null)
369
+ continue;
370
+ const rel = toRel(root, file);
371
+ const lines = content.split(/\r?\n/);
372
+ lines.forEach((line, index) => {
373
+ regex.lastIndex = 0;
374
+ if (!regex.test(line))
375
+ return;
376
+ const match = { file: rel, line: index + 1, text: compact(line) };
377
+ if (options.filter && !options.filter(match))
378
+ return;
379
+ matches.push(match);
380
+ });
381
+ }
382
+ return matches.slice(0, 30);
383
+ }
384
+ function collectScanFiles(root, dirs) {
385
+ const roots = dirs?.length ? dirs : ['app', 'src', 'components', 'ios', 'android', 'app.json', 'package.json'];
386
+ const files = [];
387
+ for (const entry of roots) {
388
+ const full = path.join(root, entry);
389
+ if (!existsSync(full))
390
+ continue;
391
+ const stat = safeStat(full);
392
+ if (!stat)
393
+ continue;
394
+ if (stat.isDirectory())
395
+ files.push(...walkFiles(full));
396
+ else if (shouldScanFile(full))
397
+ files.push(full);
398
+ }
399
+ return files;
400
+ }
401
+ function walkFiles(dir) {
402
+ const files = [];
403
+ for (const entry of safeReadDir(dir)) {
404
+ if (SKIP_DIRS.has(entry.name))
405
+ continue;
406
+ const full = path.join(dir, entry.name);
407
+ if (entry.isDirectory()) {
408
+ files.push(...walkFiles(full));
409
+ continue;
410
+ }
411
+ if (entry.isFile() && shouldScanFile(full))
412
+ files.push(full);
413
+ }
414
+ return files;
415
+ }
416
+ function walkDirs(dir) {
417
+ const dirs = [dir];
418
+ for (const entry of safeReadDir(dir)) {
419
+ if (SKIP_DIRS.has(entry.name))
420
+ continue;
421
+ const full = path.join(dir, entry.name);
422
+ if (entry.isDirectory())
423
+ dirs.push(...walkDirs(full));
424
+ }
425
+ return dirs;
426
+ }
427
+ function shouldScanFile(file) {
428
+ return TEXT_EXTENSIONS.has(path.extname(file));
429
+ }
430
+ function findByName(root, name) {
431
+ return walkFiles(root).filter((file) => path.basename(file) === name);
432
+ }
433
+ function sourceText(root, dirs) {
434
+ return collectScanFiles(root, dirs).map((file) => readText(file) ?? '').join('\n');
435
+ }
436
+ function hasLayoutFile(dir) {
437
+ return ['_layout.tsx', '_layout.ts', '_layout.jsx', '_layout.js'].some((name) => existsSync(path.join(dir, name)));
438
+ }
439
+ function routePath(root, file) {
440
+ const rel = toRel(root, file).replace(/^app\//, '').replace(/\.(tsx|ts|jsx|js)$/, '');
441
+ const parts = rel.split('/').filter((part) => part && !/^\(.+\)$/.test(part));
442
+ if (parts[parts.length - 1] === 'index')
443
+ parts.pop();
444
+ return `/${parts.join('/')}`;
445
+ }
446
+ function evidence(matches) {
447
+ return matches.slice(0, 5).map((match) => `${match.file}:${match.line}: ${match.text}`).join('\n');
448
+ }
449
+ function readJson(file) {
450
+ try {
451
+ return JSON.parse(readFileSync(file, 'utf8'));
452
+ }
453
+ catch {
454
+ return undefined;
455
+ }
456
+ }
457
+ function readText(file) {
458
+ try {
459
+ return readFileSync(file, 'utf8');
460
+ }
461
+ catch {
462
+ return null;
463
+ }
464
+ }
465
+ function safeReadDir(dir) {
466
+ try {
467
+ return readdirSync(dir, { withFileTypes: true });
468
+ }
469
+ catch {
470
+ return [];
471
+ }
472
+ }
473
+ function safeStat(file) {
474
+ try {
475
+ return statSync(file);
476
+ }
477
+ catch {
478
+ return null;
479
+ }
480
+ }
481
+ function getPath(value, keys) {
482
+ let current = value;
483
+ for (const key of keys) {
484
+ if (!current || typeof current !== 'object' || Array.isArray(current))
485
+ return undefined;
486
+ current = current[key];
487
+ }
488
+ return current;
489
+ }
490
+ function objectValue(value) {
491
+ return value && typeof value === 'object' && !Array.isArray(value) ? value : {};
492
+ }
493
+ function arrayOfStrings(value) {
494
+ return Array.isArray(value) ? value.filter((item) => typeof item === 'string') : [];
495
+ }
496
+ function stringValue(value) {
497
+ return typeof value === 'string' && value.trim() ? value.trim() : null;
498
+ }
499
+ function hasValue(value) {
500
+ if (value === undefined || value === null)
501
+ return false;
502
+ if (typeof value === 'string')
503
+ return value.trim().length > 0 && value !== 'null';
504
+ return true;
505
+ }
506
+ function platformNames(platforms) {
507
+ return platforms.map((platform) => (platform === 'ios' ? 'iOS' : 'Android')).join('/');
508
+ }
509
+ function toRel(root, file) {
510
+ return path.relative(root, file).split(path.sep).join('/');
511
+ }
512
+ function compact(line) {
513
+ const trimmed = line.trim().replace(/\s+/g, ' ');
514
+ return trimmed.length > 160 ? `${trimmed.slice(0, 157)}...` : trimmed;
515
+ }
package/dist/config.js CHANGED
@@ -32,7 +32,9 @@ export function saveCredentials(creds) {
32
32
  }
33
33
  export function loadCredentials() {
34
34
  const envToken = isRunCloud()
35
- ? process.env.RUN_CLOUD_API_TOKEN ?? process.env.RUN_CLOUD_API_KEY
35
+ ? process.env.RUN_CLOUD_API_TOKEN ??
36
+ process.env.RUN_CLOUD_API_KEY ??
37
+ process.env.RUN_CLOUD_CI_ENVIRONMENT_TOKEN
36
38
  : process.env.NEWLY_API_TOKEN ?? process.env.NEWLY_CLI_TOKEN;
37
39
  const envApiUrl = isRunCloud()
38
40
  ? process.env.RUN_CLOUD_API_URL
package/dist/index.js CHANGED
@@ -18,6 +18,7 @@ import { registerDns } from './commands/dns.js';
18
18
  import { registerDiagnose } from './diagnose.js';
19
19
  import { registerDesktop } from './commands/desktop.js';
20
20
  import { registerEnvironment } from './commands/environment.js';
21
+ import { registerAppStore } from './commands/appStore.js';
21
22
  const program = new Command('newly')
22
23
  .description('Develop, provision, and deploy Newly backends')
23
24
  .version('0.1.0');
@@ -39,4 +40,5 @@ registerDns(program);
39
40
  registerDiagnose(program);
40
41
  registerDesktop(program);
41
42
  registerEnvironment(program);
43
+ registerAppStore(program);
42
44
  await program.parseAsync(process.argv);
package/dist/version.js CHANGED
@@ -1 +1 @@
1
- export const CLI_VERSION = '0.1.21';
1
+ export const CLI_VERSION = '0.1.22';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "runcloud",
3
- "version": "0.1.21",
3
+ "version": "0.1.22",
4
4
  "description": "Create and control run.cloud remote mobile simulators and cloud sandboxes",
5
5
  "license": "Apache-2.0",
6
6
  "keywords": [