@buivietphi/skill-mobile-mt 1.0.0 → 1.0.1

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.
Files changed (2) hide show
  1. package/bin/install.mjs +120 -70
  2. package/package.json +1 -1
package/bin/install.mjs CHANGED
@@ -6,7 +6,7 @@
6
6
  * Installs skill-mobile-mt/ folder with subfolders for each platform.
7
7
  *
8
8
  * Usage:
9
- * npx @buivietphi/skill-mobile # Interactive
9
+ * npx @buivietphi/skill-mobile # Interactive checkbox UI
10
10
  * npx @buivietphi/skill-mobile --all # All detected agents
11
11
  * npx @buivietphi/skill-mobile --claude # Claude Code only
12
12
  * npx @buivietphi/skill-mobile --gemini # Gemini CLI
@@ -20,7 +20,6 @@ import { existsSync, mkdirSync, cpSync, readFileSync } from 'node:fs';
20
20
  import { join, resolve, dirname } from 'node:path';
21
21
  import { homedir } from 'node:os';
22
22
  import { fileURLToPath } from 'node:url';
23
- import { createInterface } from 'node:readline';
24
23
 
25
24
  const __dirname = dirname(fileURLToPath(import.meta.url));
26
25
  const PKG_ROOT = resolve(__dirname, '..');
@@ -39,25 +38,25 @@ const SUBFOLDERS = {
39
38
  };
40
39
 
41
40
  const AGENTS = {
42
- claude: { name: 'Claude Code', dir: join(HOME, '.claude', 'skills'), detect: () => existsSync(join(HOME, '.claude')) },
43
- codex: { name: 'Codex', dir: join(HOME, '.codex', 'skills'), detect: () => existsSync(join(HOME, '.codex')) },
44
- gemini: { name: 'Gemini CLI', dir: join(HOME, '.gemini', 'skills'), detect: () => existsSync(join(HOME, '.gemini')) },
45
- kimi: { name: 'Kimi', dir: join(HOME, '.kimi', 'skills'), detect: () => existsSync(join(HOME, '.kimi')) },
46
- antigravity: { name: 'Antigravity', dir: join(HOME, '.agents', 'skills'), detect: () => existsSync(join(HOME, '.agents')) },
47
- cursor: { name: 'Cursor', dir: join(HOME, '.cursor', 'skills'), detect: () => existsSync(join(HOME, '.cursor')) },
48
- windsurf: { name: 'Windsurf', dir: join(HOME, '.windsurf', 'skills'), detect: () => existsSync(join(HOME, '.windsurf')) },
49
- copilot: { name: 'Copilot', dir: join(HOME, '.copilot', 'skills'), detect: () => existsSync(join(HOME, '.copilot')) },
41
+ claude: { name: 'Claude Code', dir: join(HOME, '.claude', 'skills'), detect: () => existsSync(join(HOME, '.claude')) },
42
+ cursor: { name: 'Cursor', dir: join(HOME, '.cursor', 'skills'), detect: () => existsSync(join(HOME, '.cursor')) },
43
+ windsurf: { name: 'Windsurf', dir: join(HOME, '.windsurf', 'skills'), detect: () => existsSync(join(HOME, '.windsurf')) },
44
+ copilot: { name: 'Copilot', dir: join(HOME, '.copilot', 'skills'), detect: () => existsSync(join(HOME, '.copilot')) },
45
+ codex: { name: 'Codex', dir: join(HOME, '.codex', 'skills'), detect: () => existsSync(join(HOME, '.codex')) },
46
+ gemini: { name: 'Gemini CLI', dir: join(HOME, '.gemini', 'skills'), detect: () => existsSync(join(HOME, '.gemini')) },
47
+ kimi: { name: 'Kimi', dir: join(HOME, '.kimi', 'skills'), detect: () => existsSync(join(HOME, '.kimi')) },
48
+ antigravity: { name: 'Antigravity', dir: join(HOME, '.agents', 'skills'), detect: () => existsSync(join(HOME, '.agents')) },
50
49
  };
51
50
 
52
- const c = { reset: '\x1b[0m', bold: '\x1b[1m', dim: '\x1b[2m', green: '\x1b[32m', yellow: '\x1b[33m', blue: '\x1b[34m', magenta: '\x1b[35m', cyan: '\x1b[36m', red: '\x1b[31m' };
53
- const log = m => console.log(m);
54
- const ok = m => log(` ${c.green}✓${c.reset} ${m}`);
51
+ 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' };
52
+ const log = m => process.stdout.write(m + '\n');
53
+ const ok = m => log(` ${c.green}✓${c.reset} ${m}`);
55
54
  const info = m => log(` ${c.blue}ℹ${c.reset} ${m}`);
56
55
  const fail = m => log(` ${c.red}✗${c.reset} ${m}`);
57
56
 
58
57
  function banner() {
59
58
  log(`\n${c.bold}${c.cyan} ┌──────────────────────────────────────────────┐`);
60
- log(` │ 📱 @buivietphi/skill-mobile-mt v1.0.0 │`);
59
+ log(` │ 📱 @buivietphi/skill-mobile-mt v1.0.1 │`);
61
60
  log(` │ Master Senior Mobile Engineer │`);
62
61
  log(` │ │`);
63
62
  log(` │ Claude · Codex · Gemini · Kimi │`);
@@ -72,26 +71,19 @@ function tokenCount(filePath) {
72
71
  }
73
72
 
74
73
  function showContext() {
75
- log(`${c.bold} 📊 Context:${c.reset}`);
74
+ log(`${c.bold} 📊 Context budget:${c.reset}`);
76
75
  let total = 0;
77
-
78
- // Root files
79
76
  for (const f of ROOT_FILES) {
80
77
  const t = tokenCount(join(PKG_ROOT, f));
81
78
  total += t;
82
79
  log(` ${c.dim} ${f.padEnd(30)} ~${t.toLocaleString()} tokens${c.reset}`);
83
80
  }
84
-
85
- // Subfolders
86
81
  for (const [folder, files] of Object.entries(SUBFOLDERS)) {
87
- let folderTotal = 0;
88
- for (const f of files) {
89
- folderTotal += tokenCount(join(PKG_ROOT, folder, f));
90
- }
91
- total += folderTotal;
92
- log(` ${c.dim} ${(folder + '/').padEnd(30)} ~${folderTotal.toLocaleString()} tokens${c.reset}`);
82
+ let ft = 0;
83
+ for (const f of files) ft += tokenCount(join(PKG_ROOT, folder, f));
84
+ total += ft;
85
+ log(` ${c.dim} ${(folder + '/').padEnd(30)} ~${ft.toLocaleString()} tokens${c.reset}`);
93
86
  }
94
-
95
87
  log(`${c.dim} ─────────────────────────────────────────${c.reset}`);
96
88
  log(` ${c.bold} All loaded:${c.reset} ~${total.toLocaleString()} tokens`);
97
89
  log(` ${c.green} Smart load (1 platform):${c.reset} ~${Math.ceil(total * 0.55).toLocaleString()} tokens\n`);
@@ -101,16 +93,12 @@ function install(baseDir, agentName) {
101
93
  const dst = join(baseDir, SKILL_NAME);
102
94
  mkdirSync(dst, { recursive: true });
103
95
  let n = 0;
104
-
105
- // Copy root files
106
96
  for (const f of ROOT_FILES) {
107
97
  const src = join(PKG_ROOT, f);
108
98
  if (!existsSync(src)) continue;
109
99
  cpSync(src, join(dst, f), { force: true });
110
100
  n++;
111
101
  }
112
-
113
- // Copy subfolders
114
102
  for (const [folder, files] of Object.entries(SUBFOLDERS)) {
115
103
  const dstFolder = join(dst, folder);
116
104
  mkdirSync(dstFolder, { recursive: true });
@@ -121,18 +109,95 @@ function install(baseDir, agentName) {
121
109
  n++;
122
110
  }
123
111
  }
124
-
125
112
  ok(`${c.bold}${SKILL_NAME}/${c.reset} → ${agentName} ${c.dim}(${dst})${c.reset}`);
126
113
  return n;
127
114
  }
128
115
 
129
- async function ask(q) {
130
- const rl = createInterface({ input: process.stdin, output: process.stdout });
131
- return new Promise(r => { rl.question(q, a => { rl.close(); r(a.trim()); }); });
116
+ // ─── Checkbox UI ─────────────────────────────────────────────────────────────
117
+
118
+ async function selectAgents(detected) {
119
+ if (!process.stdin.isTTY) {
120
+ return detected.length ? detected : ['claude'];
121
+ }
122
+
123
+ const keys = Object.keys(AGENTS);
124
+ const selected = new Set(detected); // pre-tick detected agents
125
+ let cursor = 0;
126
+
127
+ const UP = '\x1b[A';
128
+ const DOWN = '\x1b[B';
129
+ const ERASE_DN = '\x1b[J';
130
+ const HIDE_CUR = '\x1b[?25l';
131
+ const SHOW_CUR = '\x1b[?25h';
132
+ const moveUp = n => `\x1b[${n}A`;
133
+
134
+ // header = 3 lines (title + hint + blank), items = keys.length
135
+ const TOTAL_LINES = 3 + keys.length;
136
+
137
+ function render(first) {
138
+ if (!first) process.stdout.write(moveUp(TOTAL_LINES) + ERASE_DN);
139
+
140
+ process.stdout.write(`\n${c.bold} Select agents to install:${c.reset}\n`);
141
+ process.stdout.write(` ${c.dim}↑↓ navigate Space toggle A select all Enter confirm Q cancel${c.reset}\n`);
142
+
143
+ for (let i = 0; i < keys.length; i++) {
144
+ const k = keys[i];
145
+ const agent = AGENTS[k];
146
+ const isCur = i === cursor;
147
+ const isSel = selected.has(k);
148
+ const isDet = detected.includes(k);
149
+
150
+ const ptr = isCur ? `${c.cyan}›${c.reset}` : ' ';
151
+ const box = isSel ? `${c.green}◉${c.reset}` : `${c.dim}◯${c.reset}`;
152
+ const name = isCur ? `${c.bold}${c.cyan}${agent.name}${c.reset}` : agent.name;
153
+ const badge = isDet ? ` ${c.green}[detected]${c.reset}` : `${c.dim} [not found]${c.reset}`;
154
+
155
+ process.stdout.write(` ${ptr} ${box} ${name.padEnd(14)}${badge}\n`);
156
+ }
157
+ }
158
+
159
+ process.stdout.write(HIDE_CUR);
160
+ render(true);
161
+
162
+ return new Promise(resolve => {
163
+ process.stdin.setRawMode(true);
164
+ process.stdin.resume();
165
+ process.stdin.setEncoding('utf8');
166
+
167
+ const onKey = key => {
168
+ const done = result => {
169
+ process.stdin.setRawMode(false);
170
+ process.stdin.pause();
171
+ process.stdin.off('data', onKey);
172
+ process.stdout.write(SHOW_CUR + '\n');
173
+ resolve(result);
174
+ };
175
+
176
+ if (key === '\x03') { done(null); process.exit(0); } // Ctrl+C
177
+ if (key === 'q' || key === 'Q' || key === '\x1b') { done([]); return; } // quit
178
+ if (key === '\r' || key === '\n') { done([...selected]); return; } // Enter
179
+
180
+ if (key === UP) cursor = (cursor - 1 + keys.length) % keys.length;
181
+ else if (key === DOWN) cursor = (cursor + 1) % keys.length;
182
+ else if (key === ' ') {
183
+ if (selected.has(keys[cursor])) selected.delete(keys[cursor]);
184
+ else selected.add(keys[cursor]);
185
+ } else if (key === 'a' || key === 'A') {
186
+ if (selected.size === keys.length) selected.clear();
187
+ else keys.forEach(k => selected.add(k));
188
+ }
189
+
190
+ render(false);
191
+ };
192
+
193
+ process.stdin.on('data', onKey);
194
+ });
132
195
  }
133
196
 
197
+ // ─── Main ─────────────────────────────────────────────────────────────────────
198
+
134
199
  async function main() {
135
- const args = process.argv.slice(2);
200
+ const args = process.argv.slice(2);
136
201
  const flags = new Set(args.map(a => a.replace(/^--?/, '')));
137
202
 
138
203
  banner();
@@ -140,28 +205,31 @@ async function main() {
140
205
 
141
206
  let targets = [];
142
207
 
143
- if (flags.has('all')) targets = Object.keys(AGENTS);
144
- else if (flags.has('auto')) {
208
+ if (flags.has('all')) {
209
+ targets = Object.keys(AGENTS);
210
+ } else if (flags.has('auto')) {
145
211
  targets = Object.keys(AGENTS).filter(k => AGENTS[k].detect());
146
212
  if (!targets.length) { info('No agents found. Using Claude.'); targets = ['claude']; }
147
213
  } else if (flags.has('path')) {
148
214
  const p = args[args.indexOf('--path') + 1];
149
- if (!p) { fail('--path needs dir'); process.exit(1); }
215
+ if (!p) { fail('--path needs a directory'); process.exit(1); }
150
216
  install(resolve(p), 'Custom');
151
- log(`\n${c.green}${c.bold} ✅ Done!${c.reset}\n`); return;
217
+ log(`\n${c.green}${c.bold} ✅ Done!${c.reset}\n`);
218
+ return;
152
219
  } else {
153
220
  for (const k of Object.keys(AGENTS)) if (flags.has(k)) targets.push(k);
154
221
  }
155
222
 
223
+ // Interactive checkbox when no flag given
156
224
  if (!targets.length) {
157
- const det = Object.keys(AGENTS).filter(k => AGENTS[k].detect());
158
- log(`${c.bold} Detected agents:${c.reset}`);
159
- det.forEach(k => log(` ${c.green}●${c.reset} ${AGENTS[k].name}`));
160
- Object.keys(AGENTS).filter(k => !det.includes(k)).forEach(k => log(` ${c.dim}○ ${AGENTS[k].name}${c.reset}`));
161
- log('');
162
- const a = await ask(' Install to detected agents? [Y/n] ');
163
- if (a.toLowerCase() === 'n') { info('Cancelled.'); return; }
164
- targets = det.length ? det : ['claude'];
225
+ const detected = Object.keys(AGENTS).filter(k => AGENTS[k].detect());
226
+ const chosen = await selectAgents(detected);
227
+
228
+ if (!chosen || chosen.length === 0) {
229
+ info('Cancelled.');
230
+ return;
231
+ }
232
+ targets = chosen;
165
233
  }
166
234
 
167
235
  log(`\n${c.bold} Installing...${c.reset}\n`);
@@ -171,29 +239,11 @@ async function main() {
171
239
  log(` ${c.bold}Usage:${c.reset}`);
172
240
  log(` ${c.cyan}@skill-mobile-mt${c.reset} Pre-built patterns (18 production apps)`);
173
241
  log(` ${c.cyan}@skill-mobile-mt project${c.reset} Read current project, adapt to it\n`);
174
- log(` ${c.bold}Installed structure:${c.reset}`);
175
- log(` ${SKILL_NAME}/`);
176
- log(` ├── SKILL.md Entry point + auto-detect`);
177
- log(` ├── AGENTS.md Multi-agent config`);
178
- log(` ├── react-native/`);
179
- log(` │ └── react-native.md React Native patterns`);
180
- log(` ├── flutter/`);
181
- log(` │ └── flutter.md Flutter patterns`);
182
- log(` ├── ios/`);
183
- log(` │ └── ios-native.md iOS Swift patterns`);
184
- log(` ├── android/`);
185
- log(` │ └── android-native.md Android Kotlin patterns`);
186
- log(` └── shared/`);
187
- log(` ├── code-review.md Review checklist`);
188
- log(` ├── bug-detection.md Bug scanner`);
189
- log(` ├── prompt-engineering.md Auto-think`);
190
- log(` ├── anti-patterns.md PII/cardinality detection`);
191
- log(` ├── performance-prediction.md Frame budget calculator`);
192
- log(` ├── platform-excellence.md iOS 18+ vs Android 15+`);
193
- log(` ├── version-management.md SDK compatibility matrix`);
194
- log(` ├── observability.md Sessions as 4th pillar`);
195
- log(` ├── claude-md-template.md CLAUDE.md template for Claude Code`);
196
- log(` └── agent-rules-template.md Rules template for ALL agents\n`);
242
+ log(` ${c.bold}Installed to:${c.reset}`);
243
+ for (const k of targets) {
244
+ log(` ${c.green}●${c.reset} ${AGENTS[k].name.padEnd(14)} ${c.dim}${AGENTS[k].dir}/${SKILL_NAME}${c.reset}`);
245
+ }
246
+ log('');
197
247
  }
198
248
 
199
249
  main().catch(e => { fail(e.message); process.exit(1); });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@buivietphi/skill-mobile-mt",
3
- "version": "1.0.0",
3
+ "version": "1.0.1",
4
4
  "description": "Master Senior Mobile Engineer skill for AI agents. Pre-built patterns from 18 production apps + local project adaptation. React Native, Flutter, iOS, Android. Supports Claude, Gemini, Kimi, Cursor, Copilot, Antigravity.",
5
5
  "author": "buivietphi",
6
6
  "license": "MIT",