@code-migration/wow-migrator 0.1.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/README.md +113 -0
- package/bin/kmp-skills.js +407 -0
- package/package.json +48 -0
- package/scripts/sync-skills.js +74 -0
- package/skills/android-project-analyst/MIGRATION.md +37 -0
- package/skills/android-project-analyst/SKILL.md +115 -0
- package/skills/android-project-analyst/bind.md +49 -0
- package/skills/android-project-analyst/dependencies.yaml +16 -0
- package/skills/android-project-analyst/roles/android-ecosystem.md +141 -0
- package/skills/android-project-analyst/roles/api-list.md +136 -0
- package/skills/android-project-analyst/roles/architecture-pattern.md +131 -0
- package/skills/android-project-analyst/roles/data-flow.md +143 -0
- package/skills/android-project-analyst/roles/logic-understand.md +154 -0
- package/skills/android-project-analyst/roles/resource-understand.md +151 -0
- package/skills/android-project-analyst/roles/ui-understand.md +136 -0
- package/skills/android-project-analyst/workflow.md +132 -0
- package/skills/android-to-kmp-migrator/MIGRATION.md +44 -0
- package/skills/android-to-kmp-migrator/SKILL.md +203 -0
- package/skills/android-to-kmp-migrator/bind.md +54 -0
- package/skills/android-to-kmp-migrator/dependencies.yaml +21 -0
- package/skills/android-to-kmp-migrator/roles/api-contract-parity.md +95 -0
- package/skills/android-to-kmp-migrator/roles/dataflow-logic-implementation.md +130 -0
- package/skills/android-to-kmp-migrator/roles/dependency-resolution.md +106 -0
- package/skills/android-to-kmp-migrator/roles/incremental-build-check.md +105 -0
- package/skills/android-to-kmp-migrator/roles/legacy-spec-delta-review.md +104 -0
- package/skills/android-to-kmp-migrator/roles/migration-alignment.md +119 -0
- package/skills/android-to-kmp-migrator/roles/migration-report.md +108 -0
- package/skills/android-to-kmp-migrator/roles/migration-workspace-state.md +100 -0
- package/skills/android-to-kmp-migrator/roles/module-node-migration-fix.md +111 -0
- package/skills/android-to-kmp-migrator/roles/module-node-migration-review.md +108 -0
- package/skills/android-to-kmp-migrator/roles/navigation-migration.md +104 -0
- package/skills/android-to-kmp-migrator/roles/platform-api-replacement.md +104 -0
- package/skills/android-to-kmp-migrator/roles/prd-completion-check.md +124 -0
- package/skills/android-to-kmp-migrator/roles/resource-migration.md +109 -0
- package/skills/android-to-kmp-migrator/roles/source-set-placement-guard.md +95 -0
- package/skills/android-to-kmp-migrator/roles/state-model-mapping.md +109 -0
- package/skills/android-to-kmp-migrator/roles/target-project-understand.md +118 -0
- package/skills/android-to-kmp-migrator/roles/theme-design-system-mapping.md +101 -0
- package/skills/android-to-kmp-migrator/roles/ui-mockup-implementation.md +121 -0
- package/skills/android-to-kmp-migrator/roles/ui-render-fidelity-check.md +100 -0
- package/skills/android-to-kmp-migrator/workflow.md +180 -0
- package/skills/kmp-test-validator/MIGRATION.md +43 -0
- package/skills/kmp-test-validator/SKILL.md +137 -0
- package/skills/kmp-test-validator/bind.md +53 -0
- package/skills/kmp-test-validator/dependencies.yaml +17 -0
- package/skills/kmp-test-validator/roles/android-kmp-fidelity-audit.md +102 -0
- package/skills/kmp-test-validator/roles/build-preview-gate.md +109 -0
- package/skills/kmp-test-validator/roles/kmp-validation-plan.md +108 -0
- package/skills/kmp-test-validator/roles/test-case-decomposition.md +103 -0
- package/skills/kmp-test-validator/roles/test-execution.md +104 -0
- package/skills/kmp-test-validator/roles/validation-input-contract.md +111 -0
- package/skills/kmp-test-validator/roles/validation-remediation.md +112 -0
- package/skills/kmp-test-validator/roles/validation-report.md +114 -0
- package/skills/kmp-test-validator/roles/validation-workspace-state.md +102 -0
- package/skills/kmp-test-validator/workflow.md +151 -0
package/README.md
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
# KMP Skills Installer
|
|
2
|
+
|
|
3
|
+
Install the KMP migration skills into common AI coding tools through `npm install`.
|
|
4
|
+
|
|
5
|
+
## Install From This Repo
|
|
6
|
+
|
|
7
|
+
The package is not published to the npm registry yet. Install it from the local package directory:
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install -g /Users/winson/CodeBase/Online/cli-plugins/npx_skills
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Or, from the repository root:
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
npm install -g ./npx_skills
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
To install from a tarball:
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
cd /Users/winson/CodeBase/Online/cli-plugins/npx_skills
|
|
23
|
+
npm pack
|
|
24
|
+
npm install -g ./code-migration-wow-migrator-0.1.0.tgz
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
After publishing this package to npm, the registry install command will be:
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
npm install -g @code-migration/wow-migrator
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
The package runs `postinstall` and installs bundled skills into detected tools.
|
|
34
|
+
Set `KMP_SKILLS_SKIP_POSTINSTALL=1` to skip the automatic install.
|
|
35
|
+
|
|
36
|
+
## Commands
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
kmp-skills install --yes
|
|
40
|
+
kmp-skills install --target "Claude Code,Codex"
|
|
41
|
+
kmp-skills uninstall --target all --yes
|
|
42
|
+
kmp-skills list
|
|
43
|
+
kmp-skills config
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## Supported Targets
|
|
47
|
+
|
|
48
|
+
| Tool | Detection | Skills directory |
|
|
49
|
+
| --- | --- | --- |
|
|
50
|
+
| OpenClaw | `~/.openclaw` or `openclaw` | `~/.openclaw/skills` |
|
|
51
|
+
| Claude Code | `~/.claude` or `claude` | `~/.claude/skills` |
|
|
52
|
+
| OpenCode | `~/.config/opencode` or `opencode` | `~/.config/opencode/skills` |
|
|
53
|
+
| Codex | `~/.codex` or `codex` | `~/.codex/skills` |
|
|
54
|
+
| Cursor | `~/.cursor` or `cursor` | `~/.cursor/skills` |
|
|
55
|
+
| Gemini | `~/.gemini` or `gemini` | `~/.gemini/skills` |
|
|
56
|
+
| JiuwenSwarm | `~/.jiuwenswarm` or Jiuwen CLI commands | `~/.jiuwenswarm/agent/workspace/skills` |
|
|
57
|
+
|
|
58
|
+
## Configuration
|
|
59
|
+
|
|
60
|
+
The installer creates:
|
|
61
|
+
|
|
62
|
+
```text
|
|
63
|
+
~/.kmp-skills/config.json
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Edit it to add custom tools or paths. The shape is:
|
|
67
|
+
|
|
68
|
+
```json
|
|
69
|
+
{
|
|
70
|
+
"tools": [
|
|
71
|
+
{
|
|
72
|
+
"name": "Claude Code",
|
|
73
|
+
"markerDir": "~/.claude",
|
|
74
|
+
"commands": ["claude"],
|
|
75
|
+
"skillsDir": "~/.claude/skills"
|
|
76
|
+
}
|
|
77
|
+
]
|
|
78
|
+
}
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
## Publishing From This Repo
|
|
82
|
+
|
|
83
|
+
Before packing or publishing, sync the current plugin skills into this package:
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
cd npx_skills
|
|
87
|
+
npm run sync:skills
|
|
88
|
+
npm pack
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
`prepare` and `prepack` also run the sync script when the monorepo source is available.
|
|
92
|
+
|
|
93
|
+
Publish with the existing `code-migration` npm org scope:
|
|
94
|
+
|
|
95
|
+
```bash
|
|
96
|
+
npm publish --access public
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
If npm returns `E404 Scope not found`, confirm that the npm org exists and the
|
|
100
|
+
current npm user has publish access:
|
|
101
|
+
|
|
102
|
+
```bash
|
|
103
|
+
npm whoami
|
|
104
|
+
npm org ls code-migration
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
For this package, `npm org ls code-migration` should list your user with owner
|
|
108
|
+
or publish-capable access. After that, publish and install with:
|
|
109
|
+
|
|
110
|
+
```bash
|
|
111
|
+
npm publish --access public
|
|
112
|
+
npm install -g @code-migration/wow-migrator
|
|
113
|
+
```
|
|
@@ -0,0 +1,407 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { execFile } from 'node:child_process';
|
|
4
|
+
import fs from 'node:fs/promises';
|
|
5
|
+
import os from 'node:os';
|
|
6
|
+
import path from 'node:path';
|
|
7
|
+
import { fileURLToPath } from 'node:url';
|
|
8
|
+
import readline from 'node:readline/promises';
|
|
9
|
+
|
|
10
|
+
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
11
|
+
const CONFIG_DIR = path.join(os.homedir(), '.kmp-skills');
|
|
12
|
+
const CONFIG_PATH = path.join(CONFIG_DIR, 'config.json');
|
|
13
|
+
const isWindows = process.platform === 'win32';
|
|
14
|
+
|
|
15
|
+
function expandHome(input) {
|
|
16
|
+
if (!input) return input;
|
|
17
|
+
if (input === '~') return os.homedir();
|
|
18
|
+
if (input.startsWith('~/') || input.startsWith('~\\')) {
|
|
19
|
+
return path.join(os.homedir(), input.slice(2));
|
|
20
|
+
}
|
|
21
|
+
return input;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function prettyPath(input) {
|
|
25
|
+
const home = os.homedir();
|
|
26
|
+
const normalized = input.replace(/\\/g, '/');
|
|
27
|
+
const normalizedHome = home.replace(/\\/g, '/');
|
|
28
|
+
return normalized.startsWith(normalizedHome)
|
|
29
|
+
? `~${normalized.slice(normalizedHome.length)}`
|
|
30
|
+
: normalized;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async function pathExists(input) {
|
|
34
|
+
try {
|
|
35
|
+
await fs.access(input);
|
|
36
|
+
return true;
|
|
37
|
+
} catch {
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async function dirExists(input) {
|
|
43
|
+
try {
|
|
44
|
+
return (await fs.stat(input)).isDirectory();
|
|
45
|
+
} catch {
|
|
46
|
+
return false;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function execFileAsync(command, args, options = {}) {
|
|
51
|
+
return new Promise((resolve, reject) => {
|
|
52
|
+
execFile(command, args, options, (error, stdout, stderr) => {
|
|
53
|
+
if (error) {
|
|
54
|
+
reject(error);
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
resolve({ stdout, stderr });
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async function commandExists(command) {
|
|
63
|
+
if (!command) return false;
|
|
64
|
+
const hasPathSeparator = command.includes('/') || command.includes('\\');
|
|
65
|
+
if (hasPathSeparator) return pathExists(command);
|
|
66
|
+
|
|
67
|
+
try {
|
|
68
|
+
if (isWindows) {
|
|
69
|
+
await execFileAsync('where', [command], { windowsHide: true });
|
|
70
|
+
} else {
|
|
71
|
+
await execFileAsync('sh', ['-c', `command -v ${shellQuote(command)}`]);
|
|
72
|
+
}
|
|
73
|
+
return true;
|
|
74
|
+
} catch {
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function shellQuote(value) {
|
|
80
|
+
return `'${String(value).replace(/'/g, `'\\''`)}'`;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function defaultJiuwenCommands() {
|
|
84
|
+
const home = os.homedir();
|
|
85
|
+
const bin = isWindows ? 'Scripts' : 'bin';
|
|
86
|
+
const exe = isWindows ? '.exe' : '';
|
|
87
|
+
const venv = process.env.VIRTUAL_ENV
|
|
88
|
+
? [
|
|
89
|
+
path.join(process.env.VIRTUAL_ENV, bin, `jiuwenswarm-start${exe}`),
|
|
90
|
+
path.join(process.env.VIRTUAL_ENV, bin, `jiuwenswarm-acp${exe}`),
|
|
91
|
+
path.join(process.env.VIRTUAL_ENV, bin, `jiuwenswarm-tui${exe}`)
|
|
92
|
+
]
|
|
93
|
+
: [];
|
|
94
|
+
|
|
95
|
+
return [
|
|
96
|
+
...venv,
|
|
97
|
+
'jiuwenswarm-start',
|
|
98
|
+
'jiuwenswarm-acp',
|
|
99
|
+
'jiuwenswarm-tui',
|
|
100
|
+
path.join(home, 'jiuwenclaw', 'bin', `jiuwenswarm-start${exe}`),
|
|
101
|
+
path.join(home, 'jiuwenclaw', 'bin', `jiuwenswarm-acp${exe}`),
|
|
102
|
+
path.join(home, 'jiuwenclaw', 'bin', `jiuwenswarm-tui${exe}`)
|
|
103
|
+
];
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function defaultTools() {
|
|
107
|
+
return [
|
|
108
|
+
{
|
|
109
|
+
name: 'OpenClaw',
|
|
110
|
+
markerDir: '~/.openclaw',
|
|
111
|
+
commands: ['openclaw'],
|
|
112
|
+
skillsDir: '~/.openclaw/skills'
|
|
113
|
+
},
|
|
114
|
+
{
|
|
115
|
+
name: 'Claude Code',
|
|
116
|
+
markerDir: '~/.claude',
|
|
117
|
+
commands: ['claude'],
|
|
118
|
+
skillsDir: '~/.claude/skills'
|
|
119
|
+
},
|
|
120
|
+
{
|
|
121
|
+
name: 'OpenCode',
|
|
122
|
+
markerDir: '~/.config/opencode',
|
|
123
|
+
commands: ['opencode'],
|
|
124
|
+
skillsDir: '~/.config/opencode/skills'
|
|
125
|
+
},
|
|
126
|
+
{
|
|
127
|
+
name: 'Codex',
|
|
128
|
+
markerDir: '~/.codex',
|
|
129
|
+
commands: ['codex'],
|
|
130
|
+
skillsDir: '~/.codex/skills'
|
|
131
|
+
},
|
|
132
|
+
{
|
|
133
|
+
name: 'Cursor',
|
|
134
|
+
markerDir: '~/.cursor',
|
|
135
|
+
commands: ['cursor'],
|
|
136
|
+
skillsDir: '~/.cursor/skills'
|
|
137
|
+
},
|
|
138
|
+
{
|
|
139
|
+
name: 'Gemini',
|
|
140
|
+
markerDir: '~/.gemini',
|
|
141
|
+
commands: ['gemini'],
|
|
142
|
+
skillsDir: '~/.gemini/skills'
|
|
143
|
+
},
|
|
144
|
+
{
|
|
145
|
+
name: 'JiuwenSwarm',
|
|
146
|
+
markerDir: '~/.jiuwenswarm',
|
|
147
|
+
commands: defaultJiuwenCommands(),
|
|
148
|
+
skillsDir: '~/.jiuwenswarm/agent/workspace/skills'
|
|
149
|
+
}
|
|
150
|
+
];
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function normalizeTool(raw) {
|
|
154
|
+
if (!raw || typeof raw !== 'object' || !raw.name) return null;
|
|
155
|
+
const skillsDir = raw.skillsDir ?? raw.targets?.skillsDir;
|
|
156
|
+
if (!skillsDir) return null;
|
|
157
|
+
return {
|
|
158
|
+
name: String(raw.name),
|
|
159
|
+
markerDir: raw.markerDir ? String(raw.markerDir) : '',
|
|
160
|
+
commands: Array.isArray(raw.commands) ? raw.commands.map(String).filter(Boolean) : [],
|
|
161
|
+
skillsDir: String(skillsDir)
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
async function loadConfig(options = {}) {
|
|
166
|
+
if (!(await pathExists(CONFIG_PATH))) {
|
|
167
|
+
const config = { tools: defaultTools() };
|
|
168
|
+
if (options.writeDefault) {
|
|
169
|
+
await fs.mkdir(CONFIG_DIR, { recursive: true });
|
|
170
|
+
await fs.writeFile(CONFIG_PATH, `${JSON.stringify(config, null, 2)}\n`, 'utf8');
|
|
171
|
+
}
|
|
172
|
+
return config;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const raw = await fs.readFile(CONFIG_PATH, 'utf8');
|
|
176
|
+
const parsed = JSON.parse(raw);
|
|
177
|
+
const sourceTools = Array.isArray(parsed.tools)
|
|
178
|
+
? parsed.tools
|
|
179
|
+
: Array.isArray(parsed.editors)
|
|
180
|
+
? parsed.editors
|
|
181
|
+
: [];
|
|
182
|
+
const userTools = sourceTools.map(normalizeTool).filter(Boolean);
|
|
183
|
+
const merged = new Map(defaultTools().map((tool) => [tool.name, tool]));
|
|
184
|
+
for (const tool of userTools) merged.set(tool.name, tool);
|
|
185
|
+
return { tools: [...merged.values()] };
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
async function detectTool(tool) {
|
|
189
|
+
const markerFound = tool.markerDir ? await dirExists(expandHome(tool.markerDir)) : false;
|
|
190
|
+
const commandFound = (await Promise.all(tool.commands.map(commandExists))).some(Boolean);
|
|
191
|
+
return {
|
|
192
|
+
installed: markerFound || commandFound,
|
|
193
|
+
markerFound,
|
|
194
|
+
commandFound
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
async function findSkillsRoot() {
|
|
199
|
+
const candidates = [
|
|
200
|
+
path.join(ROOT, 'skills'),
|
|
201
|
+
path.join(process.env.INIT_CWD ?? '', 'claude-code-plugins', 'kmp-migration', 'skills'),
|
|
202
|
+
path.join(ROOT, '..', 'claude-code-plugins', 'kmp-migration', 'skills'),
|
|
203
|
+
path.join(ROOT, '..', '..', 'claude-code-plugins', 'kmp-migration', 'skills')
|
|
204
|
+
].filter(Boolean);
|
|
205
|
+
|
|
206
|
+
for (const candidate of candidates) {
|
|
207
|
+
const names = await listSkillNames(candidate);
|
|
208
|
+
if (names.length > 0) return candidate;
|
|
209
|
+
}
|
|
210
|
+
return null;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
async function listSkillNames(skillsRoot) {
|
|
214
|
+
try {
|
|
215
|
+
const entries = await fs.readdir(skillsRoot, { withFileTypes: true });
|
|
216
|
+
const names = [];
|
|
217
|
+
for (const entry of entries) {
|
|
218
|
+
if (!entry.isDirectory()) continue;
|
|
219
|
+
const skillPath = path.join(skillsRoot, entry.name);
|
|
220
|
+
if (await pathExists(path.join(skillPath, 'SKILL.md'))) names.push(entry.name);
|
|
221
|
+
}
|
|
222
|
+
return names.sort();
|
|
223
|
+
} catch {
|
|
224
|
+
return [];
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
async function copySkill(skillName, skillsRoot, targetRoot, dryRun) {
|
|
229
|
+
const source = path.join(skillsRoot, skillName);
|
|
230
|
+
const target = path.join(targetRoot, skillName);
|
|
231
|
+
if (dryRun) return;
|
|
232
|
+
await fs.mkdir(targetRoot, { recursive: true });
|
|
233
|
+
await fs.rm(target, { recursive: true, force: true });
|
|
234
|
+
await fs.cp(source, target, { recursive: true });
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
async function installToTool(tool, skillsRoot, skillNames, dryRun) {
|
|
238
|
+
const targetRoot = expandHome(tool.skillsDir);
|
|
239
|
+
for (const skillName of skillNames) {
|
|
240
|
+
await copySkill(skillName, skillsRoot, targetRoot, dryRun);
|
|
241
|
+
}
|
|
242
|
+
return targetRoot;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
async function uninstallFromTool(tool, skillNames, dryRun) {
|
|
246
|
+
const targetRoot = expandHome(tool.skillsDir);
|
|
247
|
+
let removed = 0;
|
|
248
|
+
for (const skillName of skillNames) {
|
|
249
|
+
const target = path.join(targetRoot, skillName);
|
|
250
|
+
if (await dirExists(target)) {
|
|
251
|
+
removed++;
|
|
252
|
+
if (!dryRun) await fs.rm(target, { recursive: true, force: true });
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
return removed;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function parseArgs(argv) {
|
|
259
|
+
const flags = {
|
|
260
|
+
yes: false,
|
|
261
|
+
postinstall: false,
|
|
262
|
+
dryRun: false,
|
|
263
|
+
targets: null
|
|
264
|
+
};
|
|
265
|
+
const positional = [];
|
|
266
|
+
|
|
267
|
+
for (let index = 0; index < argv.length; index++) {
|
|
268
|
+
const arg = argv[index];
|
|
269
|
+
if (arg === '--yes' || arg === '-y') flags.yes = true;
|
|
270
|
+
else if (arg === '--postinstall') flags.postinstall = true;
|
|
271
|
+
else if (arg === '--dry-run') flags.dryRun = true;
|
|
272
|
+
else if (arg === '--target' || arg === '--targets') {
|
|
273
|
+
flags.targets = argv[++index]?.split(',').map((item) => item.trim()).filter(Boolean) ?? [];
|
|
274
|
+
} else if (arg.startsWith('--target=')) {
|
|
275
|
+
flags.targets = arg.slice('--target='.length).split(',').map((item) => item.trim()).filter(Boolean);
|
|
276
|
+
} else {
|
|
277
|
+
positional.push(arg);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
return { command: positional[0] ?? 'install', flags };
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function selectTools(tools, detections, flags) {
|
|
284
|
+
if (flags.targets?.length) {
|
|
285
|
+
const wanted = new Set(flags.targets.map((target) => target.toLowerCase()));
|
|
286
|
+
if (wanted.has('all')) return tools;
|
|
287
|
+
return tools.filter((tool) => wanted.has(tool.name.toLowerCase()));
|
|
288
|
+
}
|
|
289
|
+
if (flags.yes || flags.postinstall) {
|
|
290
|
+
return tools.filter((tool) => detections.get(tool.name)?.installed);
|
|
291
|
+
}
|
|
292
|
+
return tools;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
async function confirm(message) {
|
|
296
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
297
|
+
try {
|
|
298
|
+
const answer = await rl.question(`${message} [y/N] `);
|
|
299
|
+
return /^y(es)?$/i.test(answer.trim());
|
|
300
|
+
} finally {
|
|
301
|
+
rl.close();
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
async function installCommand(flags) {
|
|
306
|
+
if (process.env.KMP_SKILLS_SKIP_POSTINSTALL === '1' && flags.postinstall) return;
|
|
307
|
+
|
|
308
|
+
const skillsRoot = await findSkillsRoot();
|
|
309
|
+
if (!skillsRoot) {
|
|
310
|
+
const message = 'No bundled KMP skills found. Run `npm run sync:skills` before publishing this package.';
|
|
311
|
+
if (flags.postinstall) {
|
|
312
|
+
console.warn(`[kmp-skills] ${message}`);
|
|
313
|
+
return;
|
|
314
|
+
}
|
|
315
|
+
throw new Error(message);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
const skillNames = await listSkillNames(skillsRoot);
|
|
319
|
+
const { tools } = await loadConfig();
|
|
320
|
+
const detections = new Map();
|
|
321
|
+
for (const tool of tools) detections.set(tool.name, await detectTool(tool));
|
|
322
|
+
|
|
323
|
+
const selectedTools = selectTools(tools, detections, flags);
|
|
324
|
+
if (selectedTools.length === 0) {
|
|
325
|
+
console.log('[kmp-skills] No supported AI tools detected. Edit config with `kmp-skills config`.');
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
if (!flags.yes && !flags.postinstall && !flags.dryRun) {
|
|
330
|
+
const toolList = selectedTools.map((tool) => tool.name).join(', ');
|
|
331
|
+
const ok = await confirm(`Install ${skillNames.length} KMP skills to: ${toolList}?`);
|
|
332
|
+
if (!ok) return;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
for (const tool of selectedTools) {
|
|
336
|
+
const targetRoot = await installToTool(tool, skillsRoot, skillNames, flags.dryRun);
|
|
337
|
+
const detected = detections.get(tool.name)?.installed ? 'detected' : 'custom';
|
|
338
|
+
console.log(`[kmp-skills] ${flags.dryRun ? 'Would install' : 'Installed'} ${skillNames.length} skills -> ${tool.name} (${detected}) ${prettyPath(targetRoot)}`);
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
async function uninstallCommand(flags) {
|
|
343
|
+
const skillsRoot = await findSkillsRoot();
|
|
344
|
+
const skillNames = skillsRoot ? await listSkillNames(skillsRoot) : [];
|
|
345
|
+
if (skillNames.length === 0) throw new Error('No bundled skill names found; cannot uninstall safely.');
|
|
346
|
+
|
|
347
|
+
const { tools } = await loadConfig();
|
|
348
|
+
const detections = new Map();
|
|
349
|
+
for (const tool of tools) detections.set(tool.name, await detectTool(tool));
|
|
350
|
+
const selectedTools = selectTools(tools, detections, flags);
|
|
351
|
+
|
|
352
|
+
if (!flags.yes && !flags.dryRun) {
|
|
353
|
+
const ok = await confirm(`Remove bundled KMP skills from ${selectedTools.length} target(s)?`);
|
|
354
|
+
if (!ok) return;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
for (const tool of selectedTools) {
|
|
358
|
+
const removed = await uninstallFromTool(tool, skillNames, flags.dryRun);
|
|
359
|
+
console.log(`[kmp-skills] ${flags.dryRun ? 'Would remove' : 'Removed'} ${removed} skills from ${tool.name}`);
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
async function listCommand() {
|
|
364
|
+
const skillsRoot = await findSkillsRoot();
|
|
365
|
+
const skillNames = skillsRoot ? await listSkillNames(skillsRoot) : [];
|
|
366
|
+
console.log(`Skills root: ${skillsRoot ? prettyPath(skillsRoot) : '(not found)'}`);
|
|
367
|
+
for (const skillName of skillNames) console.log(`- ${skillName}`);
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
async function configCommand() {
|
|
371
|
+
const config = await loadConfig({ writeDefault: true });
|
|
372
|
+
console.log(`Config path: ${CONFIG_PATH}`);
|
|
373
|
+
console.log(JSON.stringify(config, null, 2));
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
function printHelp() {
|
|
377
|
+
console.log(`kmp-skills
|
|
378
|
+
|
|
379
|
+
Usage:
|
|
380
|
+
kmp-skills install [--yes] [--target Claude Code,Codex] [--dry-run]
|
|
381
|
+
kmp-skills uninstall [--yes] [--target all] [--dry-run]
|
|
382
|
+
kmp-skills list
|
|
383
|
+
kmp-skills config
|
|
384
|
+
|
|
385
|
+
Environment:
|
|
386
|
+
KMP_SKILLS_SKIP_POSTINSTALL=1 Skip npm postinstall auto-install.
|
|
387
|
+
`);
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
async function main() {
|
|
391
|
+
const { command, flags } = parseArgs(process.argv.slice(2));
|
|
392
|
+
if (command === 'help' || command === '--help' || command === '-h') {
|
|
393
|
+
printHelp();
|
|
394
|
+
return;
|
|
395
|
+
}
|
|
396
|
+
if (command === 'install' || command === 'init') return installCommand(flags);
|
|
397
|
+
if (command === 'uninstall' || command === 'remove') return uninstallCommand(flags);
|
|
398
|
+
if (command === 'list') return listCommand();
|
|
399
|
+
if (command === 'config') return configCommand();
|
|
400
|
+
throw new Error(`Unknown command: ${command}`);
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
main().catch((error) => {
|
|
404
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
405
|
+
console.error(`[kmp-skills] ${message}`);
|
|
406
|
+
process.exitCode = 1;
|
|
407
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@code-migration/wow-migrator",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Install KMP migration skills into Claude Code, Codex, Cursor, Gemini, OpenCode, OpenClaw, and JiuwenSwarm via npm install.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"android",
|
|
7
|
+
"kmp",
|
|
8
|
+
"kotlin-multiplatform",
|
|
9
|
+
"skills",
|
|
10
|
+
"claude-code",
|
|
11
|
+
"codex",
|
|
12
|
+
"cursor",
|
|
13
|
+
"gemini",
|
|
14
|
+
"opencode"
|
|
15
|
+
],
|
|
16
|
+
"license": "MIT",
|
|
17
|
+
"author": "winson-AI",
|
|
18
|
+
"type": "module",
|
|
19
|
+
"repository": {
|
|
20
|
+
"type": "git",
|
|
21
|
+
"url": "https://github.com/winson-AI/cli-plugins.git"
|
|
22
|
+
},
|
|
23
|
+
"bin": {
|
|
24
|
+
"kmp-skills": "bin/kmp-skills.js"
|
|
25
|
+
},
|
|
26
|
+
"files": [
|
|
27
|
+
"bin",
|
|
28
|
+
"scripts",
|
|
29
|
+
"skills",
|
|
30
|
+
"README.md"
|
|
31
|
+
],
|
|
32
|
+
"scripts": {
|
|
33
|
+
"install:skills": "node ./bin/kmp-skills.js install",
|
|
34
|
+
"uninstall:skills": "node ./bin/kmp-skills.js uninstall",
|
|
35
|
+
"config": "node ./bin/kmp-skills.js config",
|
|
36
|
+
"list": "node ./bin/kmp-skills.js list",
|
|
37
|
+
"sync:skills": "node ./scripts/sync-skills.js",
|
|
38
|
+
"prepare": "node ./scripts/sync-skills.js --if-present",
|
|
39
|
+
"prepack": "node ./scripts/sync-skills.js",
|
|
40
|
+
"postinstall": "node ./bin/kmp-skills.js install --yes --postinstall"
|
|
41
|
+
},
|
|
42
|
+
"engines": {
|
|
43
|
+
"node": ">=18.0.0"
|
|
44
|
+
},
|
|
45
|
+
"publishConfig": {
|
|
46
|
+
"access": "public"
|
|
47
|
+
}
|
|
48
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import fs from 'node:fs/promises';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { fileURLToPath } from 'node:url';
|
|
6
|
+
|
|
7
|
+
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
8
|
+
const ifPresent = process.argv.includes('--if-present');
|
|
9
|
+
|
|
10
|
+
async function pathExists(input) {
|
|
11
|
+
try {
|
|
12
|
+
await fs.access(input);
|
|
13
|
+
return true;
|
|
14
|
+
} catch {
|
|
15
|
+
return false;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
async function listSkillNames(skillsRoot) {
|
|
20
|
+
try {
|
|
21
|
+
const entries = await fs.readdir(skillsRoot, { withFileTypes: true });
|
|
22
|
+
const names = [];
|
|
23
|
+
for (const entry of entries) {
|
|
24
|
+
if (!entry.isDirectory()) continue;
|
|
25
|
+
if (await pathExists(path.join(skillsRoot, entry.name, 'SKILL.md'))) {
|
|
26
|
+
names.push(entry.name);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
return names.sort();
|
|
30
|
+
} catch {
|
|
31
|
+
return [];
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async function findSourceSkillsRoot() {
|
|
36
|
+
const candidates = [
|
|
37
|
+
path.join(ROOT, '..', 'claude-code-plugins', 'kmp-migration', 'skills'),
|
|
38
|
+
path.join(ROOT, '..', '..', 'claude-code-plugins', 'kmp-migration', 'skills'),
|
|
39
|
+
path.join(process.env.INIT_CWD ?? '', 'claude-code-plugins', 'kmp-migration', 'skills')
|
|
40
|
+
].filter(Boolean);
|
|
41
|
+
|
|
42
|
+
for (const candidate of candidates) {
|
|
43
|
+
if ((await listSkillNames(candidate)).length > 0) return candidate;
|
|
44
|
+
}
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async function main() {
|
|
49
|
+
const source = await findSourceSkillsRoot();
|
|
50
|
+
const target = path.join(ROOT, 'skills');
|
|
51
|
+
|
|
52
|
+
if (!source) {
|
|
53
|
+
if (ifPresent) {
|
|
54
|
+
console.log('[sync-skills] source skills not found; skipping.');
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
throw new Error('Unable to find claude-code-plugins/kmp-migration/skills.');
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
await fs.rm(target, { recursive: true, force: true });
|
|
61
|
+
await fs.mkdir(target, { recursive: true });
|
|
62
|
+
|
|
63
|
+
const skillNames = await listSkillNames(source);
|
|
64
|
+
for (const skillName of skillNames) {
|
|
65
|
+
await fs.cp(path.join(source, skillName), path.join(target, skillName), { recursive: true });
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
console.log(`[sync-skills] synced ${skillNames.length} skills from ${source} -> ${target}`);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
main().catch((error) => {
|
|
72
|
+
console.error(`[sync-skills] ${error instanceof Error ? error.message : String(error)}`);
|
|
73
|
+
process.exitCode = 1;
|
|
74
|
+
});
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# Conversion Note: `android-project-analyst` → Swarm Skill
|
|
2
|
+
|
|
3
|
+
This skill was converted from a single controller-support skill (a flat SKILL.md registry plus 7 sibling node-spec files) into a compliant **Swarm Skill** using `swarmskill-creator` convert mode.
|
|
4
|
+
|
|
5
|
+
## Source structure (before)
|
|
6
|
+
|
|
7
|
+
- `SKILL.md` — controller registry describing convert mode, node contracts, dispatch order, and the SPEC output contract.
|
|
8
|
+
- 7 flat node specs at the skill root: `ui-understand.md`, `architecture-pattern.md`, `android-ecosystem.md`, `api-list.md`, `resource-understand.md`, `data-flow.md`, `logic-understand.md`. Each contained Role / Inputs / Mandatory Input Validation & Output Storage / Specific Task / Required Outputs / Return Format / Self-Check.
|
|
9
|
+
|
|
10
|
+
## What was lost in the pre-swarm form
|
|
11
|
+
|
|
12
|
+
The registry already separated controller from nodes, but it did not encode the team as a first-class artifact: there were no per-role anti-convergence mottos, no `Forbidden`/`Mandatory` boundary blocks the validator could check, no pasteable `Inline Persona` (so each dispatch re-derived the contract by hand), no Mermaid topology making the parallel-then-pipeline shape explicit, and no resource/behavioral guardrails (`max_parallel_teammates`, token/wall-clock budgets, degraded modes). The handoff gates between stages lived only in prose.
|
|
13
|
+
|
|
14
|
+
## Decomposition
|
|
15
|
+
|
|
16
|
+
- **Pattern: Mixed B + C.** Stage A (`ui-understand`, `architecture-pattern`, `android-ecosystem`, `api-list`) is parallel decomposition (B) over disjoint slices. Stage B (`resource-understand`, `data-flow`) and Stage C (`logic-understand`) form a specialization pipeline (C) with hard handoff gates — each consumes verified upstream outputs and must not rebuild them.
|
|
17
|
+
- **Disjointness check: PASS.** No node's deliverable can substitute for another's — UI surface vs. architecture style vs. platform ecosystem vs. API contracts vs. resources vs. data movement vs. control-flow behavior are mutually exclusive ownership domains, enforced by each role's `## Boundary > Forbidden` naming its siblings.
|
|
18
|
+
|
|
19
|
+
## Content port map
|
|
20
|
+
|
|
21
|
+
| Source node-spec content | Ported to |
|
|
22
|
+
|---|---|
|
|
23
|
+
| `## Role` first paragraph | role `## Identity` (rewritten as a 1-line motto + context) |
|
|
24
|
+
| `## Specific Task` numbered steps | role `## Inline Persona for Teammate` HANDLER |
|
|
25
|
+
| `## Mandatory Input Validation And Output Storage` | role `## Boundary > Mandatory` + Inline Persona CONTROL block |
|
|
26
|
+
| `Do not:` lists + sibling routing | role `## Boundary > Forbidden` |
|
|
27
|
+
| `## Required Outputs` JSON/MD | role `## Output Schema` + Inline Persona OUTPUTS |
|
|
28
|
+
| `## Return Format` | role Inline Persona RETURN TO CONTROLLER |
|
|
29
|
+
| `## Self-Check` | role `## Success Criteria` |
|
|
30
|
+
| Controller dispatch order + verification | `workflow.md` (staged steps + gates) |
|
|
31
|
+
| Mandatory contract enforcement + agent-only rules | `bind.md` § Behavioral Constraints |
|
|
32
|
+
| Node failure / rerun handling | `bind.md` § Failure Handling |
|
|
33
|
+
| SPEC output contract + MCP context | `SKILL.md` body (preserved) |
|
|
34
|
+
|
|
35
|
+
## Team-vs-single delta
|
|
36
|
+
|
|
37
|
+
The conversion preserves every source contract while adding: explicit parallel/pipeline topology with verifiable gates, per-role anti-overlap boundaries that name siblings, self-contained pasteable personas (no re-derivation per dispatch), resource/token/wall-clock budgets, and concrete degraded modes for large monorepos and missing tooling. The same-name controller subagent in `kmp-migration/agents/android-project-analyst.md` is unchanged in behavior; its `Control Nodes` table now points at `roles/<id>.md`.
|