claude-dev-env 8.27.0 → 8.28.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.
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { cpSync, lstatSync, mkdirSync, readFileSync, readdirSync, renameSync, rmSync, mkdtempSync, writeFileSync } from 'node:fs';
|
|
3
|
+
import { dirname, join, resolve } from 'node:path';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
|
|
6
|
+
function inventory(directory, prefix = '') {
|
|
7
|
+
return readdirSync(directory, { withFileTypes: true }).sort((left, right) => left.name.localeCompare(right.name))
|
|
8
|
+
.flatMap(entry => {
|
|
9
|
+
const relativePath = prefix ? `${prefix}/${entry.name}` : entry.name;
|
|
10
|
+
const entryPath = join(directory, entry.name);
|
|
11
|
+
if (entry.isDirectory()) return inventory(entryPath, relativePath);
|
|
12
|
+
if (!entry.isFile()) throw new Error(`Unsupported source entry: ${relativePath}`);
|
|
13
|
+
return [[relativePath, createHash('sha256').update(readFileSync(entryPath)).digest('hex')]];
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function exportPstackSkills(pluginRoot, destination) {
|
|
18
|
+
const source = resolve(pluginRoot);
|
|
19
|
+
const target = resolve(destination);
|
|
20
|
+
const manifest = JSON.parse(readFileSync(join(source, '.codex-plugin', 'plugin.json'), 'utf8'));
|
|
21
|
+
if (manifest.name !== 'pstack' || manifest.skills !== './skills/' || !manifest.version || !manifest.repository) {
|
|
22
|
+
throw new Error('Expected a versioned pstack Codex plugin with a skills directory and repository.');
|
|
23
|
+
}
|
|
24
|
+
if (lstatSync(target, { throwIfNoEntry: false })) throw new Error(`Destination already exists: ${target}`);
|
|
25
|
+
const skillsRoot = join(source, 'skills');
|
|
26
|
+
const poteto = readFileSync(join(skillsRoot, 'poteto-mode', 'SKILL.md'), 'utf8');
|
|
27
|
+
if (!/^name: poteto-mode\r?$/m.test(poteto)) throw new Error('Missing upstream poteto-mode metadata.');
|
|
28
|
+
const files = Object.fromEntries(inventory(skillsRoot));
|
|
29
|
+
const license = readFileSync(join(skillsRoot, 'poteto-mode', 'references', 'licenses', 'LICENSE'));
|
|
30
|
+
const receipt = {
|
|
31
|
+
repository: manifest.repository,
|
|
32
|
+
version: manifest.version,
|
|
33
|
+
sourceManifestSha256: createHash('sha256').update(readFileSync(join(source, '.codex-plugin', 'plugin.json'))).digest('hex'),
|
|
34
|
+
licenseSha256: createHash('sha256').update(license).digest('hex'),
|
|
35
|
+
skillCount: Object.keys(files).filter(path => /^[^/]+\/SKILL\.md$/.test(path)).length,
|
|
36
|
+
expectedRepositorySkillName: 'poteto-mode',
|
|
37
|
+
cloudDiscovery: 'unverified',
|
|
38
|
+
cloudInvocation: 'unverified',
|
|
39
|
+
files,
|
|
40
|
+
};
|
|
41
|
+
mkdirSync(dirname(target), { recursive: true });
|
|
42
|
+
const staging = mkdtempSync(join(dirname(target), '.pstack-export-'));
|
|
43
|
+
try {
|
|
44
|
+
cpSync(skillsRoot, staging, { recursive: true });
|
|
45
|
+
writeFileSync(join(staging, 'PSTACK-LICENSE'), license);
|
|
46
|
+
writeFileSync(join(staging, 'pstack-export.json'), `${JSON.stringify(receipt, null, 2)}\n`);
|
|
47
|
+
if (lstatSync(target, { throwIfNoEntry: false })) throw new Error(`Destination appeared: ${target}`);
|
|
48
|
+
renameSync(staging, target);
|
|
49
|
+
} finally {
|
|
50
|
+
rmSync(staging, { recursive: true, force: true });
|
|
51
|
+
}
|
|
52
|
+
return receipt;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
|
|
56
|
+
const [pluginRoot, destination, ...extra] = process.argv.slice(2);
|
|
57
|
+
if (!pluginRoot || !destination || extra.length) {
|
|
58
|
+
console.error('Usage: node export-pstack-skills.mjs <plugin-root> <new-repository/.agents/skills>');
|
|
59
|
+
process.exitCode = 1;
|
|
60
|
+
} else {
|
|
61
|
+
try {
|
|
62
|
+
const receipt = exportPstackSkills(pluginRoot, destination);
|
|
63
|
+
console.log(JSON.stringify({ destination: resolve(destination), version: receipt.version, skillCount: receipt.skillCount,
|
|
64
|
+
expectedRepositorySkillName: receipt.expectedRepositorySkillName, cloudDiscovery: receipt.cloudDiscovery,
|
|
65
|
+
cloudInvocation: receipt.cloudInvocation }, null, 2));
|
|
66
|
+
} catch (error) {
|
|
67
|
+
console.error(error.message);
|
|
68
|
+
process.exitCode = 1;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { test } from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, symlinkSync, writeFileSync } from 'node:fs';
|
|
4
|
+
import { tmpdir } from 'node:os';
|
|
5
|
+
import { join } from 'node:path';
|
|
6
|
+
import { createHash } from 'node:crypto';
|
|
7
|
+
import { exportPstackSkills } from './export-pstack-skills.mjs';
|
|
8
|
+
|
|
9
|
+
function fixture(context) {
|
|
10
|
+
const root = mkdtempSync(join(tmpdir(), 'pstack-export-test-'));
|
|
11
|
+
context.after(() => rmSync(root, { recursive: true, force: true }));
|
|
12
|
+
const plugin = join(root, 'plugin');
|
|
13
|
+
const target = join(root, 'repo', '.agents', 'skills');
|
|
14
|
+
mkdirSync(join(plugin, '.codex-plugin'), { recursive: true });
|
|
15
|
+
writeFileSync(join(plugin, '.codex-plugin', 'plugin.json'), JSON.stringify({
|
|
16
|
+
name: 'pstack', version: '0.9.53', repository: 'https://github.com/michael-denyer/pstack-claude', skills: './skills/',
|
|
17
|
+
}));
|
|
18
|
+
mkdirSync(join(plugin, 'skills', 'poteto-mode', 'references'), { recursive: true });
|
|
19
|
+
mkdirSync(join(plugin, 'skills', 'unslop'), { recursive: true });
|
|
20
|
+
writeFileSync(join(plugin, 'skills', 'poteto-mode', 'SKILL.md'), '---\nname: poteto-mode\n---\nUpstream body\n');
|
|
21
|
+
writeFileSync(join(plugin, 'skills', 'poteto-mode', 'references', 'codex-tools.md'), 'Tool mapping\n');
|
|
22
|
+
writeFileSync(join(plugin, 'skills', 'unslop', 'SKILL.md'), 'Sibling dependency\n');
|
|
23
|
+
mkdirSync(join(plugin, 'skills', 'poteto-mode', 'references', 'licenses'));
|
|
24
|
+
writeFileSync(join(plugin, 'skills', 'poteto-mode', 'references', 'licenses', 'LICENSE'), 'Upstream license\n');
|
|
25
|
+
return { root, plugin, target };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
test('exports upstream bodies and dependencies unchanged with verifiable provenance', context => {
|
|
29
|
+
const { plugin, target } = fixture(context);
|
|
30
|
+
const receipt = exportPstackSkills(plugin, target);
|
|
31
|
+
assert.equal(receipt.skillCount, 2);
|
|
32
|
+
assert.equal(receipt.cloudInvocation, 'unverified');
|
|
33
|
+
assert.equal(receipt.expectedRepositorySkillName, 'poteto-mode');
|
|
34
|
+
for (const [path, hash] of Object.entries(receipt.files)) {
|
|
35
|
+
const exported = readFileSync(join(target, path));
|
|
36
|
+
assert.deepEqual(exported, readFileSync(join(plugin, 'skills', path)));
|
|
37
|
+
assert.equal(createHash('sha256').update(exported).digest('hex'), hash);
|
|
38
|
+
}
|
|
39
|
+
assert.deepEqual(JSON.parse(readFileSync(join(target, 'pstack-export.json'))), receipt);
|
|
40
|
+
assert.equal(readFileSync(join(target, 'PSTACK-LICENSE'), 'utf8'), 'Upstream license\n');
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
test('existing custom skills survive a rejected repeat export', context => {
|
|
44
|
+
const { plugin, target } = fixture(context);
|
|
45
|
+
exportPstackSkills(plugin, target);
|
|
46
|
+
const customPath = join(target, 'poteto-mode', 'SKILL.md');
|
|
47
|
+
writeFileSync(customPath, 'Custom instructions\n');
|
|
48
|
+
assert.throws(() => exportPstackSkills(plugin, target), /already exists/);
|
|
49
|
+
assert.equal(readFileSync(customPath, 'utf8'), 'Custom instructions\n');
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
test('a source symlink is rejected before creating the destination', context => {
|
|
53
|
+
const { plugin, target } = fixture(context);
|
|
54
|
+
symlinkSync(join(plugin, 'skills', 'poteto-mode', 'SKILL.md'), join(plugin, 'skills', 'linked-skill'));
|
|
55
|
+
assert.throws(() => exportPstackSkills(plugin, target), /Unsupported source entry/);
|
|
56
|
+
assert.equal(existsSync(target), false);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
test('a plugin without the required poteto entry cannot produce a partial export', context => {
|
|
60
|
+
const { plugin, target } = fixture(context);
|
|
61
|
+
rmSync(join(plugin, 'skills', 'poteto-mode', 'SKILL.md'));
|
|
62
|
+
assert.throws(() => exportPstackSkills(plugin, target), /ENOENT/);
|
|
63
|
+
assert.equal(existsSync(target), false);
|
|
64
|
+
});
|