@buivietphi/skill-mobile-mt 1.1.0 → 1.3.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.

Potentially problematic release.


This version of @buivietphi/skill-mobile-mt might be problematic. Click here for more details.

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