@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/AGENTS.md +103 -28
- package/README.md +246 -44
- package/SKILL.md +329 -6
- package/bin/install.mjs +711 -11
- package/package.json +7 -2
- package/shared/code-review.md +12 -0
- package/shared/prompt-engineering.md +28 -0
- package/shared/version-management.md +17 -0
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.
|
|
60
|
-
log(` │ Master Senior Mobile Engineer
|
|
61
|
-
log(` │
|
|
62
|
-
log(` │ Claude ·
|
|
63
|
-
log(` │
|
|
64
|
-
log(` │
|
|
65
|
-
log(`
|
|
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 (
|
|
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
|
-
|
|
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); });
|