@buivietphi/skill-mobile-mt 2.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.
@@ -0,0 +1,976 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * @buivietphi/skill-mobile installer
5
+ *
6
+ * Installs skill-mobile-mt/ folder with subfolders for each platform.
7
+ *
8
+ * Usage:
9
+ * npx @buivietphi/skill-mobile # Interactive checkbox UI
10
+ * npx @buivietphi/skill-mobile --all # All detected agents
11
+ * npx @buivietphi/skill-mobile --claude # Claude Code only
12
+ * npx @buivietphi/skill-mobile --gemini # Gemini CLI
13
+ * npx @buivietphi/skill-mobile --kimi # Kimi
14
+ * npx @buivietphi/skill-mobile --antigravity # Antigravity
15
+ * npx @buivietphi/skill-mobile --auto # Auto-detect (postinstall)
16
+ * npx @buivietphi/skill-mobile --path DIR # Custom path
17
+ * npx @buivietphi/skill-mobile --init # Generate project-level rules (interactive)
18
+ * npx @buivietphi/skill-mobile --init cursor # Generate .cursorrules
19
+ * npx @buivietphi/skill-mobile --init copilot # Generate .github/copilot-instructions.md
20
+ * npx @buivietphi/skill-mobile --init windsurf # Generate .windsurfrules
21
+ * npx @buivietphi/skill-mobile --init cline # Generate .clinerules/mobile-rules.md
22
+ * npx @buivietphi/skill-mobile --init roocode # Generate .roo/rules/mobile-rules.md
23
+ * npx @buivietphi/skill-mobile --init kilocode # Generate .kilocode/rules/mobile-rules.md
24
+ * npx @buivietphi/skill-mobile --init kiro # Generate .kiro/steering/mobile-rules.md
25
+ * npx @buivietphi/skill-mobile --init all # Generate all project-level files
26
+ */
27
+
28
+ import { existsSync, mkdirSync, cpSync, readFileSync, writeFileSync, readdirSync } from 'node:fs';
29
+ import { join, resolve, dirname } from 'node:path';
30
+ import { homedir } from 'node:os';
31
+ import { fileURLToPath } from 'node:url';
32
+
33
+ const __dirname = dirname(fileURLToPath(import.meta.url));
34
+ const PKG_ROOT = resolve(__dirname, '..');
35
+ const SKILL_NAME = 'skill-mobile-mt';
36
+ const HOME = homedir();
37
+
38
+ // Structure: root files + subfolders
39
+ const ROOT_FILES = ['SKILL.md', 'AGENTS.md'];
40
+
41
+ const SUBFOLDERS = ['react-native', 'flutter', 'ios', 'android', 'shared'];
42
+
43
+ // Read all .md files from a folder dynamically
44
+ function getMdFiles(folder) {
45
+ const dir = join(PKG_ROOT, folder);
46
+ if (!existsSync(dir)) return [];
47
+ return readdirSync(dir).filter(f => f.endsWith('.md'));
48
+ }
49
+
50
+ const AGENTS = {
51
+ claude: { name: 'Claude Code', dir: join(HOME, '.claude', 'skills'), detect: () => existsSync(join(HOME, '.claude')) },
52
+ cline: { name: 'Cline', dir: join(HOME, '.cline', 'skills'), detect: () => existsSync(join(HOME, '.cline')) },
53
+ roocode: { name: 'Roo Code', dir: join(HOME, '.roo', 'skills'), detect: () => existsSync(join(HOME, '.roo')) },
54
+ cursor: { name: 'Cursor', dir: join(HOME, '.cursor', 'skills'), detect: () => existsSync(join(HOME, '.cursor')) },
55
+ windsurf: { name: 'Windsurf', dir: join(HOME, '.windsurf', 'skills'), detect: () => existsSync(join(HOME, '.windsurf')) },
56
+ copilot: { name: 'Copilot', dir: join(HOME, '.copilot', 'skills'), detect: () => existsSync(join(HOME, '.copilot')) },
57
+ codex: { name: 'Codex', dir: join(HOME, '.codex', 'skills'), detect: () => existsSync(join(HOME, '.codex')) },
58
+ gemini: { name: 'Gemini CLI', dir: join(HOME, '.gemini', 'skills'), detect: () => existsSync(join(HOME, '.gemini')) },
59
+ kimi: { name: 'Kimi', dir: join(HOME, '.kimi', 'skills'), detect: () => existsSync(join(HOME, '.kimi')) },
60
+ kilocode: { name: 'Kilo Code', dir: join(HOME, '.kilocode', 'skills'), detect: () => existsSync(join(HOME, '.kilocode')) },
61
+ kiro: { name: 'Kiro', dir: join(HOME, '.kiro', 'skills'), detect: () => existsSync(join(HOME, '.kiro')) },
62
+ antigravity: { name: 'Antigravity', dir: join(HOME, '.agents', 'skills'), detect: () => existsSync(join(HOME, '.agents')) },
63
+ };
64
+
65
+ const c = { reset: '\x1b[0m', bold: '\x1b[1m', dim: '\x1b[2m', green: '\x1b[32m', yellow: '\x1b[33m', blue: '\x1b[34m', cyan: '\x1b[36m', red: '\x1b[31m' };
66
+ const log = m => process.stdout.write(m + '\n');
67
+ const ok = m => log(` ${c.green}✓${c.reset} ${m}`);
68
+ const info = m => log(` ${c.blue}ℹ${c.reset} ${m}`);
69
+ const fail = m => log(` ${c.red}✗${c.reset} ${m}`);
70
+
71
+ function banner() {
72
+ log(`\n${c.bold}${c.cyan} ┌──────────────────────────────────────────────────┐`);
73
+ log(` │ 📱 @buivietphi/skill-mobile-mt v2.0.0 │`);
74
+ log(` │ Master Senior Mobile Engineer │`);
75
+ log(` │ │`);
76
+ log(` │ Claude · Cline · Roo Code · Cursor · Windsurf │`);
77
+ log(` │ Copilot · Codex · Gemini · Kimi · Kilo · Kiro │`);
78
+ log(` │ Antigravity │`);
79
+ log(` │ React Native · Flutter · iOS · Android │`);
80
+ log(` └──────────────────────────────────────────────────┘${c.reset}\n`);
81
+ }
82
+
83
+ function tokenCount(filePath) {
84
+ if (!existsSync(filePath)) return 0;
85
+ return Math.ceil(readFileSync(filePath, 'utf-8').length / 3.5);
86
+ }
87
+
88
+ function showContext() {
89
+ log(`${c.bold} 📊 Context budget:${c.reset}`);
90
+ let total = 0;
91
+ for (const f of ROOT_FILES) {
92
+ const t = tokenCount(join(PKG_ROOT, f));
93
+ total += t;
94
+ log(` ${c.dim} ${f.padEnd(30)} ~${t.toLocaleString()} tokens${c.reset}`);
95
+ }
96
+ for (const folder of SUBFOLDERS) {
97
+ let ft = 0;
98
+ for (const f of getMdFiles(folder)) ft += tokenCount(join(PKG_ROOT, folder, f));
99
+ total += ft;
100
+ log(` ${c.dim} ${(folder + '/').padEnd(30)} ~${ft.toLocaleString()} tokens${c.reset}`);
101
+ }
102
+ log(`${c.dim} ─────────────────────────────────────────${c.reset}`);
103
+ log(` ${c.bold} All loaded:${c.reset} ~${total.toLocaleString()} tokens`);
104
+ log(` ${c.green} Smart load (1 platform):${c.reset} ~${Math.ceil(total * 0.55).toLocaleString()} tokens\n`);
105
+ }
106
+
107
+ function install(baseDir, agentName) {
108
+ // Install main skill
109
+ const dst = join(baseDir, SKILL_NAME);
110
+ mkdirSync(dst, { recursive: true });
111
+ let n = 0;
112
+ for (const f of ROOT_FILES) {
113
+ const src = join(PKG_ROOT, f);
114
+ if (!existsSync(src)) continue;
115
+ cpSync(src, join(dst, f), { force: true });
116
+ n++;
117
+ }
118
+ for (const folder of SUBFOLDERS) {
119
+ const dstFolder = join(dst, folder);
120
+ mkdirSync(dstFolder, { recursive: true });
121
+ for (const f of getMdFiles(folder)) {
122
+ const src = join(PKG_ROOT, folder, f);
123
+ cpSync(src, join(dstFolder, f), { force: true });
124
+ n++;
125
+ }
126
+ }
127
+ ok(`${c.bold}${SKILL_NAME}/${c.reset} → ${agentName} ${c.dim}(${dst})${c.reset}`);
128
+
129
+ // Auto-install humanizer-mobile as separate skill
130
+ const humanizerSrc = join(PKG_ROOT, 'humanizer', 'humanizer-mobile.md');
131
+ if (existsSync(humanizerSrc)) {
132
+ const humDst = join(baseDir, 'humanizer-mobile');
133
+ mkdirSync(humDst, { recursive: true });
134
+ cpSync(humanizerSrc, join(humDst, 'humanizer-mobile.md'), { force: true });
135
+ ok(`${c.bold}humanizer-mobile/${c.reset} → ${agentName} ${c.dim}(${humDst})${c.reset}`);
136
+ }
137
+
138
+ return n;
139
+ }
140
+
141
+ // ─── Project Auto-Detect ─────────────────────────────────────────────────────
142
+
143
+ function detectProject(dir) {
144
+ const has = f => existsSync(join(dir, f));
145
+ const readJson = f => { try { return JSON.parse(readFileSync(join(dir, f), 'utf-8')); } catch { return null; } };
146
+
147
+ let framework = '[React Native CLI / Expo / Flutter / iOS / Android]';
148
+ let language = '[TypeScript / Dart / Swift / Kotlin]';
149
+ let state = '[Redux Toolkit / Zustand / Riverpod / BLoC / StateFlow]';
150
+ let nav = '[React Navigation / Expo Router / GoRouter / UIKit / Jetpack]';
151
+ let api = '[axios / fetch / Dio / Firebase / Retrofit]';
152
+ let pkgMgr = '[yarn / npm / bun / flutter pub]';
153
+
154
+ // Detect framework + language
155
+ if (has('pubspec.yaml')) {
156
+ framework = 'Flutter'; language = 'Dart'; pkgMgr = 'flutter pub';
157
+ } else if (has('package.json')) {
158
+ const pkg = readJson('package.json');
159
+ const deps = { ...(pkg?.dependencies || {}), ...(pkg?.devDependencies || {}) };
160
+ if (deps['expo']) { framework = 'Expo (React Native)'; }
161
+ else if (deps['react-native']) { framework = 'React Native CLI'; }
162
+ language = deps['typescript'] || has('tsconfig.json') ? 'TypeScript' : 'JavaScript';
163
+ // State
164
+ if (deps['@reduxjs/toolkit'] || deps['redux']) state = 'Redux Toolkit';
165
+ else if (deps['zustand']) state = 'Zustand';
166
+ else if (deps['mobx-react-lite'] || deps['mobx'])state = 'MobX';
167
+ // Nav
168
+ if (deps['expo-router']) nav = 'Expo Router';
169
+ else if (deps['@react-navigation/native']) nav = 'React Navigation';
170
+ // API
171
+ if (deps['axios']) api = 'axios';
172
+ else if (deps['@apollo/client']) api = 'GraphQL (Apollo)';
173
+ // Pkg manager
174
+ if (has('yarn.lock')) pkgMgr = 'yarn';
175
+ else if (has('pnpm-lock.yaml')) pkgMgr = 'pnpm';
176
+ else if (has('bun.lockb')) pkgMgr = 'bun';
177
+ else pkgMgr = 'npm';
178
+ } else if (has('build.gradle') || has('build.gradle.kts')) {
179
+ framework = 'Android Native'; language = 'Kotlin'; pkgMgr = 'Gradle';
180
+ }
181
+ // iOS detection (*.xcodeproj) is harder — leave as placeholder
182
+
183
+ return { framework, language, state, nav, api, pkgMgr };
184
+ }
185
+
186
+ // ─── Project-Level File Templates ────────────────────────────────────────────
187
+
188
+ const PROJECT_AGENTS = {
189
+ cursor: {
190
+ name: 'Cursor',
191
+ file: '.cursorrules',
192
+ dir: '.',
193
+ generate: (p) => `# ${p.framework} Project — Cursor Rules
194
+ # Generated by @buivietphi/skill-mobile-mt
195
+
196
+ ## Project
197
+ - Framework: ${p.framework}
198
+ - Language: ${p.language}
199
+ - State: ${p.state}
200
+ - Navigation: ${p.nav}
201
+ - API: ${p.api}
202
+ - Package Manager: ${p.pkgMgr}
203
+
204
+ ## Code Style
205
+ - PascalCase for screens and components
206
+ - camelCase for hooks, services, utils
207
+ - Absolute imports with @/ alias (if configured)
208
+
209
+ ## Auto-Check (before every completion)
210
+ - No console.log / print in production code
211
+ - No hardcoded secrets or API keys
212
+ - All async wrapped in try/catch
213
+ - All 4 states: loading / error / empty / success
214
+ - useEffect has cleanup (return () => ...)
215
+ - FlatList (not ScrollView) for dynamic lists > 20 items
216
+ - No implicit 'any' in TypeScript
217
+ - No force unwrap (! / !!) without null check
218
+ - New screens registered in navigator
219
+
220
+ ## Performance
221
+ - React.memo / const widget for expensive components
222
+ - useMemo/useCallback for stable references
223
+ - Images cached and resized
224
+ - No main thread blocking
225
+
226
+ ## Security (non-negotiable)
227
+ - Tokens → SecureStore / Keychain / EncryptedSharedPreferences
228
+ - API calls → HTTPS only
229
+ - Sensitive data → never in logs
230
+ - User input → sanitize before display
231
+ - Deep links → validate before navigation
232
+
233
+ ## Never
234
+ - Change framework or architecture
235
+ - Change state management library
236
+ - Add packages without checking SDK compatibility
237
+ - Mix package managers
238
+ - Use ScrollView for long lists
239
+ - Leave empty catch blocks
240
+ - Store tokens in AsyncStorage / SharedPreferences / UserDefaults
241
+
242
+ ## Architecture
243
+ - Dependencies flow inward: UI → Domain → Data
244
+ - Single responsibility per file (max 300 lines)
245
+ - Feature-based organization preferred
246
+
247
+ ## Reference
248
+ - Full skill: ~/.cursor/skills/skill-mobile-mt/
249
+ - Patterns from 30+ production repos (200k+ GitHub stars)
250
+ `,
251
+ },
252
+
253
+ copilot: {
254
+ name: 'GitHub Copilot',
255
+ file: 'copilot-instructions.md',
256
+ dir: '.github',
257
+ generate: (p) => `# Copilot Instructions — ${p.framework} Project
258
+
259
+ > Generated by @buivietphi/skill-mobile-mt
260
+
261
+ ## Project Context
262
+ - **Framework:** ${p.framework}
263
+ - **Language:** ${p.language}
264
+ - **State Management:** ${p.state}
265
+ - **Navigation:** ${p.nav}
266
+ - **API:** ${p.api}
267
+ - **Package Manager:** ${p.pkgMgr}
268
+
269
+ ## Conventions
270
+ - PascalCase: components, screens, classes
271
+ - camelCase: hooks, services, utilities, variables
272
+ - Files named same as their default export
273
+
274
+ ## Required Patterns
275
+
276
+ ### Every async function
277
+ \`\`\`typescript
278
+ try {
279
+ setLoading(true);
280
+ const result = await apiCall();
281
+ setData(result);
282
+ } catch (error) {
283
+ setError(error.message);
284
+ } finally {
285
+ setLoading(false);
286
+ }
287
+ \`\`\`
288
+
289
+ ### Every screen must handle 4 states
290
+ \`\`\`typescript
291
+ if (loading) return <LoadingScreen />;
292
+ if (error) return <ErrorScreen error={error} />;
293
+ if (!data?.length) return <EmptyScreen />;
294
+ return <DataScreen data={data} />;
295
+ \`\`\`
296
+
297
+ ### Every useEffect with subscriptions
298
+ \`\`\`typescript
299
+ useEffect(() => {
300
+ const sub = subscribe();
301
+ return () => sub.unsubscribe(); // REQUIRED
302
+ }, []);
303
+ \`\`\`
304
+
305
+ ## Rules
306
+ - No console.log / print in production
307
+ - No hardcoded secrets or API keys
308
+ - FlatList (not ScrollView) for dynamic lists
309
+ - Tokens in SecureStore / Keychain only
310
+ - No force unwrap (! / !!) without null check
311
+ - No implicit 'any' in TypeScript
312
+ - No empty catch blocks
313
+ - No inline functions in render/build
314
+ - Images cached and resized
315
+ - New screens registered in navigator
316
+
317
+ ## Security
318
+ - Tokens → SecureStore / Keychain / EncryptedSharedPreferences
319
+ - API calls → HTTPS only
320
+ - Sensitive data → never in logs
321
+ - User input → sanitize before rendering
322
+ - Deep links → validate before navigation
323
+
324
+ ## Never
325
+ - Suggest migrating to a different framework
326
+ - Change state management library
327
+ - Add packages without checking SDK compatibility
328
+ - Mix package managers (yarn + npm)
329
+ - Use ScrollView for lists > 20 items
330
+ - Store tokens in AsyncStorage / SharedPreferences / UserDefaults
331
+
332
+ ## Architecture
333
+ - Clean Architecture: UI → Domain → Data
334
+ - Single responsibility per file (max 300 lines)
335
+ - Feature-based organization preferred
336
+
337
+ ## Reference
338
+ Full skill with patterns from 30+ production repos: ~/.copilot/skills/skill-mobile-mt/
339
+ `,
340
+ },
341
+
342
+ cline: {
343
+ name: 'Cline',
344
+ file: 'mobile-rules.md',
345
+ dir: '.clinerules',
346
+ generate: (p) => `# ${p.framework} Project — Mobile Rules
347
+ # Generated by @buivietphi/skill-mobile-mt
348
+
349
+ ## Project
350
+ - Framework: ${p.framework}
351
+ - Language: ${p.language}
352
+ - State: ${p.state}
353
+ - Navigation: ${p.nav}
354
+ - API: ${p.api}
355
+ - Package Manager: ${p.pkgMgr}
356
+
357
+ ## Code Style
358
+ - PascalCase for screens and components
359
+ - camelCase for hooks, services, utils
360
+ - Absolute imports with @/ alias (if configured)
361
+
362
+ ## Auto-Check (before every completion)
363
+ - No console.log / print in production code
364
+ - No hardcoded secrets or API keys
365
+ - All async wrapped in try/catch
366
+ - All 4 states: loading / error / empty / success
367
+ - useEffect has cleanup (return () => ...)
368
+ - FlatList (not ScrollView) for dynamic lists > 20 items
369
+ - No implicit 'any' in TypeScript
370
+ - No force unwrap (! / !!) without null check
371
+ - New screens registered in navigator
372
+
373
+ ## Performance
374
+ - React.memo / const widget for expensive components
375
+ - useMemo/useCallback for stable references
376
+ - Images cached and resized
377
+ - No main thread blocking
378
+
379
+ ## Security (non-negotiable)
380
+ - Tokens → SecureStore / Keychain / EncryptedSharedPreferences
381
+ - API calls → HTTPS only
382
+ - Sensitive data → never in logs
383
+ - User input → sanitize before display
384
+ - Deep links → validate before navigation
385
+
386
+ ## Never
387
+ - Change framework or architecture
388
+ - Change state management library
389
+ - Add packages without checking SDK compatibility
390
+ - Mix package managers
391
+ - Use ScrollView for long lists
392
+ - Leave empty catch blocks
393
+ - Store tokens in AsyncStorage / SharedPreferences / UserDefaults
394
+
395
+ ## Architecture
396
+ - Dependencies flow inward: UI → Domain → Data
397
+ - Single responsibility per file (max 300 lines)
398
+ - Feature-based organization preferred
399
+
400
+ ## Reference
401
+ - Full skill: ~/.cline/skills/skill-mobile-mt/
402
+ - Patterns from 30+ production repos (200k+ GitHub stars)
403
+ `,
404
+ },
405
+
406
+ roocode: {
407
+ name: 'Roo Code',
408
+ file: 'mobile-rules.md',
409
+ dir: '.roo/rules',
410
+ generate: (p) => `# ${p.framework} Project — Mobile Rules
411
+ # Generated by @buivietphi/skill-mobile-mt
412
+
413
+ ## Project
414
+ - Framework: ${p.framework}
415
+ - Language: ${p.language}
416
+ - State: ${p.state}
417
+ - Navigation: ${p.nav}
418
+ - API: ${p.api}
419
+ - Package Manager: ${p.pkgMgr}
420
+
421
+ ## Code Style
422
+ - PascalCase for screens and components
423
+ - camelCase for hooks, services, utils
424
+ - Absolute imports with @/ alias (if configured)
425
+
426
+ ## Auto-Check (before every completion)
427
+ - No console.log / print in production code
428
+ - No hardcoded secrets or API keys
429
+ - All async wrapped in try/catch
430
+ - All 4 states: loading / error / empty / success
431
+ - useEffect has cleanup (return () => ...)
432
+ - FlatList (not ScrollView) for dynamic lists > 20 items
433
+ - No implicit 'any' in TypeScript
434
+ - No force unwrap (! / !!) without null check
435
+ - New screens registered in navigator
436
+
437
+ ## Performance
438
+ - React.memo / const widget for expensive components
439
+ - useMemo/useCallback for stable references
440
+ - Images cached and resized
441
+ - No main thread blocking
442
+
443
+ ## Security (non-negotiable)
444
+ - Tokens → SecureStore / Keychain / EncryptedSharedPreferences
445
+ - API calls → HTTPS only
446
+ - Sensitive data → never in logs
447
+ - User input → sanitize before display
448
+ - Deep links → validate before navigation
449
+
450
+ ## Never
451
+ - Change framework or architecture
452
+ - Change state management library
453
+ - Add packages without checking SDK compatibility
454
+ - Mix package managers
455
+ - Use ScrollView for long lists
456
+ - Leave empty catch blocks
457
+ - Store tokens in AsyncStorage / SharedPreferences / UserDefaults
458
+
459
+ ## Architecture
460
+ - Dependencies flow inward: UI → Domain → Data
461
+ - Single responsibility per file (max 300 lines)
462
+ - Feature-based organization preferred
463
+
464
+ ## Reference
465
+ - Full skill: ~/.roo/skills/skill-mobile-mt/
466
+ - Patterns from 30+ production repos (200k+ GitHub stars)
467
+ `,
468
+ },
469
+
470
+ kilocode: {
471
+ name: 'Kilo Code',
472
+ file: 'mobile-rules.md',
473
+ dir: '.kilocode/rules',
474
+ generate: (p) => `# ${p.framework} Project — Mobile Rules
475
+ # Generated by @buivietphi/skill-mobile-mt
476
+
477
+ ## Project
478
+ - Framework: ${p.framework}
479
+ - Language: ${p.language}
480
+ - State: ${p.state}
481
+ - Navigation: ${p.nav}
482
+ - API: ${p.api}
483
+ - Package Manager: ${p.pkgMgr}
484
+
485
+ ## Code Style
486
+ - PascalCase for screens and components
487
+ - camelCase for hooks, services, utils
488
+ - Absolute imports with @/ alias (if configured)
489
+
490
+ ## Auto-Check (before every completion)
491
+ - No console.log / print in production code
492
+ - No hardcoded secrets or API keys
493
+ - All async wrapped in try/catch
494
+ - All 4 states: loading / error / empty / success
495
+ - useEffect has cleanup (return () => ...)
496
+ - FlatList (not ScrollView) for dynamic lists > 20 items
497
+ - No implicit 'any' in TypeScript
498
+ - No force unwrap (! / !!) without null check
499
+ - New screens registered in navigator
500
+
501
+ ## Performance
502
+ - React.memo / const widget for expensive components
503
+ - useMemo/useCallback for stable references
504
+ - Images cached and resized
505
+ - No main thread blocking
506
+
507
+ ## Security (non-negotiable)
508
+ - Tokens → SecureStore / Keychain / EncryptedSharedPreferences
509
+ - API calls → HTTPS only
510
+ - Sensitive data → never in logs
511
+ - User input → sanitize before display
512
+ - Deep links → validate before navigation
513
+
514
+ ## Never
515
+ - Change framework or architecture
516
+ - Change state management library
517
+ - Add packages without checking SDK compatibility
518
+ - Mix package managers
519
+ - Use ScrollView for long lists
520
+ - Leave empty catch blocks
521
+ - Store tokens in AsyncStorage / SharedPreferences / UserDefaults
522
+
523
+ ## Architecture
524
+ - Dependencies flow inward: UI → Domain → Data
525
+ - Single responsibility per file (max 300 lines)
526
+ - Feature-based organization preferred
527
+
528
+ ## Reference
529
+ - Full skill: ~/.kilocode/skills/skill-mobile-mt/
530
+ - Patterns from 30+ production repos (200k+ GitHub stars)
531
+ `,
532
+ },
533
+
534
+ kiro: {
535
+ name: 'Kiro',
536
+ file: 'mobile-rules.md',
537
+ dir: '.kiro/steering',
538
+ generate: (p) => `---
539
+ inclusion: always
540
+ ---
541
+
542
+ # ${p.framework} Project — Mobile Rules
543
+ # Generated by @buivietphi/skill-mobile-mt
544
+
545
+ ## Project
546
+ - Framework: ${p.framework}
547
+ - Language: ${p.language}
548
+ - State: ${p.state}
549
+ - Navigation: ${p.nav}
550
+ - API: ${p.api}
551
+ - Package Manager: ${p.pkgMgr}
552
+
553
+ ## Code Style
554
+ - PascalCase for screens and components
555
+ - camelCase for hooks, services, utils
556
+ - Absolute imports with @/ alias (if configured)
557
+
558
+ ## Auto-Check (before every completion)
559
+ - No console.log / print in production code
560
+ - No hardcoded secrets or API keys
561
+ - All async wrapped in try/catch
562
+ - All 4 states: loading / error / empty / success
563
+ - useEffect has cleanup (return () => ...)
564
+ - FlatList (not ScrollView) for dynamic lists > 20 items
565
+ - No implicit 'any' in TypeScript
566
+ - No force unwrap (! / !!) without null check
567
+ - New screens registered in navigator
568
+
569
+ ## Performance
570
+ - React.memo / const widget for expensive components
571
+ - useMemo/useCallback for stable references
572
+ - Images cached and resized
573
+ - No main thread blocking
574
+
575
+ ## Security (non-negotiable)
576
+ - Tokens → SecureStore / Keychain / EncryptedSharedPreferences
577
+ - API calls → HTTPS only
578
+ - Sensitive data → never in logs
579
+ - User input → sanitize before display
580
+ - Deep links → validate before navigation
581
+
582
+ ## Never
583
+ - Change framework or architecture
584
+ - Change state management library
585
+ - Add packages without checking SDK compatibility
586
+ - Mix package managers
587
+ - Use ScrollView for long lists
588
+ - Leave empty catch blocks
589
+ - Store tokens in AsyncStorage / SharedPreferences / UserDefaults
590
+
591
+ ## Architecture
592
+ - Dependencies flow inward: UI → Domain → Data
593
+ - Single responsibility per file (max 300 lines)
594
+ - Feature-based organization preferred
595
+
596
+ ## Reference
597
+ - Patterns from 30+ production repos (200k+ GitHub stars)
598
+ `,
599
+ },
600
+
601
+ windsurf: {
602
+ name: 'Windsurf',
603
+ file: '.windsurfrules',
604
+ dir: '.',
605
+ generate: (p) => `# ${p.framework} Project — Windsurf Rules
606
+ # Generated by @buivietphi/skill-mobile-mt
607
+
608
+ Project: ${p.framework}
609
+ Language: ${p.language}
610
+ State management: ${p.state}
611
+ Navigation: ${p.nav}
612
+ API: ${p.api}
613
+ Package manager: ${p.pkgMgr}
614
+
615
+ ## Coding Rules
616
+
617
+ Always:
618
+ - Wrap all async operations in try/catch
619
+ - Handle all 4 states: loading, error, empty, success
620
+ - Add cleanup to useEffect (return () => ...)
621
+ - Use FlatList for dynamic lists (not ScrollView)
622
+ - Use PascalCase for components and screens
623
+ - Use camelCase for hooks, services, and utilities
624
+ - No implicit 'any' in TypeScript
625
+ - No force unwrap (! / !!) without null check
626
+ - New screens registered in navigator
627
+ - React.memo / const widget for expensive components
628
+ - Images cached and resized
629
+
630
+ Never:
631
+ - Leave console.log / print in production code
632
+ - Hardcode secrets, tokens, or API keys
633
+ - Store tokens in AsyncStorage / SharedPreferences / UserDefaults
634
+ - Change the framework or architecture
635
+ - Change state management library
636
+ - Add packages without verifying SDK compatibility
637
+ - Mix yarn and npm
638
+ - Use ScrollView for lists > 20 items
639
+ - Leave empty catch blocks
640
+ - Use index as list key
641
+
642
+ ## Security (non-negotiable)
643
+ - Tokens → SecureStore / Keychain / EncryptedSharedPreferences
644
+ - API calls → HTTPS only
645
+ - Sensitive data → never in logs
646
+ - User input → sanitize before display
647
+ - Deep links → validate before navigation
648
+
649
+ ## Architecture
650
+ - Clean Architecture: UI → Domain → Data
651
+ - Single responsibility per file (max 300 lines)
652
+ - Feature-based organization preferred
653
+
654
+ ## Reference
655
+ Full skill: ~/.windsurf/skills/skill-mobile-mt/
656
+ Patterns from 30+ production repos (200k+ GitHub stars)
657
+ `,
658
+ },
659
+ };
660
+
661
+ // Agents that need project-level files (don't just read from skills dir)
662
+ const NEEDS_PROJECT_FILE = new Set(['cursor', 'copilot', 'windsurf', 'cline', 'roocode', 'kilocode', 'kiro']);
663
+
664
+ function initProjectFiles(dir, agents) {
665
+ const project = detectProject(dir);
666
+ let created = 0;
667
+
668
+ for (const key of agents) {
669
+ const agent = PROJECT_AGENTS[key];
670
+ if (!agent) continue;
671
+
672
+ const targetDir = join(dir, agent.dir);
673
+ const targetFile = join(targetDir, agent.file);
674
+
675
+ if (existsSync(targetFile)) {
676
+ info(`${c.yellow}${agent.file}${c.reset} already exists — skipped (won't overwrite)`);
677
+ continue;
678
+ }
679
+
680
+ mkdirSync(targetDir, { recursive: true });
681
+ writeFileSync(targetFile, agent.generate(project), 'utf-8');
682
+ ok(`${c.bold}${join(agent.dir, agent.file)}${c.reset} → ${agent.name} ${c.dim}(auto-detected: ${project.framework})${c.reset}`);
683
+ created++;
684
+ }
685
+
686
+ return created;
687
+ }
688
+
689
+ async function selectProjectAgents() {
690
+ if (!process.stdin.isTTY) return Object.keys(PROJECT_AGENTS);
691
+
692
+ const keys = Object.keys(PROJECT_AGENTS);
693
+ const selected = new Set(keys); // all selected by default
694
+ let cursor = 0;
695
+
696
+ const UP = '\x1b[A';
697
+ const DOWN = '\x1b[B';
698
+ const ERASE_DN = '\x1b[J';
699
+ const HIDE_CUR = '\x1b[?25l';
700
+ const SHOW_CUR = '\x1b[?25h';
701
+ const moveUp = n => `\x1b[${n}A`;
702
+ const TOTAL_LINES = 3 + keys.length;
703
+
704
+ function render(first) {
705
+ if (!first) process.stdout.write(moveUp(TOTAL_LINES) + ERASE_DN);
706
+ process.stdout.write(`\n${c.bold} Select project-level files to generate:${c.reset}\n`);
707
+ process.stdout.write(` ${c.dim}↑↓ navigate Space toggle A select all Enter confirm Q cancel${c.reset}\n`);
708
+
709
+ for (let i = 0; i < keys.length; i++) {
710
+ const k = keys[i];
711
+ const agent = PROJECT_AGENTS[k];
712
+ const isCur = i === cursor;
713
+ const isSel = selected.has(k);
714
+
715
+ const ptr = isCur ? `${c.cyan}›${c.reset}` : ' ';
716
+ const box = isSel ? `${c.green}◉${c.reset}` : `${c.dim}◯${c.reset}`;
717
+ const name = isCur ? `${c.bold}${c.cyan}${agent.name}${c.reset}` : agent.name;
718
+ const file = `${c.dim}→ ${join(agent.dir, agent.file)}${c.reset}`;
719
+
720
+ process.stdout.write(` ${ptr} ${box} ${name.padEnd(20)}${file}\n`);
721
+ }
722
+ }
723
+
724
+ process.stdout.write(HIDE_CUR);
725
+ render(true);
726
+
727
+ return new Promise(resolve => {
728
+ process.stdin.setRawMode(true);
729
+ process.stdin.resume();
730
+ process.stdin.setEncoding('utf8');
731
+
732
+ const onKey = key => {
733
+ const done = result => {
734
+ process.stdin.setRawMode(false);
735
+ process.stdin.pause();
736
+ process.stdin.off('data', onKey);
737
+ process.stdout.write(SHOW_CUR + '\n');
738
+ resolve(result);
739
+ };
740
+
741
+ if (key === '\x03') { done(null); process.exit(0); }
742
+ if (key === 'q' || key === 'Q' || key === '\x1b') { done([]); return; }
743
+ if (key === '\r' || key === '\n') { done([...selected]); return; }
744
+
745
+ if (key === UP) cursor = (cursor - 1 + keys.length) % keys.length;
746
+ else if (key === DOWN) cursor = (cursor + 1) % keys.length;
747
+ else if (key === ' ') {
748
+ if (selected.has(keys[cursor])) selected.delete(keys[cursor]);
749
+ else selected.add(keys[cursor]);
750
+ } else if (key === 'a' || key === 'A') {
751
+ if (selected.size === keys.length) selected.clear();
752
+ else keys.forEach(k => selected.add(k));
753
+ }
754
+
755
+ render(false);
756
+ };
757
+
758
+ process.stdin.on('data', onKey);
759
+ });
760
+ }
761
+
762
+ // ─── Checkbox UI ─────────────────────────────────────────────────────────────
763
+
764
+ async function selectAgents(detected) {
765
+ if (!process.stdin.isTTY) {
766
+ return detected.length ? detected : ['claude'];
767
+ }
768
+
769
+ const keys = Object.keys(AGENTS);
770
+ const selected = new Set(detected); // pre-tick detected agents
771
+ let cursor = 0;
772
+
773
+ const UP = '\x1b[A';
774
+ const DOWN = '\x1b[B';
775
+ const ERASE_DN = '\x1b[J';
776
+ const HIDE_CUR = '\x1b[?25l';
777
+ const SHOW_CUR = '\x1b[?25h';
778
+ const moveUp = n => `\x1b[${n}A`;
779
+
780
+ // header = 3 lines (title + hint + blank), items = keys.length
781
+ const TOTAL_LINES = 3 + keys.length;
782
+
783
+ function render(first) {
784
+ if (!first) process.stdout.write(moveUp(TOTAL_LINES) + ERASE_DN);
785
+
786
+ process.stdout.write(`\n${c.bold} Select agents to install:${c.reset}\n`);
787
+ process.stdout.write(` ${c.dim}↑↓ navigate Space toggle A select all Enter confirm Q cancel${c.reset}\n`);
788
+
789
+ for (let i = 0; i < keys.length; i++) {
790
+ const k = keys[i];
791
+ const agent = AGENTS[k];
792
+ const isCur = i === cursor;
793
+ const isSel = selected.has(k);
794
+ const isDet = detected.includes(k);
795
+
796
+ const ptr = isCur ? `${c.cyan}›${c.reset}` : ' ';
797
+ const box = isSel ? `${c.green}◉${c.reset}` : `${c.dim}◯${c.reset}`;
798
+ const name = isCur ? `${c.bold}${c.cyan}${agent.name}${c.reset}` : agent.name;
799
+ const badge = isDet ? ` ${c.green}[detected]${c.reset}` : `${c.dim} [not found]${c.reset}`;
800
+
801
+ process.stdout.write(` ${ptr} ${box} ${name.padEnd(14)}${badge}\n`);
802
+ }
803
+ }
804
+
805
+ process.stdout.write(HIDE_CUR);
806
+ render(true);
807
+
808
+ return new Promise(resolve => {
809
+ process.stdin.setRawMode(true);
810
+ process.stdin.resume();
811
+ process.stdin.setEncoding('utf8');
812
+
813
+ const onKey = key => {
814
+ const done = result => {
815
+ process.stdin.setRawMode(false);
816
+ process.stdin.pause();
817
+ process.stdin.off('data', onKey);
818
+ process.stdout.write(SHOW_CUR + '\n');
819
+ resolve(result);
820
+ };
821
+
822
+ if (key === '\x03') { done(null); process.exit(0); } // Ctrl+C
823
+ if (key === 'q' || key === 'Q' || key === '\x1b') { done([]); return; } // quit
824
+ if (key === '\r' || key === '\n') { done([...selected]); return; } // Enter
825
+
826
+ if (key === UP) cursor = (cursor - 1 + keys.length) % keys.length;
827
+ else if (key === DOWN) cursor = (cursor + 1) % keys.length;
828
+ else if (key === ' ') {
829
+ if (selected.has(keys[cursor])) selected.delete(keys[cursor]);
830
+ else selected.add(keys[cursor]);
831
+ } else if (key === 'a' || key === 'A') {
832
+ if (selected.size === keys.length) selected.clear();
833
+ else keys.forEach(k => selected.add(k));
834
+ }
835
+
836
+ render(false);
837
+ };
838
+
839
+ process.stdin.on('data', onKey);
840
+ });
841
+ }
842
+
843
+ // ─── Main ─────────────────────────────────────────────────────────────────────
844
+
845
+ async function main() {
846
+ const args = process.argv.slice(2);
847
+ const flags = new Set(args.map(a => a.replace(/^--?/, '')));
848
+
849
+ banner();
850
+
851
+ // ─── --init mode: generate project-level files ─────────────────────────────
852
+ if (flags.has('init')) {
853
+ const cwd = process.cwd();
854
+ log(`${c.bold} 📁 Project directory:${c.reset} ${c.dim}${cwd}${c.reset}`);
855
+
856
+ const project = detectProject(cwd);
857
+ log(`${c.bold} 🔍 Detected:${c.reset} ${c.cyan}${project.framework}${c.reset} (${project.language})\n`);
858
+
859
+ // Determine which agents to init
860
+ let initTargets = [];
861
+ const initIdx = args.indexOf('--init');
862
+ const initArg = args[initIdx + 1];
863
+
864
+ if (initArg === 'all') {
865
+ initTargets = Object.keys(PROJECT_AGENTS);
866
+ } else if (initArg && PROJECT_AGENTS[initArg]) {
867
+ initTargets = [initArg];
868
+ } else if (initArg && !initArg.startsWith('-')) {
869
+ fail(`Unknown agent: ${initArg}. Available: ${Object.keys(PROJECT_AGENTS).join(', ')}, all`);
870
+ process.exit(1);
871
+ } else {
872
+ // Interactive selection
873
+ const chosen = await selectProjectAgents();
874
+ if (!chosen || chosen.length === 0) { info('Cancelled.'); return; }
875
+ initTargets = chosen;
876
+ }
877
+
878
+ log(`\n${c.bold} Generating project-level rules...${c.reset}\n`);
879
+ const n = initProjectFiles(cwd, initTargets);
880
+
881
+ if (n > 0) {
882
+ log(`\n${c.green}${c.bold} ✅ Done!${c.reset} → ${n} file(s) generated\n`);
883
+ log(` ${c.bold}Generated files:${c.reset}`);
884
+ for (const k of initTargets) {
885
+ const agent = PROJECT_AGENTS[k];
886
+ if (agent) {
887
+ const fp = join(agent.dir, agent.file);
888
+ log(` ${c.green}●${c.reset} ${agent.name.padEnd(18)} ${c.dim}${fp}${c.reset}`);
889
+ }
890
+ }
891
+ log(`\n ${c.dim}Files are auto-detected for ${project.framework}.`);
892
+ log(` Edit the generated files to customize for your project.${c.reset}\n`);
893
+ } else {
894
+ info('No files generated (all already exist).\n');
895
+ }
896
+ return;
897
+ }
898
+
899
+ // ─── Normal install mode ───────────────────────────────────────────────────
900
+ showContext();
901
+
902
+ let targets = [];
903
+
904
+ if (flags.has('all')) {
905
+ targets = Object.keys(AGENTS);
906
+ } else if (flags.has('auto')) {
907
+ targets = Object.keys(AGENTS).filter(k => AGENTS[k].detect());
908
+ if (!targets.length) { info('No agents found. Using Claude.'); targets = ['claude']; }
909
+ } else if (flags.has('humanizer')) {
910
+ // Install humanizer-mobile as a separate skill
911
+ const src = join(PKG_ROOT, 'humanizer', 'humanizer-mobile.md');
912
+ if (!existsSync(src)) { fail('humanizer/humanizer-mobile.md not found'); process.exit(1); }
913
+ const detected = Object.keys(AGENTS).filter(k => AGENTS[k].detect());
914
+ const agentKeys = detected.length ? detected : ['claude'];
915
+ for (const k of agentKeys) {
916
+ const dst = join(AGENTS[k].dir, 'humanizer-mobile');
917
+ mkdirSync(dst, { recursive: true });
918
+ cpSync(src, join(dst, 'humanizer-mobile.md'), { force: true });
919
+ ok(`${c.bold}humanizer-mobile/${c.reset} → ${AGENTS[k].name} ${c.dim}(${dst})${c.reset}`);
920
+ }
921
+ log(`\n${c.green}${c.bold} ✅ Done!${c.reset}\n`);
922
+ log(` ${c.bold}Usage:${c.reset}`);
923
+ log(` ${c.cyan}@humanizer-mobile${c.reset} Humanize app store copy, release notes, error messages\n`);
924
+ return;
925
+ } else if (flags.has('path')) {
926
+ const p = args[args.indexOf('--path') + 1];
927
+ if (!p) { fail('--path needs a directory'); process.exit(1); }
928
+ install(resolve(p), 'Custom');
929
+ log(`\n${c.green}${c.bold} ✅ Done!${c.reset}\n`);
930
+ return;
931
+ } else {
932
+ for (const k of Object.keys(AGENTS)) if (flags.has(k)) targets.push(k);
933
+ }
934
+
935
+ // Interactive checkbox when no flag given
936
+ if (!targets.length) {
937
+ const detected = Object.keys(AGENTS).filter(k => AGENTS[k].detect());
938
+ const chosen = await selectAgents(detected);
939
+
940
+ if (!chosen || chosen.length === 0) {
941
+ info('Cancelled.');
942
+ return;
943
+ }
944
+ targets = chosen;
945
+ }
946
+
947
+ log(`\n${c.bold} Installing...${c.reset}\n`);
948
+ for (const k of targets) install(AGENTS[k].dir, AGENTS[k].name);
949
+
950
+ log(`\n${c.green}${c.bold} ✅ Done!${c.reset} → ${targets.length} agent(s)\n`);
951
+ log(` ${c.bold}Usage:${c.reset}`);
952
+ log(` ${c.cyan}@skill-mobile-mt${c.reset} Pre-built patterns (30+ production repos)`);
953
+ log(` ${c.cyan}@skill-mobile-mt project${c.reset} Read current project, adapt to it\n`);
954
+ log(` ${c.bold}Installed to:${c.reset}`);
955
+ for (const k of targets) {
956
+ log(` ${c.green}●${c.reset} ${AGENTS[k].name.padEnd(14)} ${c.dim}${AGENTS[k].dir}/${SKILL_NAME}${c.reset}`);
957
+ }
958
+
959
+ // Show tip for agents that need project-level files
960
+ const needsInit = targets.filter(k => NEEDS_PROJECT_FILE.has(k));
961
+ if (needsInit.length > 0) {
962
+ const names = needsInit.map(k => AGENTS[k].name).join(', ');
963
+ log('');
964
+ log(` ${c.yellow}${c.bold}💡 Important:${c.reset} ${names} read rules from ${c.bold}project-level files${c.reset},`);
965
+ log(` not from the skills directory. Run this in your project root:`);
966
+ log('');
967
+ log(` ${c.cyan}npx @buivietphi/skill-mobile-mt --init${c.reset}`);
968
+ log('');
969
+ log(` This generates project-level rules files`);
970
+ log(` (${c.bold}.cursorrules${c.reset}, ${c.bold}.clinerules/${c.reset}, ${c.bold}.roo/rules/${c.reset}, etc.) with auto-detected settings.\n`);
971
+ } else {
972
+ log('');
973
+ }
974
+ }
975
+
976
+ main().catch(e => { fail(e.message); process.exit(1); });