From clickup-pack
Sets up Node.js/TypeScript local dev environment for ClickUp API v2 with tsx hot reload, Vitest unit/integration tests, API mocks, and .env config.
npx claudepluginhub jeremylongshore/claude-code-plugins-plus-skills --plugin clickup-packThis skill is limited to using the following tools:
Set up a fast local development workflow for ClickUp API v2 integrations with hot reload, mocking, and integration testing.
Sets up GitHub Actions CI/CD pipelines for ClickUp API v2 integrations with automated testing, health checks, and task status sync using Node.js/Vitest.
Sets up local dev loop for MindTickle API with TypeScript project structure, mock responses, tsx watch scripts, and vitest tests.
Sets up Apollo.io local dev workflow with sandbox keys, axios client for logged requests, and MSW mocks for offline API testing.
Share bugs, ideas, or general feedback.
Set up a fast local development workflow for ClickUp API v2 integrations with hot reload, mocking, and integration testing.
mkdir my-clickup-integration && cd $_
npm init -y
npm install -D typescript tsx vitest @types/node dotenv
npx tsc --init --target ES2022 --module nodenext --outDir dist
my-clickup-integration/
├── src/
│ ├── clickup/
│ │ ├── client.ts # ClickUp API client (see clickup-sdk-patterns)
│ │ ├── types.ts # TypeScript interfaces
│ │ └── tasks.ts # Task operations
│ └── index.ts
├── tests/
│ ├── mocks/
│ │ └── clickup.ts # Mock ClickUp API responses
│ ├── unit/
│ │ └── tasks.test.ts
│ └── integration/
│ └── clickup.test.ts
├── .env.local # Local secrets (git-ignored)
├── .env.example # Template for team
└── package.json
# .env.example (commit this)
CLICKUP_API_TOKEN=pk_your_token_here
CLICKUP_TEAM_ID=your_team_id
CLICKUP_TEST_LIST_ID=your_test_list_id
# .env.local (git-ignored, copy from .env.example)
cp .env.example .env.local
{
"scripts": {
"dev": "tsx watch src/index.ts",
"test": "vitest",
"test:watch": "vitest --watch",
"test:integration": "CLICKUP_LIVE=1 vitest --run tests/integration/",
"build": "tsc",
"typecheck": "tsc --noEmit"
}
}
// tests/mocks/clickup.ts
export const mockTask = {
id: 'test_task_001',
name: 'Test Task',
status: { status: 'to do', color: '#d3d3d3', type: 'open' },
priority: { id: '3', priority: 'normal', color: '#6fddff' },
date_created: '1695000000000',
date_updated: '1695000000000',
due_date: null,
assignees: [],
tags: [],
url: 'https://app.clickup.com/t/test_task_001',
list: { id: '900100200300', name: 'Test List' },
folder: { id: '456', name: 'Test Folder' },
space: { id: '789' },
custom_fields: [],
};
export const mockTeam = {
teams: [{
id: '1234567',
name: 'Test Workspace',
members: [{ user: { id: 183, username: 'testuser', email: 'test@example.com' } }],
}],
};
// Mock fetch for ClickUp API calls
export function mockClickUpFetch() {
return vi.fn(async (url: string, options?: RequestInit) => {
const path = new URL(url).pathname.replace('/api/v2', '');
const routes: Record<string, any> = {
'/team': mockTeam,
'/user': { user: { id: 183, username: 'testuser' } },
};
// Match dynamic routes
if (path.match(/^\/task\/.+/)) return jsonResponse(mockTask);
if (path.match(/^\/list\/.+\/task$/) && options?.method === 'POST') {
return jsonResponse({ ...mockTask, ...JSON.parse(options.body as string) });
}
return jsonResponse(routes[path] ?? {}, routes[path] ? 200 : 404);
});
}
function jsonResponse(data: any, status = 200) {
return new Response(JSON.stringify(data), {
status,
headers: {
'Content-Type': 'application/json',
'X-RateLimit-Remaining': '95',
'X-RateLimit-Limit': '100',
'X-RateLimit-Reset': String(Math.floor(Date.now() / 1000) + 60),
},
});
}
// tests/unit/tasks.test.ts
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { mockClickUpFetch, mockTask } from '../mocks/clickup';
describe('ClickUp Task Operations', () => {
beforeEach(() => {
vi.stubGlobal('fetch', mockClickUpFetch());
vi.stubEnv('CLICKUP_API_TOKEN', 'pk_test_token');
});
it('creates a task with required fields', async () => {
const { createTask } = await import('../../src/clickup/tasks');
const task = await createTask('list123', { name: 'New Task' });
expect(task.name).toBe('New Task');
expect(fetch).toHaveBeenCalledWith(
expect.stringContaining('/list/list123/task'),
expect.objectContaining({ method: 'POST' }),
);
});
it('gets a task by ID', async () => {
const { getTask } = await import('../../src/clickup/tasks');
const task = await getTask('test_task_001');
expect(task.id).toBe(mockTask.id);
});
});
// tests/integration/clickup.test.ts
import { describe, it, expect } from 'vitest';
import 'dotenv/config';
const LIVE = process.env.CLICKUP_LIVE === '1';
describe.skipIf(!LIVE)('ClickUp Live API', () => {
it('authenticates and lists workspaces', async () => {
const response = await fetch('https://api.clickup.com/api/v2/team', {
headers: { 'Authorization': process.env.CLICKUP_API_TOKEN! },
});
expect(response.ok).toBe(true);
const data = await response.json();
expect(data.teams.length).toBeGreaterThan(0);
});
});
| Error | Cause | Solution |
|---|---|---|
CLICKUP_API_TOKEN undefined | Missing .env.local | Copy from .env.example |
| Integration tests fail | No live token | Set CLICKUP_LIVE=1 and valid token |
| Mock not matching | Route pattern wrong | Check URL path in mock router |
See clickup-sdk-patterns for production-ready client patterns.