showdar-skills 0.2.2 → 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/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
|
|
|
@@ -109,20 +117,13 @@ function uniqueRoots(targets, resolveRoot) {
|
|
|
109
117
|
return [...roots.values()];
|
|
110
118
|
}
|
|
111
119
|
|
|
112
|
-
function safeOwnedPath(baseRoot, relative, allowedRoots = []) {
|
|
113
|
-
if (typeof relative !== 'string' || !relative) return null;
|
|
114
|
-
const roots = [baseRoot, ...allowedRoots].map((root) => path.resolve(root));
|
|
115
|
-
const target = path.resolve(relative.startsWith(path.sep) ? relative : path.join(roots[0], relative));
|
|
116
|
-
if (!roots.some((root) => target === root || target.startsWith(`${root}${path.sep}`))) return null;
|
|
117
|
-
return target;
|
|
118
|
-
}
|
|
119
|
-
|
|
120
120
|
function manifestPathFor(baseRoot, destination) {
|
|
121
121
|
const relative = path.relative(baseRoot, destination);
|
|
122
122
|
return relative.replaceAll(path.sep, '/');
|
|
123
123
|
}
|
|
124
124
|
|
|
125
|
-
async function copyOwned({ baseRoot, source, destination, priorOwned, newFiles }) {
|
|
125
|
+
async function copyOwned({ baseRoot, source, destination, priorOwned, newFiles, managedRoots = [] }) {
|
|
126
|
+
await assertSafeManagedPath(baseRoot, destination, managedRoots);
|
|
126
127
|
const relative = manifestPathFor(baseRoot, destination);
|
|
127
128
|
if ((await exists(destination)) && !priorOwned.has(relative)) {
|
|
128
129
|
throw new Error(`Refusing to overwrite existing non-Showdar-managed skill or command: ${destination}`);
|
|
@@ -135,14 +136,17 @@ async function copyOwned({ baseRoot, source, destination, priorOwned, newFiles }
|
|
|
135
136
|
|
|
136
137
|
async function initInstallation({ baseRoot, manifestPath, agentsRoot, packageRoot, profile, ai, skillIds, commandNames = [], packageVersion = '0.2.0', scope, skillRootForTarget, commandRoot, managedRoots = [], homeRoot = homedir(), globalSkillRootForTarget = null }) {
|
|
137
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'));
|
|
138
142
|
const targets = resolveTargets(ai);
|
|
139
|
-
const prior = await readManifest(manifestPath);
|
|
143
|
+
const prior = await readManifest(manifestPath, baseRoot);
|
|
140
144
|
const priorOwned = ownedPathSet(prior);
|
|
141
145
|
const desiredPaths = new Set();
|
|
142
146
|
|
|
143
147
|
const skillRoots = uniqueRoots(targets, skillRootForTarget);
|
|
144
148
|
const globalManifest = scope === 'project' && globalSkillRootForTarget
|
|
145
|
-
? await readManifest(globalManifestPath(homeRoot))
|
|
149
|
+
? await readManifest(globalManifestPath(homeRoot), homeRoot)
|
|
146
150
|
: null;
|
|
147
151
|
const globalOwned = ownedPathSet(globalManifest);
|
|
148
152
|
const globalSatisfaction = [];
|
|
@@ -158,9 +162,11 @@ async function initInstallation({ baseRoot, manifestPath, agentsRoot, packageRoo
|
|
|
158
162
|
for (const { root, target } of skillRoots) {
|
|
159
163
|
for (const skillId of skillIds) {
|
|
160
164
|
const destination = path.join(root, skillId);
|
|
165
|
+
await assertSafeManagedPath(baseRoot, destination, managedRoots);
|
|
161
166
|
const relative = manifestPathFor(baseRoot, destination);
|
|
162
167
|
const destinationExists = await exists(destination);
|
|
163
168
|
const globalPath = globalSkillRootForTarget ? path.join(globalSkillRootForTarget(target), skillId) : null;
|
|
169
|
+
if (globalPath) await assertSafeManagedPath(homeRoot, globalPath);
|
|
164
170
|
const globalRelative = globalPath ? manifestPathFor(homeRoot, globalPath) : null;
|
|
165
171
|
const globalAvailable = Boolean(globalPath && globalOwned.has(globalRelative) && await exists(globalPath));
|
|
166
172
|
if (scope === 'project' && globalAvailable && !destinationExists) {
|
|
@@ -174,18 +180,28 @@ async function initInstallation({ baseRoot, manifestPath, agentsRoot, packageRoo
|
|
|
174
180
|
}
|
|
175
181
|
}
|
|
176
182
|
if (targets.includes('opencode')) {
|
|
177
|
-
|
|
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
|
+
}
|
|
178
189
|
}
|
|
179
190
|
|
|
191
|
+
const staleTargets = [];
|
|
180
192
|
for (const entry of prior?.files ?? []) {
|
|
181
193
|
const target = safeOwnedPath(baseRoot, entry.path, managedRoots);
|
|
182
|
-
if (target && !desiredPaths.has(entry.path))
|
|
194
|
+
if (target && !desiredPaths.has(entry.path)) {
|
|
195
|
+
await assertSafeManagedPath(baseRoot, target, managedRoots);
|
|
196
|
+
staleTargets.push(target);
|
|
197
|
+
}
|
|
183
198
|
}
|
|
199
|
+
for (const target of staleTargets) await rm(target, { recursive: true, force: true });
|
|
184
200
|
|
|
185
201
|
const files = [];
|
|
186
202
|
for (const { destination, skillId } of skillDestinations) {
|
|
187
203
|
const source = path.join(packageRoot, 'skills', skillId);
|
|
188
|
-
await copyOwned({ baseRoot, source, destination, priorOwned, newFiles: files });
|
|
204
|
+
await copyOwned({ baseRoot, source, destination, priorOwned, newFiles: files, managedRoots });
|
|
189
205
|
installedSkillIds.add(skillId);
|
|
190
206
|
}
|
|
191
207
|
|
|
@@ -194,7 +210,7 @@ async function initInstallation({ baseRoot, manifestPath, agentsRoot, packageRoo
|
|
|
194
210
|
for (const name of commandNames) {
|
|
195
211
|
const source = path.join(packageRoot, 'commands', 'opencode', 'showdar', `${name}.md`);
|
|
196
212
|
if (!(await exists(source))) throw new Error(`OpenCode command asset not found: ${name}`);
|
|
197
|
-
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 });
|
|
198
214
|
}
|
|
199
215
|
}
|
|
200
216
|
|
|
@@ -210,6 +226,7 @@ async function initInstallation({ baseRoot, manifestPath, agentsRoot, packageRoo
|
|
|
210
226
|
commands: targets.includes('opencode') ? [...commandNames] : [],
|
|
211
227
|
files,
|
|
212
228
|
};
|
|
229
|
+
await assertSafeManagedPath(baseRoot, manifestPath);
|
|
213
230
|
await mkdir(path.dirname(manifestPath), { recursive: true });
|
|
214
231
|
await writeJsonAtomic(manifestPath, manifest);
|
|
215
232
|
if (agentsRoot) await writeAgentsBlock(agentsRoot, skillIds);
|
|
@@ -266,7 +283,7 @@ export async function initGlobal({ homeRoot = homedir(), packageRoot, profile, a
|
|
|
266
283
|
|
|
267
284
|
async function inspectInstallation({ baseRoot, manifestPath, agentsRoot, scope, managedRoots = [], homeRoot = homedir(), globalSkillRootForTarget = null }) {
|
|
268
285
|
let manifest;
|
|
269
|
-
try { manifest = await readManifest(manifestPath); }
|
|
286
|
+
try { manifest = await readManifest(manifestPath, baseRoot); }
|
|
270
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: [] }; }
|
|
271
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: [] };
|
|
272
289
|
|
|
@@ -276,7 +293,7 @@ async function inspectInstallation({ baseRoot, manifestPath, agentsRoot, scope,
|
|
|
276
293
|
const targets = manifest.targets?.length ? manifest.targets : manifest.ai ? resolveTargets(manifest.ai) : [];
|
|
277
294
|
const skillIds = manifest.skills ?? [];
|
|
278
295
|
const globalManifest = scope === 'project' && globalSkillRootForTarget
|
|
279
|
-
? await readManifest(globalManifestPath(homeRoot))
|
|
296
|
+
? await readManifest(globalManifestPath(homeRoot), homeRoot)
|
|
280
297
|
: null;
|
|
281
298
|
const globalOwned = ownedPathSet(globalManifest);
|
|
282
299
|
const globalSatisfiedPaths = new Set();
|
|
@@ -294,7 +311,11 @@ async function inspectInstallation({ baseRoot, manifestPath, agentsRoot, scope,
|
|
|
294
311
|
if (!entry.path.startsWith(prefix)) continue;
|
|
295
312
|
const skillId = entry.path.slice(prefix.length).split('/')[0];
|
|
296
313
|
if (!skillId.startsWith('showdar-')) continue;
|
|
297
|
-
|
|
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
|
+
}
|
|
298
319
|
}
|
|
299
320
|
}
|
|
300
321
|
const extra = [...globalSkills].filter((skillId) => !skillIds.includes(skillId)).sort();
|
|
@@ -308,6 +329,8 @@ async function inspectInstallation({ baseRoot, manifestPath, agentsRoot, scope,
|
|
|
308
329
|
const projectIsOwned = projectOwned.has(projectRelative);
|
|
309
330
|
const globalPath = path.join(globalSkillRootForTarget(target), skillId);
|
|
310
331
|
const globalRelative = manifestPathFor(homeRoot, globalPath);
|
|
332
|
+
await assertSafeManagedPath(baseRoot, projectPath);
|
|
333
|
+
await assertSafeManagedPath(homeRoot, globalPath);
|
|
311
334
|
const globalExists = await exists(globalPath);
|
|
312
335
|
const globalIsOwned = globalOwned.has(globalRelative);
|
|
313
336
|
|
|
@@ -333,6 +356,7 @@ async function inspectInstallation({ baseRoot, manifestPath, agentsRoot, scope,
|
|
|
333
356
|
for (const entry of manifest.files ?? []) {
|
|
334
357
|
const target = safeOwnedPath(baseRoot, entry.path, managedRoots);
|
|
335
358
|
if (!target) { issues.push(`Invalid managed path: ${entry.path}`); continue; }
|
|
359
|
+
await assertSafeManagedPath(baseRoot, target, managedRoots);
|
|
336
360
|
if (!(await exists(target))) {
|
|
337
361
|
if (!globalSatisfiedPaths.has(entry.path)) issues.push(`Missing managed path: ${entry.path}`);
|
|
338
362
|
continue;
|
|
@@ -343,6 +367,7 @@ async function inspectInstallation({ baseRoot, manifestPath, agentsRoot, scope,
|
|
|
343
367
|
|
|
344
368
|
if (agentsRoot) {
|
|
345
369
|
const agentsFile = path.join(agentsRoot, 'AGENTS.md');
|
|
370
|
+
await assertSafeManagedPath(agentsRoot, agentsFile);
|
|
346
371
|
if (!(await exists(agentsFile))) issues.push('Missing AGENTS.md routing block');
|
|
347
372
|
else {
|
|
348
373
|
const text = await readFile(agentsFile, 'utf8');
|
|
@@ -368,13 +393,18 @@ async function inspectInstallation({ baseRoot, manifestPath, agentsRoot, scope,
|
|
|
368
393
|
}
|
|
369
394
|
|
|
370
395
|
async function removeInstallation({ baseRoot, manifestPath, agentsRoot, managedRoots = [] }) {
|
|
396
|
+
await assertSafeManagedPath(baseRoot, manifestPath);
|
|
371
397
|
let manifest;
|
|
372
|
-
try { manifest = await readManifest(manifestPath); }
|
|
398
|
+
try { manifest = await readManifest(manifestPath, baseRoot); }
|
|
373
399
|
catch { manifest = null; }
|
|
374
400
|
for (const entry of manifest?.files ?? []) {
|
|
375
401
|
const target = safeOwnedPath(baseRoot, entry.path, managedRoots);
|
|
376
|
-
if (target)
|
|
402
|
+
if (target) {
|
|
403
|
+
await assertSafeManagedPath(baseRoot, target, managedRoots);
|
|
404
|
+
await rm(target, { recursive: true, force: true });
|
|
405
|
+
}
|
|
377
406
|
}
|
|
407
|
+
await assertSafeManagedPath(baseRoot, manifestPath);
|
|
378
408
|
await rm(manifestPath, { force: true });
|
|
379
409
|
if (agentsRoot) await removeAgentsBlock(agentsRoot);
|
|
380
410
|
}
|