import { mkdtemp, writeFile } from 'node:fs/promises' import { tmpdir } from 'node:os' import { join } from 'node:path' import { afterEach, describe, expect, test } from 'bun:test' import { zipSync } from 'fflate' import { createTempHome } from '../helpers/temp-env' import { startFakeRegistry } from '../helpers/fake-registry' import { runCli } from '../helpers/run-cli' let registry: Awaited> | undefined afterEach(() => { registry?.stop() registry = undefined }) // --------------------------------------------------------------------------- // Helpers // --------------------------------------------------------------------------- async function login(env: { home: string }, registryUrl: string) { const result = await runCli(['login', '--registry', registryUrl, '--token', 'sk_ok'], { HOME: env.home, USERPROFILE: env.home }) if (result.exitCode !== 0) { throw new Error(`login failed: ${result.stderr}`) } } async function makeTempDir(...files: Array<[string, string]>) { const dir = await mkdtemp(join(tmpdir(), 'skillhub-publish-dir-')) for (const [name, content] of files) { await writeFile(join(dir, name), content) } return dir } async function makeTempZip(name: string) { const dir = await mkdtemp(join(tmpdir(), 'skillhub-publish-zip-')) const zipPath = join(dir, name) const bytes = zipSync({ 'SKILL.md': new TextEncoder().encode('# Demo') }) await writeFile(zipPath, bytes) return zipPath } async function makeTempTxt(name: string) { const dir = await mkdtemp(join(tmpdir(), 'skillhub-publish-txt-')) const txtPath = join(dir, name) await writeFile(txtPath, 'not a zip') return txtPath } // --------------------------------------------------------------------------- // P0 — must-have // --------------------------------------------------------------------------- describe('publish command — P0', () => { test('unauthenticated publish is rejected with EXIT.auth', async () => { const env = await createTempHome() registry = await startFakeRegistry({ token: 'sk_ok' }) const dir = await makeTempDir(['SKILL.md', '# Demo']) const result = await runCli(['publish', dir, '--registry', registry.url], { HOME: env.home, USERPROFILE: env.home }) expect(result.exitCode).toBe(2) expect(result.stderr).toContain('authentication') }) test('path not found returns filesystem error', async () => { const env = await createTempHome() registry = await startFakeRegistry({ token: 'sk_ok' }) await login(env, registry.url) const result = await runCli(['publish', '/does/not/exist', '--registry', registry.url], { HOME: env.home, USERPROFILE: env.home }) expect(result.exitCode).not.toBe(0) expect(result.stderr).toContain('not found') }) test('non-zip file is rejected', async () => { const env = await createTempHome() registry = await startFakeRegistry({ token: 'sk_ok' }) await login(env, registry.url) const txtPath = await makeTempTxt('skill.txt') const result = await runCli(['publish', txtPath, '--registry', registry.url], { HOME: env.home, USERPROFILE: env.home }) expect(result.exitCode).not.toBe(0) expect(result.stderr).toContain('zip') }) test('directory happy path: exit 0, default namespace=global, fileName ends in .zip, visibility=PUBLIC', async () => { const env = await createTempHome() registry = await startFakeRegistry({ token: 'sk_ok' }) await login(env, registry.url) const dir = await makeTempDir(['SKILL.md', '# Demo'], ['index.js', 'console.log("hi")']) const result = await runCli(['publish', dir, '--registry', registry.url], { HOME: env.home, USERPROFILE: env.home }) expect(result.exitCode).toBe(0) expect(registry.received.publish).not.toBeNull() expect(registry.received.publish!.namespace).toBe('global') expect(registry.received.publish!.fileName).toMatch(/\.zip$/) expect(registry.received.publish!.visibility).toBe('PUBLIC') }) test('zip file happy path: exit 0, fileName matches passed file', async () => { const env = await createTempHome() registry = await startFakeRegistry({ token: 'sk_ok' }) await login(env, registry.url) const zipPath = await makeTempZip('my-skill.zip') const result = await runCli(['publish', zipPath, '--registry', registry.url], { HOME: env.home, USERPROFILE: env.home }) expect(result.exitCode).toBe(0) expect(registry.received.publish).not.toBeNull() expect(registry.received.publish!.fileName).toBe('my-skill.zip') }) describe('visibility mapping', () => { test('--visibility public → PUBLIC', async () => { const env = await createTempHome() registry = await startFakeRegistry({ token: 'sk_ok' }) await login(env, registry.url) const dir = await makeTempDir(['SKILL.md', '# Demo']) await runCli(['publish', dir, '--registry', registry.url, '--visibility', 'public'], { HOME: env.home, USERPROFILE: env.home }) expect(registry.received.publish!.visibility).toBe('PUBLIC') }) test('--visibility namespace-only → NAMESPACE_ONLY', async () => { const env = await createTempHome() registry = await startFakeRegistry({ token: 'sk_ok' }) await login(env, registry.url) const dir = await makeTempDir(['SKILL.md', '# Demo']) await runCli(['publish', dir, '--registry', registry.url, '--visibility', 'namespace-only'], { HOME: env.home, USERPROFILE: env.home }) expect(registry.received.publish!.visibility).toBe('NAMESPACE_ONLY') }) test('--visibility private → PRIVATE', async () => { const env = await createTempHome() registry = await startFakeRegistry({ token: 'sk_ok' }) await login(env, registry.url) const dir = await makeTempDir(['SKILL.md', '# Demo']) await runCli(['publish', dir, '--registry', registry.url, '--visibility', 'private'], { HOME: env.home, USERPROFILE: env.home }) expect(registry.received.publish!.visibility).toBe('PRIVATE') }) }) }) // --------------------------------------------------------------------------- // P1 — should-have // --------------------------------------------------------------------------- describe('publish command — P1', () => { test('--namespace override is forwarded to server', async () => { const env = await createTempHome() registry = await startFakeRegistry({ token: 'sk_ok' }) await login(env, registry.url) const dir = await makeTempDir(['SKILL.md', '# Demo']) const result = await runCli(['publish', dir, '--registry', registry.url, '--namespace', 'myteam'], { HOME: env.home, USERPROFILE: env.home }) expect(result.exitCode).toBe(0) expect(registry.received.publish!.namespace).toBe('myteam') }) test('--json output has correct shape including detailUrl', async () => { const env = await createTempHome() registry = await startFakeRegistry({ token: 'sk_ok' }) await login(env, registry.url) const dir = await makeTempDir(['SKILL.md', '# Demo']) const result = await runCli(['publish', dir, '--registry', registry.url, '--json'], { HOME: env.home, USERPROFILE: env.home }) expect(result.exitCode).toBe(0) const json = JSON.parse(result.stdout) expect(json.ok).toBe(true) expect(json.namespace).toBe('global') expect(typeof json.slug).toBe('string') expect(typeof json.version).toBe('string') expect(typeof json.visibility).toBe('string') expect(json.detailUrl).toContain(registry.url) expect(json.detailUrl).toContain('global') expect(json.detailUrl).toContain(encodeURIComponent(json.slug)) }) test('server error during publish returns EXIT.generic', async () => { const env = await createTempHome() // 'server_error' returns HTTP 500; request reached registry but failed, so EXIT.generic. registry = await startFakeRegistry({ token: 'sk_ok', failures: { publish: 'server_error' } }) await login(env, registry.url) const dir = await makeTempDir(['SKILL.md', '# Demo']) const result = await runCli(['publish', dir, '--registry', registry.url], { HOME: env.home, USERPROFILE: env.home }) expect(result.exitCode).toBe(1) expect(result.stderr).toContain('registry') }) })