@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.
- package/bin/install.mjs +120 -70
- 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',
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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',
|
|
53
|
-
const log = m =>
|
|
54
|
-
const ok
|
|
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.
|
|
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
|
|
88
|
-
for (const f of files)
|
|
89
|
-
|
|
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
|
-
|
|
130
|
-
|
|
131
|
-
|
|
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
|
|
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'))
|
|
144
|
-
|
|
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
|
|
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`);
|
|
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
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
targets =
|
|
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
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
log(
|
|
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.
|
|
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",
|