@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.
- package/AGENTS.md +482 -0
- package/README.md +528 -0
- package/SKILL.md +1399 -0
- package/android/android-native.md +480 -0
- package/bin/install.mjs +976 -0
- package/flutter/flutter.md +304 -0
- package/humanizer/humanizer-mobile.md +295 -0
- package/ios/ios-native.md +182 -0
- package/package.json +56 -0
- package/react-native/react-native.md +743 -0
- package/shared/agent-rules-template.md +343 -0
- package/shared/ai-dlc-workflow.md +237 -0
- package/shared/anti-patterns.md +407 -0
- package/shared/architecture-intelligence.md +416 -0
- package/shared/bug-detection.md +71 -0
- package/shared/ci-cd.md +423 -0
- package/shared/claude-md-template.md +125 -0
- package/shared/code-review.md +133 -0
- package/shared/common-pitfalls.md +117 -0
- package/shared/document-analysis.md +167 -0
- package/shared/error-recovery.md +467 -0
- package/shared/observability.md +688 -0
- package/shared/offline-first.md +377 -0
- package/shared/performance-prediction.md +210 -0
- package/shared/platform-excellence.md +244 -0
- package/shared/prompt-engineering.md +705 -0
- package/shared/release-checklist.md +82 -0
- package/shared/testing-strategy.md +332 -0
- package/shared/ui-ux-mobile.md +667 -0
- package/shared/version-management.md +526 -0
package/bin/install.mjs
ADDED
|
@@ -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); });
|