diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 25387c23..12b001ce 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -29,7 +29,7 @@ jobs: - run: pnpm i - - run: pnpm build + - run: pnpm -r build - run: pnpm publish -r --access public --no-git-checks env: diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index c3ea63ec..42e8e831 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -25,6 +25,7 @@ jobs: node-version: lts/* cache: pnpm - run: pnpm i + - run: pnpm --filter skilld-protocol build - run: pnpm lint - run: pnpm typecheck - run: pnpm build diff --git a/build.config.ts b/build.config.ts index f5d02ce9..17a2c0a8 100644 --- a/build.config.ts +++ b/build.config.ts @@ -5,12 +5,14 @@ export default defineBuildConfig({ { type: 'bundle', input: [ - './src/index.ts', './src/cli-entry.ts', './src/cli.ts', './src/prepare.ts', './src/retriv/worker.ts', ], + rolldown: { + external: ['@napi-rs/keyring', /@napi-rs\/keyring-/], + }, }, ], }) diff --git a/package.json b/package.json index a67c3032..459b60f3 100644 --- a/package.json +++ b/package.json @@ -31,14 +31,6 @@ "cursor", "codex" ], - "exports": { - ".": { - "types": "./dist/index.d.mts", - "import": "./dist/index.mjs" - } - }, - "main": "./dist/index.mjs", - "types": "./dist/index.d.mts", "bin": { "skilld": "./dist/cli-entry.mjs" }, @@ -57,7 +49,7 @@ "typecheck": "tsc --noEmit", "test": "vitest", "test:run": "vitest run", - "release": "pnpm build && bumpp -x \"npx changelogen --output=CHANGELOG.md\"", + "release": "pnpm -r build && bumpp -r -x \"npx changelogen --output=CHANGELOG.md\"", "prepack": "pnpm run build", "prepare": "test -z \"$CI\" && skilld prepare || true" }, @@ -79,18 +71,23 @@ "pathe": "catalog:", "retriv": "catalog:", "semver": "catalog:", + "skilld-protocol": "workspace:*", "sqlite-vec": "catalog:deps", "std-env": "catalog:", "typebox": "catalog:", "typescript": "catalog:", "unagent": "catalog:" }, + "optionalDependencies": { + "@napi-rs/keyring": "^1.1.6" + }, "devDependencies": { "@antfu/eslint-config": "catalog:dev-lint", "@types/node": "catalog:dev-build", "@types/semver": "catalog:", "@vitest/coverage-v8": "catalog:dev-test", "bumpp": "catalog:", + "eslint": "catalog:dev-lint", "obuild": "catalog:dev-build", "tsx": "catalog:", "vitest": "catalog:dev-test" diff --git a/packages/protocol/README.md b/packages/protocol/README.md new file mode 100644 index 00000000..20552f25 --- /dev/null +++ b/packages/protocol/README.md @@ -0,0 +1,21 @@ +# skilld-protocol + +Wire shapes and constants shared between the [skilld CLI](https://github.com/skilld-dev/skilld) and [skilld.dev](https://skilld.dev). The single source of truth for everything that crosses that boundary: telemetry, audit, auth, device flow, collection manifests. + +## Install + +```sh +pnpm add skilld-protocol +``` + +ESM-only. Node ≥18. One peer-free dep: `zod` v4. + +## Subpaths + +- `skilld-protocol/wire` — every endpoint shape as a zod schema (suffix `Schema`) and the matching inferred TS type (no suffix). Use `import { FooSchema }` for runtime validation; `import type { Foo }` for the type. +- `skilld-protocol/constants` — readonly tuples backing the closed enums plus their inferred unions. +- `skilld-protocol/test-fixtures` — canonical payloads each consumer round-trips through their schema on CI. + +## Repo + +This package lives inside the [skilld CLI](https://github.com/skilld-dev/skilld) monorepo at `packages/protocol`. The CLI consumes it as a workspace dep; skilld.dev consumes the published npm version. diff --git a/packages/protocol/build.config.ts b/packages/protocol/build.config.ts new file mode 100644 index 00000000..03c4d9de --- /dev/null +++ b/packages/protocol/build.config.ts @@ -0,0 +1,15 @@ +import { defineBuildConfig } from 'obuild/config' + +export default defineBuildConfig({ + entries: [ + { + type: 'bundle', + input: [ + './src/wire.ts', + './src/constants.ts', + './src/test-fixtures.ts', + ], + outDir: './dist', + }, + ], +}) diff --git a/packages/protocol/eslint.config.mjs b/packages/protocol/eslint.config.mjs new file mode 100644 index 00000000..61eac19e --- /dev/null +++ b/packages/protocol/eslint.config.mjs @@ -0,0 +1,8 @@ +import antfu from '@antfu/eslint-config' + +export default antfu({ + type: 'lib', + typescript: true, + stylistic: true, + ignores: ['dist', 'node_modules'], +}) diff --git a/packages/protocol/package.json b/packages/protocol/package.json new file mode 100644 index 00000000..72018180 --- /dev/null +++ b/packages/protocol/package.json @@ -0,0 +1,61 @@ +{ + "name": "skilld-protocol", + "type": "module", + "version": "0.2.2", + "description": "Wire shapes and constants shared between the skilld CLI and skilld.dev", + "author": { + "name": "Harlan Wilton", + "email": "harlan@harlanzw.com", + "url": "https://harlanzw.com/" + }, + "license": "MIT", + "repository": { + "type": "git", + "url": "git+https://github.com/skilld-dev/skilld.git", + "directory": "packages/protocol" + }, + "keywords": ["skilld", "protocol", "zod", "schema"], + "sideEffects": false, + "exports": { + "./wire": { + "types": "./dist/wire.d.mts", + "import": "./dist/wire.mjs" + }, + "./constants": { + "types": "./dist/constants.d.mts", + "import": "./dist/constants.mjs" + }, + "./test-fixtures": { + "types": "./dist/test-fixtures.d.mts", + "import": "./dist/test-fixtures.mjs" + } + }, + "files": ["dist"], + "engines": { + "node": ">=18" + }, + "scripts": { + "build": "obuild", + "lint": "eslint .", + "lint:fix": "eslint . --fix", + "typecheck": "tsc --noEmit", + "test": "vitest", + "test:run": "vitest run", + "publint": "publint", + "attw": "pnpm pack && attw skilld-protocol-*.tgz --ignore-rules no-resolution cjs-resolves-to-esm && rm skilld-protocol-*.tgz", + "prepack": "pnpm build" + }, + "dependencies": { + "zod": "catalog:" + }, + "devDependencies": { + "@antfu/eslint-config": "catalog:dev-lint", + "@arethetypeswrong/cli": "catalog:", + "@types/node": "catalog:dev-build", + "eslint": "catalog:dev-lint", + "obuild": "catalog:dev-build", + "publint": "catalog:", + "typescript": "catalog:", + "vitest": "catalog:dev-test" + } +} diff --git a/packages/protocol/src/constants.ts b/packages/protocol/src/constants.ts new file mode 100644 index 00000000..031a5a96 --- /dev/null +++ b/packages/protocol/src/constants.ts @@ -0,0 +1,63 @@ +/** + * Canonical readonly tuples for closed enums and their inferred TS unions. + * + * The wire schemas import these tuples for `z.enum(...)`. The TS unions are + * exported for tooling (autocomplete, exhaustive switches). Telemetry surface + * is intentionally open on the wire (`z.string().min(1).max(32)` in + * `wire/telemetry.ts`) so the CLI can ship new surfaces without a coordinated + * protocol bump; the closed tuple here is the *currently canonical* list. + */ + +export const TELEMETRY_EVENTS = [ + 'install', + 'install-failed', + 'update', + 'audit-warn', + 'audit-fail', + 'audit-blocked', + 'auth-flow', + 'pull-checklist', +] as const + +export const TELEMETRY_SURFACES = [ + 'cli:add', + 'cli:pull', + 'cli:prepare', + 'cli:update', + 'cli:wizard', + 'cli:auth', +] as const + +export const SOURCE_KINDS = [ + 'npm', + 'gh', + 'crate', + 'collection', + 'curator', +] as const + +export const AUDIT_STATUSES = [ + 'pass', + 'warn', + 'fail', + 'unaudited', +] as const + +export const AUDIT_ENTRY_STATUSES = [ + 'pass', + 'warn', + 'fail', +] as const + +export const AUTH_FLOWS = [ + 'pkce', + 'device', + 'oidc', +] as const + +export type TelemetryEvent = typeof TELEMETRY_EVENTS[number] +export type TelemetrySurface = typeof TELEMETRY_SURFACES[number] +export type SourceKind = typeof SOURCE_KINDS[number] +export type AuditStatus = typeof AUDIT_STATUSES[number] +export type AuditEntryStatus = typeof AUDIT_ENTRY_STATUSES[number] +export type AuthFlow = typeof AUTH_FLOWS[number] diff --git a/packages/protocol/src/test-fixtures.ts b/packages/protocol/src/test-fixtures.ts new file mode 100644 index 00000000..611bf514 --- /dev/null +++ b/packages/protocol/src/test-fixtures.ts @@ -0,0 +1,215 @@ +/** + * Canonical payloads each consumer round-trips through the wire schemas on CI. + * A breaking schema change invalidates a fixture → both consumer test suites + * go red on the bump. Drift detector at zero conditional-logic cost. + */ + +import type { + AuditEntry, + ChangeEntry, + CliEventInput, + CollectionManifest, + CollectionSummary, + DevicePollInput, + DevicePollResponse, + DeviceStartInput, + DeviceStartResponse, + DigestResponse, + OauthRefreshInput, + OauthTokenInput, + OidcExchangeInput, + SkillDetailResponse, + SkillLiveResponse, + SkillsResolveInput, + SkillsResolveResponse, + TokenResponse, +} from './wire.ts' + +export const fixtures = { + audit: { + skillLivePass: { + id: 'antfu/skills/vue', + installs: 1234, + formatted: '1.2k', + audits: [ + { provider: 'skills.sh', slug: 'static', status: 'pass' }, + { provider: 'skills.sh', slug: 'license', status: 'pass' }, + ], + source: 'skills.sh', + fetchedAt: '2026-05-13T00:00:00.000Z', + }, + skillLiveWarn: { + id: 'antfu/skills/motion-v', + installs: 42, + formatted: '42', + audits: [ + { provider: 'skills.sh', slug: 'static', status: 'pass' }, + { provider: 'skills.sh', slug: 'deps', status: 'warn', summary: 'wildcard import', riskLevel: 'medium', categories: ['imports'] }, + ], + source: 'skills.sh', + fetchedAt: '2026-05-13T00:00:00.000Z', + }, + entryFail: { + provider: 'skills.sh', + slug: 'static', + status: 'fail', + summary: 'detected eval()', + auditedAt: '2026-05-12T18:00:00.000Z', + }, + }, + auth: { + tokenResponse: { + accessToken: 'eyJhbGc...stub', + refreshToken: 'r-32-bytes-base64url', + expiresAt: 1_715_600_000, + login: 'harlanzw', + scopes: 'cli', + }, + tokenResponseOidc: { + accessToken: 'eyJhbGc...oidc', + expiresAt: 1_715_600_000, + login: 'harlanzw', + }, + oauthTokenInput: { + code: 'auth-code-1234567890abcdef', + code_verifier: 'verifier-32-bytes-or-more-padding-here', + redirect_uri: 'http://127.0.0.1:50123/', + }, + oauthRefreshInput: { + refresh_token: 'r-32-bytes-base64url', + }, + oidcExchangeInput: { + id_token: `${'a'.repeat(64)}.payload.signature`, + }, + }, + device: { + startInput: { + cli_version: '2.0.0', + machine_hint: 'darwin-arm64', + }, + pollInput: { + device_code: 'dc-128-bytes-base64url-string', + }, + startResponse: { + device_code: 'dc-128-bytes', + user_code: 'WDJB-MJHT', + verification_uri: 'https://skilld.dev/cli/authorize', + interval: 5, + expires_in: 600, + }, + pollPending: { status: 'pending' }, + pollAuthorized: { + status: 'authorized', + tokens: { + accessToken: 'eyJhbGc...stub', + refreshToken: 'r-32-bytes-base64url', + expiresAt: 1_715_600_000, + login: 'harlanzw', + }, + }, + }, + telemetry: { + installEvent: { + event: 'install', + surface: 'cli:add', + sourceKind: 'npm', + slug: 'vue', + cliVersion: '2.0.0', + agent: 'claude-code', + }, + auditFailEvent: { + event: 'audit-fail', + surface: 'cli:pull', + sourceKind: 'gh', + slug: 'antfu/skills', + cliVersion: '2.0.0', + }, + authFlowEvent: { + event: 'auth-flow', + surface: 'cli:auth', + cliVersion: '2.0.0', + flow: 'pkce', + }, + }, + collections: { + manifest: { + name: 'My Vue stack', + preamble: 'Tools I reach for on every Vue project.', + items: [ + { kind: 'npm', package: 'vue' }, + { kind: 'npm', package: 'motion-v' }, + { kind: 'gh', owner: 'antfu', repo: 'skills' }, + ], + }, + summary: { + slug: 'vue-stack', + name: 'My Vue stack', + itemCount: 3, + }, + change: { + repo: 'antfu/skills', + skill: 'vue', + at: '2026-05-13T00:00:00.000Z', + summary: 'Added Pinia composables.', + }, + digestResponse: { + user: { id: 2, login: 'harlan-zw' }, + windowStart: 1_715_500_000, + windowEnd: 1_715_600_000, + entries: [ + { repo: 'antfu/skills', skill: 'vue', at: '2026-05-13T00:00:00.000Z', summary: 'Added Pinia composables.' }, + ], + }, + }, + skills: { + resolveInput: { + items: [ + { packageName: 'vue' }, + { packageName: 'motion-v', owner: 'antfu' }, + ], + }, + resolveResponse: { + 'vue': { owner: 'antfu', repo: 'skills', official: true }, + 'motion-v': { owner: 'antfu', repo: 'skills', official: false }, + }, + detail: { + owner: 'antfu', + repo: 'skills', + name: 'vue', + displayName: 'Vue', + installs: 1234, + branch: 'main', + skillPath: 'vue/SKILL.md', + raw: '# Vue\n\nUse