diff --git a/.claude/agents/codebase-analyzer.md b/.claude/agents/codebase-analyzer.md new file mode 100644 index 00000000..aa55c96c --- /dev/null +++ b/.claude/agents/codebase-analyzer.md @@ -0,0 +1,12 @@ +--- +name: codebase-analyzer +description: Comprehensive analysis using parallel exploration tasks. +tools: Read, Glob, Grep, Task +--- + +Orchestrate parallel analysis: +1. Spawn explore-resources, explore-models, explore-tests tasks +2. Aggregate findings +3. Report: patterns, relationships, gaps + +Output: Summary, resources found, patterns identified, recommendations. diff --git a/.claude/agents/explore-models.md b/.claude/agents/explore-models.md new file mode 100644 index 00000000..a85d6e6f --- /dev/null +++ b/.claude/agents/explore-models.md @@ -0,0 +1,9 @@ +--- +name: explore-models +description: Search src/models/ for TypeScript type definitions. +tools: Read, Glob, Grep +--- + +Search `src/models/` for interfaces and types. +Look for: `export interface`, `export type`, `extends ListQueryParams` +Return: interface names, file paths, key properties. diff --git a/.claude/agents/explore-resources.md b/.claude/agents/explore-resources.md new file mode 100644 index 00000000..f2216bb7 --- /dev/null +++ b/.claude/agents/explore-resources.md @@ -0,0 +1,9 @@ +--- +name: explore-resources +description: Search src/resources/ for API resource classes and methods. +tools: Read, Glob, Grep +--- + +Search `src/resources/` for resource classes extending `Resource`. +Look for: `public methodName(`, `/v3/grants/`, `interface.*Params` +Return: file paths, line numbers, method signatures. diff --git a/.claude/agents/explore-tests.md b/.claude/agents/explore-tests.md new file mode 100644 index 00000000..c2e1efd7 --- /dev/null +++ b/.claude/agents/explore-tests.md @@ -0,0 +1,9 @@ +--- +name: explore-tests +description: Search tests/ for Jest test patterns and mocks. +tools: Read, Glob, Grep +--- + +Search `tests/` for test files (*.spec.ts). +Look for: `describe('`, `it('should`, `mockResolvedValue` +Return: test file, describe blocks, test case names. diff --git a/.claude/agents/parallel-explore.md b/.claude/agents/parallel-explore.md new file mode 100644 index 00000000..6e429660 --- /dev/null +++ b/.claude/agents/parallel-explore.md @@ -0,0 +1,13 @@ +--- +name: parallel-explore +description: Parallel codebase search. Use for broad searches across multiple directories. +tools: Read, Glob, Grep, Task +--- + +Spawn 3-5 parallel Task agents to search different areas: +- Task 1: `src/resources/` +- Task 2: `src/models/` +- Task 3: `tests/` +- Task 4: Root configs + +Use `subagent_type: Explore`. Aggregate results into unified summary. diff --git a/.claude/agents/quick-search.md b/.claude/agents/quick-search.md new file mode 100644 index 00000000..455d86a5 --- /dev/null +++ b/.claude/agents/quick-search.md @@ -0,0 +1,9 @@ +--- +name: quick-search +description: Fast keyword search across codebase using grep. +tools: Grep, Glob +--- + +Use Grep for content, Glob for file patterns. +Return results as: `file/path.ts:123 - line preview` +Keep responses concise. diff --git a/.claude/commands/add-endpoint.md b/.claude/commands/add-endpoint.md new file mode 100644 index 00000000..7d3cc389 --- /dev/null +++ b/.claude/commands/add-endpoint.md @@ -0,0 +1,8 @@ +# Add Endpoint: $ARGUMENTS + +Format: `{resource} {method}` (e.g., "calendars getAvailability") + +1. Add request/response types to `src/models/{resource}.ts` +2. Add method to `src/resources/{resource}.ts` +3. Add tests to `tests/resources/{resource}.spec.ts` +4. Run `make test-resource NAME={resource}` diff --git a/.claude/commands/add-resource.md b/.claude/commands/add-resource.md new file mode 100644 index 00000000..5be58870 --- /dev/null +++ b/.claude/commands/add-resource.md @@ -0,0 +1,9 @@ +# Add Resource: $ARGUMENTS + +1. Create `src/models/{name}.ts` with interfaces +2. Create `src/resources/{name}.ts` extending Resource +3. Register in `src/nylas.ts` +4. Create `tests/resources/{name}.spec.ts` +5. Run `make ci` + +Use templates in `.claude/shared/` as reference. diff --git a/.claude/commands/build.md b/.claude/commands/build.md new file mode 100644 index 00000000..67df68e3 --- /dev/null +++ b/.claude/commands/build.md @@ -0,0 +1,7 @@ +# Build SDK + +```bash +make build +``` + +Output: `lib/esm/`, `lib/cjs/`, `lib/types/` diff --git a/.claude/commands/explore.md b/.claude/commands/explore.md new file mode 100644 index 00000000..b3cbb824 --- /dev/null +++ b/.claude/commands/explore.md @@ -0,0 +1,10 @@ +# Parallel Explore + +Search for: $ARGUMENTS + +Spawn parallel Task agents (subagent_type: Explore) to search: +- src/resources/ +- src/models/ +- tests/ + +Aggregate and summarize results. diff --git a/.claude/commands/fix.md b/.claude/commands/fix.md new file mode 100644 index 00000000..b7807d0a --- /dev/null +++ b/.claude/commands/fix.md @@ -0,0 +1,5 @@ +# Fix Code + +```bash +make lint-fix && make format +``` diff --git a/.claude/commands/review.md b/.claude/commands/review.md new file mode 100644 index 00000000..de3a4632 --- /dev/null +++ b/.claude/commands/review.md @@ -0,0 +1,8 @@ +# Review Code + +Check: $ARGUMENTS + +1. TypeScript: camelCase props, proper types, JSDoc comments +2. Resources: extends Resource, uses _list/_find/_create/_update/_destroy +3. Tests: mocks setup, assertions correct +4. Run: `make lint` and `make test` diff --git a/.claude/commands/test.md b/.claude/commands/test.md new file mode 100644 index 00000000..96169375 --- /dev/null +++ b/.claude/commands/test.md @@ -0,0 +1,14 @@ +# Run Tests + +Target: $ARGUMENTS + +```bash +# No args = all tests +make test + +# Resource name (e.g., "calendars") +make test-resource NAME={name} + +# File path +make test-single FILE={path} +``` diff --git a/.claude/docs/adding-resources.md b/.claude/docs/adding-resources.md new file mode 100644 index 00000000..2eb3ffe0 --- /dev/null +++ b/.claude/docs/adding-resources.md @@ -0,0 +1,40 @@ +# Adding New Resources + +## Step 1: Create Model +Copy `.claude/shared/model-template.ts` to `src/models/{name}.ts` + +Replace placeholders: +- `{{ResourceName}}` → PascalCase (e.g., `Calendar`) +- `{{resourceName}}` → camelCase (e.g., `calendar`) + +Add to `src/models/index.ts` exports. + +## Step 2: Create Resource +Copy `.claude/shared/resource-template.ts` to `src/resources/{name}.ts` + +Key patterns: +- Extend `Resource` class +- Use `_list`, `_find`, `_create`, `_update`, `_destroy` helpers +- Define path: `/v3/grants/${identifier}/{resources}` +- Accept `Overrides` as last parameter + +## Step 3: Register Resource +In `src/nylas.ts`: +1. Import the resource class +2. Add public property: `public {name}: {Name}s;` +3. Initialize in constructor: `this.{name} = new {Name}s(this.apiClient);` + +## Step 4: Create Tests +Copy `.claude/shared/test-template.spec.ts` to `tests/resources/{name}.spec.ts` + +Test patterns: +- Mock APIClient with `jest.mock('../../src/apiClient')` +- Test each public method +- Verify request parameters +- Test with/without overrides + +## Step 5: Verify +```bash +make test-resource NAME={name} +make ci +``` diff --git a/.claude/docs/architecture.md b/.claude/docs/architecture.md new file mode 100644 index 00000000..78ab03d7 --- /dev/null +++ b/.claude/docs/architecture.md @@ -0,0 +1,42 @@ +# Architecture + +## Entry Point +`src/nylas.ts:15` - Nylas class constructor instantiates all resources with configured APIClient. + +## API Client +`src/apiClient.ts` handles: +- HTTP requests with fetch +- Bearer token authentication +- Error response handling (NylasApiError) +- JSON serialization/deserialization +- Request/response logging + +## Resource Pattern +All resources extend `src/resources/resource.ts`: + +| Method | HTTP | Returns | +|--------|------|---------| +| `_list()` | GET | Async iterator with pagination | +| `_find()` | GET | Single resource | +| `_create()` | POST | Created resource | +| `_update()` | PUT | Updated resource | +| `_updatePatch()` | PATCH | Patched resource | +| `_destroy()` | DELETE | Deletion response | +| `_getRaw()` | GET | Binary data | +| `_getStream()` | GET | ReadableStream | + +## Models Structure +`src/models/` contains TypeScript interfaces: +- Resource objects: `Calendar`, `Event`, `Message`, etc. +- Request types: `Create{Resource}Request` +- Query params: `List{Resource}QueryParams`, `Find{Resource}QueryParams` +- Responses: `NylasResponse`, `NylasListResponse` + +## API Paths +- Grant-scoped: `/v3/grants/{grantId}/{resource}` +- App-scoped: `/v3/{resource}` + +## Build Output +- `lib/esm/` - ES modules (import/export) +- `lib/cjs/` - CommonJS (require) +- `lib/types/` - TypeScript declarations diff --git a/.claude/docs/testing-guide.md b/.claude/docs/testing-guide.md new file mode 100644 index 00000000..51e91b29 --- /dev/null +++ b/.claude/docs/testing-guide.md @@ -0,0 +1,44 @@ +# Testing Guide + +## Setup +Tests use Jest with `jest-fetch-mock`. Mock setup in `tests/setupTests.ts`. + +## Mocking APIClient +```typescript +import APIClient from '../../src/apiClient'; +jest.mock('../../src/apiClient'); +``` + +## Test Structure +```typescript +describe('ResourceName', () => { + let apiClient: jest.Mocked; + let resource: Resources; + + beforeEach(() => { + apiClient = new APIClient({ apiKey: 'test' }) as jest.Mocked; + resource = new Resources(apiClient); + }); + + describe('list', () => { + it('calls API with correct parameters', async () => { + apiClient.request.mockResolvedValue({ data: [] }); + await resource.list({ identifier: 'grant-id' }); + expect(apiClient.request).toHaveBeenCalledWith({ + method: 'GET', + path: '/v3/grants/grant-id/resources', + overrides: {} + }); + }); + }); +}); +``` + +## Coverage +Threshold: 80% functions, lines, statements. Run `make test` for coverage report. + +## Commands +- `make test` - All tests with coverage +- `make test-resource NAME=calendars` - Single resource +- `make test-watch` - Watch mode +- `make test-verbose` - Verbose output diff --git a/.claude/hooks/post-tool-use.sh b/.claude/hooks/post-tool-use.sh new file mode 100755 index 00000000..ffc5659d --- /dev/null +++ b/.claude/hooks/post-tool-use.sh @@ -0,0 +1,22 @@ +#!/bin/bash +# Post-tool hook - Auto-format code after edits +set -e + +INPUT=$(cat) +TOOL_NAME=$(echo "$INPUT" | jq -r '.tool_name // empty') +TOOL_INPUT=$(echo "$INPUT" | jq -r '.tool_input // empty') + +# Auto-format TypeScript/JavaScript files after editing +if [ "$TOOL_NAME" = "Edit" ] || [ "$TOOL_NAME" = "Write" ]; then + FILE=$(echo "$TOOL_INPUT" | jq -r '.file_path // empty') + + # Format .ts/.js files in src/ or tests/ + if echo "$FILE" | grep -qE '\.(ts|js)$' && echo "$FILE" | grep -qE '(src|tests)/'; then + if [ -f "$FILE" ]; then + npx prettier --write "$FILE" 2>/dev/null || true + npx eslint --fix "$FILE" 2>/dev/null || true + fi + fi +fi + +exit 0 diff --git a/.claude/hooks/pre-tool-use.sh b/.claude/hooks/pre-tool-use.sh new file mode 100755 index 00000000..389767c7 --- /dev/null +++ b/.claude/hooks/pre-tool-use.sh @@ -0,0 +1,55 @@ +#!/bin/bash +# Pre-tool hook - validates tool calls before execution +# Exit: 0 = allow, 2 = block + +set -e + +INPUT=$(cat) +TOOL_NAME=$(echo "$INPUT" | jq -r '.tool_name // empty') +TOOL_INPUT=$(echo "$INPUT" | jq -r '.tool_input // empty') + +block() { echo "$1" >&2; exit 2; } + +# Bash command restrictions +if [ "$TOOL_NAME" = "Bash" ]; then + CMD=$(echo "$TOOL_INPUT" | jq -r '.command // empty') + + # Block git write operations + echo "$CMD" | grep -qE 'git\s+(commit|push|add|merge|rebase|reset)' && \ + block "BLOCKED: Git write operations not allowed. User handles manually." + + # Block dangerous rm + echo "$CMD" | grep -qE 'rm\s+(-rf|-fr)\s+(/|~|\.|src|lib|tests)' && \ + block "BLOCKED: Dangerous rm command." + + # Block npm publish without dry-run + echo "$CMD" | grep -qE 'npm\s+publish' && ! echo "$CMD" | grep -qE '--dry-run' && \ + block "BLOCKED: Use 'npm publish --dry-run' first." +fi + +# File protection +if [ "$TOOL_NAME" = "Edit" ] || [ "$TOOL_NAME" = "Write" ]; then + FILE=$(echo "$TOOL_INPUT" | jq -r '.file_path // empty') + + echo "$FILE" | grep -qE '\.env' && block "BLOCKED: Cannot edit .env files." + echo "$FILE" | grep -qE 'package-lock\.json$' && block "BLOCKED: Use npm commands for package-lock.json." + echo "$FILE" | grep -qE 'src/version\.ts$' && block "BLOCKED: Auto-generated. Run 'make build'." + echo "$FILE" | grep -qE 'src/models/index\.ts$' && block "BLOCKED: Auto-generated. Run 'make build'." +fi + +# New file size limit (500 lines max for NEW files only) +if [ "$TOOL_NAME" = "Write" ]; then + FILE=$(echo "$TOOL_INPUT" | jq -r '.file_path // empty') + + # Only check if file doesn't exist (new file) + if [ ! -f "$FILE" ]; then + CONTENT=$(echo "$TOOL_INPUT" | jq -r '.content // empty') + LINE_COUNT=$(echo "$CONTENT" | wc -l) + + if [ "$LINE_COUNT" -gt 500 ]; then + block "BLOCKED: New files cannot exceed 500 lines ($LINE_COUNT lines). Split into smaller modules." + fi + fi +fi + +exit 0 diff --git a/.claude/hooks/user-prompt-submit.sh b/.claude/hooks/user-prompt-submit.sh new file mode 100755 index 00000000..6bc995fe --- /dev/null +++ b/.claude/hooks/user-prompt-submit.sh @@ -0,0 +1,3 @@ +#!/bin/bash +# User prompt hook - minimal, CLAUDE.md handles context +exit 0 diff --git a/.claude/settings.json b/.claude/settings.json new file mode 100644 index 00000000..5dc9cc6a --- /dev/null +++ b/.claude/settings.json @@ -0,0 +1,65 @@ +{ + "permissions": { + "allow": [ + "Bash(make *)", + "Bash(npm *)", + "Bash(npx *)", + "Bash(git status*)", + "Bash(git diff*)", + "Bash(git log*)", + "Bash(git branch*)", + "Bash(git show*)", + "Bash(ls *)", + "Bash(mkdir *)", + "Bash(find *)", + "Bash(wc *)", + "Bash(echo *)", + "Bash(cat *)", + "Bash(head *)", + "Bash(tail *)", + "Read", + "Write", + "Edit", + "Glob", + "Grep", + "Task", + "WebFetch", + "WebSearch" + ], + "deny": [ + "Bash(git commit*)", + "Bash(git push*)", + "Bash(git add*)", + "Bash(git merge*)", + "Bash(git rebase*)", + "Bash(git reset*)", + "Bash(npm publish*)", + "Read(.env*)", + "Edit(.env*)" + ] + }, + "hooks": { + "PreToolUse": [ + { + "matcher": "Bash", + "hooks": [ + { + "type": "command", + "command": "bash .claude/hooks/pre-tool-use.sh" + } + ] + } + ], + "PostToolUse": [ + { + "matcher": "Edit|Write", + "hooks": [ + { + "type": "command", + "command": "bash .claude/hooks/post-tool-use.sh" + } + ] + } + ] + } +} diff --git a/.claude/shared/README.md b/.claude/shared/README.md new file mode 100644 index 00000000..6b1c9f5e --- /dev/null +++ b/.claude/shared/README.md @@ -0,0 +1,13 @@ +# SDK Templates + +## Placeholders +Replace in templates: +- `{{ResourceName}}` = PascalCase (e.g., `Widget`) +- `{{resourceName}}` = camelCase (e.g., `widget`) + +## Templates +- `model-template.ts` → `src/models/{name}.ts` +- `resource-template.ts` → `src/resources/{name}.ts` +- `test-template.spec.ts` → `tests/resources/{name}.spec.ts` + +See `.claude/docs/adding-resources.md` for full guide. diff --git a/.claude/shared/model-template.ts b/.claude/shared/model-template.ts new file mode 100644 index 00000000..361053a1 --- /dev/null +++ b/.claude/shared/model-template.ts @@ -0,0 +1,58 @@ +/** + * Template for creating new Nylas API model types. + * Copy this file and replace {{ResourceName}} and {{resourceName}} with your resource names. + * + * Usage: + * - {{ResourceName}} = PascalCase (e.g., "Widget", "CustomField") + * - {{resourceName}} = camelCase (e.g., "widget", "customField") + */ + +import { ListQueryParams } from './listQueryParams.js'; + +/** + * Interface representing a Nylas {{ResourceName}} object. + */ +export interface {{ResourceName}} { + /** + * Globally unique object identifier. + */ + id: string; + + /** + * Grant ID of the Nylas account. + */ + grantId: string; + + /** + * The type of object. + */ + object: '{{resourceName}}'; + + // Add resource-specific properties below +} + +/** + * Interface representing the query parameters for listing {{ResourceName}}s. + */ +export interface List{{ResourceName}}QueryParams extends ListQueryParams { + // Add resource-specific query parameters +} + +/** + * Interface representing the query parameters for finding a single {{ResourceName}}. + */ +export interface Find{{ResourceName}}QueryParams { + // Add resource-specific query parameters (usually empty or minimal) +} + +/** + * Interface for creating a {{ResourceName}}. + */ +export type Create{{ResourceName}}Request = { + // Add all required and optional properties for creation +}; + +/** + * Interface for updating a {{ResourceName}}. + */ +export type Update{{ResourceName}}Request = Partial; diff --git a/.claude/shared/resource-template.ts b/.claude/shared/resource-template.ts new file mode 100644 index 00000000..44895f89 --- /dev/null +++ b/.claude/shared/resource-template.ts @@ -0,0 +1,163 @@ +/** + * Template for creating new Nylas API resources. + * Copy this file and replace {{ResourceName}} and {{resourceName}} with your resource names. + * + * Usage: + * - {{ResourceName}} = PascalCase (e.g., "Widgets", "CustomFields") + * - {{resourceName}} = camelCase (e.g., "widgets", "customFields") + * - {{resource-name}} = kebab-case for API paths (e.g., "widgets", "custom-fields") + */ + +import { Overrides } from '../config.js'; +import { + Create{{ResourceName}}Request, + {{ResourceName}}, + List{{ResourceName}}QueryParams, + Find{{ResourceName}}QueryParams, + Update{{ResourceName}}Request, +} from '../models/{{resourceName}}.js'; +import { + NylasResponse, + NylasListResponse, + NylasBaseResponse, +} from '../models/response.js'; +import { AsyncListResponse, Resource } from './resource.js'; + +/** + * @property identifier The identifier of the grant to act upon + * @property queryParams The query parameters to include in the request + */ +interface List{{ResourceName}}Params { + identifier: string; + queryParams?: List{{ResourceName}}QueryParams; +} + +/** + * @property {{resourceName}}Id The id of the {{ResourceName}} to retrieve + * @property identifier The identifier of the grant to act upon + * @property queryParams The query parameters to include in the request + */ +interface Find{{ResourceName}}Params { + identifier: string; + {{resourceName}}Id: string; + queryParams?: Find{{ResourceName}}QueryParams; +} + +/** + * @property identifier The identifier of the grant to act upon + * @property requestBody The values to create the {{ResourceName}} with + */ +interface Create{{ResourceName}}Params { + identifier: string; + requestBody: Create{{ResourceName}}Request; +} + +/** + * @property identifier The identifier of the grant to act upon + * @property {{resourceName}}Id The id of the {{ResourceName}} to update + * @property requestBody The values to update the {{ResourceName}} with + */ +interface Update{{ResourceName}}Params { + identifier: string; + {{resourceName}}Id: string; + requestBody: Update{{ResourceName}}Request; +} + +/** + * @property identifier The identifier of the grant to act upon + * @property {{resourceName}}Id The id of the {{ResourceName}} to delete + */ +interface Destroy{{ResourceName}}Params { + identifier: string; + {{resourceName}}Id: string; +} + +/** + * Nylas {{ResourceName}} API + * + * The {{ResourceName}} API allows you to [describe purpose]. + */ +export class {{ResourceName}}s extends Resource { + /** + * Return all {{ResourceName}}s + * @return The list of {{ResourceName}}s + */ + public list({ + identifier, + queryParams, + overrides, + }: List{{ResourceName}}Params & Overrides): AsyncListResponse< + NylasListResponse<{{ResourceName}}> + > { + return super._list({ + queryParams, + path: `/v3/grants/${identifier}/{{resource-name}}s`, + overrides, + }); + } + + /** + * Return a {{ResourceName}} + * @return The {{ResourceName}} + */ + public find({ + identifier, + {{resourceName}}Id, + queryParams, + overrides, + }: Find{{ResourceName}}Params & Overrides): Promise> { + return super._find({ + path: `/v3/grants/${identifier}/{{resource-name}}s/${{{resourceName}}Id}`, + queryParams, + overrides, + }); + } + + /** + * Create a {{ResourceName}} + * @return The created {{ResourceName}} + */ + public create({ + identifier, + requestBody, + overrides, + }: Create{{ResourceName}}Params & Overrides): Promise> { + return super._create({ + path: `/v3/grants/${identifier}/{{resource-name}}s`, + requestBody, + overrides, + }); + } + + /** + * Update a {{ResourceName}} + * @return The updated {{ResourceName}} + */ + public update({ + identifier, + {{resourceName}}Id, + requestBody, + overrides, + }: Update{{ResourceName}}Params & Overrides): Promise> { + return super._update({ + path: `/v3/grants/${identifier}/{{resource-name}}s/${{{resourceName}}Id}`, + requestBody, + overrides, + }); + } + + /** + * Delete a {{ResourceName}} + * @return The deletion response + */ + public destroy({ + identifier, + {{resourceName}}Id, + overrides, + }: Destroy{{ResourceName}}Params & Overrides): Promise { + return super._destroy({ + path: `/v3/grants/${identifier}/{{resource-name}}s/${{{resourceName}}Id}`, + overrides, + }); + } +} diff --git a/.claude/shared/test-template.spec.ts b/.claude/shared/test-template.spec.ts new file mode 100644 index 00000000..19aaa0e6 --- /dev/null +++ b/.claude/shared/test-template.spec.ts @@ -0,0 +1,167 @@ +/** + * Template for creating tests for new Nylas API resources. + * Copy this file and replace {{ResourceName}} and {{resourceName}} with your resource names. + * + * Usage: + * - {{ResourceName}} = PascalCase (e.g., "Widgets", "CustomFields") + * - {{resourceName}} = camelCase (e.g., "widgets", "customFields") + * - {{resource-name}} = kebab-case for API paths (e.g., "widgets", "custom-fields") + */ + +import APIClient from '../../src/apiClient'; +import { {{ResourceName}}s } from '../../src/resources/{{resourceName}}'; +jest.mock('../../src/apiClient'); + +describe('{{ResourceName}}s', () => { + let apiClient: jest.Mocked; + let {{resourceName}}: {{ResourceName}}s; + + beforeAll(() => { + apiClient = new APIClient({ + apiKey: 'apiKey', + apiUri: 'https://test.api.nylas.com', + timeout: 30, + headers: {}, + }) as jest.Mocked; + + {{resourceName}} = new {{ResourceName}}s(apiClient); + apiClient.request.mockResolvedValue({}); + }); + + describe('list', () => { + it('should call apiClient.request with the correct params', async () => { + await {{resourceName}}.list({ + identifier: 'grant-id', + overrides: { + apiUri: 'https://test.api.nylas.com', + headers: { override: 'bar' }, + }, + }); + + expect(apiClient.request).toHaveBeenCalledWith({ + method: 'GET', + path: '/v3/grants/grant-id/{{resource-name}}s', + overrides: { + apiUri: 'https://test.api.nylas.com', + headers: { override: 'bar' }, + }, + }); + }); + + it('should call apiClient.request with query params', async () => { + await {{resourceName}}.list({ + identifier: 'grant-id', + queryParams: { + limit: 10, + }, + }); + + expect(apiClient.request).toHaveBeenCalledWith({ + method: 'GET', + path: '/v3/grants/grant-id/{{resource-name}}s', + queryParams: { + limit: 10, + }, + }); + }); + }); + + describe('find', () => { + it('should call apiClient.request with the correct params', async () => { + await {{resourceName}}.find({ + identifier: 'grant-id', + {{resourceName}}Id: '{{resourceName}}-123', + overrides: { + apiUri: 'https://test.api.nylas.com', + headers: { override: 'bar' }, + }, + }); + + expect(apiClient.request).toHaveBeenCalledWith({ + method: 'GET', + path: '/v3/grants/grant-id/{{resource-name}}s/{{resourceName}}-123', + overrides: { + apiUri: 'https://test.api.nylas.com', + headers: { override: 'bar' }, + }, + }); + }); + }); + + describe('create', () => { + it('should call apiClient.request with the correct params', async () => { + await {{resourceName}}.create({ + identifier: 'grant-id', + requestBody: { + // Add required request body properties + }, + overrides: { + apiUri: 'https://test.api.nylas.com', + headers: { override: 'bar' }, + }, + }); + + expect(apiClient.request).toHaveBeenCalledWith({ + method: 'POST', + path: '/v3/grants/grant-id/{{resource-name}}s', + body: { + // Match request body properties + }, + overrides: { + apiUri: 'https://test.api.nylas.com', + headers: { override: 'bar' }, + }, + }); + }); + }); + + describe('update', () => { + it('should call apiClient.request with the correct params', async () => { + await {{resourceName}}.update({ + identifier: 'grant-id', + {{resourceName}}Id: '{{resourceName}}-123', + requestBody: { + // Add update request body properties + }, + overrides: { + apiUri: 'https://test.api.nylas.com', + headers: { override: 'bar' }, + }, + }); + + expect(apiClient.request).toHaveBeenCalledWith({ + method: 'PUT', + path: '/v3/grants/grant-id/{{resource-name}}s/{{resourceName}}-123', + body: { + // Match request body properties + }, + overrides: { + apiUri: 'https://test.api.nylas.com', + headers: { override: 'bar' }, + }, + }); + }); + }); + + describe('destroy', () => { + it('should call apiClient.request with the correct params', async () => { + await {{resourceName}}.destroy({ + identifier: 'grant-id', + {{resourceName}}Id: '{{resourceName}}-123', + overrides: { + apiUri: 'https://test.api.nylas.com', + headers: { override: 'bar' }, + }, + }); + + expect(apiClient.request).toHaveBeenCalledWith({ + method: 'DELETE', + path: '/v3/grants/grant-id/{{resource-name}}s/{{resourceName}}-123', + overrides: { + apiUri: 'https://test.api.nylas.com', + headers: { override: 'bar' }, + }, + }); + }); + }); +}); diff --git a/.claudeignore b/.claudeignore new file mode 100644 index 00000000..c9e120c3 --- /dev/null +++ b/.claudeignore @@ -0,0 +1,78 @@ +# ============================================================================= +# .claudeignore - Files Claude Code should ignore +# ============================================================================= + +# Build outputs (auto-generated, don't need to read) +lib/ +docs/ +cjs/ +esm/ +coverage/ + +# Dependencies (too large, not useful) +node_modules/ + +# Package manager files +package-lock.json +yarn.lock +pnpm-lock.yaml + +# IDE and editor files +.idea/ +.vscode/ +*.swp +*.swo +.DS_Store + +# Environment and secrets +.env +.env.* +*.pem +*.key +credentials.json + +# Git internals +.git/ + +# Debug logs +npm-debug.log +yarn-debug.log +yarn-error.log +*.log + +# Test artifacts +*.tsbuildinfo +.grunt/ +_SpecRunner.html +tags +CTAGS + +# TypeScript cache +tsconfig.tsbuildinfo + +# Auto-generated files (use make build instead of reading) +src/version.ts +src/models/index.ts + +# Example projects (not part of SDK core) +examples/ + +# Diagrams and images (not code) +diagrams/ +*.png +*.jpg +*.jpeg +*.gif +*.svg +*.ico + +# GitHub templates (not relevant to code) +.github/ISSUE_TEMPLATE/ +pull_request_template.md + +# Changelog (too large, use git log instead) +CHANGELOG.md + +# Other AI agent files (not for Claude) +AGENTS.md +.codex/ diff --git a/.codex/skills/add-endpoint/SKILL.md b/.codex/skills/add-endpoint/SKILL.md new file mode 100644 index 00000000..28f83195 --- /dev/null +++ b/.codex/skills/add-endpoint/SKILL.md @@ -0,0 +1,65 @@ +--- +name: add-endpoint +description: Add a new API endpoint method to an existing Nylas SDK resource +--- + +# Add Endpoint to Existing Resource + +Add a new method to an existing resource class. + +## Steps + +1. **Add types** to `src/models/{resource}.ts`: + ```typescript + export interface {Method}Request { + // Request body fields + } + + export interface {Method}Response { + // Response fields (if different from main type) + } + + export interface {Method}QueryParams { + // Query parameters (if needed) + } + ``` + +2. **Add method** to `src/resources/{resource}.ts`: + ```typescript + public async {methodName}( + params: { + identifier: string; + id: string; + requestBody: {Method}Request; + }, + overrides?: Overrides + ) { + return this._create<{Method}Response>({ + path: `/v3/grants/${params.identifier}/{resource}/${params.id}/action`, + requestBody: params.requestBody, + overrides, + }); + } + ``` + +3. **Add test** to `tests/resources/{resource}.spec.ts`: + ```typescript + describe('{methodName}', () => { + it('should call API with correct parameters', async () => { + apiClient.request.mockResolvedValue({ data: {} }); + await resource.{methodName}({ + identifier: 'grant-123', + id: 'resource-456', + requestBody: { /* test data */ }, + }); + expect(apiClient.request).toHaveBeenCalledWith({ + method: 'POST', + path: '/v3/grants/grant-123/{resource}/resource-456/action', + body: { /* test data */ }, + overrides: undefined, + }); + }); + }); + ``` + +4. **Verify**: `npm test -- tests/resources/{resource}.spec.ts` diff --git a/.codex/skills/add-pagination/SKILL.md b/.codex/skills/add-pagination/SKILL.md new file mode 100644 index 00000000..00aef768 --- /dev/null +++ b/.codex/skills/add-pagination/SKILL.md @@ -0,0 +1,81 @@ +--- +name: add-pagination +description: Implement pagination support for Nylas SDK list endpoints +--- + +# Add Pagination Support + +Implement paginated list endpoints with async iterator support. + +## Implementation + +The `_list` method automatically returns an `AsyncListResponse` with pagination: + +```typescript +public list( + params: { + identifier: string; + queryParams?: ListQueryParams; + }, + overrides?: Overrides +) { + return this._list({ + path: `/v3/grants/${params.identifier}/resources`, + queryParams: params.queryParams, + overrides, + }); +} +``` + +## Query Params Interface + +```typescript +export interface ListQueryParams { + limit?: number; // Items per page (default varies by endpoint) + pageToken?: string; // Token for next page +} +``` + +## Usage Examples + +**Async Iterator (recommended)**: +```typescript +const resources = nylas.resources.list({ identifier: 'grant-123' }); + +for await (const resource of resources) { + console.log(resource); +} +``` + +**Manual Pagination**: +```typescript +const page1 = await nylas.resources.list({ + identifier: 'grant-123', + queryParams: { limit: 10 }, +}); + +if (page1.nextPageToken) { + const page2 = await nylas.resources.list({ + identifier: 'grant-123', + queryParams: { limit: 10, pageToken: page1.nextPageToken }, + }); +} +``` + +## Test Pattern + +```typescript +it('should support pagination', async () => { + apiClient.request.mockResolvedValue({ + data: [{ id: '1' }], + nextPageToken: 'token123', + }); + + const result = await resource.list({ + identifier: 'grant-123', + queryParams: { limit: 1 }, + }); + + expect(result.nextPageToken).toBe('token123'); +}); +``` diff --git a/.codex/skills/add-query-params/SKILL.md b/.codex/skills/add-query-params/SKILL.md new file mode 100644 index 00000000..05dda4a8 --- /dev/null +++ b/.codex/skills/add-query-params/SKILL.md @@ -0,0 +1,57 @@ +--- +name: add-query-params +description: Add query parameter support to a Nylas SDK endpoint +--- + +# Add Query Parameters + +Add query parameter support to an endpoint. + +## Steps + +1. **Define interface** in `src/models/{resource}.ts`: + ```typescript + export interface List{Resource}QueryParams { + limit?: number; + pageToken?: string; + // Add new params + filter?: string; + orderBy?: 'asc' | 'desc'; + } + ``` + +2. **Update method signature** in `src/resources/{resource}.ts`: + ```typescript + public list( + params: { + identifier: string; + queryParams?: List{Resource}QueryParams; + }, + overrides?: Overrides + ) { + return this._list<{Resource}>({ + path: `/v3/grants/${params.identifier}/{resource}s`, + queryParams: params.queryParams, + overrides, + }); + } + ``` + +3. **Add test case**: + ```typescript + it('should pass query params', async () => { + apiClient.request.mockResolvedValue({ data: [] }); + await resource.list({ + identifier: 'grant-123', + queryParams: { limit: 10, filter: 'active' }, + }); + expect(apiClient.request).toHaveBeenCalledWith({ + method: 'GET', + path: '/v3/grants/grant-123/{resource}s', + queryParams: { limit: 10, filter: 'active' }, + overrides: undefined, + }); + }); + ``` + +4. **Verify**: `npm test -- tests/resources/{resource}.spec.ts` diff --git a/.codex/skills/add-resource/SKILL.md b/.codex/skills/add-resource/SKILL.md new file mode 100644 index 00000000..fa3ca029 --- /dev/null +++ b/.codex/skills/add-resource/SKILL.md @@ -0,0 +1,71 @@ +--- +name: add-resource +description: Create a new API resource with model, resource class, and tests for the Nylas SDK +--- + +# Add New Resource + +Create a complete new API resource for the Nylas Node.js SDK. + +## Steps + +1. **Create model** `src/models/{name}.ts`: + ```typescript + export interface {Name} { + id: string; + grantId: string; + // Add properties + } + + export interface Create{Name}Request { + // Required fields + } + + export interface List{Name}QueryParams { + limit?: number; + pageToken?: string; + } + ``` + +2. **Create resource** `src/resources/{name}.ts`: + ```typescript + import { Resource } from './resource.js'; + import { Overrides } from '../config.js'; + import { {Name}, Create{Name}Request } from '../models/{name}.js'; + + export class {Name}s extends Resource { + public list(params: { identifier: string }, overrides?: Overrides) { + return this._list<{Name}>({ + path: `/v3/grants/${params.identifier}/{name}s`, + overrides, + }); + } + + public find(params: { identifier: string; id: string }, overrides?: Overrides) { + return this._find<{Name}>({ + path: `/v3/grants/${params.identifier}/{name}s/${params.id}`, + overrides, + }); + } + + public create(params: { identifier: string; requestBody: Create{Name}Request }, overrides?: Overrides) { + return this._create<{Name}>({ + path: `/v3/grants/${params.identifier}/{name}s`, + requestBody: params.requestBody, + overrides, + }); + } + } + ``` + +3. **Register in** `src/nylas.ts`: + - Add import: `import { {Name}s } from './resources/{name}.js';` + - Add property: `public {name}s: {Name}s;` + - Initialize: `this.{name}s = new {Name}s(this.apiClient);` + +4. **Create test** `tests/resources/{name}.spec.ts`: + - Mock APIClient + - Test list, find, create methods + - Verify request parameters + +5. **Verify**: `npm test -- tests/resources/{name}.spec.ts` diff --git a/.codex/skills/fix-lint/SKILL.md b/.codex/skills/fix-lint/SKILL.md new file mode 100644 index 00000000..3f912a1e --- /dev/null +++ b/.codex/skills/fix-lint/SKILL.md @@ -0,0 +1,54 @@ +--- +name: fix-lint +description: Fix ESLint and Prettier issues in the Nylas SDK +--- + +# Fix Lint Errors + +Resolve linting and formatting issues. + +## Steps + +1. **Auto-fix most issues**: + ```bash + npm run lint:fix + npm run lint:prettier + ``` + +2. **Common manual fixes**: + + - **Missing return type**: + ```typescript + // Wrong + public async list(params) { + // Correct + public async list(params: ListParams): Promise> { + ``` + + - **Unused variable**: + ```typescript + // Remove or prefix with underscore + const _unusedVar = something; + ``` + + - **Prefer const**: + ```typescript + // Wrong + let value = 'fixed'; + // Correct + const value = 'fixed'; + ``` + + - **No explicit any**: + ```typescript + // Wrong + function process(data: any) { + // Correct + function process(data: Record) { + ``` + +3. **Verify**: + ```bash + npm run lint + npm run lint:prettier:check + ``` diff --git a/.codex/skills/fix-tests/SKILL.md b/.codex/skills/fix-tests/SKILL.md new file mode 100644 index 00000000..ef0d2bb5 --- /dev/null +++ b/.codex/skills/fix-tests/SKILL.md @@ -0,0 +1,56 @@ +--- +name: fix-tests +description: Fix failing Jest tests in the Nylas SDK +--- + +# Fix Test Failures + +Resolve failing Jest tests. + +## Steps + +1. **Run tests** to see failures: + ```bash + npm test + ``` + +2. **Common fixes**: + + - **Mock not set up**: + ```typescript + import APIClient from '../../src/apiClient'; + jest.mock('../../src/apiClient'); + + let apiClient: jest.Mocked; + beforeEach(() => { + apiClient = new APIClient({ apiKey: 'test' }) as jest.Mocked; + }); + ``` + + - **Mock return value missing**: + ```typescript + apiClient.request.mockResolvedValue({ data: expectedData }); + ``` + + - **Wrong path in assertion**: + ```typescript + expect(apiClient.request).toHaveBeenCalledWith({ + method: 'GET', + path: '/v3/grants/grant-id/resources', // Check path matches + overrides: undefined, + }); + ``` + + - **Async not awaited**: + ```typescript + it('should work', async () => { + await resource.list({ identifier: 'grant-id' }); + }); + ``` + +3. **Run single test**: + ```bash + npm test -- tests/resources/{name}.spec.ts + ``` + +4. **Verify all**: `npm test` diff --git a/.codex/skills/fix-typescript/SKILL.md b/.codex/skills/fix-typescript/SKILL.md new file mode 100644 index 00000000..6c11b93e --- /dev/null +++ b/.codex/skills/fix-typescript/SKILL.md @@ -0,0 +1,47 @@ +--- +name: fix-typescript +description: Fix TypeScript compilation errors in the Nylas SDK +--- + +# Fix TypeScript Errors + +Resolve TypeScript compilation errors. + +## Steps + +1. **Run build** to see errors: + ```bash + npm run build + ``` + +2. **Common fixes**: + + - **Missing `.js` extension** in imports (required for ESM): + ```typescript + // Wrong + import { Calendar } from '../models/calendars'; + // Correct + import { Calendar } from '../models/calendars.js'; + ``` + + - **Missing optional marker**: + ```typescript + // Wrong - if property can be undefined + createdAt: number; + // Correct + createdAt?: number; + ``` + + - **Type mismatch**: + ```typescript + // Check interface definitions in src/models/ + // Ensure request/response types match API spec + ``` + + - **Missing export**: + ```typescript + // Add to src/models/index.ts if new types + export * from './{model}.js'; + ``` + +3. **Verify**: `npm run build` diff --git a/.codex/skills/handle-binary/SKILL.md b/.codex/skills/handle-binary/SKILL.md new file mode 100644 index 00000000..39c01d6d --- /dev/null +++ b/.codex/skills/handle-binary/SKILL.md @@ -0,0 +1,64 @@ +--- +name: handle-binary +description: Handle binary and stream responses in Nylas SDK endpoints +--- + +# Handle Binary/Stream Responses + +Implement endpoints that return binary data or streams. + +## For Binary Data (Buffer) + +Use `_getRaw` method: + +```typescript +public async download( + params: { + identifier: string; + attachmentId: string; + }, + overrides?: Overrides +): Promise { + return this._getRaw({ + path: `/v3/grants/${params.identifier}/attachments/${params.attachmentId}/download`, + overrides, + }); +} +``` + +## For Streaming Data + +Use `_getStream` method: + +```typescript +public async downloadStream( + params: { + identifier: string; + attachmentId: string; + }, + overrides?: Overrides +): Promise { + return this._getStream({ + path: `/v3/grants/${params.identifier}/attachments/${params.attachmentId}/download`, + overrides, + }); +} +``` + +## Test Pattern + +```typescript +describe('download', () => { + it('should return binary data', async () => { + const mockBuffer = Buffer.from('test data'); + apiClient.requestRaw.mockResolvedValue(mockBuffer); + + const result = await resource.download({ + identifier: 'grant-123', + attachmentId: 'attach-456', + }); + + expect(result).toEqual(mockBuffer); + }); +}); +``` diff --git a/.eslintignore b/.eslintignore index c7d08e78..f847214f 100644 --- a/.eslintignore +++ b/.eslintignore @@ -2,3 +2,4 @@ node_modules lib examples docs +.claude diff --git a/.gitignore b/.gitignore index 79aca361..ec9e8435 100644 --- a/.gitignore +++ b/.gitignore @@ -10,7 +10,15 @@ tags CTAGS .idea coverage -docs +/docs cjs esm -.env \ No newline at end of file +.env + +# Auto-generated CLAUDE.md in subdirectories (keep root one) +**/CLAUDE.md +!/CLAUDE.md + +# Ignore local dirs and files +local/ +*_PR.md \ No newline at end of file diff --git a/.prettierignore b/.prettierignore index 7b52c4b2..b3b72ed2 100644 --- a/.prettierignore +++ b/.prettierignore @@ -1,3 +1,4 @@ node_modules lib -docs \ No newline at end of file +docs +.claude \ No newline at end of file diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 00000000..180e66dc --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,173 @@ +# Nylas Node.js SDK - Agent Instructions + +## Repository Overview + +This is the official Nylas Node.js SDK for the Nylas API v3. It provides TypeScript/JavaScript bindings for email, calendar, and contacts APIs. + +## Tech Stack + +- **Language**: TypeScript 5.x +- **Runtime**: Node.js 18+ +- **Build**: Dual ESM/CJS output +- **Testing**: Jest with jest-fetch-mock +- **Linting**: ESLint + Prettier + +## Project Structure + +``` +src/ +├── nylas.ts # Main entry point, Nylas class +├── apiClient.ts # HTTP client, auth, error handling +├── resources/ # API resource classes +│ ├── resource.ts # Base class with _list, _find, _create, _update, _destroy +│ ├── calendars.ts # Calendar operations +│ ├── events.ts # Event operations +│ ├── messages.ts # Email operations +│ └── ... +├── models/ # TypeScript interfaces +│ ├── calendars.ts # Calendar types +│ ├── events.ts # Event types +│ └── ... +tests/ +├── resources/ # Resource tests (mirror src/resources/) +└── *.spec.ts # Other tests +lib/ # Build output (generated) +├── esm/ # ES modules +├── cjs/ # CommonJS +└── types/ # TypeScript declarations +``` + +## Code Patterns + +### Resource Class Pattern + +All resources extend the base `Resource` class: + +```typescript +import { Resource } from './resource.js'; +import { Overrides } from '../config.js'; +import { MyResource, CreateMyResourceRequest } from '../models/myResource.js'; + +export class MyResources extends Resource { + public async list(params: { identifier: string }, overrides?: Overrides) { + return this._list({ + path: `/v3/grants/${params.identifier}/myresources`, + overrides, + }); + } + + public async find(params: { identifier: string; id: string }, overrides?: Overrides) { + return this._find({ + path: `/v3/grants/${params.identifier}/myresources/${params.id}`, + overrides, + }); + } + + public async create(params: { identifier: string; requestBody: CreateMyResourceRequest }, overrides?: Overrides) { + return this._create({ + path: `/v3/grants/${params.identifier}/myresources`, + requestBody: params.requestBody, + overrides, + }); + } +} +``` + +### Model Interface Pattern + +```typescript +export interface MyResource { + id: string; + grantId: string; + name: string; + createdAt?: number; + updatedAt?: number; +} + +export interface CreateMyResourceRequest { + name: string; +} + +export interface ListMyResourceQueryParams { + limit?: number; + pageToken?: string; +} +``` + +### Test Pattern + +```typescript +import APIClient from '../../src/apiClient'; +import { MyResources } from '../../src/resources/myResources'; + +jest.mock('../../src/apiClient'); + +describe('MyResources', () => { + let apiClient: jest.Mocked; + let myResources: MyResources; + + beforeEach(() => { + apiClient = new APIClient({ apiKey: 'test-key' }) as jest.Mocked; + myResources = new MyResources(apiClient); + }); + + describe('list', () => { + it('should call API with correct parameters', async () => { + apiClient.request.mockResolvedValue({ data: [] }); + await myResources.list({ identifier: 'grant-123' }); + expect(apiClient.request).toHaveBeenCalledWith({ + method: 'GET', + path: '/v3/grants/grant-123/myresources', + overrides: undefined, + }); + }); + }); +}); +``` + +## Commands + +```bash +# Install dependencies +npm install + +# Build +npm run build + +# Test +npm test +npm test -- tests/resources/calendars.spec.ts # Single file + +# Lint +npm run lint +npm run lint:fix + +# Format +npm run lint:prettier +npm run lint:prettier:check +``` + +## API Path Conventions + +- **Grant-scoped**: `/v3/grants/{grantId}/{resource}` +- **Application-scoped**: `/v3/{resource}` + +## Naming Conventions + +- **Classes**: PascalCase (`Calendars`, `Events`) +- **Interfaces**: PascalCase (`Calendar`, `CreateEventRequest`) +- **Functions/methods**: camelCase (`findById`, `listAll`) +- **Files**: camelCase (`calendars.ts`, `apiClient.ts`) +- **Protected methods**: underscore prefix (`_list`, `_find`) + +## Response Types + +- Single item: `NylasResponse` +- List with pagination: `NylasListResponse` +- Delete response: `NylasBaseResponse` + +## Do Not Edit + +- `src/version.ts` - Auto-generated +- `src/models/index.ts` - Auto-generated +- `lib/` - Build output diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 00000000..2758b033 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,76 @@ +# Nylas Node.js SDK + +## Quick Start + +```bash +make help # See all commands +make ci # Full CI (lint, format, test) +make test # Run tests +make build # Build SDK +``` + +## Automation (Hands-Free) + +**Auto-formatting**: Code is automatically formatted with Prettier + ESLint after every edit. + +**Pre-allowed tools**: Most operations run without permission prompts. + +**Git safety**: Claude handles code, you handle commits. + +## Architecture + +- **Entry**: `src/nylas.ts` - Nylas class with APIClient +- **Resources**: `src/resources/` - Extend `resource.ts` with `_list`, `_find`, `_create`, `_update`, `_destroy` +- **Models**: `src/models/` - TypeScript interfaces for API objects +- **Build output**: `lib/esm/`, `lib/cjs/`, `lib/types/` + +## Git Workflow + +Claude handles: +- Writing code, running tests, fixing issues +- `git status`, `git diff`, `git log` + +**You handle** (for review control): +- `git add`, `git commit`, `git push` + +## Protected Files + +Auto-generated (don't edit manually): +- `src/version.ts` - Run `make build` +- `src/models/index.ts` - Run `make build` + +Secrets (blocked): +- `.env*` files + +## Code Quality Rules + +**New file limit**: 500 lines max. Split large files into smaller modules. + +*Note: This applies to new files only, not existing files.* + +## Adding New Resources + +1. Copy templates from `.claude/shared/` +2. Register in `src/nylas.ts` +3. Run `make ci` + +See `.claude/docs/adding-resources.md` for details. + +## Slash Commands + +| Command | Action | +|---------|--------| +| `/project:test` | Run tests | +| `/project:build` | Build SDK | +| `/project:fix` | Auto-fix lint issues | +| `/project:explore {query}` | Parallel codebase search | +| `/project:add-resource {name}` | Scaffold new resource | +| `/project:review` | Review code changes | + +## Detailed Guides + +| Guide | Path | +|-------|------| +| Architecture | `.claude/docs/architecture.md` | +| Adding Resources | `.claude/docs/adding-resources.md` | +| Testing | `.claude/docs/testing-guide.md` | diff --git a/Makefile b/Makefile new file mode 100644 index 00000000..b93e1f17 --- /dev/null +++ b/Makefile @@ -0,0 +1,202 @@ +# Nylas Node.js SDK - Development Makefile +# This Makefile provides shortcuts for common development tasks +# Run `make help` to see all available commands + +.PHONY: help install build build-esm build-cjs clean test test-watch test-single test-coverage lint lint-fix format format-check docs typecheck ci pre-commit new-resource + +# Default target +.DEFAULT_GOAL := help + +# Colors for output +CYAN := \033[36m +GREEN := \033[32m +YELLOW := \033[33m +RED := \033[31m +RESET := \033[0m + +#============================================================================== +# Help +#============================================================================== + +help: ## Show this help message + @echo "$(CYAN)Nylas Node.js SDK - Development Commands$(RESET)" + @echo "" + @echo "$(GREEN)Usage:$(RESET) make [target]" + @echo "" + @awk 'BEGIN {FS = ":.*##"; printf ""} /^[a-zA-Z_-]+:.*?##/ { printf " $(CYAN)%-18s$(RESET) %s\n", $$1, $$2 }' $(MAKEFILE_LIST) + @echo "" + @echo "$(YELLOW)Examples:$(RESET)" + @echo " make test-single FILE=tests/resources/calendars.spec.ts" + @echo " make new-resource NAME=widgets" + +#============================================================================== +# Installation +#============================================================================== + +install: ## Install all dependencies + @echo "$(GREEN)Installing dependencies...$(RESET)" + npm install + +reinstall: clean-deps install ## Clean and reinstall dependencies + +clean-deps: ## Remove node_modules + @echo "$(YELLOW)Removing node_modules...$(RESET)" + rm -rf node_modules + +#============================================================================== +# Building +#============================================================================== + +build: ## Build both ESM and CJS outputs + @echo "$(GREEN)Building SDK...$(RESET)" + npm run build + +build-esm: ## Build ESM output only + @echo "$(GREEN)Building ESM...$(RESET)" + npm run build-esm + +build-cjs: ## Build CJS output only + @echo "$(GREEN)Building CJS...$(RESET)" + npm run build-cjs + +clean: ## Remove build artifacts + @echo "$(YELLOW)Cleaning build artifacts...$(RESET)" + rm -rf lib docs + +rebuild: clean build ## Clean and rebuild + +#============================================================================== +# Testing +#============================================================================== + +test: ## Run all tests with coverage + @echo "$(GREEN)Running tests...$(RESET)" + npm test + +test-watch: ## Run tests in watch mode + @echo "$(GREEN)Running tests in watch mode...$(RESET)" + npm test -- --watch + +test-single: ## Run a single test file (use FILE=path/to/test.spec.ts) + @echo "$(GREEN)Running test: $(FILE)$(RESET)" + npm test -- $(FILE) + +test-coverage: ## Run tests with detailed coverage report + @echo "$(GREEN)Running tests with coverage...$(RESET)" + npm run test:coverage + +test-verbose: ## Run tests with verbose output + @echo "$(GREEN)Running tests (verbose)...$(RESET)" + npm test -- --verbose + +test-resource: ## Run tests for a specific resource (use NAME=calendars) + @echo "$(GREEN)Running tests for resource: $(NAME)$(RESET)" + npm test -- tests/resources/$(NAME).spec.ts + +#============================================================================== +# Linting & Formatting +#============================================================================== + +lint: ## Run ESLint + @echo "$(GREEN)Running ESLint...$(RESET)" + npm run lint + +lint-fix: ## Run ESLint with auto-fix + @echo "$(GREEN)Running ESLint with auto-fix...$(RESET)" + npm run lint:fix + +format: ## Format code with Prettier + @echo "$(GREEN)Formatting code...$(RESET)" + npm run lint:prettier + +format-check: ## Check code formatting + @echo "$(GREEN)Checking code formatting...$(RESET)" + npm run lint:prettier:check + +#============================================================================== +# Type Checking +#============================================================================== + +typecheck: ## Run TypeScript type checking + @echo "$(GREEN)Running TypeScript type check...$(RESET)" + npx tsc --noEmit + +typecheck-watch: ## Run TypeScript type checking in watch mode + @echo "$(GREEN)Running TypeScript type check (watch mode)...$(RESET)" + npx tsc --noEmit --watch + +#============================================================================== +# Documentation +#============================================================================== + +docs: ## Generate TypeDoc documentation + @echo "$(GREEN)Generating documentation...$(RESET)" + npm run build:docs + +docs-serve: docs ## Generate and serve documentation + @echo "$(GREEN)Serving documentation on http://localhost:8080...$(RESET)" + cd docs && npx http-server -p 8080 + +#============================================================================== +# CI/CD +#============================================================================== + +ci: lint-fix format-check test ## Run full CI pipeline (lint, format check, test) + @echo "$(GREEN)CI pipeline complete!$(RESET)" + +pre-commit: lint-fix format typecheck test ## Run pre-commit checks + @echo "$(GREEN)Pre-commit checks complete!$(RESET)" + +#============================================================================== +# Development Helpers +#============================================================================== + +new-resource: ## Create scaffolding for a new resource (use NAME=resourceName) +ifndef NAME + @echo "$(RED)Error: NAME is required. Usage: make new-resource NAME=widgets$(RESET)" + @exit 1 +endif + @echo "$(GREEN)Creating scaffolding for resource: $(NAME)$(RESET)" + @echo "$(YELLOW)Please copy and customize these templates:$(RESET)" + @echo " 1. .claude/shared/model-template.ts -> src/models/$(NAME).ts" + @echo " 2. .claude/shared/resource-template.ts -> src/resources/$(NAME).ts" + @echo " 3. .claude/shared/test-template.spec.ts -> tests/resources/$(NAME).spec.ts" + @echo "" + @echo "$(YELLOW)Then update:$(RESET)" + @echo " 4. Add export to src/models/index.ts" + @echo " 5. Import and register in src/nylas.ts" + +link: build ## Build and link for local development + @echo "$(GREEN)Linking package for local development...$(RESET)" + npm link + +unlink: ## Unlink the local package + @echo "$(YELLOW)Unlinking package...$(RESET)" + npm unlink + +#============================================================================== +# Version Management +#============================================================================== + +version-patch: ## Bump patch version + @echo "$(GREEN)Bumping patch version...$(RESET)" + npm version patch + +version-minor: ## Bump minor version + @echo "$(GREEN)Bumping minor version...$(RESET)" + npm version minor + +version-major: ## Bump major version + @echo "$(GREEN)Bumping major version...$(RESET)" + npm version major + +#============================================================================== +# Quick Shortcuts +#============================================================================== + +t: test ## Alias for test +w: test-watch ## Alias for test-watch +l: lint-fix ## Alias for lint-fix +f: format ## Alias for format +b: build ## Alias for build +c: ci ## Alias for ci diff --git a/jest.config.js b/jest.config.js index b3785f22..b55c6700 100644 --- a/jest.config.js +++ b/jest.config.js @@ -1,5 +1,6 @@ const config = { preset: 'ts-jest/presets/js-with-ts', + testPathIgnorePatterns: ['/node_modules/', '/.claude/'], transform: { '^.+\\.tsx?$': [ 'ts-jest',