feat: add complete monorepo structure and all packages

This completes the initial monorepo setup with all packages and configuration.

Packages added:
- @tpmjs/ui - React component library with .ts-only components
- @tpmjs/utils - Utility functions (cn, format)
- @tpmjs/types - TypeScript types and Zod schemas
- @tpmjs/env - Environment variable validation
- @tpmjs/test - Shared Vitest configuration
- @tpmjs/mocks - MSW mock server
- @tpmjs/config - Shared configurations (Biome, ESLint, Tailwind, TypeScript)

Applications added:
- @tpmjs/web - Next.js 16 App Router

Infrastructure:
- Turborepo configuration with build pipeline
- pnpm workspace setup
- GitHub Actions CI/CD
- Changesets for versioning
- Lefthook pre-commit hooks
- VSCode workspace settings
- Comprehensive documentation

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Ajax Davis 2025-11-25 21:59:40 +10:00
parent bc2f4a01cf
commit 78e6b9f4b5
67 changed files with 9889 additions and 0 deletions

11
.changeset/README.md Normal file
View file

@ -0,0 +1,11 @@
# Changesets
This folder contains changeset files which describe changes to packages in this monorepo.
To create a new changeset, run:
```bash
pnpm changeset
```
Then follow the prompts to describe your changes.

20
.changeset/config.json Normal file
View file

@ -0,0 +1,20 @@
{
"$schema": "https://unpkg.com/@changesets/config@3.0.3/schema.json",
"changelog": "@changesets/cli/changelog",
"commit": false,
"fixed": [],
"linked": [],
"access": "public",
"baseBranch": "main",
"updateInternalDependencies": "patch",
"ignore": [
"@tpmjs/config",
"@tpmjs/eslint-config",
"@tpmjs/tailwind-config",
"@tpmjs/tsconfig",
"@tpmjs/test",
"@tpmjs/mocks",
"@tpmjs/storybook",
"@tpmjs/web"
]
}

95
.github/workflows/ci.yml vendored Normal file
View file

@ -0,0 +1,95 @@
name: CI
on:
push:
branches: [main]
pull_request:
branches: [main]
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
with:
version: 10.14.0
- uses: actions/setup-node@v4
with:
node-version: 21
cache: 'pnpm'
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Lint
run: pnpm lint
- name: Format check
run: pnpm format:check
type-check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
with:
version: 10.14.0
- uses: actions/setup-node@v4
with:
node-version: 21
cache: 'pnpm'
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Type check
run: pnpm type-check
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
with:
version: 10.14.0
- uses: actions/setup-node@v4
with:
node-version: 21
cache: 'pnpm'
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Test
run: pnpm test
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
with:
version: 10.14.0
- uses: actions/setup-node@v4
with:
node-version: 21
cache: 'pnpm'
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Build
run: pnpm build

47
.github/workflows/release.yml vendored Normal file
View file

@ -0,0 +1,47 @@
name: Release
on:
push:
branches:
- main
concurrency: ${{ github.workflow }}-${{ github.ref }}
jobs:
release:
name: Release
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: write
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
with:
version: 10.14.0
- uses: actions/setup-node@v4
with:
node-version: 21
cache: 'pnpm'
registry-url: 'https://registry.npmjs.org'
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Build
run: pnpm build
- name: Create Release Pull Request or Publish
id: changesets
uses: changesets/action@v1
with:
publish: pnpm changeset:publish
version: pnpm changeset:version
commit: 'chore: version packages'
title: 'chore: version packages'
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}

44
.gitignore vendored Normal file
View file

@ -0,0 +1,44 @@
# dependencies
node_modules
.pnp
.pnp.js
# testing
coverage
.nyc_output
# next.js
.next/
out/
build
dist
# misc
.DS_Store
*.pem
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
# local env files
.env*.local
.env
# turbo
.turbo
# typescript
*.tsbuildinfo
# ide
.idea
# storybook
storybook-static
# changesets
.changeset/*.md
!.changeset/README.md

10
.npmrc Normal file
View file

@ -0,0 +1,10 @@
# Strict installation
auto-install-peers=true
strict-peer-dependencies=false
# Performance
prefer-frozen-lockfile=true
shamefully-hoist=false
# Publishing
access=public

10
.vscode/settings.json vendored Normal file
View file

@ -0,0 +1,10 @@
{
"editor.formatOnSave": true,
"editor.defaultFormatter": "biomejs.biome",
"editor.codeActionsOnSave": {
"quickfix.biome": "explicit",
"source.organizeImports.biome": "explicit"
},
"typescript.tsdk": "node_modules/typescript/lib",
"typescript.enablePromptUseWorkspaceTsdk": true
}

242
CLAUDE.md Normal file
View file

@ -0,0 +1,242 @@
# TPMJS - Tool Package Manager for AI Agents
## Philosophy
TPMJS is a registry and package manager for AI agent tools. Just as npm transformed how developers share and consume JavaScript packages, TPMJS aims to do the same for the emerging ecosystem of AI agent tooling.
### The Problem We're Solving
AI agents are becoming increasingly capable, but they face a fundamental challenge: **tool discovery and selection at scale**.
1. **Context Window Limitations** - When an agent has access to 10+ tools, LLMs struggle to remember and correctly select from all available options. Tool schemas consume precious context tokens.
2. **Tool Hallucination** - Models sometimes attempt to call tools that don't exist, or use incorrect parameter schemas, leading to failed executions and poor user experiences.
3. **Static Tool Sets** - Most agent implementations hardcode their available tools at build time. There's no standard way to discover, add, or share tools dynamically.
4. **Fragmented Ecosystem** - Developers building AI agents are recreating the same tools (web search, file operations, API integrations) over and over. There's no central place to share and discover production-ready implementations.
### Our Vision
We believe AI agent development should be:
- **Elegant** - Simple APIs, clear conventions, minimal boilerplate
- **Productive** - Leverage community-built tools instead of reinventing wheels
- **Safe** - Vetted tools with clear security boundaries and permissions
## Core Concepts
### Tools
A tool is a capability that an AI agent can invoke. Tools have:
- A unique name/identifier
- A description (used for semantic search and LLM understanding)
- A parameter schema (typically defined with Zod or JSON Schema)
- An implementation function
### Registry
The registry is the central index of available tools. It enables:
- Browsing by category
- Semantic search (find tools by what they do, not just their name)
- Version management
- Usage analytics
### Meta-Tools
Meta-tools are tools that help agents work with other tools. The most important is `tool-search`, which allows an agent to query the registry and load only the tools relevant to its current task. This "search-then-execute" pattern dramatically improves accuracy and token efficiency.
## How It Works
### The Search-Then-Execute Pattern
Instead of loading all tool schemas into context upfront (expensive and error-prone), agents using TPMJS:
1. **Search** - Use the `tool-search` meta-tool to find relevant tools based on the current task
2. **Load** - Dynamically load only the matched tools into context
3. **Execute** - Make a follow-up call with the focused tool set
This pattern:
- Reduces token usage (only load what you need)
- Improves selection accuracy (smaller choice set)
- Eliminates hallucination (tools are confirmed to exist before use)
- Enables runtime flexibility (tools can be added/removed without restarts)
## Technical Details
### Compatibility
- TypeScript-first with full type safety via Zod schemas
- Compatible with Anthropic AI SDK, OpenAI, and other major providers
- Minimal footprint (~340 tokens for the meta-tool)
### Scale
- Supports registries with 1,000+ tools
- Sub-2ms search latency
- Semantic search, fuzzy matching, and category filtering
## Categories
Tools in the registry span:
- Web & APIs
- Databases
- Documents
- Images
- Email
- Calendar
- Search
- Code Execution
- Communication
- Analytics
- Security
- Workflows
## Development Notes
This project is in early development. Key areas to work on:
- [ ] Core registry API design
- [ ] Tool schema specification
- [ ] CLI for publishing and discovering tools
- [ ] SDK integrations (Anthropic, OpenAI, etc.)
- [ ] Search algorithm (semantic + fuzzy matching)
- [ ] Security model and sandboxing
- [ ] Documentation and examples
## Open Questions
1. **Trust & Security** - How do we vet tools? What sandboxing is needed?
2. **Versioning** - How do tools handle breaking changes?
3. **Monetization** - Free tier + Pro? Marketplace cuts?
4. **Governance** - Who decides what gets published? Moderation?
5. **Offline/Local** - Can tools be cached locally? Private registries?
---
*"The registry for AI tools. Discover, share, and integrate tools that give your agents superpowers."*
---
## Monorepo Setup
This project uses a Turborepo monorepo architecture with the following structure:
### Packages
**Published to npm (@tpmjs scope):**
- `@tpmjs/ui` - React component library with .ts-only components
- `@tpmjs/utils` - Utility functions (cn, format, etc.)
- `@tpmjs/types` - Shared TypeScript types and Zod schemas
- `@tpmjs/env` - Environment variable validation with Zod
**Internal tooling (private):**
- `@tpmjs/config` - Shared configurations (Biome, ESLint, Tailwind, TypeScript)
- `@tpmjs/eslint-config` - ESLint configuration with module boundary rules
- `@tpmjs/tailwind-config` - Tailwind configuration with design tokens
- `@tpmjs/tsconfig` - TypeScript configurations (base, nextjs, react-library)
- `@tpmjs/test` - Vitest shared configuration
- `@tpmjs/mocks` - MSW mock server for testing
- `@tpmjs/storybook` - Component documentation and showcase
### Applications
- `@tpmjs/web` - Next.js 16 App Router application (main website)
### Architecture Principles
#### 1. .ts-only React Components
All UI components use `.ts` extension instead of `.tsx` and utilize `createElement`:
```typescript
import { createElement, forwardRef } from 'react';
export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
(props, ref) => createElement('button', { ref, ...props })
);
```
**Why?**
- Explicit runtime behavior
- Prevents JSX spreading anti-patterns
- Better for code generation
- Forces consideration of every prop
#### 2. No Barrel Exports
Components are imported directly without `index.ts` files:
```typescript
// Good
import { Button } from '@tpmjs/ui/Button/Button';
// Bad (not allowed)
import { Button } from '@tpmjs/ui';
```
**Benefits:**
- Clearer dependency graphs
- Better tree-shaking
- Prevents circular dependencies
- Explicit imports
#### 3. Module Boundaries
ESLint enforces strict module boundaries:
- Apps can only import from published packages
- Packages cannot import from apps
- UI package cannot import from utils (stays dependency-free)
#### 4. Shared Configurations
All configuration is centralized in `packages/config/`:
- **Biome** - Formatting + basic linting
- **ESLint** - Semantic rules and module boundaries
- **Tailwind** - Design tokens and shared theme
- **TypeScript** - Multiple configs for different contexts
### Development Workflow
```bash
# Install dependencies
pnpm install
# Run development servers
pnpm dev
# Build all packages
pnpm build
# Run tests
pnpm test
# Lint and format
pnpm lint
pnpm format
```
### Component Development
1. Create component in `packages/ui/src/ComponentName/ComponentName.ts`
2. Use `.ts` extension with `createElement`
3. Add tests in `ComponentName.test.ts`
4. Export in `package.json` exports map
5. Add Storybook story in `packages/storybook/stories/`
### Publishing Flow
1. Make changes to packages
2. Create changeset: `pnpm changeset`
3. Version packages: `pnpm changeset:version`
4. Publish to npm: `pnpm changeset:publish`
5. Push with tags: `git push --follow-tags`
### Tech Stack
- **Build System:** Turborepo
- **Package Manager:** pnpm
- **TypeScript:** Strict mode, composite projects
- **React:** v19 with .ts-only components
- **Next.js:** v16 App Router
- **Styling:** Tailwind CSS
- **Testing:** Vitest + Testing Library
- **Linting:** Biome + ESLint
- **Documentation:** Storybook
- **CI/CD:** GitHub Actions + Changesets
- **Git Hooks:** Lefthook

181
README.md Normal file
View file

@ -0,0 +1,181 @@
# TPMJS Monorepo
Tool Package Manager for AI Agents - A Turborepo monorepo with strict TypeScript, Next.js 16, and best practices.
## Structure
```
apps/
web/ - Next.js 16 App Router application
packages/
config/ - Shared configurations (Biome, ESLint, Tailwind, TypeScript)
ui/ - React component library (.ts-only, no barrels)
utils/ - Utility functions
types/ - Shared TypeScript types
env/ - Zod environment schema loader
test/ - Vitest shared configuration
mocks/ - MSW mock server
storybook/ - Storybook documentation
```
## Getting Started
### Prerequisites
- Node.js >= 18
- pnpm >= 8
### Installation
```bash
pnpm install
```
### Development
```bash
# Run all apps in development mode
pnpm dev
# Run specific app
pnpm --filter @tpmjs/web dev
pnpm --filter @tpmjs/storybook dev
```
### Building
```bash
# Build all packages and apps
pnpm build
# Build specific package
pnpm --filter @tpmjs/ui build
```
### Testing
```bash
# Run all tests
pnpm test
# Run tests in watch mode with UI
pnpm test:ui
```
### Linting & Formatting
```bash
# Lint all packages
pnpm lint
# Format all files
pnpm format
# Check formatting
pnpm format:check
```
## Component Usage
Components are imported directly without barrel exports:
```typescript
import { Button } from '@tpmjs/ui/Button/Button';
import { Card, CardHeader } from '@tpmjs/ui/Card/Card';
```
**Important:** All UI components use `.ts` extension (not `.tsx`) and use `createElement` instead of JSX.
## Publishing Workflow
### 1. Create Changesets
After making changes to publishable packages:
```bash
pnpm changeset
```
Follow the prompts to describe your changes and select which packages are affected.
### 2. Version Packages
When ready to release:
```bash
pnpm changeset:version
```
This updates package versions and generates CHANGELOGs.
### 3. Publish to npm
```bash
pnpm changeset:publish
```
This builds and publishes all packages with changesets to npm.
### 4. Push to GitHub
```bash
git push --follow-tags
```
## Published Packages
- `@tpmjs/ui` - React component library
- `@tpmjs/utils` - Utility functions
- `@tpmjs/types` - TypeScript types
- `@tpmjs/env` - Environment schema loader
## Module Boundaries
ESLint enforces module boundaries:
- Apps can import from published packages only
- Packages cannot import from apps
- No barrel exports (`index.ts`) allowed
- Direct imports required: `@tpmjs/ui/Button/Button`
## Architecture Decisions
### Why .ts-only Components?
Using `.ts` instead of `.tsx` for React components:
- Enforces explicit `createElement` calls
- Makes React's runtime nature more visible
- Prevents JSX spreading anti-patterns
- Better for code generation and tooling
### Why No Barrel Exports?
- Clearer dependency graphs
- Better tree-shaking
- Explicit imports show what's actually used
- Prevents circular dependencies
### Why Biome + ESLint?
- Biome: Fast formatting and basic linting
- ESLint: Semantic rules (module boundaries, TypeScript strictness)
- Each tool focuses on what it does best
## Scripts Reference
- `dev` - Start development servers
- `build` - Build all packages
- `test` - Run tests
- `test:ui` - Run tests with UI
- `lint` - Lint code
- `format` - Format code with Biome
- `format:check` - Check formatting
- `type-check` - TypeScript type checking
- `clean` - Remove build artifacts
- `changeset` - Create a changeset
- `changeset:version` - Version packages
- `changeset:publish` - Publish to npm
## License
MIT

View file

@ -0,0 +1,3 @@
import reactConfig from '@tpmjs/eslint-config/react.js';
export default reactConfig;

8
apps/web/next.config.ts Normal file
View file

@ -0,0 +1,8 @@
import type { NextConfig } from 'next';
const nextConfig: NextConfig = {
transpilePackages: ['@tpmjs/ui', '@tpmjs/utils'],
reactStrictMode: true,
};
export default nextConfig;

36
apps/web/package.json Normal file
View file

@ -0,0 +1,36 @@
{
"name": "@tpmjs/web",
"version": "0.0.0",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint",
"type-check": "tsc --noEmit",
"clean": "rm -rf .next .turbo"
},
"dependencies": {
"@tpmjs/env": "workspace:*",
"@tpmjs/types": "workspace:*",
"@tpmjs/ui": "workspace:*",
"@tpmjs/utils": "workspace:*",
"next": "^16.0.4",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"zod": "^3.24.1"
},
"devDependencies": {
"@tpmjs/eslint-config": "workspace:*",
"@tpmjs/tailwind-config": "workspace:*",
"@tpmjs/tsconfig": "workspace:*",
"@types/node": "^22.10.2",
"@types/react": "^19.0.2",
"@types/react-dom": "^19.0.2",
"autoprefixer": "^10.4.20",
"eslint-config-next": "^16.0.4",
"postcss": "^8.5.1",
"tailwindcss": "^3.4.17",
"typescript": "^5.9.3"
}
}

View file

@ -0,0 +1,6 @@
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
};

View file

@ -0,0 +1,30 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer base {
:root {
--background: 0 0% 100%;
--foreground: 222.2 84% 4.9%;
--card: 0 0% 100%;
--card-foreground: 222.2 84% 4.9%;
--primary: 222.2 47.4% 11.2%;
--primary-foreground: 210 40% 98%;
--secondary: 210 40% 96.1%;
--secondary-foreground: 222.2 47.4% 11.2%;
--muted: 210 40% 96.1%;
--muted-foreground: 215.4 16.3% 46.9%;
--accent: 210 40% 96.1%;
--accent-foreground: 222.2 47.4% 11.2%;
--destructive: 0 84.2% 60.2%;
--destructive-foreground: 210 40% 98%;
--border: 214.3 31.8% 91.4%;
--input: 214.3 31.8% 91.4%;
--ring: 222.2 84% 4.9%;
--radius: 0.5rem;
}
}
body {
@apply bg-background text-foreground;
}

View file

@ -0,0 +1,19 @@
import type { Metadata } from 'next';
import './globals.css';
export const metadata: Metadata = {
title: 'TPMJS - Tool Package Manager for AI Agents',
description: 'The registry for AI tools. Discover, share, and integrate tools that give your agents superpowers.',
};
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}): React.ReactElement {
return (
<html lang="en">
<body>{children}</body>
</html>
);
}

38
apps/web/src/app/page.tsx Normal file
View file

@ -0,0 +1,38 @@
import { Button } from '@tpmjs/ui/Button/Button';
import { Card, CardHeader, CardTitle, CardContent } from '@tpmjs/ui/Card/Card';
import { createElement } from 'react';
export default function HomePage(): React.ReactElement {
return (
<main className="flex min-h-screen flex-col items-center justify-center p-24">
<div className="space-y-8 max-w-2xl">
<h1 className="text-4xl font-bold text-center">TPMJS</h1>
<p className="text-xl text-center text-muted-foreground">
Tool Package Manager for AI Agents
</p>
{createElement(
Card,
{},
createElement(
CardHeader,
{},
createElement(CardTitle, {}, 'Welcome to TPMJS')
),
createElement(
CardContent,
{ className: 'space-y-4' },
createElement('p', {},
'The registry for AI tools. Discover, share, and integrate tools that give your agents superpowers.'
),
createElement(
Button,
{ className: 'w-full' },
'Get Started'
)
)
)}
</div>
</main>
);
}

7
apps/web/src/env.ts Normal file
View file

@ -0,0 +1,7 @@
import { createEnv } from '@tpmjs/env';
import { z } from 'zod';
export const env = createEnv({
NODE_ENV: z.enum(['development', 'production', 'test']).default('development'),
NEXT_PUBLIC_API_URL: z.string().url().optional(),
});

View file

@ -0,0 +1,11 @@
import type { Config } from 'tailwindcss';
import baseConfig from '@tpmjs/tailwind-config/base';
export default {
...baseConfig,
content: [
'./src/app/**/*.{ts,tsx}',
'./src/components/**/*.{ts,tsx}',
'../../packages/ui/src/**/*.ts',
],
} satisfies Config;

16
apps/web/tsconfig.json Normal file
View file

@ -0,0 +1,16 @@
{
"extends": "@tpmjs/tsconfig/nextjs.json",
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
}
},
"include": [
"next-env.d.ts",
"**/*.ts",
"**/*.tsx",
".next/types/**/*.ts"
],
"exclude": ["node_modules"]
}

18
lefthook.yml Normal file
View file

@ -0,0 +1,18 @@
pre-commit:
parallel: true
commands:
format:
glob: "*.{ts,tsx,js,jsx,json,md}"
run: pnpm biome check --write --no-errors-on-unmatched --files-ignore-unknown=true {staged_files}
stage_fixed: true
type-check:
run: pnpm type-check
lint:
run: pnpm lint
pre-push:
commands:
test:
run: pnpm test

34
package.json Normal file
View file

@ -0,0 +1,34 @@
{
"name": "@tpmjs/monorepo",
"version": "0.0.0",
"private": true,
"type": "module",
"engines": {
"node": ">=18",
"pnpm": ">=8"
},
"packageManager": "pnpm@10.14.0",
"scripts": {
"dev": "turbo dev",
"build": "turbo build",
"test": "turbo test",
"test:ui": "turbo test:ui",
"lint": "turbo lint",
"format": "biome format --write .",
"format:check": "biome check .",
"type-check": "turbo type-check",
"clean": "turbo clean && rm -rf node_modules .turbo",
"changeset": "changeset",
"changeset:version": "changeset version && pnpm install --no-frozen-lockfile",
"changeset:publish": "pnpm build && changeset publish",
"prepare": "lefthook install"
},
"devDependencies": {
"@biomejs/biome": "^1.9.4",
"@changesets/cli": "^2.27.10",
"@types/node": "^22.10.2",
"lefthook": "^1.10.1",
"turbo": "^2.6.1",
"typescript": "^5.9.3"
}
}

View file

@ -0,0 +1,41 @@
{
"$schema": "https://biomejs.dev/schemas/1.9.4/schema.json",
"vcs": {
"enabled": true,
"clientKind": "git",
"useIgnoreFile": true
},
"formatter": {
"enabled": true,
"indentStyle": "space",
"indentWidth": 2,
"lineWidth": 100
},
"linter": {
"enabled": true,
"rules": {
"recommended": true,
"complexity": {
"noExcessiveCognitiveComplexity": "warn",
"noForEach": "off"
},
"style": {
"useImportType": "error",
"useNodejsImportProtocol": "error"
},
"suspicious": {
"noExplicitAny": "warn"
}
}
},
"organizeImports": {
"enabled": true
},
"javascript": {
"formatter": {
"quoteStyle": "single",
"trailingCommas": "es5",
"semicolons": "always"
}
}
}

View file

@ -0,0 +1,32 @@
import js from '@eslint/js';
import tseslint from 'typescript-eslint';
import importPlugin from 'eslint-plugin-import';
export default tseslint.config(
js.configs.recommended,
...tseslint.configs.recommended,
{
plugins: {
import: importPlugin,
},
rules: {
// Module boundaries
'import/no-internal-modules': ['error', {
allow: [
'@tpmjs/ui/*/[!index]*',
'@tpmjs/utils/*',
'@tpmjs/types/*',
]
}],
'import/no-restricted-paths': ['error', {
zones: [
{ target: './packages', from: './apps' },
{ target: './packages/ui', from: './packages/utils' },
]
}],
'import/no-anonymous-default-export': 'error',
'@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_' }],
'@typescript-eslint/no-explicit-any': 'warn',
},
}
);

View file

@ -0,0 +1,19 @@
{
"name": "@tpmjs/eslint-config",
"version": "0.0.0",
"private": true,
"main": "./base.js",
"files": [
"base.js",
"react.js"
],
"dependencies": {
"@eslint/js": "^9.18.0",
"eslint": "^9.18.0",
"eslint-plugin-import": "^2.31.0",
"eslint-plugin-jsx-a11y": "^6.10.2",
"eslint-plugin-react": "^7.37.3",
"eslint-plugin-react-hooks": "^5.1.0",
"typescript-eslint": "^8.19.1"
}
}

28
packages/config/eslint/react.js vendored Normal file
View file

@ -0,0 +1,28 @@
import reactPlugin from 'eslint-plugin-react';
import reactHooksPlugin from 'eslint-plugin-react-hooks';
import jsxA11y from 'eslint-plugin-jsx-a11y';
import baseConfig from './base.js';
export default [
...baseConfig,
{
files: ['**/*.ts', '**/*.tsx'],
plugins: {
react: reactPlugin,
'react-hooks': reactHooksPlugin,
'jsx-a11y': jsxA11y,
},
rules: {
...reactPlugin.configs.recommended.rules,
...reactHooksPlugin.configs.recommended.rules,
...jsxA11y.configs.recommended.rules,
'react/react-in-jsx-scope': 'off',
'react/prop-types': 'off',
},
settings: {
react: {
version: 'detect',
},
},
},
];

View file

@ -0,0 +1,11 @@
{
"name": "@tpmjs/config",
"version": "0.0.0",
"private": true,
"files": [
"biome.json",
"eslint",
"tailwind",
"tsconfig"
]
}

View file

@ -0,0 +1,42 @@
import type { Config } from 'tailwindcss';
export default {
content: [],
theme: {
extend: {
colors: {
border: 'hsl(var(--border))',
input: 'hsl(var(--input))',
ring: 'hsl(var(--ring))',
background: 'hsl(var(--background))',
foreground: 'hsl(var(--foreground))',
primary: {
DEFAULT: 'hsl(var(--primary))',
foreground: 'hsl(var(--primary-foreground))',
},
secondary: {
DEFAULT: 'hsl(var(--secondary))',
foreground: 'hsl(var(--secondary-foreground))',
},
destructive: {
DEFAULT: 'hsl(var(--destructive))',
foreground: 'hsl(var(--destructive-foreground))',
},
muted: {
DEFAULT: 'hsl(var(--muted))',
foreground: 'hsl(var(--muted-foreground))',
},
accent: {
DEFAULT: 'hsl(var(--accent))',
foreground: 'hsl(var(--accent-foreground))',
},
},
borderRadius: {
lg: 'var(--radius)',
md: 'calc(var(--radius) - 2px)',
sm: 'calc(var(--radius) - 4px)',
},
},
},
plugins: [],
} satisfies Config;

View file

@ -0,0 +1,15 @@
{
"name": "@tpmjs/tailwind-config",
"version": "0.0.0",
"private": true,
"main": "./base.ts",
"files": [
"base.ts"
],
"dependencies": {
"tailwindcss": "^3.4.17"
},
"devDependencies": {
"typescript": "^5.9.3"
}
}

View file

@ -0,0 +1,28 @@
{
"$schema": "https://json.schemastore.org/tsconfig",
"compilerOptions": {
"composite": true,
"declaration": true,
"declarationMap": true,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"incremental": true,
"isolatedModules": true,
"lib": ["ES2022"],
"module": "ESNext",
"moduleResolution": "Bundler",
"noUncheckedIndexedAccess": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"resolveJsonModule": true,
"skipLibCheck": true,
"strict": true,
"target": "ES2022",
"verbatimModuleSyntax": true
},
"exclude": [
"node_modules",
"dist",
".turbo"
]
}

View file

@ -0,0 +1,24 @@
{
"$schema": "https://json.schemastore.org/tsconfig",
"extends": "./base.json",
"compilerOptions": {
"jsx": "preserve",
"lib": ["ES2022", "DOM", "DOM.Iterable"],
"module": "ESNext",
"plugins": [
{
"name": "next"
}
],
"target": "ES2022"
},
"include": [
"next-env.d.ts",
"**/*.ts",
"**/*.tsx",
".next/types/**/*.ts"
],
"exclude": [
"node_modules"
]
}

View file

@ -0,0 +1,10 @@
{
"name": "@tpmjs/tsconfig",
"version": "0.0.0",
"private": true,
"files": [
"base.json",
"nextjs.json",
"react-library.json"
]
}

View file

@ -0,0 +1,8 @@
{
"$schema": "https://json.schemastore.org/tsconfig",
"extends": "./base.json",
"compilerOptions": {
"jsx": "react-jsx",
"lib": ["ES2022", "DOM", "DOM.Iterable"]
}
}

32
packages/env/package.json vendored Normal file
View file

@ -0,0 +1,32 @@
{
"name": "@tpmjs/env",
"version": "0.1.0",
"type": "module",
"exports": {
".": {
"types": "./dist/index.d.ts",
"default": "./dist/index.js"
}
},
"files": [
"dist"
],
"scripts": {
"build": "tsup",
"dev": "tsup --watch",
"type-check": "tsc --noEmit",
"clean": "rm -rf dist .turbo"
},
"dependencies": {
"zod": "^3.24.1"
},
"devDependencies": {
"@tpmjs/tsconfig": "workspace:*",
"@types/node": "^22.10.2",
"tsup": "^8.3.5",
"typescript": "^5.9.3"
},
"publishConfig": {
"access": "public"
}
}

14
packages/env/src/index.ts vendored Normal file
View file

@ -0,0 +1,14 @@
import { z } from 'zod';
export function createEnv<T extends z.ZodRawShape>(schema: T): z.infer<z.ZodObject<T>> {
const envSchema = z.object(schema);
const parsed = envSchema.safeParse(process.env);
if (!parsed.success) {
console.error('Invalid environment variables:', parsed.error.flatten().fieldErrors);
throw new Error('Invalid environment variables');
}
return parsed.data;
}

11
packages/env/tsconfig.json vendored Normal file
View file

@ -0,0 +1,11 @@
{
"extends": "@tpmjs/tsconfig/base.json",
"compilerOptions": {
"outDir": "dist",
"rootDir": "src",
"incremental": false,
"composite": false
},
"include": ["src"],
"exclude": ["node_modules", "dist"]
}

9
packages/env/tsup.config.ts vendored Normal file
View file

@ -0,0 +1,9 @@
import { defineConfig } from 'tsup';
export default defineConfig({
entry: ['src/index.ts'],
format: ['esm'],
dts: true,
clean: true,
treeshake: true,
});

View file

@ -0,0 +1,33 @@
{
"name": "@tpmjs/mocks",
"version": "0.0.0",
"private": true,
"type": "module",
"exports": {
"./server": {
"types": "./dist/server.d.ts",
"default": "./dist/server.js"
},
"./handlers": {
"types": "./dist/handlers.d.ts",
"default": "./dist/handlers.js"
}
},
"files": [
"dist"
],
"scripts": {
"build": "tsup",
"dev": "tsup --watch",
"type-check": "tsc --noEmit",
"clean": "rm -rf dist .turbo"
},
"dependencies": {
"msw": "^2.7.0"
},
"devDependencies": {
"@tpmjs/tsconfig": "workspace:*",
"tsup": "^8.3.5",
"typescript": "^5.9.3"
}
}

View file

@ -0,0 +1,14 @@
import { http, HttpResponse } from 'msw';
export const handlers = [
http.get('/api/tools', () => {
return HttpResponse.json([
{
id: 'web-search',
name: 'Web Search',
description: 'Search the web for information',
category: 'search',
},
]);
}),
];

View file

@ -0,0 +1,4 @@
import { setupServer } from 'msw/node';
import { handlers } from './handlers';
export const server = setupServer(...handlers);

View file

@ -0,0 +1,11 @@
{
"extends": "@tpmjs/tsconfig/base.json",
"compilerOptions": {
"outDir": "dist",
"rootDir": "src",
"incremental": false,
"composite": false
},
"include": ["src"],
"exclude": ["node_modules", "dist"]
}

View file

@ -0,0 +1,8 @@
import { defineConfig } from 'tsup';
export default defineConfig({
entry: ['src/server.ts', 'src/handlers.ts'],
format: ['esm'],
dts: true,
clean: true,
});

View file

@ -0,0 +1,14 @@
{
"name": "@tpmjs/test",
"version": "0.0.0",
"private": true,
"exports": {
"./vitest": "./vitest.config.ts"
},
"files": [
"vitest.config.ts"
],
"dependencies": {
"vitest": "^2.1.8"
}
}

View file

@ -0,0 +1,11 @@
import { defineConfig } from 'vitest/config';
export default defineConfig({
test: {
globals: true,
coverage: {
provider: 'v8',
reporter: ['text', 'json', 'html'],
},
},
});

View file

@ -0,0 +1,35 @@
{
"name": "@tpmjs/types",
"version": "0.1.0",
"type": "module",
"exports": {
"./tool": {
"types": "./dist/tool.d.ts",
"default": "./dist/tool.js"
},
"./registry": {
"types": "./dist/registry.d.ts",
"default": "./dist/registry.js"
}
},
"files": [
"dist"
],
"scripts": {
"build": "tsup",
"dev": "tsup --watch",
"type-check": "tsc --noEmit",
"clean": "rm -rf dist .turbo"
},
"dependencies": {
"zod": "^3.24.1"
},
"devDependencies": {
"@tpmjs/tsconfig": "workspace:*",
"tsup": "^8.3.5",
"typescript": "^5.9.3"
},
"publishConfig": {
"access": "public"
}
}

View file

@ -0,0 +1,17 @@
import { z } from 'zod';
export const RegistrySearchResultSchema = z.object({
toolId: z.string(),
score: z.number(),
matchType: z.enum(['exact', 'fuzzy', 'semantic']),
});
export const RegistrySearchOptionsSchema = z.object({
query: z.string(),
category: z.string().optional(),
limit: z.number().default(10),
offset: z.number().default(0),
});
export type RegistrySearchResult = z.infer<typeof RegistrySearchResultSchema>;
export type RegistrySearchOptions = z.infer<typeof RegistrySearchOptionsSchema>;

View file

@ -0,0 +1,21 @@
import { z } from 'zod';
export const ToolParameterSchema = z.object({
name: z.string(),
description: z.string(),
schema: z.record(z.unknown()),
required: z.boolean().default(false),
});
export const ToolSchema = z.object({
id: z.string(),
name: z.string(),
description: z.string(),
category: z.string(),
version: z.string(),
parameters: z.array(ToolParameterSchema),
tags: z.array(z.string()).default([]),
});
export type Tool = z.infer<typeof ToolSchema>;
export type ToolParameter = z.infer<typeof ToolParameterSchema>;

View file

@ -0,0 +1,11 @@
{
"extends": "@tpmjs/tsconfig/base.json",
"compilerOptions": {
"outDir": "dist",
"rootDir": "src",
"incremental": false,
"composite": false
},
"include": ["src"],
"exclude": ["node_modules", "dist"]
}

View file

@ -0,0 +1,10 @@
import { defineConfig } from 'tsup';
export default defineConfig({
entry: ['src/tool.ts', 'src/registry.ts'],
format: ['esm'],
dts: true,
clean: true,
treeshake: true,
splitting: false,
});

View file

@ -0,0 +1,3 @@
import reactConfig from '@tpmjs/eslint-config/react.js';
export default reactConfig;

51
packages/ui/package.json Normal file
View file

@ -0,0 +1,51 @@
{
"name": "@tpmjs/ui",
"version": "0.1.0",
"type": "module",
"exports": {
"./Button/Button": {
"types": "./dist/Button/Button.d.ts",
"default": "./dist/Button/Button.js"
},
"./Card/Card": {
"types": "./dist/Card/Card.d.ts",
"default": "./dist/Card/Card.js"
}
},
"files": [
"dist"
],
"scripts": {
"build": "tsup",
"dev": "tsup --watch",
"test": "vitest run",
"test:ui": "vitest --ui",
"type-check": "tsc --noEmit",
"lint": "eslint .",
"clean": "rm -rf dist .turbo"
},
"peerDependencies": {
"react": "^18.0.0 || ^19.0.0",
"react-dom": "^18.0.0 || ^19.0.0"
},
"dependencies": {
"@tpmjs/utils": "workspace:*"
},
"devDependencies": {
"@tpmjs/eslint-config": "workspace:*",
"@tpmjs/test": "workspace:*",
"@tpmjs/tsconfig": "workspace:*",
"@testing-library/react": "^16.3.0",
"@types/react": "^19.0.2",
"@types/react-dom": "^19.0.2",
"happy-dom": "^15.11.7",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"tsup": "^8.3.5",
"typescript": "^5.9.3",
"vitest": "^2.1.8"
},
"publishConfig": {
"access": "public"
}
}

View file

@ -0,0 +1,23 @@
import { describe, it, expect } from 'vitest';
import { render, screen } from '@testing-library/react';
import { Button } from './Button';
import { createElement } from 'react';
describe('Button', () => {
it('renders with default variant', () => {
render(createElement(Button, { children: 'Click me' }));
expect(screen.getByRole('button')).toHaveTextContent('Click me');
});
it('applies variant classes', () => {
render(createElement(Button, { variant: 'destructive', children: 'Delete' }));
const button = screen.getByRole('button');
expect(button.className).toContain('bg-destructive');
});
it('applies size classes', () => {
render(createElement(Button, { size: 'lg', children: 'Large' }));
const button = screen.getByRole('button');
expect(button.className).toContain('h-11');
});
});

View file

@ -0,0 +1,40 @@
import { createElement, forwardRef, type ButtonHTMLAttributes } from 'react';
import { cn } from '@tpmjs/utils/cn';
export interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
variant?: 'default' | 'destructive' | 'outline' | 'secondary' | 'ghost' | 'link';
size?: 'default' | 'sm' | 'lg' | 'icon';
}
const buttonVariants = {
default: 'bg-primary text-primary-foreground hover:bg-primary/90',
destructive: 'bg-destructive text-destructive-foreground hover:bg-destructive/90',
outline: 'border border-input bg-background hover:bg-accent hover:text-accent-foreground',
secondary: 'bg-secondary text-secondary-foreground hover:bg-secondary/80',
ghost: 'hover:bg-accent hover:text-accent-foreground',
link: 'text-primary underline-offset-4 hover:underline',
};
const buttonSizes = {
default: 'h-10 px-4 py-2',
sm: 'h-9 rounded-md px-3',
lg: 'h-11 rounded-md px-8',
icon: 'h-10 w-10',
};
export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant = 'default', size = 'default', ...props }, ref) => {
return createElement('button', {
className: cn(
'inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50',
buttonVariants[variant],
buttonSizes[size],
className
),
ref,
...props,
});
}
);
Button.displayName = 'Button';

View file

@ -0,0 +1,53 @@
import { createElement, forwardRef, type HTMLAttributes } from 'react';
import { cn } from '@tpmjs/utils/cn';
export interface CardProps extends HTMLAttributes<HTMLDivElement> {}
export const Card = forwardRef<HTMLDivElement, CardProps>(({ className, ...props }, ref) => {
return createElement('div', {
ref,
className: cn(
'rounded-lg border border-border bg-card text-card-foreground shadow-sm',
className
),
...props,
});
});
Card.displayName = 'Card';
export const CardHeader = forwardRef<HTMLDivElement, CardProps>(
({ className, ...props }, ref) => {
return createElement('div', {
ref,
className: cn('flex flex-col space-y-1.5 p-6', className),
...props,
});
}
);
CardHeader.displayName = 'CardHeader';
export const CardTitle = forwardRef<HTMLParagraphElement, HTMLAttributes<HTMLHeadingElement>>(
({ className, ...props }, ref) => {
return createElement('h3', {
ref,
className: cn('text-2xl font-semibold leading-none tracking-tight', className),
...props,
});
}
);
CardTitle.displayName = 'CardTitle';
export const CardContent = forwardRef<HTMLDivElement, CardProps>(
({ className, ...props }, ref) => {
return createElement('div', {
ref,
className: cn('p-6 pt-0', className),
...props,
});
}
);
CardContent.displayName = 'CardContent';

11
packages/ui/tsconfig.json Normal file
View file

@ -0,0 +1,11 @@
{
"extends": "@tpmjs/tsconfig/react-library.json",
"compilerOptions": {
"outDir": "dist",
"rootDir": "src",
"incremental": false,
"composite": false
},
"include": ["src"],
"exclude": ["node_modules", "dist"]
}

View file

@ -0,0 +1,14 @@
import { defineConfig } from 'tsup';
export default defineConfig({
entry: ['src/Button/Button.ts', 'src/Card/Card.ts'],
format: ['esm'],
dts: true,
clean: true,
treeshake: true,
splitting: false,
external: ['react', 'react-dom'],
esbuildOptions(options) {
options.jsx = 'automatic';
},
});

View file

@ -0,0 +1,8 @@
import { defineConfig } from 'vitest/config';
export default defineConfig({
test: {
globals: true,
environment: 'happy-dom',
},
});

View file

@ -0,0 +1,40 @@
{
"name": "@tpmjs/utils",
"version": "0.1.0",
"type": "module",
"exports": {
"./cn": {
"types": "./dist/cn.d.ts",
"default": "./dist/cn.js"
},
"./format": {
"types": "./dist/format.d.ts",
"default": "./dist/format.js"
}
},
"files": [
"dist"
],
"scripts": {
"build": "tsup",
"dev": "tsup --watch",
"test": "vitest run",
"test:ui": "vitest --ui",
"type-check": "tsc --noEmit",
"clean": "rm -rf dist .turbo"
},
"dependencies": {
"clsx": "^2.1.1",
"tailwind-merge": "^2.6.0"
},
"devDependencies": {
"@tpmjs/test": "workspace:*",
"@tpmjs/tsconfig": "workspace:*",
"tsup": "^8.3.5",
"typescript": "^5.9.3",
"vitest": "^2.1.8"
},
"publishConfig": {
"access": "public"
}
}

View file

@ -0,0 +1,16 @@
import { describe, it, expect } from 'vitest';
import { cn } from './cn';
describe('cn', () => {
it('merges class names', () => {
expect(cn('foo', 'bar')).toBe('foo bar');
});
it('handles conditional classes', () => {
expect(cn('foo', false && 'bar', 'baz')).toBe('foo baz');
});
it('merges tailwind classes correctly', () => {
expect(cn('px-2 py-1', 'px-4')).toBe('py-1 px-4');
});
});

6
packages/utils/src/cn.ts Normal file
View file

@ -0,0 +1,6 @@
import { clsx, type ClassValue } from 'clsx';
import { twMerge } from 'tailwind-merge';
export function cn(...inputs: ClassValue[]): string {
return twMerge(clsx(inputs));
}

View file

@ -0,0 +1,11 @@
export function formatDate(date: Date): string {
return new Intl.DateTimeFormat('en-US', {
year: 'numeric',
month: 'long',
day: 'numeric',
}).format(date);
}
export function formatNumber(num: number): string {
return new Intl.NumberFormat('en-US').format(num);
}

View file

@ -0,0 +1,11 @@
{
"extends": "@tpmjs/tsconfig/base.json",
"compilerOptions": {
"outDir": "dist",
"rootDir": "src",
"incremental": false,
"composite": false
},
"include": ["src"],
"exclude": ["node_modules", "dist"]
}

View file

@ -0,0 +1,10 @@
import { defineConfig } from 'tsup';
export default defineConfig({
entry: ['src/cn.ts', 'src/format.ts'],
format: ['esm'],
dts: true,
clean: true,
treeshake: true,
splitting: false,
});

View file

@ -0,0 +1,8 @@
import { defineConfig } from 'vitest/config';
export default defineConfig({
test: {
globals: true,
environment: 'node',
},
});

8131
pnpm-lock.yaml generated Normal file

File diff suppressed because it is too large Load diff

6
pnpm-workspace.yaml Normal file
View file

@ -0,0 +1,6 @@
packages:
- 'apps/*'
- 'packages/*'
- 'packages/config/eslint'
- 'packages/config/tailwind'
- 'packages/config/tsconfig'

13
tsconfig.json Normal file
View file

@ -0,0 +1,13 @@
{
"files": [],
"references": [
{ "path": "./apps/web" },
{ "path": "./packages/ui" },
{ "path": "./packages/utils" },
{ "path": "./packages/types" },
{ "path": "./packages/env" },
{ "path": "./packages/test" },
{ "path": "./packages/mocks" },
{ "path": "./packages/storybook" }
]
}

35
turbo.json Normal file
View file

@ -0,0 +1,35 @@
{
"$schema": "https://turbo.build/schema.json",
"globalDependencies": [
"**/.env.*local",
"tsconfig.json"
],
"tasks": {
"build": {
"dependsOn": ["^build"],
"outputs": ["dist/**", ".next/**", "!.next/cache/**", "storybook-static/**"]
},
"dev": {
"cache": false,
"persistent": true
},
"lint": {
"dependsOn": ["^build"]
},
"type-check": {
"dependsOn": ["^build"],
"outputs": []
},
"test": {
"dependsOn": ["^build"],
"outputs": ["coverage/**"]
},
"test:ui": {
"cache": false,
"persistent": true
},
"clean": {
"cache": false
}
}
}