showdar-skills 0.2.1 → 0.2.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/showdar.js +10 -0
- package/package.json +1 -1
- package/skills/showdar-review/scripts/collect-diff.mjs +9 -2
- package/src/path-safety.js +39 -0
- package/src/project.js +190 -42
package/bin/showdar.js
CHANGED
|
@@ -49,6 +49,10 @@ async function main() {
|
|
|
49
49
|
const version = await packageVersion();
|
|
50
50
|
|
|
51
51
|
if (command === 'help' || command === '--help' || command === '-h') return printHelp(version);
|
|
52
|
+
if (command === '--version' || command === '-V') {
|
|
53
|
+
console.log(version);
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
52
56
|
if (args.includes('--help') || args.includes('-h')) return printHelp(version, command);
|
|
53
57
|
|
|
54
58
|
const scope = ['init', 'status', 'doctor', 'remove'].includes(command) ? scopeAfter(args) : null;
|
|
@@ -83,6 +87,10 @@ async function main() {
|
|
|
83
87
|
? await initGlobal({ homeRoot: homedir(), packageRoot, profile, ai, skillIds, commandNames, packageVersion: version })
|
|
84
88
|
: await initProject({ projectRoot, packageRoot, profile, ai, skillIds, commandNames, packageVersion: version });
|
|
85
89
|
console.log(`Showdar Skills installed.\nScope: ${scope}\nProfile: ${profile}\nAI: ${ai}\nTargets: ${result.targets.join(', ')}\nSkills: ${result.skills}\nOpenCode commands: ${result.commands}`);
|
|
90
|
+
if (scope === 'project') {
|
|
91
|
+
console.log(`Requested: ${result.requestedSkills}\nInstalled in project: ${result.installedSkills}\nSatisfied by global: ${result.satisfiedByGlobal}\nSkipped duplicate copies: ${result.skippedDuplicates}`);
|
|
92
|
+
}
|
|
93
|
+
for (const warning of result.warnings ?? []) console.log(`warning: ${warning}`);
|
|
86
94
|
if (scope === 'global') console.log(`Manifest: ${globalManifestPath()}`);
|
|
87
95
|
if (result.targets.includes('codex')) console.log('Codex: invoke skills directly with $showdar-<name> or let native skill discovery route by description.');
|
|
88
96
|
if (result.targets.includes('opencode')) console.log('OpenCode: use native skill discovery or /showdar/<command>.');
|
|
@@ -97,7 +105,9 @@ async function main() {
|
|
|
97
105
|
return;
|
|
98
106
|
}
|
|
99
107
|
console.log(`Showdar Skills\nScope: ${result.scope}\nProfile: ${result.profile}\nAI: ${result.ai}\nTargets: ${result.targets.join(', ')}\nSkills: ${result.skills}\nCommands: ${result.commands}\nHealth: ${result.healthy ? 'OK' : 'BROKEN'}`);
|
|
108
|
+
if (scope === 'project') console.log(`Requested: ${result.requestedSkills}\nInstalled in project: ${result.installedSkills}\nSatisfied by global: ${result.satisfiedByGlobal}`);
|
|
100
109
|
for (const issue of result.issues) console.log(`- ${issue}`);
|
|
110
|
+
for (const warning of result.warnings ?? []) console.log(`warning: ${warning}`);
|
|
101
111
|
if (command === 'doctor' && !result.healthy) process.exitCode = 1;
|
|
102
112
|
return;
|
|
103
113
|
}
|
package/package.json
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { spawnSync } from 'node:child_process';
|
|
3
3
|
const cwd=process.argv[2]??process.cwd(); const base=process.argv[3]??'HEAD';
|
|
4
|
-
const run=
|
|
5
|
-
|
|
4
|
+
const run=(args)=>{const r=spawnSync('git',args,{cwd,encoding:'utf8',shell:false});if(r.error)throw r.error;if(r.status!==0)throw new Error((r.stderr||r.stdout).trim()||`git ${args.join(' ')} failed`);return r.stdout.trim()};
|
|
5
|
+
try {
|
|
6
|
+
if (base.startsWith('-')) throw new Error('revision must not begin with "-"');
|
|
7
|
+
const resolved=run(['rev-parse','--verify',`${base}^{commit}`]);
|
|
8
|
+
console.log(JSON.stringify({base,status:run(['status','--short']),files:run(['diff','--no-ext-diff','--name-only',resolved])?.split('\n').filter(Boolean)??[],stat:run(['diff','--no-ext-diff','--stat',resolved]),diff:run(['diff','--no-ext-diff','--unified=3',resolved])},null,2));
|
|
9
|
+
} catch (error) {
|
|
10
|
+
console.error(`collect-diff: ${error.message}`);
|
|
11
|
+
process.exitCode=1;
|
|
12
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { lstat } from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
|
|
4
|
+
export function safeOwnedPath(baseRoot, relative, allowedRoots = []) {
|
|
5
|
+
if (typeof relative !== 'string' || !relative) return null;
|
|
6
|
+
const roots = [baseRoot, ...allowedRoots].map((root) => path.resolve(root));
|
|
7
|
+
const target = path.resolve(relative.startsWith(path.sep) ? relative : path.join(roots[0], relative));
|
|
8
|
+
return roots.some((root) => target === root || target.startsWith(`${root}${path.sep}`)) ? target : null;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export async function lstatWithoutSymlink(target) {
|
|
12
|
+
const info = await lstat(target);
|
|
13
|
+
if (info.isSymbolicLink()) throw new Error(`Refusing managed path through symlink: ${target}`);
|
|
14
|
+
return info;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export async function assertSafeManagedPath(baseRoot, target, allowedRoots = []) {
|
|
18
|
+
const root = path.resolve(baseRoot);
|
|
19
|
+
const resolved = safeOwnedPath(root, target, allowedRoots);
|
|
20
|
+
if (!resolved) throw new Error(`Invalid managed path: ${target}`);
|
|
21
|
+
|
|
22
|
+
try {
|
|
23
|
+
await lstatWithoutSymlink(root);
|
|
24
|
+
} catch (error) {
|
|
25
|
+
if (error?.code === 'ENOENT') return resolved;
|
|
26
|
+
throw error;
|
|
27
|
+
}
|
|
28
|
+
let current = root;
|
|
29
|
+
for (const component of path.relative(root, resolved).split(path.sep).filter(Boolean)) {
|
|
30
|
+
current = path.join(current, component);
|
|
31
|
+
try {
|
|
32
|
+
await lstatWithoutSymlink(current);
|
|
33
|
+
} catch (error) {
|
|
34
|
+
if (error?.code === 'ENOENT') break;
|
|
35
|
+
throw error;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return resolved;
|
|
39
|
+
}
|
package/src/project.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import { access, cp, mkdir, readFile, readdir, rename, rm,
|
|
1
|
+
import { access, cp, mkdir, readFile, readdir, rename, rm, writeFile } from 'node:fs/promises';
|
|
2
2
|
import { createHash } from 'node:crypto';
|
|
3
3
|
import { homedir } from 'node:os';
|
|
4
4
|
import path from 'node:path';
|
|
5
5
|
import { globalCommandRootFor, globalSkillRootFor, NATIVE_TARGETS, opencodeCommandRoot, resolveTargets, skillRootFor } from './adapters.js';
|
|
6
|
+
import { assertSafeManagedPath, lstatWithoutSymlink, safeOwnedPath } from './path-safety.js';
|
|
6
7
|
|
|
7
8
|
const PROJECT_MANIFEST = '.showdar.json';
|
|
8
9
|
const START = '<!-- showdar-skills:start -->';
|
|
@@ -19,7 +20,7 @@ async function exists(target) {
|
|
|
19
20
|
async function hashTree(target) {
|
|
20
21
|
const h = createHash('sha256');
|
|
21
22
|
async function walk(current, relative = '') {
|
|
22
|
-
const info = await
|
|
23
|
+
const info = await lstatWithoutSymlink(current);
|
|
23
24
|
if (info.isDirectory()) {
|
|
24
25
|
const entries = await readdir(current, { withFileTypes: true });
|
|
25
26
|
entries.sort((a, b) => a.name.localeCompare(b.name));
|
|
@@ -35,15 +36,20 @@ async function hashTree(target) {
|
|
|
35
36
|
return h.digest('hex');
|
|
36
37
|
}
|
|
37
38
|
|
|
38
|
-
async function readManifest(manifestPath) {
|
|
39
|
+
async function readManifest(manifestPath, baseRoot) {
|
|
40
|
+
await assertSafeManagedPath(baseRoot, manifestPath);
|
|
39
41
|
if (!(await exists(manifestPath))) return null;
|
|
40
42
|
try { return JSON.parse(await readFile(manifestPath, 'utf8')); }
|
|
41
43
|
catch (error) { throw new Error(`Invalid Showdar manifest: ${error.message}`); }
|
|
42
44
|
}
|
|
43
45
|
|
|
44
46
|
async function writeJsonAtomic(target, value) {
|
|
47
|
+
await writeTextAtomic(target, `${JSON.stringify(value, null, 2)}\n`);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async function writeTextAtomic(target, value) {
|
|
45
51
|
const tmp = `${target}.tmp-${process.pid}-${Date.now()}`;
|
|
46
|
-
await writeFile(tmp,
|
|
52
|
+
await writeFile(tmp, value, { flag: 'wx' });
|
|
47
53
|
await rename(tmp, target);
|
|
48
54
|
}
|
|
49
55
|
|
|
@@ -81,18 +87,20 @@ function stripManagedBlock(content) {
|
|
|
81
87
|
|
|
82
88
|
async function writeAgentsBlock(projectRoot, skillIds) {
|
|
83
89
|
const target = path.join(projectRoot, 'AGENTS.md');
|
|
90
|
+
await assertSafeManagedPath(projectRoot, target);
|
|
84
91
|
const current = (await exists(target)) ? await readFile(target, 'utf8') : '';
|
|
85
92
|
const clean = stripManagedBlock(current).trimEnd();
|
|
86
93
|
const block = managedBlock(skillIds);
|
|
87
|
-
await
|
|
94
|
+
await writeTextAtomic(target, clean ? `${clean}\n\n${block}\n` : `${block}\n`);
|
|
88
95
|
}
|
|
89
96
|
|
|
90
97
|
async function removeAgentsBlock(projectRoot) {
|
|
91
98
|
const target = path.join(projectRoot, 'AGENTS.md');
|
|
99
|
+
await assertSafeManagedPath(projectRoot, target);
|
|
92
100
|
if (!(await exists(target))) return;
|
|
93
101
|
const current = await readFile(target, 'utf8');
|
|
94
102
|
const clean = stripManagedBlock(current).trim();
|
|
95
|
-
if (clean) await
|
|
103
|
+
if (clean) await writeTextAtomic(target, `${clean}\n`);
|
|
96
104
|
else await rm(target, { force: true });
|
|
97
105
|
}
|
|
98
106
|
|
|
@@ -100,12 +108,13 @@ function ownedPathSet(manifest) {
|
|
|
100
108
|
return new Set((manifest?.files ?? []).map((entry) => entry.path));
|
|
101
109
|
}
|
|
102
110
|
|
|
103
|
-
function
|
|
104
|
-
|
|
105
|
-
const
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
111
|
+
function uniqueRoots(targets, resolveRoot) {
|
|
112
|
+
const roots = new Map();
|
|
113
|
+
for (const target of targets) {
|
|
114
|
+
const root = resolveRoot(target);
|
|
115
|
+
if (!roots.has(path.resolve(root))) roots.set(path.resolve(root), { root, target });
|
|
116
|
+
}
|
|
117
|
+
return [...roots.values()];
|
|
109
118
|
}
|
|
110
119
|
|
|
111
120
|
function manifestPathFor(baseRoot, destination) {
|
|
@@ -113,7 +122,8 @@ function manifestPathFor(baseRoot, destination) {
|
|
|
113
122
|
return relative.replaceAll(path.sep, '/');
|
|
114
123
|
}
|
|
115
124
|
|
|
116
|
-
async function copyOwned({ baseRoot, source, destination, priorOwned, newFiles }) {
|
|
125
|
+
async function copyOwned({ baseRoot, source, destination, priorOwned, newFiles, managedRoots = [] }) {
|
|
126
|
+
await assertSafeManagedPath(baseRoot, destination, managedRoots);
|
|
117
127
|
const relative = manifestPathFor(baseRoot, destination);
|
|
118
128
|
if ((await exists(destination)) && !priorOwned.has(relative)) {
|
|
119
129
|
throw new Error(`Refusing to overwrite existing non-Showdar-managed skill or command: ${destination}`);
|
|
@@ -124,37 +134,75 @@ async function copyOwned({ baseRoot, source, destination, priorOwned, newFiles }
|
|
|
124
134
|
newFiles.push({ path: relative, hash: await hashTree(destination) });
|
|
125
135
|
}
|
|
126
136
|
|
|
127
|
-
async function initInstallation({ baseRoot, manifestPath, agentsRoot, packageRoot, profile, ai, skillIds, commandNames = [], packageVersion = '0.2.0', scope, skillRootForTarget, commandRoot, managedRoots = [] }) {
|
|
137
|
+
async function initInstallation({ baseRoot, manifestPath, agentsRoot, packageRoot, profile, ai, skillIds, commandNames = [], packageVersion = '0.2.0', scope, skillRootForTarget, commandRoot, managedRoots = [], homeRoot = homedir(), globalSkillRootForTarget = null }) {
|
|
128
138
|
await mkdir(baseRoot, { recursive: true });
|
|
139
|
+
await assertSafeManagedPath(baseRoot, baseRoot);
|
|
140
|
+
await assertSafeManagedPath(baseRoot, manifestPath);
|
|
141
|
+
if (agentsRoot) await assertSafeManagedPath(agentsRoot, path.join(agentsRoot, 'AGENTS.md'));
|
|
129
142
|
const targets = resolveTargets(ai);
|
|
130
|
-
const prior = await readManifest(manifestPath);
|
|
143
|
+
const prior = await readManifest(manifestPath, baseRoot);
|
|
131
144
|
const priorOwned = ownedPathSet(prior);
|
|
132
145
|
const desiredPaths = new Set();
|
|
133
146
|
|
|
134
|
-
const skillRoots =
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
147
|
+
const skillRoots = uniqueRoots(targets, skillRootForTarget);
|
|
148
|
+
const globalManifest = scope === 'project' && globalSkillRootForTarget
|
|
149
|
+
? await readManifest(globalManifestPath(homeRoot), homeRoot)
|
|
150
|
+
: null;
|
|
151
|
+
const globalOwned = ownedPathSet(globalManifest);
|
|
152
|
+
const globalSatisfaction = [];
|
|
153
|
+
const installedSkillIds = new Set();
|
|
154
|
+
const globallySatisfiedSkillIds = new Set();
|
|
155
|
+
let skippedDuplicates = 0;
|
|
156
|
+
const skillDestinations = [];
|
|
157
|
+
|
|
158
|
+
for (const skillId of skillIds) {
|
|
159
|
+
if (!(await exists(path.join(packageRoot, 'skills', skillId, 'SKILL.md')))) throw new Error(`Skill asset not found: ${skillId}`);
|
|
138
160
|
}
|
|
139
|
-
|
|
140
|
-
|
|
161
|
+
|
|
162
|
+
for (const { root, target } of skillRoots) {
|
|
163
|
+
for (const skillId of skillIds) {
|
|
164
|
+
const destination = path.join(root, skillId);
|
|
165
|
+
await assertSafeManagedPath(baseRoot, destination, managedRoots);
|
|
166
|
+
const relative = manifestPathFor(baseRoot, destination);
|
|
167
|
+
const destinationExists = await exists(destination);
|
|
168
|
+
const globalPath = globalSkillRootForTarget ? path.join(globalSkillRootForTarget(target), skillId) : null;
|
|
169
|
+
if (globalPath) await assertSafeManagedPath(homeRoot, globalPath);
|
|
170
|
+
const globalRelative = globalPath ? manifestPathFor(homeRoot, globalPath) : null;
|
|
171
|
+
const globalAvailable = Boolean(globalPath && globalOwned.has(globalRelative) && await exists(globalPath));
|
|
172
|
+
if (scope === 'project' && globalAvailable && !destinationExists) {
|
|
173
|
+
skippedDuplicates += 1;
|
|
174
|
+
globallySatisfiedSkillIds.add(skillId);
|
|
175
|
+
globalSatisfaction.push({ skill: skillId, target, path: globalRelative });
|
|
176
|
+
} else {
|
|
177
|
+
desiredPaths.add(relative);
|
|
178
|
+
skillDestinations.push({ destination, skillId });
|
|
179
|
+
}
|
|
180
|
+
}
|
|
141
181
|
}
|
|
142
182
|
if (targets.includes('opencode')) {
|
|
143
|
-
|
|
183
|
+
const root = commandRoot();
|
|
184
|
+
for (const name of commandNames) {
|
|
185
|
+
const destination = path.join(root, `${name}.md`);
|
|
186
|
+
await assertSafeManagedPath(baseRoot, destination, managedRoots);
|
|
187
|
+
desiredPaths.add(manifestPathFor(baseRoot, destination));
|
|
188
|
+
}
|
|
144
189
|
}
|
|
145
190
|
|
|
191
|
+
const staleTargets = [];
|
|
146
192
|
for (const entry of prior?.files ?? []) {
|
|
147
193
|
const target = safeOwnedPath(baseRoot, entry.path, managedRoots);
|
|
148
|
-
if (target && !desiredPaths.has(entry.path))
|
|
194
|
+
if (target && !desiredPaths.has(entry.path)) {
|
|
195
|
+
await assertSafeManagedPath(baseRoot, target, managedRoots);
|
|
196
|
+
staleTargets.push(target);
|
|
197
|
+
}
|
|
149
198
|
}
|
|
199
|
+
for (const target of staleTargets) await rm(target, { recursive: true, force: true });
|
|
150
200
|
|
|
151
201
|
const files = [];
|
|
152
|
-
for (const
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
await copyOwned({ baseRoot, source, destination: path.join(root, skillId), priorOwned, newFiles: files });
|
|
157
|
-
}
|
|
202
|
+
for (const { destination, skillId } of skillDestinations) {
|
|
203
|
+
const source = path.join(packageRoot, 'skills', skillId);
|
|
204
|
+
await copyOwned({ baseRoot, source, destination, priorOwned, newFiles: files, managedRoots });
|
|
205
|
+
installedSkillIds.add(skillId);
|
|
158
206
|
}
|
|
159
207
|
|
|
160
208
|
if (targets.includes('opencode')) {
|
|
@@ -162,7 +210,7 @@ async function initInstallation({ baseRoot, manifestPath, agentsRoot, packageRoo
|
|
|
162
210
|
for (const name of commandNames) {
|
|
163
211
|
const source = path.join(packageRoot, 'commands', 'opencode', 'showdar', `${name}.md`);
|
|
164
212
|
if (!(await exists(source))) throw new Error(`OpenCode command asset not found: ${name}`);
|
|
165
|
-
await copyOwned({ baseRoot, source, destination: path.join(root, `${name}.md`), priorOwned, newFiles: files });
|
|
213
|
+
await copyOwned({ baseRoot, source, destination: path.join(root, `${name}.md`), priorOwned, newFiles: files, managedRoots });
|
|
166
214
|
}
|
|
167
215
|
}
|
|
168
216
|
|
|
@@ -174,16 +222,28 @@ async function initInstallation({ baseRoot, manifestPath, agentsRoot, packageRoo
|
|
|
174
222
|
ai,
|
|
175
223
|
targets,
|
|
176
224
|
skills: [...skillIds],
|
|
225
|
+
satisfiedByGlobal: globalSatisfaction,
|
|
177
226
|
commands: targets.includes('opencode') ? [...commandNames] : [],
|
|
178
227
|
files,
|
|
179
228
|
};
|
|
229
|
+
await assertSafeManagedPath(baseRoot, manifestPath);
|
|
180
230
|
await mkdir(path.dirname(manifestPath), { recursive: true });
|
|
181
231
|
await writeJsonAtomic(manifestPath, manifest);
|
|
182
232
|
if (agentsRoot) await writeAgentsBlock(agentsRoot, skillIds);
|
|
183
|
-
|
|
233
|
+
const result = await inspectInstallation({
|
|
234
|
+
baseRoot, manifestPath, agentsRoot, scope, homeRoot,
|
|
235
|
+
globalSkillRootForTarget,
|
|
236
|
+
});
|
|
237
|
+
return {
|
|
238
|
+
...result,
|
|
239
|
+
requestedSkills: skillIds.length,
|
|
240
|
+
installedSkills: installedSkillIds.size,
|
|
241
|
+
satisfiedByGlobal: globallySatisfiedSkillIds.size,
|
|
242
|
+
skippedDuplicates,
|
|
243
|
+
};
|
|
184
244
|
}
|
|
185
245
|
|
|
186
|
-
export async function initProject({ projectRoot, packageRoot, profile, ai, skillIds, commandNames = [], packageVersion = '0.2.0' }) {
|
|
246
|
+
export async function initProject({ projectRoot, homeRoot = homedir(), packageRoot, profile, ai, skillIds, commandNames = [], packageVersion = '0.2.0' }) {
|
|
187
247
|
return initInstallation({
|
|
188
248
|
baseRoot: projectRoot,
|
|
189
249
|
manifestPath: path.join(projectRoot, PROJECT_MANIFEST),
|
|
@@ -195,7 +255,9 @@ export async function initProject({ projectRoot, packageRoot, profile, ai, skill
|
|
|
195
255
|
commandNames,
|
|
196
256
|
packageVersion,
|
|
197
257
|
scope: 'project',
|
|
258
|
+
homeRoot,
|
|
198
259
|
skillRootForTarget: (target) => skillRootFor(target, projectRoot),
|
|
260
|
+
globalSkillRootForTarget: (target) => globalSkillRootFor(target, { homeRoot }),
|
|
199
261
|
commandRoot: () => opencodeCommandRoot(projectRoot),
|
|
200
262
|
});
|
|
201
263
|
}
|
|
@@ -219,23 +281,93 @@ export async function initGlobal({ homeRoot = homedir(), packageRoot, profile, a
|
|
|
219
281
|
});
|
|
220
282
|
}
|
|
221
283
|
|
|
222
|
-
async function inspectInstallation({ baseRoot, manifestPath, agentsRoot, scope, managedRoots = [] }) {
|
|
284
|
+
async function inspectInstallation({ baseRoot, manifestPath, agentsRoot, scope, managedRoots = [], homeRoot = homedir(), globalSkillRootForTarget = null }) {
|
|
223
285
|
let manifest;
|
|
224
|
-
try { manifest = await readManifest(manifestPath); }
|
|
225
|
-
catch (error) { return { installed: true, healthy: false, scope, profile: null, ai: null, targets: [], skills: 0, commands: 0, issues: [error.message] }; }
|
|
226
|
-
if (!manifest) return { installed: false, healthy: false, scope, profile: null, ai: null, targets: [], skills: 0, commands: 0, issues: ['Showdar is not installed.'] };
|
|
286
|
+
try { manifest = await readManifest(manifestPath, baseRoot); }
|
|
287
|
+
catch (error) { return { installed: true, healthy: false, scope, profile: null, ai: null, targets: [], skills: 0, requestedSkills: 0, installedSkills: 0, satisfiedByGlobal: 0, commands: 0, issues: [error.message], warnings: [] }; }
|
|
288
|
+
if (!manifest) return { installed: false, healthy: false, scope, profile: null, ai: null, targets: [], skills: 0, requestedSkills: 0, installedSkills: 0, satisfiedByGlobal: 0, commands: 0, issues: ['Showdar is not installed.'], warnings: [] };
|
|
227
289
|
|
|
228
290
|
const issues = [];
|
|
291
|
+
const warnings = [];
|
|
292
|
+
const projectOwned = ownedPathSet(manifest);
|
|
293
|
+
const targets = manifest.targets?.length ? manifest.targets : manifest.ai ? resolveTargets(manifest.ai) : [];
|
|
294
|
+
const skillIds = manifest.skills ?? [];
|
|
295
|
+
const globalManifest = scope === 'project' && globalSkillRootForTarget
|
|
296
|
+
? await readManifest(globalManifestPath(homeRoot), homeRoot)
|
|
297
|
+
: null;
|
|
298
|
+
const globalOwned = ownedPathSet(globalManifest);
|
|
299
|
+
const globalSatisfiedPaths = new Set();
|
|
300
|
+
const installedSkillIds = new Set();
|
|
301
|
+
const globallySatisfiedSkillIds = new Set();
|
|
302
|
+
const recordedGlobalSkills = new Set((manifest.satisfiedByGlobal ?? []).map((entry) => entry?.skill).filter(Boolean));
|
|
303
|
+
|
|
304
|
+
if (scope === 'project' && globalSkillRootForTarget) {
|
|
305
|
+
const projectRoots = uniqueRoots(targets, (target) => skillRootFor(target, baseRoot));
|
|
306
|
+
const globalRoots = uniqueRoots(targets, globalSkillRootForTarget);
|
|
307
|
+
const globalSkills = new Set();
|
|
308
|
+
for (const { root } of globalRoots) {
|
|
309
|
+
const prefix = `${manifestPathFor(homeRoot, root)}/`;
|
|
310
|
+
for (const entry of globalManifest?.files ?? []) {
|
|
311
|
+
if (!entry.path.startsWith(prefix)) continue;
|
|
312
|
+
const skillId = entry.path.slice(prefix.length).split('/')[0];
|
|
313
|
+
if (!skillId.startsWith('showdar-')) continue;
|
|
314
|
+
const globalTarget = safeOwnedPath(homeRoot, entry.path);
|
|
315
|
+
if (globalTarget && globalTarget.startsWith(`${root}${path.sep}`) && globalOwned.has(entry.path)) {
|
|
316
|
+
await assertSafeManagedPath(homeRoot, globalTarget);
|
|
317
|
+
if (await exists(globalTarget)) globalSkills.add(skillId);
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
const extra = [...globalSkills].filter((skillId) => !skillIds.includes(skillId)).sort();
|
|
322
|
+
if (extra.length) warnings.push(`Global Showdar installation exposes skills outside project profile "${manifest.profile ?? 'unknown'}": ${extra.join(', ')}. Project deduplication prevents duplicate copies but cannot hide globally installed skills. For strict project profile isolation: showdar remove --scope global`);
|
|
323
|
+
|
|
324
|
+
for (const { root, target } of projectRoots) {
|
|
325
|
+
for (const skillId of skillIds) {
|
|
326
|
+
const projectPath = path.join(root, skillId);
|
|
327
|
+
const projectRelative = manifestPathFor(baseRoot, projectPath);
|
|
328
|
+
const projectExists = await exists(projectPath);
|
|
329
|
+
const projectIsOwned = projectOwned.has(projectRelative);
|
|
330
|
+
const globalPath = path.join(globalSkillRootForTarget(target), skillId);
|
|
331
|
+
const globalRelative = manifestPathFor(homeRoot, globalPath);
|
|
332
|
+
await assertSafeManagedPath(baseRoot, projectPath);
|
|
333
|
+
await assertSafeManagedPath(homeRoot, globalPath);
|
|
334
|
+
const globalExists = await exists(globalPath);
|
|
335
|
+
const globalIsOwned = globalOwned.has(globalRelative);
|
|
336
|
+
|
|
337
|
+
if (projectExists && projectIsOwned) installedSkillIds.add(skillId);
|
|
338
|
+
if (projectExists && projectIsOwned && globalExists && globalIsOwned) {
|
|
339
|
+
warnings.push(`Duplicate Showdar skill discovery:\n ${skillId}\n project: ${projectPath}\n global: ${globalPath}\n To prefer project isolation: showdar remove --scope global`);
|
|
340
|
+
} else if (!projectExists && globalExists && globalIsOwned) {
|
|
341
|
+
globalSatisfiedPaths.add(projectRelative);
|
|
342
|
+
globallySatisfiedSkillIds.add(skillId);
|
|
343
|
+
} else if (!projectExists && !globalExists) {
|
|
344
|
+
issues.push(recordedGlobalSkills.has(skillId)
|
|
345
|
+
? `Globally satisfied skill is missing: ${skillId} (${globalPath})`
|
|
346
|
+
: `Missing requested skill: ${projectPath}`);
|
|
347
|
+
} else if (projectExists && !projectIsOwned) {
|
|
348
|
+
issues.push(`Project skill path is not Showdar-owned: ${projectPath}`);
|
|
349
|
+
} else if (globalExists && !globalIsOwned) {
|
|
350
|
+
warnings.push(`Global skill path is not Showdar-owned: ${globalPath}`);
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
229
356
|
for (const entry of manifest.files ?? []) {
|
|
230
357
|
const target = safeOwnedPath(baseRoot, entry.path, managedRoots);
|
|
231
358
|
if (!target) { issues.push(`Invalid managed path: ${entry.path}`); continue; }
|
|
232
|
-
|
|
359
|
+
await assertSafeManagedPath(baseRoot, target, managedRoots);
|
|
360
|
+
if (!(await exists(target))) {
|
|
361
|
+
if (!globalSatisfiedPaths.has(entry.path)) issues.push(`Missing managed path: ${entry.path}`);
|
|
362
|
+
continue;
|
|
363
|
+
}
|
|
233
364
|
const actual = await hashTree(target);
|
|
234
365
|
if (actual !== entry.hash) issues.push(`Managed path drift detected: ${entry.path}`);
|
|
235
366
|
}
|
|
236
367
|
|
|
237
368
|
if (agentsRoot) {
|
|
238
369
|
const agentsFile = path.join(agentsRoot, 'AGENTS.md');
|
|
370
|
+
await assertSafeManagedPath(agentsRoot, agentsFile);
|
|
239
371
|
if (!(await exists(agentsFile))) issues.push('Missing AGENTS.md routing block');
|
|
240
372
|
else {
|
|
241
373
|
const text = await readFile(agentsFile, 'utf8');
|
|
@@ -250,26 +382,42 @@ async function inspectInstallation({ baseRoot, manifestPath, agentsRoot, scope,
|
|
|
250
382
|
profile: manifest.profile ?? null,
|
|
251
383
|
ai: manifest.ai ?? null,
|
|
252
384
|
targets: manifest.targets ?? [],
|
|
253
|
-
skills:
|
|
385
|
+
skills: skillIds.length,
|
|
386
|
+
requestedSkills: skillIds.length,
|
|
387
|
+
installedSkills: scope === 'project' && globalSkillRootForTarget ? installedSkillIds.size : skillIds.length,
|
|
388
|
+
satisfiedByGlobal: scope === 'project' && globalSkillRootForTarget ? globallySatisfiedSkillIds.size : 0,
|
|
254
389
|
commands: (manifest.commands ?? []).length,
|
|
255
390
|
issues,
|
|
391
|
+
warnings,
|
|
256
392
|
};
|
|
257
393
|
}
|
|
258
394
|
|
|
259
395
|
async function removeInstallation({ baseRoot, manifestPath, agentsRoot, managedRoots = [] }) {
|
|
396
|
+
await assertSafeManagedPath(baseRoot, manifestPath);
|
|
260
397
|
let manifest;
|
|
261
|
-
try { manifest = await readManifest(manifestPath); }
|
|
398
|
+
try { manifest = await readManifest(manifestPath, baseRoot); }
|
|
262
399
|
catch { manifest = null; }
|
|
263
400
|
for (const entry of manifest?.files ?? []) {
|
|
264
401
|
const target = safeOwnedPath(baseRoot, entry.path, managedRoots);
|
|
265
|
-
if (target)
|
|
402
|
+
if (target) {
|
|
403
|
+
await assertSafeManagedPath(baseRoot, target, managedRoots);
|
|
404
|
+
await rm(target, { recursive: true, force: true });
|
|
405
|
+
}
|
|
266
406
|
}
|
|
407
|
+
await assertSafeManagedPath(baseRoot, manifestPath);
|
|
267
408
|
await rm(manifestPath, { force: true });
|
|
268
409
|
if (agentsRoot) await removeAgentsBlock(agentsRoot);
|
|
269
410
|
}
|
|
270
411
|
|
|
271
|
-
export async function inspectProject(projectRoot) {
|
|
272
|
-
return inspectInstallation({
|
|
412
|
+
export async function inspectProject(projectRoot, { homeRoot = homedir() } = {}) {
|
|
413
|
+
return inspectInstallation({
|
|
414
|
+
baseRoot: projectRoot,
|
|
415
|
+
manifestPath: path.join(projectRoot, PROJECT_MANIFEST),
|
|
416
|
+
agentsRoot: projectRoot,
|
|
417
|
+
scope: 'project',
|
|
418
|
+
homeRoot,
|
|
419
|
+
globalSkillRootForTarget: (target) => globalSkillRootFor(target, { homeRoot }),
|
|
420
|
+
});
|
|
273
421
|
}
|
|
274
422
|
|
|
275
423
|
export async function inspectGlobal({ homeRoot = homedir() } = {}) {
|