This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
create-pipedrive-app is a CLI scaffolding tool for external Pipedrive Marketplace developers. It generates a production-ready integration project via npx create-pipedrive-app <project-name>.
npm run build # compile TypeScript to dist/
npm run typecheck # type-check without emitting
npm run lint # ESLint
npm run format # Prettier (120 char width, tabs, trailing commas)
npm test # Vitest suite
npm run generate # generate test project in apps/test-app/ (gitignored)
npm run clean # delete generated apps/ directoryRun a single test file:
npx vitest run src/generators/node/app.test.tsThe tool is CLI-first, with an AI plugin layer built on top:
- CLI core: Collects user choices via interactive prompts, then generates a project scaffold from templates.
- AI plugin layer (secondary): Claude/Codex skills that wrap the CLI — guide developers, modify existing projects, and explain generated code.
The CLI asks for:
- Project name
- Database: Postgres, MySQL, or SQLite
- App Extensions frontend: multi-select of
custom-paneland/orcustom-modal(or neither)
GeneratorOptions.appExtensions is AppExtensionType[] where AppExtensionType is derived from the APP_EXTENSION_TYPES const ('custom-panel' | 'custom-modal'). Use isAppExtensionType(value) to validate CLI input. Check membership with .includes('custom-panel'), not boolean equality.
Beyond the interactive main flow, the CLI supports subcommands invoked by the AI plugin skills:
npx create-pipedrive-app add-app-extension --app-extensions custom-panel|custom-modal [--output-dir <dir>]Subcommand dispatch happens in dispatchSubcommand() in cli.ts before the interactive flow runs.
cli.ts (collects prompts)
→ prompts/ (projectName, database, appExtensions)
→ nodeGenerator (orchestrates sub-generators via NodeProjectBuilder)
→ oauth.ts, database.ts, app.ts, pipedriveClient.ts, crypto.ts
→ appExtensions.ts (conditional)
→ appExtensions/panel.ts — backend router + React snippet contributions
→ appExtensions/modal.ts — backend router + React snippet contributions
→ appExtensions/frontend.ts — Vite + React frontend (index.html, App.tsx, etc.)
→ appExtensions/sdk.ts — usePipedriveSdk hook wrapper
→ appExtensions/router.ts — shared Express static-file router
→ serverEntry, packageJson, tsConfig, envExample, dockerCompose, readme
There is no template directory. Generators build file content as strings using dedent(), with conditional string interpolation for optional features. The src/utils/writeFile.ts utility writes files, creates parent directories, and auto-formats output with Prettier — generated code is formatted automatically without an explicit format step.
<project-name>/
src/
app.ts # Express app, mounts all routers
index.ts # Server entry with DB retry loop
oauth/ # Authorization redirect, callback, token exchange, refresh
pipedrive/ # Official API client wrapper (getClient, token refresh via onTokenUpdate)
database/ # Drizzle schema, migrations, db setup
app-extensions/
panel/ # Express router serving built frontend (custom-panel)
modal/ # Express router serving built frontend (custom-modal)
frontend/
app-extension-ui/ # Vite + React iframe UI (only when App Extensions selected)
.env.example
README.md
docker-compose.yml
Each extension type (panel, modal) contributes a ReactSnippetContribution — an object with { sdkImports, handlers, buttons } — that gets merged into the generated App.tsx. This lets panel.ts and modal.ts independently declare what SDK imports and JSX they need without knowing about each other.
When App Extensions are enabled, docker-compose up --watch starts both the Express backend and the Vite dev server in containers with Compose Watch for live code sync. The Vite server must be exposed via a public HTTPS tunnel and configured in Developer Hub as the iframe URL.
The backend serves the built frontend at /extensions/panel and /extensions/modal via Express static routing (using the shared routerContent() from appExtensions/router.ts). In production, npm run build builds both backend TypeScript and the Vite bundle.
The scaffold generator uses NodeProjectBuilder + BuildStep (src/generators/node/projectBuilder.ts) to compose features without scattering conditional logic across generators.
How it works:
BuildStepinterface:execute(outputDir: string, options: GeneratorOptions): Promise<void>- Each feature is a private class implementing
BuildStep(e.g.OAuthStep,DatabaseStep) NodeProjectBuilderqueues steps and runs them in order via.build()- Named methods like
.addOAuth(),.addDatabase()push steps unconditionally when(condition, fn)adds steps conditionally at the call site inindex.ts— never insideexecute()
Adding a new feature:
- Create a private
BuildStepclass inprojectBuilder.ts - Add a named method to
NodeProjectBuilder:addMyFeature(): this { return this.addStep(new MyFeatureStep()); } - Call it in
index.ts, viawhen()if conditional:.when(options.appExtensions.length > 0, b => b.addAppExtensions())
Rule: never put conditional logic inside execute() — use when() at the call site. Steps must be unconditional internally; the builder chain controls what runs.
File content helpers:
- Use
SourceFileBuilder(src/utils/sourceFileBuilder.ts) for TypeScript files with conditional imports or blocks — handles deduplication and formatting automatically - Use
RouterMountBuilder(src/utils/templates.ts) to accumulateapp.use()calls conditionally - Use plain
dedentfor static content (YAML, JSON,.env, SQL)
- New prompt: add
src/prompts/<feature>.ts+.test.ts, export fromcli.ts - New generator: add
src/generators/node/<feature>.ts+.test.ts, call fromnodeGenerator - New App Extension type: add a file under
src/generators/node/appExtensions/that exports anasync generate*function and a*ReactSnippets()function returningReactSnippetContribution, then wire it inappExtensions.tsandfrontend.ts - Modify generated scaffold: edit template strings in the corresponding generator file
Full OAuth 2.0: authorization redirect, callback handling, token exchange, HMAC-signed state parameter with TTL validation.
Uses Drizzle ORM (drizzle-orm + drizzle-kit) for schema definition and migrations. Supports Postgres, MySQL, and SQLite with the same TypeScript API.
schema.ts— Drizzle table definitions (tenants, oauth_tokens, installations)migrations/— SQL migration files managed bydrizzle-kitdb.ts— driver setup (selectspostgres-js,mysql2, orbetter-sqlite3based on chosen DB)
Wraps the official Pipedrive Node.js SDK. getClient(companyId) loads the stored OAuth token and configures onTokenUpdate so the SDK handles token refresh automatically.
Generated when the user selects custom-panel and/or custom-modal. Iframe-based React + Vite UI using the App Extensions SDK (usePipedriveSdk hook), with: SDK initialization, theme handling, resize, snackbar, confirmation dialog, signed token, and extension-type-specific actions (open modal from panel, close modal).
Vitest. Tests generate files into a tmpdir()/cpa-app-test directory, read them back to verify content, and clean up in afterEach.
The plugin/ directory is shipped with the npm package ("files": ["dist", "plugin"]) and contains Claude Code skills that wrap the CLI for AI-assisted development. Each skill is a markdown instruction document in plugin/skills/<name>/SKILL.md.
| Skill | Purpose |
|---|---|
pipedrive-new-app |
Scaffold a new app interactively via npx create-pipedrive-app |
pipedrive-add-app-extension |
Add a panel or modal extension via the add-app-extension subcommand |
pipedrive-api |
Guide on using the Pipedrive REST API and SDK within a generated project |
pipedrive-review-marketplace-readiness |
Gap analysis before marketplace submission: checks token refresh, HTTPS, error handling, and rate limit handling |
Skills use allowed-tools frontmatter to restrict which Claude tools they can invoke. Adding a new skill means creating a SKILL.md in a new subdirectory under plugin/skills/.