Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 30 additions & 1 deletion bin/pixelslop.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -252,6 +252,31 @@ export function agentMdToCodexToml(md) {
].join('\n');
}

/**
* Rewrite bin/ and resources/ paths in every Markdown file under an installed
* skill tree, exactly the way agent specs are rewritten. Without this, SKILL.md
* and fix guides like checkpoint-protocol.md keep relative `bin/pixelslop-tools.cjs`
* paths that only resolve from the repo checkout — so `/pixelslop` fails in any
* other project ("tools.cjs not installed").
*
* @param {string} skillDir - The installed skill directory (INSTALL_ROOT/skill)
* @param {string} installRoot - Install root for resolving absolute paths
*/
export function rewriteSkillTreePaths(skillDir, installRoot) {
const walk = (dir) => {
for (const entry of readdirSync(dir, { withFileTypes: true })) {
if (entry.name.startsWith('._')) continue;
const full = join(dir, entry.name);
if (entry.isDirectory()) { walk(full); continue; }
if (!entry.name.endsWith('.md')) continue;
const raw = readFileSync(full, 'utf8');
const rewritten = rewriteAgentPaths(raw, installRoot);
if (rewritten !== raw) writeFileSync(full, rewritten);
}
};
walk(skillDir);
}

// ─────────────────────────────────────────────
// Browser Runtime Detection
// ─────────────────────────────────────────────
Expand Down Expand Up @@ -1190,11 +1215,15 @@ function install(options = {}) {
// Step 3: Ensure a browser runtime exists before wiring clients
const browserRuntime = ensureBrowserRuntime();

// Step 4: Copy skill directory to install root (source of truth)
// Step 4: Copy skill directory to install root (source of truth), then rewrite
// its bin/ and resources/ paths to absolute — same as agents. Without this the
// skill keeps relative `bin/pixelslop-tools.cjs` paths that only resolve from
// the repo, so /pixelslop reports "not installed" in every other project.
copyDir(
join(PACKAGE_ROOT, 'dist', 'skill'),
join(INSTALL_ROOT, 'skill')
);
rewriteSkillTreePaths(join(INSTALL_ROOT, 'skill'), INSTALL_ROOT);
const resourceCount = readdirSync(join(INSTALL_ROOT, 'skill', 'resources')).length;
log('✓', `skill/SKILL.md + ${resourceCount} resources`);

Expand Down
5 changes: 3 additions & 2 deletions tests/codex-toml.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -80,9 +80,10 @@ describe('agentMdToCodexToml — edge cases', () => {
});

describe('every shipped agent spec converts cleanly', () => {
const md = (f) => f.endsWith('.md') && !f.startsWith('._');
const specs = [
...readdirSync(join(ROOT, 'dist', 'agents')).filter(f => f.endsWith('.md')),
...readdirSync(join(ROOT, 'dist', 'agents', 'internal')).filter(f => f.endsWith('.md') && !f.startsWith('._')).map(f => join('internal', f)),
...readdirSync(join(ROOT, 'dist', 'agents')).filter(md),
...readdirSync(join(ROOT, 'dist', 'agents', 'internal')).filter(md).map(f => join('internal', f)),
];

for (const rel of specs) {
Expand Down
72 changes: 72 additions & 0 deletions tests/skill-path-rewrite.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
/**
* Skill Path Rewrite Tests
*
* Regression guard for a real bug: the installer copied the skill tree to the
* install root WITHOUT rewriting its `bin/pixelslop-tools.cjs` references, so
* SKILL.md (and fix guides like checkpoint-protocol.md) kept relative paths.
* That made `/pixelslop` work only from the repo checkout and report
* "tools.cjs not installed" in every other project.
*
* rewriteSkillTreePaths rewrites every .md under the installed skill the same
* way agent specs are rewritten. These tests pin that, and that the shipped
* source genuinely needs it (so the test can't pass vacuously).
*
* Run: node --test tests/skill-path-rewrite.test.js
*/

import { describe, it } from 'node:test';
import { strict as assert } from 'node:assert';
import { readFileSync, writeFileSync, mkdtempSync, mkdirSync, rmSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { fileURLToPath } from 'node:url';
import { dirname, join } from 'node:path';
import { rewriteSkillTreePaths } from '../bin/pixelslop.mjs';

const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..');

describe('rewriteSkillTreePaths', () => {
it('rewrites relative tool paths in SKILL.md and nested resources to absolute', () => {
const dir = mkdtempSync(join(tmpdir(), 'pxs-skillrw-'));
const installRoot = join(dir, 'root');
const skillDir = join(installRoot, 'skill');
mkdirSync(join(skillDir, 'resources'), { recursive: true });

writeFileSync(join(skillDir, 'SKILL.md'), 'Run `node bin/pixelslop-tools.cjs init`.\n');
writeFileSync(join(skillDir, 'resources', 'checkpoint-protocol.md'),
'Run `node bin/pixelslop-tools.cjs checkpoint create`.\n');
// a non-md file must be left alone
writeFileSync(join(skillDir, 'resources', 'report-template.html'), '<p>bin/pixelslop-tools.cjs</p>');

rewriteSkillTreePaths(skillDir, installRoot);

const skill = readFileSync(join(skillDir, 'SKILL.md'), 'utf8');
const guide = readFileSync(join(skillDir, 'resources', 'checkpoint-protocol.md'), 'utf8');
const html = readFileSync(join(skillDir, 'resources', 'report-template.html'), 'utf8');

assert.ok(skill.includes(join(installRoot, 'bin', 'pixelslop-tools.cjs')), 'SKILL.md → absolute');
assert.ok(!/[^/]bin\/pixelslop-tools\.cjs/.test(skill), 'no relative ref left in SKILL.md');
assert.ok(guide.includes(join(installRoot, 'bin', 'pixelslop-tools.cjs')), 'resource guide → absolute');
assert.ok(html.includes('bin/pixelslop-tools.cjs'), 'non-md files are untouched');

rmSync(dir, { recursive: true, force: true });
});

it('is a no-op on files without paths to rewrite', () => {
const dir = mkdtempSync(join(tmpdir(), 'pxs-skillrw-'));
const skillDir = join(dir, 'skill');
mkdirSync(skillDir, { recursive: true });
const original = '# Just docs, no tool calls.\n';
writeFileSync(join(skillDir, 'notes.md'), original);
rewriteSkillTreePaths(skillDir, dir);
assert.equal(readFileSync(join(skillDir, 'notes.md'), 'utf8'), original);
rmSync(dir, { recursive: true, force: true });
});
});

describe('the shipped skill genuinely needs rewriting (no vacuous pass)', () => {
it('dist/skill/SKILL.md ships with relative tool paths', () => {
const skill = readFileSync(join(ROOT, 'dist', 'skill', 'SKILL.md'), 'utf8');
assert.ok(/[^/]bin\/pixelslop-tools\.cjs/.test(skill),
'source SKILL.md uses relative paths — that is what the install must rewrite');
});
});
Loading