codemini-cli 0.5.10 → 0.5.11
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/OPERATIONS.md +242 -242
- package/README.md +588 -588
- package/codemini-web/dist/assets/{highlighted-body-OFNGDK62-7HL7yft8.js → highlighted-body-OFNGDK62-CANOG7Xg.js} +1 -1
- package/codemini-web/dist/assets/{index-BK75hMb2.js → index-B71xykPM.js} +108 -108
- package/codemini-web/dist/assets/index-Dkq1DdDX.css +2 -0
- package/codemini-web/dist/assets/mermaid-GHXKKRXX-Z_w7M93P.js +1 -0
- package/codemini-web/dist/index.html +23 -23
- package/codemini-web/lib/approval-manager.js +32 -32
- package/codemini-web/lib/runtime-bridge.js +17 -11
- package/codemini-web/server.js +534 -205
- package/deployment.md +212 -212
- package/package.json +1 -1
- package/skills/brainstorm/SKILL.md +77 -77
- package/skills/codemini.skills.json +40 -40
- package/skills/grill-me/SKILL.md +30 -30
- package/skills/superpowers-lite/SKILL.md +82 -82
- package/src/cli.js +74 -74
- package/src/commands/chat.js +210 -210
- package/src/commands/run.js +313 -313
- package/src/commands/skill.js +438 -304
- package/src/commands/web.js +57 -57
- package/src/core/agent-loop.js +980 -980
- package/src/core/ast.js +309 -307
- package/src/core/chat-runtime.js +6261 -6253
- package/src/core/command-evaluator.js +72 -72
- package/src/core/command-loader.js +311 -311
- package/src/core/command-policy.js +301 -301
- package/src/core/command-risk.js +156 -156
- package/src/core/config-store.js +289 -289
- package/src/core/constants.js +18 -1
- package/src/core/context-compact.js +365 -365
- package/src/core/default-system-prompt.js +114 -107
- package/src/core/dream-audit.js +105 -105
- package/src/core/dream-consolidate.js +229 -229
- package/src/core/dream-evaluator.js +185 -185
- package/src/core/fff-adapter.js +383 -383
- package/src/core/memory-store.js +543 -543
- package/src/core/project-index.js +737 -548
- package/src/core/project-instructions.js +98 -98
- package/src/core/provider/anthropic.js +514 -514
- package/src/core/provider/openai-compatible.js +501 -501
- package/src/core/reflect-skill.js +178 -178
- package/src/core/reply-language.js +40 -40
- package/src/core/session-store.js +474 -474
- package/src/core/shell-profile.js +237 -237
- package/src/core/shell.js +323 -323
- package/src/core/soul.js +69 -69
- package/src/core/system-prompt-composer.js +52 -52
- package/src/core/tool-args.js +199 -154
- package/src/core/tool-output.js +184 -184
- package/src/core/tool-result-store.js +206 -206
- package/src/core/tools.js +3024 -2893
- package/src/core/version.js +11 -11
- package/src/tui/chat-app.js +5171 -5171
- package/src/tui/tool-activity/presenters/misc.js +30 -30
- package/src/tui/tool-activity/presenters/system.js +20 -20
- package/templates/project-requirements/report-shell.html +582 -582
- package/codemini-web/dist/assets/index-BSdIdn3L.css +0 -2
- package/codemini-web/dist/assets/mermaid-GHXKKRXX-Dg9qh8mg.js +0 -1
package/src/commands/skill.js
CHANGED
|
@@ -1,367 +1,501 @@
|
|
|
1
1
|
import fs from 'node:fs/promises';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import os from 'node:os';
|
|
4
|
-
import { spawn } from 'node:child_process';
|
|
5
|
-
import { copyRecursive } from '../core/fs-utils.js';
|
|
6
|
-
import { loadConfig, saveConfig } from '../core/config-store.js';
|
|
7
|
-
import { loadCommandsAndSkills } from '../core/command-loader.js';
|
|
8
|
-
import { getProjectSkillsDir, getSkillsDir } from '../core/paths.js';
|
|
9
|
-
import {
|
|
10
|
-
computeFileSha256,
|
|
11
|
-
readSkillRegistry,
|
|
12
|
-
upsertSkillRegistryEntry,
|
|
13
|
-
writeSkillRegistry
|
|
14
|
-
} from '../core/skill-registry.js';
|
|
15
|
-
|
|
16
|
-
function parseScopeArgs(args = [], { defaultScope = 'project', allowAll = false } = {}) {
|
|
17
|
-
let scope = defaultScope;
|
|
18
|
-
const rest = [];
|
|
19
|
-
for (let index = 0; index < args.length; index += 1) {
|
|
20
|
-
const arg = String(args[index] || '');
|
|
21
|
-
if (arg === '--global') {
|
|
22
|
-
scope = 'global';
|
|
23
|
-
continue;
|
|
24
|
-
}
|
|
25
|
-
if (arg === '--project') {
|
|
26
|
-
scope = 'project';
|
|
27
|
-
continue;
|
|
28
|
-
}
|
|
29
|
-
if (arg === '--scope') {
|
|
30
|
-
const next = String(args[index + 1] || '').toLowerCase();
|
|
31
|
-
if (['project', 'global', ...(allowAll ? ['all', 'builtin'] : [])].includes(next)) {
|
|
32
|
-
scope = next;
|
|
33
|
-
index += 1;
|
|
34
|
-
continue;
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
if (arg.startsWith('--scope=')) {
|
|
38
|
-
const value = arg.slice('--scope='.length).toLowerCase();
|
|
39
|
-
if (['project', 'global', ...(allowAll ? ['all', 'builtin'] : [])].includes(value)) {
|
|
40
|
-
scope = value;
|
|
41
|
-
continue;
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
rest.push(arg);
|
|
45
|
-
}
|
|
46
|
-
return { scope, rest };
|
|
47
|
-
}
|
|
48
|
-
|
|
4
|
+
import { spawn, execFile } from 'node:child_process';
|
|
5
|
+
import { copyRecursive } from '../core/fs-utils.js';
|
|
6
|
+
import { loadConfig, saveConfig } from '../core/config-store.js';
|
|
7
|
+
import { loadCommandsAndSkills } from '../core/command-loader.js';
|
|
8
|
+
import { getProjectSkillsDir, getSkillsDir } from '../core/paths.js';
|
|
9
|
+
import {
|
|
10
|
+
computeFileSha256,
|
|
11
|
+
readSkillRegistry,
|
|
12
|
+
upsertSkillRegistryEntry,
|
|
13
|
+
writeSkillRegistry
|
|
14
|
+
} from '../core/skill-registry.js';
|
|
15
|
+
|
|
16
|
+
function parseScopeArgs(args = [], { defaultScope = 'project', allowAll = false } = {}) {
|
|
17
|
+
let scope = defaultScope;
|
|
18
|
+
const rest = [];
|
|
19
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
20
|
+
const arg = String(args[index] || '');
|
|
21
|
+
if (arg === '--global') {
|
|
22
|
+
scope = 'global';
|
|
23
|
+
continue;
|
|
24
|
+
}
|
|
25
|
+
if (arg === '--project') {
|
|
26
|
+
scope = 'project';
|
|
27
|
+
continue;
|
|
28
|
+
}
|
|
29
|
+
if (arg === '--scope') {
|
|
30
|
+
const next = String(args[index + 1] || '').toLowerCase();
|
|
31
|
+
if (['project', 'global', ...(allowAll ? ['all', 'builtin'] : [])].includes(next)) {
|
|
32
|
+
scope = next;
|
|
33
|
+
index += 1;
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
if (arg.startsWith('--scope=')) {
|
|
38
|
+
const value = arg.slice('--scope='.length).toLowerCase();
|
|
39
|
+
if (['project', 'global', ...(allowAll ? ['all', 'builtin'] : [])].includes(value)) {
|
|
40
|
+
scope = value;
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
rest.push(arg);
|
|
45
|
+
}
|
|
46
|
+
return { scope, rest };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
49
|
function baseDirForScope(scope, cwd = process.cwd()) {
|
|
50
50
|
return scope === 'global' ? getSkillsDir() : getProjectSkillsDir(cwd);
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
-
function
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
53
|
+
function isGitLikeSource(value = '') {
|
|
54
|
+
const text = String(value || '').trim();
|
|
55
|
+
return (
|
|
56
|
+
/^https:\/\/github\.com\/[^/\s]+\/[^/\s]+(?:\/tree\/[^/\s]+)?\/?$/i.test(text) ||
|
|
57
|
+
/^git@github\.com:[^/\s]+\/[^/\s]+(?:\.git)?$/i.test(text) ||
|
|
58
|
+
/^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/.test(text)
|
|
59
|
+
);
|
|
58
60
|
}
|
|
59
61
|
|
|
60
|
-
|
|
61
|
-
const
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
config.skills.enabled[name] = enabled;
|
|
65
|
-
await saveConfig(config);
|
|
62
|
+
function normalizeNpxSkillSource(value = '') {
|
|
63
|
+
const text = String(value || '').trim();
|
|
64
|
+
const match = text.match(/^npx\s+skills(?:@[\w.-]+)?\s+add\s+(.+)$/i);
|
|
65
|
+
return match ? match[1].trim().split(/\s+/)[0] : text;
|
|
66
66
|
}
|
|
67
67
|
|
|
68
|
-
|
|
69
|
-
const
|
|
70
|
-
const
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
const itemScope = scopeFromSource(command.source);
|
|
75
|
-
if (scope !== 'all' && itemScope !== scope) continue;
|
|
76
|
-
entries.push({
|
|
77
|
-
name: command.name,
|
|
78
|
-
version: command.metadata?.version || '0.0.0',
|
|
79
|
-
description: command.metadata?.description || '',
|
|
80
|
-
mode: command.metadata?.mode || '',
|
|
81
|
-
triggers: Array.isArray(command.metadata?.triggers) ? command.metadata.triggers : [],
|
|
82
|
-
scope: itemScope,
|
|
83
|
-
path: command.path,
|
|
84
|
-
enabled: command.metadata?.enabled === false
|
|
85
|
-
? false
|
|
86
|
-
: itemScope === 'builtin'
|
|
87
|
-
? true
|
|
88
|
-
: config.skills?.enabled?.[command.name] !== false
|
|
89
|
-
});
|
|
68
|
+
function normalizeGitSource(source = '') {
|
|
69
|
+
const raw = normalizeNpxSkillSource(source);
|
|
70
|
+
const githubTree = raw.match(/^https:\/\/github\.com\/([^/\s]+)\/([^/\s]+)\/tree\/([^/\s]+)(?:\/)?$/i);
|
|
71
|
+
if (githubTree) {
|
|
72
|
+
const [, owner, repo, branch] = githubTree;
|
|
73
|
+
return { url: `https://github.com/${owner}/${repo}.git`, branch };
|
|
90
74
|
}
|
|
91
|
-
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
async function readSkillMeta(name, { scope = 'all', cwd = process.cwd() } = {}) {
|
|
95
|
-
const entries = await listSkillEntries({ scope, cwd });
|
|
96
|
-
const found = entries.find((item) => item.name === name);
|
|
97
|
-
if (!found) {
|
|
98
|
-
return { exists: false, path: '', preview: '', manifest: null };
|
|
75
|
+
if (/^https:\/\/github\.com\/[^/\s]+\/[^/\s]+\/?$/i.test(raw)) {
|
|
76
|
+
return { url: raw.replace(/\/$/, '') + '.git', branch: null };
|
|
99
77
|
}
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
const catalogPath = path.join(path.dirname(dir), 'codemini.skills.json');
|
|
103
|
-
let manifest = null;
|
|
104
|
-
try {
|
|
105
|
-
const catalog = JSON.parse(await fs.readFile(catalogPath, 'utf8'));
|
|
106
|
-
manifest = catalog?.skills?.[found.name] || null;
|
|
107
|
-
} catch {
|
|
108
|
-
try {
|
|
109
|
-
manifest = JSON.parse(await fs.readFile(manifestPath, 'utf8'));
|
|
110
|
-
} catch {
|
|
111
|
-
manifest = null;
|
|
112
|
-
}
|
|
78
|
+
if (/^git@github\.com:/i.test(raw)) {
|
|
79
|
+
return { url: raw.endsWith('.git') ? raw : `${raw}.git`, branch: null };
|
|
113
80
|
}
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
const content = await fs.readFile(skillPath, 'utf8');
|
|
117
|
-
const firstLines = content.split('\n').slice(0, 20).join('\n');
|
|
118
|
-
return { exists: true, path: skillPath, preview: firstLines, manifest, scope: found.scope };
|
|
119
|
-
} catch {
|
|
120
|
-
return { exists: false, path: skillPath, preview: '', manifest, scope: found.scope };
|
|
81
|
+
if (/^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/.test(raw)) {
|
|
82
|
+
return { url: `https://github.com/${raw}.git`, branch: null };
|
|
121
83
|
}
|
|
84
|
+
return null;
|
|
122
85
|
}
|
|
123
86
|
|
|
124
|
-
async function
|
|
87
|
+
async function runGitClone(source, destDir) {
|
|
88
|
+
const normalized = normalizeGitSource(source);
|
|
89
|
+
if (!normalized) {
|
|
90
|
+
throw new Error(`unsupported git skill source: ${source}`);
|
|
91
|
+
}
|
|
125
92
|
await new Promise((resolve, reject) => {
|
|
126
|
-
const
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
});
|
|
133
|
-
child.on('error', reject);
|
|
134
|
-
child.on('close', (code) => {
|
|
135
|
-
if (code !== 0) {
|
|
136
|
-
reject(new Error(`tar extract failed: ${stderr || `exit ${code}`}`));
|
|
93
|
+
const args = ['clone', '--depth', '1'];
|
|
94
|
+
if (normalized.branch) args.push('--branch', normalized.branch);
|
|
95
|
+
args.push(normalized.url, destDir);
|
|
96
|
+
const child = execFile('git', args, { windowsHide: true }, (error, stdout, stderr) => {
|
|
97
|
+
if (error) {
|
|
98
|
+
reject(new Error(`git clone failed: ${stderr || stdout || error.message}`));
|
|
137
99
|
return;
|
|
138
100
|
}
|
|
139
101
|
resolve();
|
|
140
102
|
});
|
|
103
|
+
child.stdin?.end();
|
|
141
104
|
});
|
|
142
105
|
}
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
106
|
+
|
|
107
|
+
function scopeFromSource(source = '') {
|
|
108
|
+
if (source === 'bundled-skill') return 'builtin';
|
|
109
|
+
if (source === 'project-skill') return 'project';
|
|
110
|
+
if (source === 'global-skill' || source === 'registry-skill') return 'global';
|
|
111
|
+
return source || 'unknown';
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
async function setSkillEnabledConfig(name, enabled) {
|
|
115
|
+
const config = await loadConfig();
|
|
116
|
+
config.skills = config.skills || {};
|
|
117
|
+
config.skills.enabled = config.skills.enabled || {};
|
|
118
|
+
config.skills.enabled[name] = enabled;
|
|
119
|
+
await saveConfig(config);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export async function listSkillEntries({ scope = 'all', cwd = process.cwd() } = {}) {
|
|
123
|
+
const commands = await loadCommandsAndSkills(cwd);
|
|
124
|
+
const config = await loadConfig();
|
|
125
|
+
const entries = [];
|
|
126
|
+
for (const command of commands.values()) {
|
|
127
|
+
if (command.metadata?.type !== 'skill') continue;
|
|
128
|
+
const itemScope = scopeFromSource(command.source);
|
|
129
|
+
if (scope !== 'all' && itemScope !== scope) continue;
|
|
130
|
+
entries.push({
|
|
131
|
+
name: command.name,
|
|
132
|
+
version: command.metadata?.version || '0.0.0',
|
|
133
|
+
description: command.metadata?.description || '',
|
|
134
|
+
mode: command.metadata?.mode || '',
|
|
135
|
+
triggers: Array.isArray(command.metadata?.triggers) ? command.metadata.triggers : [],
|
|
136
|
+
scope: itemScope,
|
|
137
|
+
path: command.path,
|
|
138
|
+
enabled: command.metadata?.enabled === false
|
|
139
|
+
? false
|
|
140
|
+
: itemScope === 'builtin'
|
|
141
|
+
? true
|
|
142
|
+
: config.skills?.enabled?.[command.name] !== false
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
return entries.sort((a, b) => `${a.scope}:${a.name}`.localeCompare(`${b.scope}:${b.name}`));
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
async function readSkillMeta(name, { scope = 'all', cwd = process.cwd() } = {}) {
|
|
149
|
+
const entries = await listSkillEntries({ scope, cwd });
|
|
150
|
+
const found = entries.find((item) => item.name === name);
|
|
151
|
+
if (!found) {
|
|
152
|
+
return { exists: false, path: '', preview: '', manifest: null };
|
|
153
|
+
}
|
|
154
|
+
const dir = path.dirname(found.path);
|
|
155
|
+
const manifestPath = path.join(dir, 'manifest.json');
|
|
156
|
+
const catalogPath = path.join(path.dirname(dir), 'codemini.skills.json');
|
|
157
|
+
let manifest = null;
|
|
158
|
+
try {
|
|
159
|
+
const catalog = JSON.parse(await fs.readFile(catalogPath, 'utf8'));
|
|
160
|
+
manifest = catalog?.skills?.[found.name] || null;
|
|
161
|
+
} catch {
|
|
162
|
+
try {
|
|
163
|
+
manifest = JSON.parse(await fs.readFile(manifestPath, 'utf8'));
|
|
164
|
+
} catch {
|
|
165
|
+
manifest = null;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
const skillPath = found.path || path.join(dir, 'SKILL.md');
|
|
169
|
+
try {
|
|
170
|
+
const content = await fs.readFile(skillPath, 'utf8');
|
|
171
|
+
const firstLines = content.split('\n').slice(0, 20).join('\n');
|
|
172
|
+
return { exists: true, path: skillPath, preview: firstLines, manifest, scope: found.scope };
|
|
173
|
+
} catch {
|
|
174
|
+
return { exists: false, path: skillPath, preview: '', manifest, scope: found.scope };
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
async function runTarExtract(tgzPath, destDir) {
|
|
179
|
+
await new Promise((resolve, reject) => {
|
|
180
|
+
const child = spawn('tar', ['-xzf', tgzPath, '-C', destDir], {
|
|
181
|
+
stdio: ['ignore', 'pipe', 'pipe']
|
|
182
|
+
});
|
|
183
|
+
let stderr = '';
|
|
184
|
+
child.stderr.on('data', (chunk) => {
|
|
185
|
+
stderr += chunk.toString();
|
|
186
|
+
});
|
|
187
|
+
child.on('error', reject);
|
|
188
|
+
child.on('close', (code) => {
|
|
189
|
+
if (code !== 0) {
|
|
190
|
+
reject(new Error(`tar extract failed: ${stderr || `exit ${code}`}`));
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
resolve();
|
|
194
|
+
});
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
async function readManifestSafe(skillRoot) {
|
|
199
|
+
const p = path.join(skillRoot, 'manifest.json');
|
|
200
|
+
try {
|
|
201
|
+
const raw = await fs.readFile(p, 'utf8');
|
|
202
|
+
return JSON.parse(raw);
|
|
203
|
+
} catch {
|
|
204
|
+
return null;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
154
208
|
async function resolveSkillSourceDir(sourcePath) {
|
|
155
209
|
const absSrc = path.resolve(sourcePath);
|
|
156
210
|
const srcStat = await fs.stat(absSrc);
|
|
211
|
+
|
|
212
|
+
if (srcStat.isFile() && absSrc.endsWith('.tgz')) {
|
|
213
|
+
const tmp = await fs.mkdtemp(path.join(os.tmpdir(), 'codemini-skill-'));
|
|
214
|
+
await runTarExtract(absSrc, tmp);
|
|
215
|
+
const candidates = ['package', ...((await fs.readdir(tmp, { withFileTypes: true }))
|
|
216
|
+
.filter((d) => d.isDirectory())
|
|
217
|
+
.map((d) => d.name))];
|
|
218
|
+
for (const c of candidates) {
|
|
219
|
+
const dir = path.join(tmp, c);
|
|
220
|
+
try {
|
|
221
|
+
await fs.access(path.join(dir, 'SKILL.md'));
|
|
222
|
+
return { dir, cleanupDir: tmp };
|
|
223
|
+
} catch {
|
|
224
|
+
continue;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
throw new Error('No SKILL.md found in tgz package');
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
if (srcStat.isFile() && path.basename(absSrc) === 'SKILL.md') {
|
|
231
|
+
return { dir: path.dirname(absSrc), cleanupDir: null };
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
if (srcStat.isDirectory()) {
|
|
235
|
+
await fs.access(path.join(absSrc, 'SKILL.md'));
|
|
236
|
+
return { dir: absSrc, cleanupDir: null };
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
throw new Error('skill install supports <skill-dir>, <SKILL.md>, or <skill.tgz>');
|
|
240
|
+
}
|
|
157
241
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
242
|
+
async function findSkillDirs(rootDir) {
|
|
243
|
+
const found = [];
|
|
244
|
+
async function walk(dir, depth = 0) {
|
|
245
|
+
if (depth > 5) return;
|
|
246
|
+
let entries;
|
|
247
|
+
try {
|
|
248
|
+
entries = await fs.readdir(dir, { withFileTypes: true });
|
|
249
|
+
} catch {
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
if (entries.some((entry) => entry.isFile() && entry.name === 'SKILL.md')) {
|
|
253
|
+
found.push(dir);
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
for (const entry of entries) {
|
|
257
|
+
if (!entry.isDirectory()) continue;
|
|
258
|
+
if (['.git', 'node_modules', 'dist', 'build'].includes(entry.name)) continue;
|
|
259
|
+
await walk(path.join(dir, entry.name), depth + 1);
|
|
172
260
|
}
|
|
173
|
-
throw new Error('No SKILL.md found in tgz package');
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
if (srcStat.isFile() && path.basename(absSrc) === 'SKILL.md') {
|
|
177
|
-
return { dir: path.dirname(absSrc), cleanupDir: null };
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
if (srcStat.isDirectory()) {
|
|
181
|
-
await fs.access(path.join(absSrc, 'SKILL.md'));
|
|
182
|
-
return { dir: absSrc, cleanupDir: null };
|
|
183
261
|
}
|
|
184
|
-
|
|
185
|
-
|
|
262
|
+
await walk(rootDir);
|
|
263
|
+
return found;
|
|
186
264
|
}
|
|
187
265
|
|
|
188
|
-
async function installSkill(sourcePath, { scope = 'project', cwd = process.cwd() } = {}) {
|
|
266
|
+
export async function installSkill(sourcePath, { scope = 'project', cwd = process.cwd(), sourceLabel = sourcePath } = {}) {
|
|
189
267
|
const resolved = await resolveSkillSourceDir(sourcePath);
|
|
190
268
|
const manifest = await readManifestSafe(resolved.dir);
|
|
191
269
|
const folderName = manifest?.name || path.basename(resolved.dir);
|
|
192
|
-
const bundled = (await listSkillEntries({ scope: 'builtin', cwd })).find((item) => item.name === folderName);
|
|
193
|
-
if (bundled) {
|
|
194
|
-
throw new Error(`cannot install over builtin skill: ${folderName}`);
|
|
195
|
-
}
|
|
196
|
-
const targetDir = path.join(baseDirForScope(scope, cwd), folderName);
|
|
197
|
-
await fs.rm(targetDir, { recursive: true, force: true });
|
|
198
|
-
await copyRecursive(resolved.dir, targetDir);
|
|
199
|
-
|
|
200
|
-
const entryFile = manifest?.entry || 'SKILL.md';
|
|
201
|
-
const entryPath = path.join(targetDir, entryFile);
|
|
202
|
-
await fs.access(entryPath);
|
|
203
|
-
|
|
204
|
-
const hash = await computeFileSha256(entryPath);
|
|
205
|
-
if (scope === 'global') {
|
|
206
|
-
await upsertSkillRegistryEntry(undefined, {
|
|
270
|
+
const bundled = (await listSkillEntries({ scope: 'builtin', cwd })).find((item) => item.name === folderName);
|
|
271
|
+
if (bundled) {
|
|
272
|
+
throw new Error(`cannot install over builtin skill: ${folderName}`);
|
|
273
|
+
}
|
|
274
|
+
const targetDir = path.join(baseDirForScope(scope, cwd), folderName);
|
|
275
|
+
await fs.rm(targetDir, { recursive: true, force: true });
|
|
276
|
+
await copyRecursive(resolved.dir, targetDir);
|
|
277
|
+
|
|
278
|
+
const entryFile = manifest?.entry || 'SKILL.md';
|
|
279
|
+
const entryPath = path.join(targetDir, entryFile);
|
|
280
|
+
await fs.access(entryPath);
|
|
281
|
+
|
|
282
|
+
const hash = await computeFileSha256(entryPath);
|
|
283
|
+
if (scope === 'global') {
|
|
284
|
+
await upsertSkillRegistryEntry(undefined, {
|
|
207
285
|
name: folderName,
|
|
208
286
|
version: manifest?.version || '0.0.0',
|
|
209
287
|
description: manifest?.description || '',
|
|
210
288
|
enabled: true,
|
|
211
|
-
source:
|
|
289
|
+
source: sourceLabel,
|
|
212
290
|
entryFile,
|
|
213
291
|
sha256: hash,
|
|
214
292
|
installedAt: new Date().toISOString()
|
|
215
|
-
});
|
|
216
|
-
}
|
|
217
|
-
await setSkillEnabledConfig(folderName, true);
|
|
218
|
-
|
|
219
|
-
if (resolved.cleanupDir) {
|
|
220
|
-
await fs.rm(resolved.cleanupDir, { recursive: true, force: true });
|
|
221
|
-
}
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
await setSkillEnabledConfig(folderName, true);
|
|
296
|
+
|
|
297
|
+
if (resolved.cleanupDir) {
|
|
298
|
+
await fs.rm(resolved.cleanupDir, { recursive: true, force: true });
|
|
299
|
+
}
|
|
222
300
|
|
|
223
301
|
return folderName;
|
|
224
302
|
}
|
|
225
303
|
|
|
226
|
-
async function
|
|
227
|
-
const
|
|
228
|
-
const
|
|
229
|
-
|
|
230
|
-
throw new Error(`skill not found: ${name}`);
|
|
231
|
-
}
|
|
232
|
-
if (found.scope === 'builtin') {
|
|
233
|
-
throw new Error(`builtin skill cannot be ${enabled ? 'enabled' : 'disabled'}: ${name}`);
|
|
234
|
-
}
|
|
235
|
-
await setSkillEnabledConfig(name, enabled);
|
|
236
|
-
const registry = await readSkillRegistry();
|
|
237
|
-
const idx = registry.skills.findIndex((s) => s.name === name);
|
|
238
|
-
if (idx !== -1) {
|
|
239
|
-
registry.skills[idx].enabled = enabled;
|
|
240
|
-
await writeSkillRegistry(undefined, registry);
|
|
241
|
-
}
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
async function reindexSkills({ scope = 'global', cwd = process.cwd() } = {}) {
|
|
245
|
-
const baseDir = baseDirForScope(scope, cwd);
|
|
246
|
-
await fs.mkdir(baseDir, { recursive: true });
|
|
247
|
-
const entries = await fs.readdir(baseDir, { withFileTypes: true });
|
|
248
|
-
const registry = await readSkillRegistry();
|
|
249
|
-
const byName = new Map((registry.skills || []).map((s) => [s.name, s]));
|
|
250
|
-
const rebuilt = [];
|
|
251
|
-
|
|
252
|
-
for (const entry of entries) {
|
|
253
|
-
if (!entry.isDirectory()) continue;
|
|
254
|
-
const name = entry.name;
|
|
255
|
-
const dir = path.join(baseDir, name);
|
|
256
|
-
const manifest = await readManifestSafe(dir);
|
|
257
|
-
const entryFile = manifest?.entry || 'SKILL.md';
|
|
258
|
-
const entryPath = path.join(dir, entryFile);
|
|
304
|
+
async function installSkillDirs(skillDirs, { scope, cwd, sourceLabel }) {
|
|
305
|
+
const installed = [];
|
|
306
|
+
const skipped = [];
|
|
307
|
+
for (const dir of skillDirs) {
|
|
259
308
|
try {
|
|
260
|
-
await
|
|
261
|
-
} catch {
|
|
262
|
-
|
|
309
|
+
installed.push(await installSkill(dir, { scope, cwd, sourceLabel }));
|
|
310
|
+
} catch (err) {
|
|
311
|
+
if (/cannot install over builtin skill:/i.test(err.message || '')) {
|
|
312
|
+
skipped.push({ dir, reason: err.message });
|
|
313
|
+
continue;
|
|
314
|
+
}
|
|
315
|
+
throw err;
|
|
263
316
|
}
|
|
264
|
-
const hash = await computeFileSha256(entryPath);
|
|
265
|
-
const prior = byName.get(name);
|
|
266
|
-
rebuilt.push({
|
|
267
|
-
name: manifest?.name || name,
|
|
268
|
-
version: manifest?.version || prior?.version || '0.0.0',
|
|
269
|
-
description: manifest?.description || prior?.description || '',
|
|
270
|
-
enabled: prior?.enabled !== false,
|
|
271
|
-
source: prior?.source || 'reindex',
|
|
272
|
-
entryFile,
|
|
273
|
-
sha256: hash,
|
|
274
|
-
installedAt: prior?.installedAt || new Date().toISOString()
|
|
275
|
-
});
|
|
276
317
|
}
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
await writeSkillRegistry(undefined, {
|
|
280
|
-
version: 1,
|
|
281
|
-
skills: rebuilt
|
|
282
|
-
});
|
|
318
|
+
if (installed.length === 0 && skipped.length > 0) {
|
|
319
|
+
throw new Error(skipped.map((item) => item.reason).join('\n'));
|
|
283
320
|
}
|
|
284
|
-
|
|
285
|
-
return rebuilt.length;
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
function usage() {
|
|
289
|
-
console.log(`Usage:
|
|
290
|
-
codemini skill list [--scope=all|project|global|builtin]
|
|
291
|
-
codemini skill install [--scope=project|global] <path>
|
|
292
|
-
codemini skill enable <name>
|
|
293
|
-
codemini skill disable <name>
|
|
294
|
-
codemini skill inspect [--scope=all|project|global|builtin] <name>
|
|
295
|
-
codemini skill reindex [--scope=project|global]`);
|
|
321
|
+
return installed;
|
|
296
322
|
}
|
|
297
323
|
|
|
298
|
-
export async function
|
|
299
|
-
const
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
324
|
+
export async function installSkillSource(source, { scope = 'project', cwd = process.cwd() } = {}) {
|
|
325
|
+
const normalizedSource = normalizeNpxSkillSource(source);
|
|
326
|
+
const tmp = isGitLikeSource(normalizedSource)
|
|
327
|
+
? await fs.mkdtemp(path.join(os.tmpdir(), 'codemini-skill-git-'))
|
|
328
|
+
: null;
|
|
329
|
+
try {
|
|
330
|
+
if (tmp) {
|
|
331
|
+
await runGitClone(normalizedSource, tmp);
|
|
332
|
+
const skillDirs = await findSkillDirs(tmp);
|
|
333
|
+
if (skillDirs.length === 0) {
|
|
334
|
+
throw new Error('No SKILL.md found in git repository');
|
|
335
|
+
}
|
|
336
|
+
return await installSkillDirs(skillDirs, { scope, cwd, sourceLabel: normalizedSource });
|
|
337
|
+
}
|
|
304
338
|
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
return;
|
|
339
|
+
try {
|
|
340
|
+
const resolved = await resolveSkillSourceDir(normalizedSource);
|
|
341
|
+
if (resolved.cleanupDir) {
|
|
342
|
+
await fs.rm(resolved.cleanupDir, { recursive: true, force: true });
|
|
343
|
+
}
|
|
344
|
+
return [await installSkill(normalizedSource, { scope, cwd })];
|
|
345
|
+
} catch (err) {
|
|
346
|
+
const absSrc = path.resolve(normalizedSource);
|
|
347
|
+
const stat = await fs.stat(absSrc);
|
|
348
|
+
if (!stat.isDirectory()) throw err;
|
|
349
|
+
const skillDirs = await findSkillDirs(absSrc);
|
|
350
|
+
if (skillDirs.length === 0) throw err;
|
|
351
|
+
return await installSkillDirs(skillDirs, { scope, cwd, sourceLabel: normalizedSource });
|
|
311
352
|
}
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
353
|
+
} finally {
|
|
354
|
+
if (tmp) {
|
|
355
|
+
await fs.rm(tmp, { recursive: true, force: true });
|
|
315
356
|
}
|
|
316
|
-
return;
|
|
317
357
|
}
|
|
318
|
-
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
async function setEnabled(name, enabled, { cwd = process.cwd() } = {}) {
|
|
361
|
+
const entries = await listSkillEntries({ scope: 'all', cwd });
|
|
362
|
+
const found = entries.find((item) => item.name === name);
|
|
363
|
+
if (!found) {
|
|
364
|
+
throw new Error(`skill not found: ${name}`);
|
|
365
|
+
}
|
|
366
|
+
if (found.scope === 'builtin') {
|
|
367
|
+
throw new Error(`builtin skill cannot be ${enabled ? 'enabled' : 'disabled'}: ${name}`);
|
|
368
|
+
}
|
|
369
|
+
await setSkillEnabledConfig(name, enabled);
|
|
370
|
+
const registry = await readSkillRegistry();
|
|
371
|
+
const idx = registry.skills.findIndex((s) => s.name === name);
|
|
372
|
+
if (idx !== -1) {
|
|
373
|
+
registry.skills[idx].enabled = enabled;
|
|
374
|
+
await writeSkillRegistry(undefined, registry);
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
async function reindexSkills({ scope = 'global', cwd = process.cwd() } = {}) {
|
|
379
|
+
const baseDir = baseDirForScope(scope, cwd);
|
|
380
|
+
await fs.mkdir(baseDir, { recursive: true });
|
|
381
|
+
const entries = await fs.readdir(baseDir, { withFileTypes: true });
|
|
382
|
+
const registry = await readSkillRegistry();
|
|
383
|
+
const byName = new Map((registry.skills || []).map((s) => [s.name, s]));
|
|
384
|
+
const rebuilt = [];
|
|
385
|
+
|
|
386
|
+
for (const entry of entries) {
|
|
387
|
+
if (!entry.isDirectory()) continue;
|
|
388
|
+
const name = entry.name;
|
|
389
|
+
const dir = path.join(baseDir, name);
|
|
390
|
+
const manifest = await readManifestSafe(dir);
|
|
391
|
+
const entryFile = manifest?.entry || 'SKILL.md';
|
|
392
|
+
const entryPath = path.join(dir, entryFile);
|
|
393
|
+
try {
|
|
394
|
+
await fs.access(entryPath);
|
|
395
|
+
} catch {
|
|
396
|
+
continue;
|
|
397
|
+
}
|
|
398
|
+
const hash = await computeFileSha256(entryPath);
|
|
399
|
+
const prior = byName.get(name);
|
|
400
|
+
rebuilt.push({
|
|
401
|
+
name: manifest?.name || name,
|
|
402
|
+
version: manifest?.version || prior?.version || '0.0.0',
|
|
403
|
+
description: manifest?.description || prior?.description || '',
|
|
404
|
+
enabled: prior?.enabled !== false,
|
|
405
|
+
source: prior?.source || 'reindex',
|
|
406
|
+
entryFile,
|
|
407
|
+
sha256: hash,
|
|
408
|
+
installedAt: prior?.installedAt || new Date().toISOString()
|
|
409
|
+
});
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
if (scope === 'global') {
|
|
413
|
+
await writeSkillRegistry(undefined, {
|
|
414
|
+
version: 1,
|
|
415
|
+
skills: rebuilt
|
|
416
|
+
});
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
return rebuilt.length;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
function usage() {
|
|
423
|
+
console.log(`Usage:
|
|
424
|
+
codemini skill list [--scope=all|project|global|builtin]
|
|
425
|
+
codemini skill install [--scope=project|global] <path>
|
|
426
|
+
codemini skill enable <name>
|
|
427
|
+
codemini skill disable <name>
|
|
428
|
+
codemini skill inspect [--scope=all|project|global|builtin] <name>
|
|
429
|
+
codemini skill reindex [--scope=project|global]`);
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
export async function handleSkill(args) {
|
|
433
|
+
const [sub, ...rest] = args;
|
|
434
|
+
if (!sub) {
|
|
435
|
+
usage();
|
|
436
|
+
return;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
if (sub === 'list') {
|
|
440
|
+
const { scope } = parseScopeArgs(rest, { defaultScope: 'all', allowAll: true });
|
|
441
|
+
const entries = await listSkillEntries({ scope });
|
|
442
|
+
if (entries.length === 0) {
|
|
443
|
+
console.log('No installed skills');
|
|
444
|
+
return;
|
|
445
|
+
}
|
|
446
|
+
for (const item of entries) {
|
|
447
|
+
const state = item.scope === 'builtin' ? 'builtin/default' : (item.enabled !== false ? 'enabled' : 'disabled');
|
|
448
|
+
console.log(`${item.name}@${item.version || '0.0.0'} [${item.scope}] (${state})`);
|
|
449
|
+
}
|
|
450
|
+
return;
|
|
451
|
+
}
|
|
452
|
+
|
|
319
453
|
if (sub === 'install') {
|
|
320
454
|
const { scope, rest: positional } = parseScopeArgs(rest, { defaultScope: 'project' });
|
|
321
|
-
const sourcePath = positional
|
|
455
|
+
const sourcePath = positional.join(' ').trim();
|
|
322
456
|
if (!sourcePath) {
|
|
323
457
|
throw new Error('skill install requires <path>');
|
|
324
458
|
}
|
|
325
|
-
const
|
|
326
|
-
console.log(`Installed skill: ${
|
|
327
|
-
return;
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
if (sub === 'enable' || sub === 'disable') {
|
|
331
|
-
const name = rest[0];
|
|
332
|
-
if (!name) {
|
|
333
|
-
throw new Error(`skill ${sub} requires <name>`);
|
|
334
|
-
}
|
|
335
|
-
await setEnabled(name, sub === 'enable');
|
|
336
|
-
console.log(`${sub}d skill: ${name}`);
|
|
459
|
+
const installedNames = await installSkillSource(sourcePath, { scope });
|
|
460
|
+
console.log(`Installed skill${installedNames.length === 1 ? '' : 's'}: ${installedNames.join(', ')} (${scope})`);
|
|
337
461
|
return;
|
|
338
462
|
}
|
|
339
|
-
|
|
340
|
-
if (sub === '
|
|
341
|
-
const
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
}
|
|
463
|
+
|
|
464
|
+
if (sub === 'enable' || sub === 'disable') {
|
|
465
|
+
const name = rest[0];
|
|
466
|
+
if (!name) {
|
|
467
|
+
throw new Error(`skill ${sub} requires <name>`);
|
|
468
|
+
}
|
|
469
|
+
await setEnabled(name, sub === 'enable');
|
|
470
|
+
console.log(`${sub}d skill: ${name}`);
|
|
471
|
+
return;
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
if (sub === 'inspect') {
|
|
475
|
+
const { scope, rest: positional } = parseScopeArgs(rest, { defaultScope: 'all', allowAll: true });
|
|
476
|
+
const name = positional[0];
|
|
477
|
+
if (!name) {
|
|
478
|
+
throw new Error('skill inspect requires <name>');
|
|
479
|
+
}
|
|
480
|
+
const meta = await readSkillMeta(name, { scope });
|
|
481
|
+
if (!meta.exists) {
|
|
482
|
+
throw new Error(`skill not found: ${name}`);
|
|
483
|
+
}
|
|
484
|
+
if (meta.manifest) {
|
|
485
|
+
console.log(`Manifest: ${JSON.stringify(meta.manifest, null, 2)}\n`);
|
|
486
|
+
}
|
|
487
|
+
console.log(`Scope: ${meta.scope}\n`);
|
|
488
|
+
console.log(`Path: ${meta.path}\n`);
|
|
489
|
+
console.log(meta.preview);
|
|
490
|
+
return;
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
if (sub === 'reindex') {
|
|
494
|
+
const { scope } = parseScopeArgs(rest, { defaultScope: 'global' });
|
|
495
|
+
const count = await reindexSkills({ scope });
|
|
496
|
+
console.log(`Reindexed skills: ${count} (${scope})`);
|
|
497
|
+
return;
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
usage();
|
|
501
|
+
}
|