Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
10fff08
AINATIVEM-44 update CLAUDE.md and README with commands, architecture,…
youssef-saber-3 May 11, 2026
0d936af
Add database layer design spec for AINATIVEM-43
dmitriyeff May 11, 2026
2f5d60a
AINATIVEM-44 add design spec for pipedrive client and builder pattern…
youssef-saber-3 May 11, 2026
6f48c52
AINATIVEM-44 add implementation plan for pipedrive client and builder…
youssef-saber-3 May 11, 2026
82d613a
AINATIVEM-44 add SourceFileBuilder utility
youssef-saber-3 May 11, 2026
c94a08a
test(database): add full test suite for database layer generator
dmitriyeff May 11, 2026
eccf9b7
AINATIVEM-44 fix SourceFileBuilder quality issues
youssef-saber-3 May 11, 2026
c5aacba
AINATIVEM-44 add template helper functions
youssef-saber-3 May 11, 2026
64cf64f
style(database): fix indentation to use tabs in test file
dmitriyeff May 11, 2026
8711110
AINATIVEM-44 add pipedriveClient generator
youssef-saber-3 May 11, 2026
4e715e2
feat(database): generate schema.ts with pipedrive_tokens table
dmitriyeff May 11, 2026
a057f29
AINATIVEM-44 refactor app generator to use SourceFileBuilder
youssef-saber-3 May 11, 2026
8280086
AINATIVEM-44 refactor oauth generator to use templates
youssef-saber-3 May 11, 2026
1619ad0
feat(database): generate src/database/index.ts Drizzle client
dmitriyeff May 11, 2026
6835523
AINATIVEM-44 add NodeProjectBuilder with BuildStep pattern
youssef-saber-3 May 11, 2026
6ea4643
AINATIVEM-44 fix misleading test name in projectBuilder
youssef-saber-3 May 11, 2026
16cd62a
feat(database): generate migrate.ts with runMigrations
dmitriyeff May 11, 2026
09a3534
feat(database): generate 0000_init.sql for all three dialects
dmitriyeff May 11, 2026
af18b21
feat(database): generate drizzle.config.ts
dmitriyeff May 11, 2026
4b7e83c
AINATIVEM-44 refactor index to use NodeProjectBuilder
youssef-saber-3 May 11, 2026
f31640c
refactor(database): use typed Record for dialect mapping in drizzle c…
dmitriyeff May 11, 2026
ca2d00e
AINATIVEM-44 fix node: protocol prefix in index.test.ts imports
youssef-saber-3 May 11, 2026
e43854f
feat(database): move docker-compose to database generator, add health…
dmitriyeff May 11, 2026
f3d79ac
AINATIVEM-44 add RouterMountBuilder, generate script, and deps instal…
youssef-saber-3 May 11, 2026
dbe1d1e
feat(database): add DB driver, drizzle-kit, and db:migrate to generat…
dmitriyeff May 11, 2026
6cab584
fix(database): update drizzle-kit to ^0.21.0 to match drizzle-orm ^0.…
dmitriyeff May 11, 2026
5c268e3
feat(database): call runMigrations() in generated server entry before…
dmitriyeff May 11, 2026
cafed17
AINATIVEM-44 add husky pre-commit hook to run prettier
youssef-saber-3 May 11, 2026
5403619
fix(database): async SQLite runMigrations, MySQL createPool, fix test…
dmitriyeff May 11, 2026
e6a9e52
style: apply prettier formatting to test files and spec doc
dmitriyeff May 11, 2026
caeb90b
merge: integrate AINATIVEM-44 NodeProjectBuilder refactor
dmitriyeff May 11, 2026
8d80e4f
style: apply prettier formatting to source and test files
dmitriyeff May 11, 2026
a08dbfe
chore: remove database layer design spec from implementation branch
dmitriyeff May 11, 2026
42e26dd
AINATIVEM-44 update pipedrive client to v32 with v2/v1 namespace imports
youssef-saber-3 May 11, 2026
134d378
AINATIVEM-44 add .nvmrc pinned to Node.js 22
youssef-saber-3 May 11, 2026
c57a580
AINATIVEM-44 add .nvmrc pinned to Node.js 22
youssef-saber-3 May 11, 2026
36316d8
merge: resolve conflicts with AINATIVEM-44 (pipedrive v32, husky)
github-actions[bot] May 11, 2026
11bafca
AINATIVEM-44 merge AINATIVEM-43 database generator, keep pipedrive v32
youssef-saber-3 May 11, 2026
33b9c38
AINATIVEM-44 merge AINATIVEM-43, prioritize v32 pipedrive client fixes
youssef-saber-3 May 11, 2026
5c38dec
AINATIVEM-44 move docker-compose into database generator, add builder…
youssef-saber-3 May 11, 2026
cf41ced
AINATIVEM-43-sqlite-fix
dmitriyeff May 11, 2026
63eb257
AINATIVEM-44 remove unused imports from projectBuilder.test.ts
youssef-saber-3 May 11, 2026
9066398
AINATIVEM-43-sqlite-fix resolve
dmitriyeff May 11, 2026
8e8aafa
Merge pull request #4 from pipedrive/AINATIVEM-43-sqlite-fix
youssef-saber-3 May 11, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,6 @@ docs/superpowers/

# locally generated test apps
*-app/
apps/

.idea/
1 change: 1 addition & 0 deletions .husky/pre-commit
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
npm run format
1 change: 1 addition & 0 deletions .nvmrc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
22
70 changes: 62 additions & 8 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,17 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co

`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>`.

## Commands

```bash
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)
```

## Architecture

The tool is **CLI-first**, with an **AI plugin layer** built on top:
Expand All @@ -21,6 +32,21 @@ The CLI asks for:
- App Extensions frontend: React, Vanilla JS, or none
- Webhooks: Yes/No

### Generator flow

```
cli.ts (collects prompts)
→ prompts/ (projectName, database, appExtensions, webhooks)
→ nodeGenerator (orchestrates 5 sub-generators)
→ oauth.ts, database.ts, app.ts
→ webhooks.ts (conditional), appExtensions.ts (conditional)
→ serverEntry, packageJson, tsConfig, envExample, dockerCompose
```
Comment on lines +39 to +44

**There is no template directory.** Generators build file content as strings using `dedent()`, with conditional string interpolation for optional features (webhooks, app extension types). 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.

`app.ts` is the main example of the conditional pattern: imports and router mounts are included only when the relevant features are enabled, and the result is written once.
Comment on lines +46 to +48

### Generated project structure

```
Expand All @@ -38,6 +64,38 @@ The CLI asks for:
marketplace-checklist.md
```

## Adding features

- **New prompt**: add `src/prompts/<feature>.ts` + `.test.ts`, export from `cli.ts`
- **New generator**: add `src/generators/node/<feature>.ts` + `.test.ts`, call from `nodeGenerator`
- **Modify generated scaffold**: edit template strings in the corresponding generator file

## Builder Pattern

The scaffold generator uses `NodeProjectBuilder` + `BuildStep` (`src/generators/node/projectBuilder.ts`) to compose features without scattering conditional logic across generators.

**How it works:**
- `BuildStep` interface: `execute(outputDir: string, options: GeneratorOptions): Promise<void>`
- Each feature is a private class implementing `BuildStep` (e.g. `OAuthStep`, `DatabaseStep`)
- `NodeProjectBuilder` queues 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 in `index.ts` — never inside `execute()`

**Adding a new feature:**
1. Create a private `BuildStep` class in `projectBuilder.ts`
2. Add a named method to `NodeProjectBuilder`: `addMyFeature(): this { return this.addStep(new MyFeatureStep()); }`
3. Call it in `index.ts`, via `when()` if conditional:
```ts
.when(options.webhooks, b => b.addWebhooks())
```

**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 accumulate `app.use()` calls conditionally
- Use plain `dedent` for static content (YAML, JSON, `.env`, SQL)

## MVP Scope

The initial implementation targets:
Expand All @@ -49,7 +107,7 @@ The initial implementation targets:
- **Frontend** (optional): React App Extensions UI
- Outputs `.env.example` and a Marketplace readiness checklist

PHP and MySQL/SQLite backends come after MVP.
PHP and MySQL/SQLite backends come after MVP. The PHP generator exists but throws "not yet implemented". App Extensions frontend is prompted but not yet generated.

## Core Modules

Expand All @@ -64,24 +122,20 @@ Structure:
- `migrations/` — SQL migration files managed by `drizzle-kit`
- `db.ts` — driver setup (selects `postgres-js`, `mysql2`, or `better-sqlite3` based on the chosen DB)

Key packages: `drizzle-orm`, `drizzle-kit`, and the appropriate driver for the selected database.

### Pipedrive API client (`backend/pipedrive-client/`)
Wrapper around the official Pipedrive Node.js client with preconfigured authentication and helpers for common API calls.

### App Extensions frontend (`frontend/app-extension-ui/`)
Only generated when the user opts in. Iframe-based UI using the App Extensions SDK, supporting: initialization, resizing, modals, notifications/snackbars, theme handling.

## Development
## Tests

To test app generation locally:
Vitest. Tests generate files into a `tmpdir()/cpa-app-test` directory, read them back to verify content, and clean up in `afterEach`. Run a single test file:

```bash
npx tsx src/cli.ts app
npx vitest run src/generators/node/app.test.ts
```

This creates an `app/` directory in the repo root (gitignored via `*-app/`).

## AI Plugin Commands (future layer)

```
Expand Down
52 changes: 51 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,52 @@
# create-pipedrive-app
Scaffold a production-ready Pipedrive Marketplace app with OAuth, database, webhooks, and App Extensions in seconds.

CLI scaffolding tool for Pipedrive Marketplace integrations.

## Usage

```bash
npx create-pipedrive-app <project-name>
```

The CLI will prompt for:

- **Database**: Postgres, MySQL, or SQLite
- **App Extensions**: custom panel, custom modal, or none
- **Webhooks**: yes or no

## Generated project

```
<project-name>/
src/
index.ts # server entry point (port 3000)
app.ts # Express app with OAuth router (+ optional webhooks/extensions)
oauth/ # OAuth 2.0 install, callback, token exchange, refresh
database/ # Drizzle ORM schema, migrations, db driver
pipedrive-client/ # Pipedrive API client wrapper
webhooks/ # Webhook handlers (if selected)
app-extensions/ # App Extensions handlers (if selected)
.env.example
docker-compose.yml # Postgres or MySQL (if applicable)
package.json
tsconfig.json
```

The generated project uses **Express + TypeScript + Drizzle ORM** (ESM, Node.js).

## Next steps after generation

```bash
cd <project-name>
cp .env.example .env
docker-compose up -d # if Postgres or MySQL was selected
npm install
npm run dev
```

Fill in `PIPEDRIVE_CLIENT_ID`, `PIPEDRIVE_CLIENT_SECRET`, and `DATABASE_URL` in `.env`.

## Requirements

- Node.js 18+
- Docker (if using Postgres or MySQL)
17 changes: 17 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 4 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@
"format": "prettier --write src",
"lint": "eslint src",
"test": "vitest run",
"typecheck": "tsc --noEmit"
"typecheck": "tsc --noEmit",
"generate": "tsx src/cli.ts apps/test-app",
"prepare": "husky"
},
"dependencies": {
"@clack/prompts": "^0.9.0",
Expand All @@ -23,6 +25,7 @@
"@types/dedent": "^0.7.2",
"@types/node": "^20.19.39",
"eslint": "^9.0.0",
"husky": "^9.1.7",
"tsx": "^4.7.0",
"typescript": "^5.4.0",
"typescript-eslint": "^8.0.0",
Expand Down
18 changes: 16 additions & 2 deletions src/cli.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import * as clack from '@clack/prompts';
import { basename, resolve } from 'path';
import { spawn } from 'node:child_process';
import { basename, resolve } from 'node:path';
import { promptAppExtensions } from './prompts/appExtensions.js';
import { promptDatabase } from './prompts/database.js';
import { promptProjectName } from './prompts/projectName.js';
Expand All @@ -26,12 +27,25 @@ async function main(): Promise<void> {

clack.outro(`✓ Created ${projectName}`);

const installDeps = await clack.confirm({ message: 'Install dependencies now?' });
if (clack.isCancel(installDeps)) process.exit(0);

if (installDeps) {
const spinner = clack.spinner();
spinner.start('Installing dependencies');
const ok = await new Promise<boolean>((resolve) => {
const child = spawn('npm', ['install'], { cwd: outputDir, stdio: 'ignore' });
child.on('close', (code) => resolve(code === 0));
});
spinner.stop(ok ? 'Dependencies installed' : 'npm install failed — run it manually');
}

const needsDocker = database === 'postgres' || database === 'mysql';
console.log('\nNext steps:');
console.log(` cd ${nameOrPath}`);
console.log(' cp .env.example .env');
if (needsDocker) console.log(' docker-compose up -d');
console.log(' npm install');
if (!installDeps) console.log(' npm install');
console.log(' npm run dev');
}

Expand Down
51 changes: 20 additions & 31 deletions src/generators/node/app.ts
Original file line number Diff line number Diff line change
@@ -1,41 +1,30 @@
import dedent from 'dedent';
import { join } from 'path';
import { writeFile } from '../../utils/writeFile.js';
import type { GeneratorOptions } from '../interface.js';
import { SourceFileBuilder } from '../../utils/sourceFileBuilder.js';
import { RouterMountBuilder } from '../../utils/templates.js';

export async function generateApp(outputDir: string, options: GeneratorOptions): Promise<void> {
const webhooksImport = options.webhooks ? `import webhooksRouter from './webhooks/index.js';` : '';
const panelImport = options.appExtensions.includes('custom-panel')
? `import panelRouter from './app-extensions/panel/index.js';`
: '';
const modalImport = options.appExtensions.includes('custom-modal')
? `import modalRouter from './app-extensions/modal/index.js';`
: '';
const hasPanel = options.appExtensions.includes('custom-panel');
const hasModal = options.appExtensions.includes('custom-modal');

const webhooksMount = options.webhooks ? `app.use('/webhooks', webhooksRouter);` : '';
const panelMount = options.appExtensions.includes('custom-panel')
? `app.use('/extensions/panel', panelRouter);`
: '';
const modalMount = options.appExtensions.includes('custom-modal')
? `app.use('/extensions/modal', modalRouter);`
: '';
const mounts = new RouterMountBuilder()
.add('/oauth', 'oauthRouter')
.addIf(options.webhooks, '/webhooks', 'webhooksRouter')
.addIf(hasPanel, '/extensions/panel', 'panelRouter')
.addIf(hasModal, '/extensions/modal', 'modalRouter')
.build();

const content = dedent`
import express from 'express';
import oauthRouter from './oauth/index.js';
${webhooksImport}
${panelImport}
${modalImport}

const app = express();

app.use('/oauth', oauthRouter);
${webhooksMount}
${panelMount}
${modalMount}

export default app;
`;
const content = new SourceFileBuilder()
.importDefault('express', 'express')
.importDefault('./oauth/index.js', 'oauthRouter')
.importDefaultIf(options.webhooks, './webhooks/index.js', 'webhooksRouter')
.importDefaultIf(hasPanel, './app-extensions/panel/index.js', 'panelRouter')
.importDefaultIf(hasModal, './app-extensions/modal/index.js', 'modalRouter')
.addBlock('const app = express();')
.addBlock(mounts)
.exportDefault('app')
.build();

await writeFile(join(outputDir, 'src/app.ts'), content);
}
6 changes: 5 additions & 1 deletion src/generators/node/appExtensions.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,11 @@ import { tmpdir } from 'os';
import type { GeneratorOptions } from '../interface.js';

const tmpDir = join(tmpdir(), 'cpa-appext-test');
const exists = (p: string) => access(p).then(() => true, () => false);
const exists = (p: string) =>
access(p).then(
() => true,
() => false,
);

afterEach(async () => {
await rm(tmpDir, { recursive: true, force: true });
Expand Down
Loading
Loading