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
3 changes: 3 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@ jobs:
- name: Install dependencies
run: bun install --frozen-lockfile

- name: Build dependencies
run: bun run --cwd packages/registry-schema build

- name: Run tests with coverage and JUnit report
working-directory: packages/cli
run: |
Expand Down
9 changes: 9 additions & 0 deletions .please/docs/knowledge/tech-stack.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,15 @@
- **Package manager**: bun (workspaces)
- **Workspaces**: `packages/*`, `apps/*`

## Shared (`packages/registry-schema` — `@pleaseai/registry-schema`)

| Category | Technology |
|---|---|
| Runtime | Node.js (Pure ESM) |
| Language | TypeScript 5.7+ |
| Validation | Zod 3.x |
| Linting | ESLint 10 + @pleaseai/eslint-config |

## CLI (`packages/cli` — `@pleaseai/ask`)

| Category | Technology |
Expand Down
1 change: 1 addition & 0 deletions .please/docs/tracks.jsonl
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@
{"id":"registry-meta-20260407","type":"feature","status":"completed","phase":"implement","issue":"#5","created":"2026-04-07","section":"completed"}
{"id":"ecosystem-resolvers-20260407","type":"feature","status":"completed","phase":"implement","issue":"#6","created":"2026-04-07","section":"completed"}
{"id":"npm-publish-release-please-20260408","type":"chore","status":"planned","phase":"spec","issue":"#16","created":"2026-04-08","section":"active"}
{"id":"extract-shared-registry-20260408","type":"refactor","status":"in_progress","phase":"implement","issue":"#14","created":"2026-04-08","section":"active"}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"track_id": "extract-shared-registry-20260408",
"type": "refactor",
"status": "review",
"created_at": "2026-04-08T00:00:00Z",
"updated_at": "2026-04-08T00:00:00Z",
"issue": "#14",
"pr": "#19",
"project": ""
}
150 changes: 150 additions & 0 deletions .please/docs/tracks/completed/extract-shared-registry-20260408/plan.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
# Plan: Extract Shared Registry Schema Package

> Track: extract-shared-registry-20260408
> Spec: [spec.md](./spec.md)

## Overview

- **Source**: /please:plan
- **Track**: extract-shared-registry-20260408
- **Issue**: #14
- **Created**: 2026-04-08
- **Approach**: Pragmatic

## Purpose

After this change, both the CLI and registry app will import registry types, Zod schemas, and the `expandStrategies()` utility from a single shared package (`@pleaseai/registry-schema`). Developers can verify it works by running `bun run build` at the root and confirming all existing tests pass.

## Context

The CLI (`packages/cli/src/registry.ts`) defines `RegistryStrategy`, `RegistryAlias`, and `RegistryEntry` as TypeScript interfaces. The registry app (`apps/registry/content.config.ts`) defines semantically identical Zod schemas (`strategySchema`, `aliasSchema`) independently. The `expandStrategies()` function exists in both `packages/cli/src/registry-schema.ts` and `apps/registry/server/api/registry/[...slug].get.ts` with an explicit comment noting the intentional duplication due to Nitro's inability to resolve workspace imports.

A dedicated shared package with built output (`dist/`) resolves the Nitro constraint and eliminates all duplication. The CLI already depends on `zod`, so switching from hand-written interfaces to `z.infer<>` types is a net simplification. The registry app's `content.config.ts` uses `z` from `@nuxt/content` (a zod re-export), so it can consume exported Zod schemas from the shared package directly.

## Architecture Decision

A new `packages/registry-schema` package provides Zod schemas as the single source of truth. TypeScript types are derived via `z.infer<>`, eliminating the need to maintain parallel interface definitions. The package builds with `tsc` to `dist/`, the same as the CLI. Because `turbo.json` already has `"dependsOn": ["^build"]`, the shared package builds before its consumers automatically. The `content.config.ts` in the registry app will import the Zod schema objects and spread them into `defineCollection`, while the API handler will import `expandStrategies` directly — no more intentional duplication.

## Architecture Diagram

```
packages/registry-schema
(@pleaseai/registry-schema)
┌───────────────────────┐
│ src/index.ts │
│ • strategySchema (Zod) │
│ • aliasSchema (Zod) │
│ • registryEntrySchema │
│ • RegistryStrategy (T) │
│ • RegistryAlias (T) │
│ • RegistryEntry (T) │
│ • expandStrategies() │
└───────────┬───────────┘
┌───────────┴───────────┐
▼ ▼
packages/cli apps/registry
• registry.ts • content.config.ts
(re-exports types, (imports schemas)
removes interfaces) • server/api/[...slug].get.ts
• registry-schema.ts (imports expandStrategies,
(deleted) removes local duplication)
```

## Tasks

- [x] T001 Create `packages/registry-schema` package scaffold (file: packages/registry-schema/package.json)
- [x] T002 Define Zod schemas and inferred types with `expandStrategies()` (file: packages/registry-schema/src/index.ts, depends on T001)
- [x] T003 Add unit tests for shared package (file: packages/registry-schema/test/index.test.ts, depends on T002)
- [x] T004 [P] Migrate CLI to import from `@pleaseai/registry-schema` (file: packages/cli/src/registry.ts, depends on T002)
- [x] T005 [P] Migrate registry `content.config.ts` to import schemas (file: apps/registry/content.config.ts, depends on T002)
- [x] T006 [P] Migrate registry API handler to import from shared package (file: apps/registry/server/api/registry/[...slug].get.ts, depends on T002)
- [x] T007 Verify monorepo build, lint, and tests (depends on T004, T005, T006)

## Dependencies

```mermaid
graph LR
T001 --> T002 --> T003
T002 --> T004
T002 --> T005
T002 --> T006
T004 --> T007
T005 --> T007
T006 --> T007
```

## Key Files

### Create

- `packages/registry-schema/package.json` — shared package manifest
- `packages/registry-schema/tsconfig.json` — TypeScript config (mirrors CLI)
- `packages/registry-schema/eslint.config.ts` — ESLint config
- `packages/registry-schema/src/index.ts` — Zod schemas, inferred types, `expandStrategies()`
- `packages/registry-schema/test/index.test.ts` — unit tests

### Modify

- `packages/cli/src/registry.ts` — remove interfaces, re-export types from shared package
- `packages/cli/src/registry-schema.ts` — delete (moved to shared package)
- `packages/cli/package.json` — add `@pleaseai/registry-schema` dependency
- `apps/registry/content.config.ts` — import schemas from shared package
- `apps/registry/server/api/registry/[...slug].get.ts` — import `expandStrategies` and types
- `apps/registry/package.json` — add `@pleaseai/registry-schema` dependency

### Reuse

- `turbo.json` — no changes needed (`^build` already handles dependency ordering)
- `package.json` (root) — no changes needed (`packages/*` glob already includes new package)

## Verification

### Automated Tests

- [ ] `expandStrategies()` unit tests pass in shared package
- [ ] Existing CLI tests pass unchanged (`bun test --cwd packages/cli`)

### Observable Outcomes

- Running `bun run build` at root succeeds with no errors
- Running `bun run lint` at root succeeds with no errors
- `grep -r "expandStrategies" apps/registry/server/` shows import from `@pleaseai/registry-schema`, not local definition
- `grep -r "interface RegistryStrategy" packages/cli/src/` returns no matches

### Acceptance Criteria Check

- [ ] SC-1: `packages/registry-schema` exists with schemas, types, and `expandStrategies()`
- [ ] SC-2: CLI imports types from `@pleaseai/registry-schema`
- [ ] SC-3: Registry imports schemas/functions from `@pleaseai/registry-schema`
- [ ] SC-4: All CLI tests pass
- [ ] SC-5: Registry builds successfully
- [ ] SC-6: `bun run build` succeeds
- [ ] SC-7: `bun run lint` passes

## Decision Log

- Decision: Use Zod schemas as single source of truth, derive TypeScript types via `z.infer<>`
Rationale: Eliminates parallel maintenance of interfaces and schemas; CLI already depends on zod
Date/Author: 2026-04-08 / Claude
- Decision: Move zod to peerDependencies instead of dependencies
Rationale: Avoids dual Zod instance between shared package and @nuxt/content (review finding)
Date/Author: 2026-04-08 / Claude

## Outcomes & Retrospective

### What Was Shipped
- New `packages/registry-schema` package with Zod schemas, inferred types, and `expandStrategies()`
- CLI and registry app migrated to import from shared package
- All duplicated type definitions and functions eliminated

### What Went Well
- Turbo's `^build` dependency ordering worked out of the box — no config changes needed
- Workspace `packages/*` glob automatically included the new package
- All 146 existing CLI tests passed without modification after migration

### What Could Improve
- The Nitro "can't resolve workspace imports" constraint turned out to be solvable with a built package — could have been resolved earlier

### Tech Debt Created
- Pre-existing lint errors in `apps/registry/` Vue files remain (not introduced by this PR)
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
# Extract Shared Registry Schema Package

> Track: extract-shared-registry-20260408

## Overview

Extract duplicated registry types, Zod schemas, and utility functions from `packages/cli` and `apps/registry` into a new shared workspace package `packages/registry-schema` (`@pleaseai/registry-schema`).

Currently, `RegistryStrategy`, `RegistryAlias`, `RegistryEntry` interfaces are defined in `packages/cli/src/registry.ts`, while equivalent Zod schemas exist in `apps/registry/content.config.ts`. The `expandStrategies()` function is intentionally duplicated in both `packages/cli/src/registry-schema.ts` and `apps/registry/server/api/registry/[...slug].get.ts` because Nitro could not resolve cross-package imports. A dedicated shared package resolves this constraint.

## Scope

### In Scope

- **New package**: `packages/registry-schema` with `@pleaseai/registry-schema` as the package name
- **Zod schemas**: Define canonical Zod schemas for `Strategy`, `Alias`, and `RegistryEntry` in the shared package
- **TypeScript types**: Infer TypeScript types from Zod schemas (single source of truth)
- **Shared utilities**: Move `expandStrategies()` to the shared package
- **CLI migration**: Replace `RegistryStrategy`, `RegistryAlias`, `RegistryEntry` interfaces in `packages/cli/src/registry.ts` with re-exports from the shared package
- **Registry migration**: Replace local Zod schemas in `apps/registry/content.config.ts` and local interfaces/functions in `apps/registry/server/api/registry/[...slug].get.ts` with imports from the shared package
- **Workspace config**: Add `packages/registry-schema` to the bun workspace

### Out of Scope

- CLI command logic or behavior changes
- Registry API endpoint behavior changes
- New features or additional schemas beyond what currently exists
- Publishing `@pleaseai/registry-schema` to npm (internal workspace package only)

## Success Criteria

- [ ] SC-1: `packages/registry-schema` package exists with Zod schemas, inferred types, and `expandStrategies()`
- [ ] SC-2: `packages/cli` imports all registry types from `@pleaseai/registry-schema` — no local type definitions remain
- [ ] SC-3: `apps/registry` imports schemas and `expandStrategies()` from `@pleaseai/registry-schema` — no duplicated code remains
- [ ] SC-4: All existing CLI tests pass without modification
- [ ] SC-5: Registry app builds and dev server starts successfully
- [ ] SC-6: `bun run build` succeeds at the monorepo root
- [ ] SC-7: `bun run lint` passes across all packages

## Constraints

- No special constraints — free to restructure as needed
- External behavior (CLI output, API responses) must remain identical

## Technical Notes

- The shared package must be Pure ESM (`"type": "module"`) with `.js` import extensions
- Use the same ESLint config (`@pleaseai/eslint-config`) as other packages
- Zod is already used in `apps/registry` via `@nuxt/content` — the shared package should use `zod` directly as a dependency
- TypeScript types should be inferred from Zod schemas using `z.infer<>` to maintain a single source of truth
33 changes: 3 additions & 30 deletions apps/registry/content.config.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,5 @@
import { defineCollection, defineContentConfig, z } from '@nuxt/content'

const strategySchema = z.object({
source: z.enum(['npm', 'github', 'web', 'llms-txt']),
package: z.string().optional(),
repo: z.string().optional(),
branch: z.string().optional(),
tag: z.string().optional(),
docsPath: z.string().optional(),
url: z.string().optional(),
urls: z.array(z.string()).optional(),
maxDepth: z.number().optional(),
allowedPathPrefix: z.string().optional(),
})

const aliasSchema = z.object({
ecosystem: z.enum(['npm', 'pypi', 'pub', 'go', 'crates', 'hex', 'nuget', 'maven']),
name: z.string(),
})
import { defineCollection, defineContentConfig } from '@nuxt/content'
import { registryEntrySchema } from '@pleaseai/registry-schema'

export default defineContentConfig({
collections: {
Expand All @@ -30,17 +13,7 @@ export default defineContentConfig({
registry: defineCollection({
type: 'page',
source: 'registry/**/*.md',
schema: z.object({
name: z.string(),
description: z.string(),
repo: z.string().regex(/^[^/]+\/[^/]+$/, 'repo must be in "owner/name" form'),
docsPath: z.string().optional(),
homepage: z.string().optional(),
license: z.string().optional(),
aliases: z.array(aliasSchema).optional().default([]),
strategies: z.array(strategySchema).optional().default([]),
tags: z.array(z.string()).optional(),
}),
schema: registryEntrySchema,
}),
},
})
1 change: 1 addition & 0 deletions apps/registry/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
"dependencies": {
"@nuxt/content": "^3",
"@nuxt/ui": "^4",
"@pleaseai/registry-schema": "workspace:*",
"nuxt": "^4",
"tailwindcss": "^4",
"vue": "latest",
Expand Down
49 changes: 5 additions & 44 deletions apps/registry/server/api/registry/[...slug].get.ts
Original file line number Diff line number Diff line change
@@ -1,44 +1,5 @@
interface Strategy {
source: 'npm' | 'github' | 'web' | 'llms-txt'
package?: string
repo?: string
branch?: string
tag?: string
docsPath?: string
url?: string
urls?: string[]
maxDepth?: number
allowedPathPrefix?: string
}

interface Alias {
ecosystem: string
name: string
}

/**
* Expand a registry entry's strategies from `repo` when strategies is empty.
*
* NOTE: This function is intentionally duplicated from
* `packages/cli/src/registry-schema.ts`. The Nitro build process cannot
* reliably resolve cross-package workspace imports, so sharing via a
* workspace package is not feasible here. Keep this implementation in sync
* with the canonical source in registry-schema.ts.
*/
function expandStrategies(entry: {
repo?: string
docsPath?: string
strategies?: Strategy[]
}): Strategy[] {
const { repo, docsPath, strategies } = entry
if (strategies && strategies.length > 0) return strategies
if (repo) {
const s: Strategy = { source: 'github', repo }
if (docsPath) s.docsPath = docsPath
return [s]
}
throw new Error('Registry entry requires at least one of `repo` or `strategies`')
}
import type { RegistryAlias, RegistryStrategy } from '@pleaseai/registry-schema'
import { expandStrategies } from '@pleaseai/registry-schema'

export default defineEventHandler(async (event) => {
const slug = getRouterParam(event, 'slug')
Expand All @@ -63,7 +24,7 @@ export default defineEventHandler(async (event) => {
if (directEntries.length > 0) {
const entry = directEntries[0]

let strategies: Strategy[]
let strategies: RegistryStrategy[]
try {
strategies = expandStrategies({
repo: entry.repo,
Expand Down Expand Up @@ -94,7 +55,7 @@ export default defineEventHandler(async (event) => {
// 2. Fallback: search by alias (ecosystem/name)
const allEntries = await queryCollection(event, 'registry').all()
const matched = allEntries.find((entry) => {
const aliases = entry.aliases as Alias[] | undefined
const aliases = entry.aliases as RegistryAlias[] | undefined
if (!aliases) return false
return aliases.some(a => a.ecosystem === first && a.name === second)
})
Expand All @@ -103,7 +64,7 @@ export default defineEventHandler(async (event) => {
throw createError({ statusCode: 404, statusMessage: `Entry not found: ${slug}` })
}

let strategies: Strategy[]
let strategies: RegistryStrategy[]
try {
strategies = expandStrategies({
repo: matched.repo,
Expand Down
Loading