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.
Files changed (59) hide show
  1. package/OPERATIONS.md +242 -242
  2. package/README.md +588 -588
  3. package/codemini-web/dist/assets/{highlighted-body-OFNGDK62-7HL7yft8.js → highlighted-body-OFNGDK62-CANOG7Xg.js} +1 -1
  4. package/codemini-web/dist/assets/{index-BK75hMb2.js → index-B71xykPM.js} +108 -108
  5. package/codemini-web/dist/assets/index-Dkq1DdDX.css +2 -0
  6. package/codemini-web/dist/assets/mermaid-GHXKKRXX-Z_w7M93P.js +1 -0
  7. package/codemini-web/dist/index.html +23 -23
  8. package/codemini-web/lib/approval-manager.js +32 -32
  9. package/codemini-web/lib/runtime-bridge.js +17 -11
  10. package/codemini-web/server.js +534 -205
  11. package/deployment.md +212 -212
  12. package/package.json +1 -1
  13. package/skills/brainstorm/SKILL.md +77 -77
  14. package/skills/codemini.skills.json +40 -40
  15. package/skills/grill-me/SKILL.md +30 -30
  16. package/skills/superpowers-lite/SKILL.md +82 -82
  17. package/src/cli.js +74 -74
  18. package/src/commands/chat.js +210 -210
  19. package/src/commands/run.js +313 -313
  20. package/src/commands/skill.js +438 -304
  21. package/src/commands/web.js +57 -57
  22. package/src/core/agent-loop.js +980 -980
  23. package/src/core/ast.js +309 -307
  24. package/src/core/chat-runtime.js +6261 -6253
  25. package/src/core/command-evaluator.js +72 -72
  26. package/src/core/command-loader.js +311 -311
  27. package/src/core/command-policy.js +301 -301
  28. package/src/core/command-risk.js +156 -156
  29. package/src/core/config-store.js +289 -289
  30. package/src/core/constants.js +18 -1
  31. package/src/core/context-compact.js +365 -365
  32. package/src/core/default-system-prompt.js +114 -107
  33. package/src/core/dream-audit.js +105 -105
  34. package/src/core/dream-consolidate.js +229 -229
  35. package/src/core/dream-evaluator.js +185 -185
  36. package/src/core/fff-adapter.js +383 -383
  37. package/src/core/memory-store.js +543 -543
  38. package/src/core/project-index.js +737 -548
  39. package/src/core/project-instructions.js +98 -98
  40. package/src/core/provider/anthropic.js +514 -514
  41. package/src/core/provider/openai-compatible.js +501 -501
  42. package/src/core/reflect-skill.js +178 -178
  43. package/src/core/reply-language.js +40 -40
  44. package/src/core/session-store.js +474 -474
  45. package/src/core/shell-profile.js +237 -237
  46. package/src/core/shell.js +323 -323
  47. package/src/core/soul.js +69 -69
  48. package/src/core/system-prompt-composer.js +52 -52
  49. package/src/core/tool-args.js +199 -154
  50. package/src/core/tool-output.js +184 -184
  51. package/src/core/tool-result-store.js +206 -206
  52. package/src/core/tools.js +3024 -2893
  53. package/src/core/version.js +11 -11
  54. package/src/tui/chat-app.js +5171 -5171
  55. package/src/tui/tool-activity/presenters/misc.js +30 -30
  56. package/src/tui/tool-activity/presenters/system.js +20 -20
  57. package/templates/project-requirements/report-shell.html +582 -582
  58. package/codemini-web/dist/assets/index-BSdIdn3L.css +0 -2
  59. package/codemini-web/dist/assets/mermaid-GHXKKRXX-Dg9qh8mg.js +0 -1
@@ -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 scopeFromSource(source = '') {
54
- if (source === 'bundled-skill') return 'builtin';
55
- if (source === 'project-skill') return 'project';
56
- if (source === 'global-skill' || source === 'registry-skill') return 'global';
57
- return source || 'unknown';
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
- async function setSkillEnabledConfig(name, enabled) {
61
- const config = await loadConfig();
62
- config.skills = config.skills || {};
63
- config.skills.enabled = config.skills.enabled || {};
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
- export async function listSkillEntries({ scope = 'all', cwd = process.cwd() } = {}) {
69
- const commands = await loadCommandsAndSkills(cwd);
70
- const config = await loadConfig();
71
- const entries = [];
72
- for (const command of commands.values()) {
73
- if (command.metadata?.type !== 'skill') continue;
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
- return entries.sort((a, b) => `${a.scope}:${a.name}`.localeCompare(`${b.scope}:${b.name}`));
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
- const dir = path.dirname(found.path);
101
- const manifestPath = path.join(dir, 'manifest.json');
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
- const skillPath = found.path || path.join(dir, 'SKILL.md');
115
- try {
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 runTarExtract(tgzPath, destDir) {
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 child = spawn('tar', ['-xzf', tgzPath, '-C', destDir], {
127
- stdio: ['ignore', 'pipe', 'pipe']
128
- });
129
- let stderr = '';
130
- child.stderr.on('data', (chunk) => {
131
- stderr += chunk.toString();
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
- async function readManifestSafe(skillRoot) {
145
- const p = path.join(skillRoot, 'manifest.json');
146
- try {
147
- const raw = await fs.readFile(p, 'utf8');
148
- return JSON.parse(raw);
149
- } catch {
150
- return null;
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
- if (srcStat.isFile() && absSrc.endsWith('.tgz')) {
159
- const tmp = await fs.mkdtemp(path.join(os.tmpdir(), 'codemini-skill-'));
160
- await runTarExtract(absSrc, tmp);
161
- const candidates = ['package', ...((await fs.readdir(tmp, { withFileTypes: true }))
162
- .filter((d) => d.isDirectory())
163
- .map((d) => d.name))];
164
- for (const c of candidates) {
165
- const dir = path.join(tmp, c);
166
- try {
167
- await fs.access(path.join(dir, 'SKILL.md'));
168
- return { dir, cleanupDir: tmp };
169
- } catch {
170
- continue;
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
- throw new Error('skill install supports <skill-dir>, <SKILL.md>, or <skill.tgz>');
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: sourcePath,
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 setEnabled(name, enabled, { cwd = process.cwd() } = {}) {
227
- const entries = await listSkillEntries({ scope: 'all', cwd });
228
- const found = entries.find((item) => item.name === name);
229
- if (!found) {
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 fs.access(entryPath);
261
- } catch {
262
- continue;
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
- if (scope === 'global') {
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 handleSkill(args) {
299
- const [sub, ...rest] = args;
300
- if (!sub) {
301
- usage();
302
- return;
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
- if (sub === 'list') {
306
- const { scope } = parseScopeArgs(rest, { defaultScope: 'all', allowAll: true });
307
- const entries = await listSkillEntries({ scope });
308
- if (entries.length === 0) {
309
- console.log('No installed skills');
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
- for (const item of entries) {
313
- const state = item.scope === 'builtin' ? 'builtin/default' : (item.enabled !== false ? 'enabled' : 'disabled');
314
- console.log(`${item.name}@${item.version || '0.0.0'} [${item.scope}] (${state})`);
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[0];
455
+ const sourcePath = positional.join(' ').trim();
322
456
  if (!sourcePath) {
323
457
  throw new Error('skill install requires <path>');
324
458
  }
325
- const installedName = await installSkill(sourcePath, { scope });
326
- console.log(`Installed skill: ${installedName} (${scope})`);
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 === 'inspect') {
341
- const { scope, rest: positional } = parseScopeArgs(rest, { defaultScope: 'all', allowAll: true });
342
- const name = positional[0];
343
- if (!name) {
344
- throw new Error('skill inspect requires <name>');
345
- }
346
- const meta = await readSkillMeta(name, { scope });
347
- if (!meta.exists) {
348
- throw new Error(`skill not found: ${name}`);
349
- }
350
- if (meta.manifest) {
351
- console.log(`Manifest: ${JSON.stringify(meta.manifest, null, 2)}\n`);
352
- }
353
- console.log(`Scope: ${meta.scope}\n`);
354
- console.log(`Path: ${meta.path}\n`);
355
- console.log(meta.preview);
356
- return;
357
- }
358
-
359
- if (sub === 'reindex') {
360
- const { scope } = parseScopeArgs(rest, { defaultScope: 'global' });
361
- const count = await reindexSkills({ scope });
362
- console.log(`Reindexed skills: ${count} (${scope})`);
363
- return;
364
- }
365
-
366
- usage();
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
+ }