diff --git a/.gitignore b/.gitignore index 47fcfe5..3f1b308 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,5 @@ target/ .release-version binaries/ release-assets/ +# written from README.md at release (scripts/release.mjs stamp) +npm/skillpouch/README.md diff --git a/npm/skillpouch/README.md b/npm/skillpouch/README.md deleted file mode 100644 index 0e43320..0000000 --- a/npm/skillpouch/README.md +++ /dev/null @@ -1,10 +0,0 @@ -# SkillPouch - -Sync your AI assistant skills, agents, MCP servers and instructions across -every computer and every assistant, end-to-end encrypted. - -**Coming soon.** https://skillpouch.net - -```sh -npx skillpouch login -``` diff --git a/scripts/release.mjs b/scripts/release.mjs index 87c658e..0a21135 100755 --- a/scripts/release.mjs +++ b/scripts/release.mjs @@ -4,7 +4,8 @@ // build jobs: // // node scripts/release.mjs verify check the version, note it in .release-version -// node scripts/release.mjs stamp write it into Cargo.toml, Cargo.lock and npm/skillpouch +// node scripts/release.mjs stamp write it into Cargo.toml, Cargo.lock and npm/skillpouch, +// with README.md as the npm package's README // node scripts/release.mjs publish publish the platform packages built from $BINARIES // // The version is planned once (a dry run), the binaries are built with it, and @@ -67,6 +68,26 @@ export function stampPackageJson(text, version) { return `${JSON.stringify(stamped, null, '\t')}\n`; } +const REPOSITORY = 'skillpouch/skillpouch-cli'; +const IMAGE = /\.(png|jpe?g|gif|webp|svg)$/i; + +/** + * The repository README for the npm page: npm doesn't know where relative + * links point, so they go to this release's tag on GitHub. + */ +export function npmReadme(text, version) { + const absolute = (link) => { + if (/^([a-z][a-z0-9+.-]*:|#|\/\/)/i.test(link)) return link; + const path = link.replace(/^\.?\//, ''); + return IMAGE.test(path.split(/[?#]/)[0]) + ? `https://raw.githubusercontent.com/${REPOSITORY}/v${version}/${path}` + : `https://github.com/${REPOSITORY}/blob/v${version}/${path}`; + }; + return text + .replace(/\b(src|href)="([^"]+)"/g, (_, attribute, link) => `${attribute}="${absolute(link)}"`) + .replace(/(\]\()([^)\s]+)/g, (_, open, link) => `${open}${absolute(link)}`); +} + function rewrite(path, change) { const file = join(root, path); writeFileSync(file, change(readFileSync(file, 'utf8'))); @@ -76,6 +97,10 @@ function stamp(version) { rewrite('Cargo.toml', (text) => stampCargoToml(text, version)); rewrite('Cargo.lock', (text) => stampCargoLock(text, version)); rewrite('npm/skillpouch/package.json', (text) => stampPackageJson(text, version)); + writeFileSync( + join(root, 'npm/skillpouch/README.md'), + npmReadme(readFileSync(join(root, 'README.md'), 'utf8'), version), + ); } function published(name, version) { diff --git a/scripts/release.test.mjs b/scripts/release.test.mjs index 6441afb..2a43c94 100644 --- a/scripts/release.test.mjs +++ b/scripts/release.test.mjs @@ -2,10 +2,17 @@ import assert from 'node:assert/strict'; import { mkdirSync, mkdtempSync, readFileSync, writeFileSync } from 'node:fs'; import { tmpdir } from 'node:os'; -import { join } from 'node:path'; +import { dirname, join } from 'node:path'; +import { fileURLToPath } from 'node:url'; import { test } from 'node:test'; import { PLATFORMS, build } from './npm-packages.mjs'; -import { checkVersion, stampCargoLock, stampCargoToml, stampPackageJson } from './release.mjs'; +import { + checkVersion, + npmReadme, + stampCargoLock, + stampCargoToml, + stampPackageJson, +} from './release.mjs'; test('only the workspace version in Cargo.toml changes', () => { const text = [ @@ -117,3 +124,32 @@ test('Windows line endings are stamped too', () => { assert.match(stamped, /name = "serde"\r\nversion = "1.0.0"\r\n/); assert.match(stamped, /name = "skillpouch"\r\nversion = "2.0.0"\r\n/); }); + +test('the npm README links to the release on GitHub', () => { + const text = [ + '', + 'Website ยท Install', + '![Dashboard](.github/assets/tui-overview.webp)', + 'See [`sp-harness`](crates/sp-harness) and [the licence](./LICENSE).', + ].join('\n'); + const readme = npmReadme(text, '1.2.3'); + const raw = 'https://raw.githubusercontent.com/skillpouch/skillpouch-cli/v1.2.3'; + const blob = 'https://github.com/skillpouch/skillpouch-cli/blob/v1.2.3'; + assert.match(readme, new RegExp(`src="${raw}/.github/assets/banner.png"`)); + assert.match(readme, new RegExp(`\\(${raw}/.github/assets/tui-overview.webp\\)`)); + assert.match(readme, new RegExp(`\\(${blob}/crates/sp-harness\\)`)); + assert.match(readme, new RegExp(`\\(${blob}/LICENSE\\)`)); + assert.match(readme, /href="https:\/\/skillpouch.net"/); + assert.match(readme, /href="#install"/); +}); + +test('every link in the real README works from npm', () => { + const root = join(dirname(fileURLToPath(import.meta.url)), '..'); + const readme = npmReadme(readFileSync(join(root, 'README.md'), 'utf8'), '1.2.3'); + const links = [ + ...readme.matchAll(/\b(?:src|href)="([^"]+)"/g), + ...readme.matchAll(/\]\(([^)\s]+)/g), + ].map(([, link]) => link); + assert.ok(links.length > 10); + for (const link of links) assert.match(link, /^(https:\/\/|#)/, link); +});