hive-lite 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 +443 -0
- package/bin/hive.js +6 -0
- package/docs/cli-semantics.md +386 -0
- package/docs/skills/hive-lite-finish/SKILL.md +282 -0
- package/docs/skills/hive-lite-finish/agents/openai.yaml +4 -0
- package/docs/skills/hive-lite-finish/references/safety.md +95 -0
- package/docs/skills/hive-lite-finish/references/verdicts.md +123 -0
- package/docs/skills/hive-lite-map-maintainer/SKILL.md +203 -0
- package/docs/skills/hive-lite-map-maintainer/agents/openai.yaml +7 -0
- package/docs/skills/hive-lite-map-maintainer/references/lifecycle.md +114 -0
- package/docs/skills/hive-lite-map-maintainer/references/repair-rules.md +201 -0
- package/docs/skills/hive-lite-start-prompt/SKILL.md +283 -0
- package/docs/skills/hive-lite-start-prompt/agents/openai.yaml +4 -0
- package/docs/skills/hive-lite-start-prompt/references/input-calibration.md +82 -0
- package/docs/skills/hive-lite-start-prompt/references/preflight.md +116 -0
- package/package.json +40 -0
- package/src/cli.js +910 -0
- package/src/lib/change.js +642 -0
- package/src/lib/context.js +1104 -0
- package/src/lib/evidence.js +230 -0
- package/src/lib/fsx.js +54 -0
- package/src/lib/git.js +128 -0
- package/src/lib/glob.js +47 -0
- package/src/lib/health.js +1012 -0
- package/src/lib/id.js +13 -0
- package/src/lib/map.js +713 -0
- package/src/lib/next.js +341 -0
- package/src/lib/risk.js +122 -0
- package/src/lib/roles.js +109 -0
- package/src/lib/scope.js +168 -0
- package/src/lib/skills.js +349 -0
- package/src/lib/status.js +344 -0
- package/src/lib/yaml.js +223 -0
package/src/lib/scope.js
ADDED
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
const path = require('path');
|
|
2
|
+
const { tryGit } = require('./git');
|
|
3
|
+
const { matchesPattern } = require('./glob');
|
|
4
|
+
|
|
5
|
+
function normalizePath(value) {
|
|
6
|
+
return String(value || '').replace(/\\/g, '/').replace(/^\.\//, '');
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function patternFrom(value) {
|
|
10
|
+
if (!value) return '';
|
|
11
|
+
if (typeof value === 'string') return normalizePath(value.trim());
|
|
12
|
+
return normalizePath((value.pattern || value.path || '').trim());
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function normalizePattern(value, defaults = {}) {
|
|
16
|
+
const pattern = patternFrom(value);
|
|
17
|
+
if (!pattern) return null;
|
|
18
|
+
if (typeof value === 'string') {
|
|
19
|
+
return {
|
|
20
|
+
pattern,
|
|
21
|
+
reason: defaults.reason || '',
|
|
22
|
+
requiresReview: defaults.requiresReview === true,
|
|
23
|
+
source: defaults.source || 'map',
|
|
24
|
+
matchCount: defaults.matchCount == null ? null : defaults.matchCount,
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
return {
|
|
28
|
+
pattern,
|
|
29
|
+
reason: value.reason || defaults.reason || '',
|
|
30
|
+
requiresReview: value.requires_review === true || value.requiresReview === true || defaults.requiresReview === true,
|
|
31
|
+
source: value.source || defaults.source || 'map',
|
|
32
|
+
matchCount: value.matchCount == null ? (defaults.matchCount == null ? null : defaults.matchCount) : value.matchCount,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function normalizePatternList(values, defaults = {}) {
|
|
37
|
+
return (values || []).map((value) => normalizePattern(value, defaults)).filter(Boolean);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function trackedFiles(root) {
|
|
41
|
+
const output = tryGit(['ls-files'], { cwd: root });
|
|
42
|
+
return output.split(/\r?\n/).filter(Boolean).map(normalizePath);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function matchCount(root, pattern) {
|
|
46
|
+
const files = trackedFiles(root);
|
|
47
|
+
if (files.length === 0) return null;
|
|
48
|
+
return files.filter((file) => matchesPattern(file, pattern)).length;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function looksLikeFile(pattern) {
|
|
52
|
+
if (pattern.includes('*')) return false;
|
|
53
|
+
const base = path.basename(pattern);
|
|
54
|
+
return /\.[A-Za-z0-9]+$/.test(base);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function isBroadPattern(root, pattern) {
|
|
58
|
+
const normalized = normalizePath(pattern);
|
|
59
|
+
const count = matchCount(root, normalized);
|
|
60
|
+
if (count != null && count > 30) return { broad: true, matchCount: count, reason: `matches ${count} tracked files` };
|
|
61
|
+
if (looksLikeFile(normalized)) return { broad: false, matchCount: count, reason: '' };
|
|
62
|
+
if (normalized.includes('**')) return { broad: true, matchCount: count, reason: 'recursive glob' };
|
|
63
|
+
if (normalized.endsWith('/*')) return { broad: true, matchCount: count, reason: 'directory wildcard' };
|
|
64
|
+
if (!normalized.includes('*') && !looksLikeFile(normalized)) {
|
|
65
|
+
if (count == null || count > 8) return { broad: true, matchCount: count, reason: count == null ? 'directory scope' : `matches ${count} tracked files` };
|
|
66
|
+
}
|
|
67
|
+
if (count != null && count > 8) return { broad: true, matchCount: count, reason: `matches ${count} tracked files` };
|
|
68
|
+
return { broad: false, matchCount: count, reason: '' };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function splitDirectAndBroad(root, values, defaults = {}) {
|
|
72
|
+
const direct = [];
|
|
73
|
+
const broad = [];
|
|
74
|
+
for (const value of values || []) {
|
|
75
|
+
const pattern = patternFrom(value);
|
|
76
|
+
if (!pattern) continue;
|
|
77
|
+
const classification = isBroadPattern(root, pattern);
|
|
78
|
+
const item = normalizePattern(value, {
|
|
79
|
+
...defaults,
|
|
80
|
+
matchCount: classification.matchCount,
|
|
81
|
+
});
|
|
82
|
+
if (!item) continue;
|
|
83
|
+
if (classification.broad) {
|
|
84
|
+
broad.push({
|
|
85
|
+
...item,
|
|
86
|
+
reason: item.reason || classification.reason || 'broad writable scope',
|
|
87
|
+
requiresReview: true,
|
|
88
|
+
});
|
|
89
|
+
} else {
|
|
90
|
+
direct.push(item.pattern);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return { direct, broad };
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function normalizeAreaScope(root, area = {}) {
|
|
97
|
+
const configured = area.scope || {};
|
|
98
|
+
const readable = (configured.readable || area.readable_scope || []).map(patternFrom).filter(Boolean);
|
|
99
|
+
const forbidden = (configured.forbidden || area.do_not_touch || []).map(patternFrom).filter(Boolean);
|
|
100
|
+
|
|
101
|
+
const directValues = configured.writable_direct || [];
|
|
102
|
+
const conditionalValues = configured.writable_conditional || [];
|
|
103
|
+
const broadValues = configured.writable_broad_fallback || [];
|
|
104
|
+
const legacyWritable = !area.scope ? (area.writable_scope || []) : [];
|
|
105
|
+
|
|
106
|
+
const directSplit = splitDirectAndBroad(root, directValues, { source: 'writable_direct' });
|
|
107
|
+
const legacySplit = splitDirectAndBroad(root, legacyWritable, {
|
|
108
|
+
source: 'legacy_writable_scope',
|
|
109
|
+
reason: 'legacy writable_scope is broad; move this to scope.writable_broad_fallback or add writable_direct',
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
const writableDirect = [...directSplit.direct, ...legacySplit.direct];
|
|
113
|
+
const writableConditional = normalizePatternList(conditionalValues, {
|
|
114
|
+
source: 'writable_conditional',
|
|
115
|
+
requiresReview: true,
|
|
116
|
+
});
|
|
117
|
+
const writableBroadFallback = [
|
|
118
|
+
...directSplit.broad,
|
|
119
|
+
...normalizePatternList(broadValues, {
|
|
120
|
+
source: 'writable_broad_fallback',
|
|
121
|
+
requiresReview: true,
|
|
122
|
+
}).map((item) => {
|
|
123
|
+
const classification = isBroadPattern(root, item.pattern);
|
|
124
|
+
return {
|
|
125
|
+
...item,
|
|
126
|
+
matchCount: classification.matchCount,
|
|
127
|
+
reason: item.reason || classification.reason || 'broad fallback scope',
|
|
128
|
+
requiresReview: true,
|
|
129
|
+
};
|
|
130
|
+
}),
|
|
131
|
+
...legacySplit.broad,
|
|
132
|
+
];
|
|
133
|
+
|
|
134
|
+
let quality = 'unknown';
|
|
135
|
+
if (writableBroadFallback.length > 0 && writableDirect.length === 0) quality = 'broad';
|
|
136
|
+
else if (writableConditional.length > 0 && writableDirect.length === 0) quality = 'conditional';
|
|
137
|
+
else if (writableDirect.length > 0 && (writableConditional.length > 0 || writableBroadFallback.length > 0)) quality = 'mixed';
|
|
138
|
+
else if (writableDirect.length > 0) quality = 'narrow';
|
|
139
|
+
|
|
140
|
+
return {
|
|
141
|
+
readable,
|
|
142
|
+
writableDirect,
|
|
143
|
+
writableConditional,
|
|
144
|
+
writableBroadFallback,
|
|
145
|
+
forbidden,
|
|
146
|
+
quality,
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function itemPattern(item) {
|
|
151
|
+
return typeof item === 'string' ? item : item.pattern;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function patternDisplay(item) {
|
|
155
|
+
const pattern = itemPattern(item);
|
|
156
|
+
if (!pattern) return '';
|
|
157
|
+
if (typeof item === 'string') return pattern;
|
|
158
|
+
const count = item.matchCount == null ? '' : `, matches ${item.matchCount}`;
|
|
159
|
+
const reason = item.reason ? ` (${item.reason}${count})` : (count ? ` (${count.trim().replace(/^, /, '')})` : '');
|
|
160
|
+
return `${pattern}${reason}`;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
module.exports = {
|
|
164
|
+
itemPattern,
|
|
165
|
+
normalizeAreaScope,
|
|
166
|
+
normalizePatternList,
|
|
167
|
+
patternDisplay,
|
|
168
|
+
};
|
|
@@ -0,0 +1,349 @@
|
|
|
1
|
+
const crypto = require('crypto');
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
const os = require('os');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
|
|
6
|
+
const SKILL_NAMES = [
|
|
7
|
+
'hive-lite-start-prompt',
|
|
8
|
+
'hive-lite-finish',
|
|
9
|
+
'hive-lite-map-maintainer',
|
|
10
|
+
];
|
|
11
|
+
|
|
12
|
+
const AGENTS = {
|
|
13
|
+
codex: {
|
|
14
|
+
label: 'Codex global user skills',
|
|
15
|
+
path: '~/.codex/skills',
|
|
16
|
+
scope: 'global_user',
|
|
17
|
+
note: 'Codex installs use the global user skills directory, not repo-local .codex/skills. Use --path .codex/skills only when you intentionally want a custom repo-local copy.',
|
|
18
|
+
},
|
|
19
|
+
claude: {
|
|
20
|
+
label: 'Claude global user skills',
|
|
21
|
+
path: '~/.claude/skills',
|
|
22
|
+
scope: 'global_user',
|
|
23
|
+
note: 'Claude installs use the global user skills directory.',
|
|
24
|
+
},
|
|
25
|
+
gemini: {
|
|
26
|
+
label: 'Gemini global user skills',
|
|
27
|
+
path: '~/.gemini/skills',
|
|
28
|
+
scope: 'global_user',
|
|
29
|
+
note: 'Gemini installs use the global user skills directory, not repo-local .gemini/skills. Use --path .gemini/skills only when you intentionally want a custom repo-local copy.',
|
|
30
|
+
},
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
function packageRoot() {
|
|
34
|
+
return path.resolve(__dirname, '..', '..');
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function sourceSkillsDir() {
|
|
38
|
+
return path.join(packageRoot(), 'docs', 'skills');
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function expandPath(value, cwd) {
|
|
42
|
+
const text = String(value || '');
|
|
43
|
+
if (text === '~') return os.homedir();
|
|
44
|
+
if (text.startsWith('~/')) return path.join(os.homedir(), text.slice(2));
|
|
45
|
+
if (path.isAbsolute(text)) return text;
|
|
46
|
+
return path.resolve(cwd, text);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function relPath(root, file) {
|
|
50
|
+
return path.relative(root, file).replace(/\\/g, '/');
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function sourceSkillPath(name) {
|
|
54
|
+
return path.join(sourceSkillsDir(), name);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function listFiles(dir, base = dir) {
|
|
58
|
+
if (!fs.existsSync(dir)) return [];
|
|
59
|
+
const result = [];
|
|
60
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
61
|
+
if (entry.name === '.DS_Store') continue;
|
|
62
|
+
const full = path.join(dir, entry.name);
|
|
63
|
+
if (entry.isDirectory()) result.push(...listFiles(full, base));
|
|
64
|
+
else if (entry.isFile()) result.push(relPath(base, full));
|
|
65
|
+
}
|
|
66
|
+
return result.sort();
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function dirDigest(dir) {
|
|
70
|
+
if (!fs.existsSync(dir)) return null;
|
|
71
|
+
const hash = crypto.createHash('sha256');
|
|
72
|
+
for (const file of listFiles(dir)) {
|
|
73
|
+
hash.update(file);
|
|
74
|
+
hash.update('\0');
|
|
75
|
+
hash.update(fs.readFileSync(path.join(dir, file)));
|
|
76
|
+
hash.update('\0');
|
|
77
|
+
}
|
|
78
|
+
return `sha256:${hash.digest('hex')}`;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function copyDir(src, dest) {
|
|
82
|
+
fs.mkdirSync(dest, { recursive: true });
|
|
83
|
+
for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
|
|
84
|
+
if (entry.name === '.DS_Store') continue;
|
|
85
|
+
const from = path.join(src, entry.name);
|
|
86
|
+
const to = path.join(dest, entry.name);
|
|
87
|
+
if (entry.isDirectory()) {
|
|
88
|
+
copyDir(from, to);
|
|
89
|
+
} else if (entry.isFile()) {
|
|
90
|
+
fs.mkdirSync(path.dirname(to), { recursive: true });
|
|
91
|
+
fs.copyFileSync(from, to);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function validateSources() {
|
|
97
|
+
const missing = SKILL_NAMES.filter((name) => !fs.existsSync(path.join(sourceSkillsDir(), name, 'SKILL.md')));
|
|
98
|
+
if (missing.length > 0) throw new Error(`missing bundled Hive Lite skill sources: ${missing.join(', ')}`);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function targetSpecs(cwd, options = {}, command = 'doctor') {
|
|
102
|
+
if (options.path && options.path !== true) {
|
|
103
|
+
return [{
|
|
104
|
+
agent: 'custom',
|
|
105
|
+
label: 'Custom skills path',
|
|
106
|
+
root: expandPath(options.path, cwd),
|
|
107
|
+
configuredPath: options.path,
|
|
108
|
+
scope: 'custom',
|
|
109
|
+
note: 'Custom --path targets are copied exactly where requested.',
|
|
110
|
+
}];
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
let agents = [];
|
|
114
|
+
if (options.agent && options.agent !== true) {
|
|
115
|
+
agents = String(options.agent).split(',').map((item) => item.trim()).filter(Boolean);
|
|
116
|
+
} else if (options.all) {
|
|
117
|
+
agents = ['codex', 'claude', 'gemini'];
|
|
118
|
+
} else if (command === 'doctor') {
|
|
119
|
+
agents = ['codex', 'claude', 'gemini'];
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (agents.includes('all')) agents = ['codex', 'claude', 'gemini'];
|
|
123
|
+
|
|
124
|
+
if (agents.length === 0) {
|
|
125
|
+
throw new Error(`${command} requires --agent codex|claude|gemini|all or --path <skills-dir>`);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return agents.map((agent) => {
|
|
129
|
+
const config = AGENTS[agent];
|
|
130
|
+
if (!config) throw new Error(`unknown skill agent: ${agent}`);
|
|
131
|
+
return {
|
|
132
|
+
agent,
|
|
133
|
+
label: config.label,
|
|
134
|
+
root: expandPath(config.path, cwd),
|
|
135
|
+
configuredPath: config.path,
|
|
136
|
+
scope: config.scope,
|
|
137
|
+
note: config.note,
|
|
138
|
+
};
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function inspectTarget(spec) {
|
|
143
|
+
return {
|
|
144
|
+
agent: spec.agent,
|
|
145
|
+
label: spec.label,
|
|
146
|
+
path: spec.root,
|
|
147
|
+
configuredPath: spec.configuredPath,
|
|
148
|
+
scope: spec.scope || 'unknown',
|
|
149
|
+
note: spec.note || '',
|
|
150
|
+
exists: fs.existsSync(spec.root),
|
|
151
|
+
skills: SKILL_NAMES.map((name) => {
|
|
152
|
+
const source = sourceSkillPath(name);
|
|
153
|
+
const target = path.join(spec.root, name);
|
|
154
|
+
const sourceDigest = dirDigest(source);
|
|
155
|
+
const targetDigest = dirDigest(target);
|
|
156
|
+
let status = 'missing';
|
|
157
|
+
if (targetDigest && targetDigest === sourceDigest) status = 'current';
|
|
158
|
+
else if (targetDigest) status = 'stale';
|
|
159
|
+
return {
|
|
160
|
+
name,
|
|
161
|
+
status,
|
|
162
|
+
sourcePath: source,
|
|
163
|
+
targetPath: target,
|
|
164
|
+
sourceDigest,
|
|
165
|
+
targetDigest,
|
|
166
|
+
};
|
|
167
|
+
}),
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function skillsDoctor(cwd, options = {}) {
|
|
172
|
+
validateSources();
|
|
173
|
+
const specs = targetSpecs(cwd, options, 'doctor');
|
|
174
|
+
const targets = specs.map(inspectTarget);
|
|
175
|
+
return {
|
|
176
|
+
version: 1,
|
|
177
|
+
command: 'skills doctor',
|
|
178
|
+
generatedAt: new Date().toISOString(),
|
|
179
|
+
sourceDir: sourceSkillsDir(),
|
|
180
|
+
targetSelection: targetSelection(options, 'doctor'),
|
|
181
|
+
targets,
|
|
182
|
+
summary: summarizeTargets(targets),
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function targetSelection(options = {}, command = 'doctor') {
|
|
187
|
+
if (options.path && options.path !== true) {
|
|
188
|
+
return {
|
|
189
|
+
mode: 'custom_path',
|
|
190
|
+
detectsRunningAgentCli: false,
|
|
191
|
+
note: 'Hive Lite checks the requested skills directory. It does not detect which agent CLI is running.',
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
if (options.agent && options.agent !== true) {
|
|
195
|
+
return {
|
|
196
|
+
mode: 'explicit_agent',
|
|
197
|
+
detectsRunningAgentCli: false,
|
|
198
|
+
note: 'Hive Lite checks the explicitly requested target path(s). It does not detect which agent CLI is running.',
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
if (options.all) {
|
|
202
|
+
return {
|
|
203
|
+
mode: 'all_known_targets',
|
|
204
|
+
detectsRunningAgentCli: false,
|
|
205
|
+
note: 'Hive Lite checks all known target paths. It does not detect which agent CLI is running.',
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
return {
|
|
209
|
+
mode: command === 'doctor' ? 'default_known_targets' : 'none',
|
|
210
|
+
detectsRunningAgentCli: false,
|
|
211
|
+
note: command === 'doctor'
|
|
212
|
+
? 'By default, skills doctor checks all known target paths. It does not detect installed or currently running agent CLIs.'
|
|
213
|
+
: 'Hive Lite does not detect which agent CLI is running.',
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function installCommandForOptions(options = {}) {
|
|
218
|
+
if (options.path && options.path !== true) return `hive-lite skills install --path ${options.path}`;
|
|
219
|
+
if (options.agent && options.agent !== true) return `hive-lite skills install --agent ${options.agent}`;
|
|
220
|
+
if (options.all) return 'hive-lite skills install --agent all';
|
|
221
|
+
return 'hive-lite skills install --agent <codex|claude|gemini>';
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function skillPreflight(cwd, skillName, options = {}) {
|
|
225
|
+
if (!skillName) return null;
|
|
226
|
+
if (!options.agent && !options.path && !options.all) return null;
|
|
227
|
+
validateSources();
|
|
228
|
+
const specs = targetSpecs(cwd, options, 'doctor');
|
|
229
|
+
const targets = specs.map((spec) => {
|
|
230
|
+
const target = inspectTarget(spec);
|
|
231
|
+
const skill = target.skills.find((item) => item.name === skillName);
|
|
232
|
+
return {
|
|
233
|
+
agent: target.agent,
|
|
234
|
+
label: target.label,
|
|
235
|
+
path: target.path,
|
|
236
|
+
configuredPath: target.configuredPath,
|
|
237
|
+
scope: target.scope,
|
|
238
|
+
note: target.note,
|
|
239
|
+
exists: target.exists,
|
|
240
|
+
status: skill ? skill.status : 'missing',
|
|
241
|
+
targetPath: skill ? skill.targetPath : null,
|
|
242
|
+
};
|
|
243
|
+
});
|
|
244
|
+
return {
|
|
245
|
+
requiredSkill: skillName,
|
|
246
|
+
targetSelection: targetSelection(options, 'doctor'),
|
|
247
|
+
ready: targets.length > 0 && targets.every((target) => target.status === 'current'),
|
|
248
|
+
installCommand: installCommandForOptions(options),
|
|
249
|
+
targets,
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function summarizeTargets(targets) {
|
|
254
|
+
const counts = { current: 0, stale: 0, missing: 0 };
|
|
255
|
+
for (const target of targets) {
|
|
256
|
+
for (const skill of target.skills) counts[skill.status] += 1;
|
|
257
|
+
}
|
|
258
|
+
return counts;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function installIntoTarget(spec, options) {
|
|
262
|
+
const before = inspectTarget(spec);
|
|
263
|
+
const dryRun = Boolean(options.dryRun);
|
|
264
|
+
const overwrite = Boolean(options.overwrite);
|
|
265
|
+
const installed = [];
|
|
266
|
+
const updated = [];
|
|
267
|
+
const unchanged = [];
|
|
268
|
+
const skipped = [];
|
|
269
|
+
|
|
270
|
+
for (const skill of before.skills) {
|
|
271
|
+
const source = sourceSkillPath(skill.name);
|
|
272
|
+
const target = path.join(spec.root, skill.name);
|
|
273
|
+
if (skill.status === 'current') {
|
|
274
|
+
unchanged.push(skill.name);
|
|
275
|
+
continue;
|
|
276
|
+
}
|
|
277
|
+
if (skill.status === 'stale' && !overwrite) {
|
|
278
|
+
skipped.push({
|
|
279
|
+
name: skill.name,
|
|
280
|
+
reason: 'target exists but differs; use skills sync or --force to overwrite',
|
|
281
|
+
});
|
|
282
|
+
continue;
|
|
283
|
+
}
|
|
284
|
+
if (!dryRun) {
|
|
285
|
+
if (overwrite && fs.existsSync(target)) fs.rmSync(target, { recursive: true, force: true });
|
|
286
|
+
copyDir(source, target);
|
|
287
|
+
}
|
|
288
|
+
if (skill.status === 'missing') installed.push(skill.name);
|
|
289
|
+
else updated.push(skill.name);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
const after = dryRun ? before : inspectTarget(spec);
|
|
293
|
+
return {
|
|
294
|
+
agent: spec.agent,
|
|
295
|
+
label: spec.label,
|
|
296
|
+
path: spec.root,
|
|
297
|
+
configuredPath: spec.configuredPath,
|
|
298
|
+
scope: spec.scope || 'unknown',
|
|
299
|
+
note: spec.note || '',
|
|
300
|
+
dryRun,
|
|
301
|
+
installed,
|
|
302
|
+
updated,
|
|
303
|
+
unchanged,
|
|
304
|
+
skipped,
|
|
305
|
+
before,
|
|
306
|
+
after,
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
function installSkills(cwd, options = {}) {
|
|
311
|
+
validateSources();
|
|
312
|
+
const specs = targetSpecs(cwd, options, 'install');
|
|
313
|
+
const results = specs.map((spec) => installIntoTarget(spec, {
|
|
314
|
+
dryRun: options.dryRun,
|
|
315
|
+
overwrite: options.force,
|
|
316
|
+
}));
|
|
317
|
+
return {
|
|
318
|
+
version: 1,
|
|
319
|
+
command: 'skills install',
|
|
320
|
+
generatedAt: new Date().toISOString(),
|
|
321
|
+
sourceDir: sourceSkillsDir(),
|
|
322
|
+
results,
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
function syncSkills(cwd, options = {}) {
|
|
327
|
+
validateSources();
|
|
328
|
+
const specs = targetSpecs(cwd, options, 'sync');
|
|
329
|
+
const results = specs.map((spec) => installIntoTarget(spec, {
|
|
330
|
+
dryRun: options.dryRun,
|
|
331
|
+
overwrite: true,
|
|
332
|
+
}));
|
|
333
|
+
return {
|
|
334
|
+
version: 1,
|
|
335
|
+
command: 'skills sync',
|
|
336
|
+
generatedAt: new Date().toISOString(),
|
|
337
|
+
sourceDir: sourceSkillsDir(),
|
|
338
|
+
results,
|
|
339
|
+
};
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
module.exports = {
|
|
343
|
+
AGENTS,
|
|
344
|
+
SKILL_NAMES,
|
|
345
|
+
installSkills,
|
|
346
|
+
skillPreflight,
|
|
347
|
+
skillsDoctor,
|
|
348
|
+
syncSkills,
|
|
349
|
+
};
|