@framers/agentos-ext-cloud-vercel 0.1.0 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/manifest.json +2 -2
- package/package.json +14 -6
- package/src/VercelService.ts +0 -408
- package/src/index.ts +0 -117
- package/src/tools/configureDomain.ts +0 -91
- package/src/tools/deploy.ts +0 -59
- package/src/tools/getDeployment.ts +0 -35
- package/src/tools/listProjects.ts +0 -35
- package/src/tools/setEnvVars.ts +0 -65
- package/test/VercelService.test.ts +0 -464
- package/test/index.test.ts +0 -63
- package/test/tools.test.ts +0 -332
- package/tsconfig.json +0 -22
package/src/tools/deploy.ts
DELETED
|
@@ -1,59 +0,0 @@
|
|
|
1
|
-
// @ts-nocheck
|
|
2
|
-
/**
|
|
3
|
-
* @fileoverview VercelDeployTool — deploy a project to Vercel from a Git repository.
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import type { VercelService, DeployResult } from '../VercelService.js';
|
|
7
|
-
|
|
8
|
-
export interface VercelDeployInput {
|
|
9
|
-
gitUrl: string;
|
|
10
|
-
projectName?: string;
|
|
11
|
-
framework?: string;
|
|
12
|
-
buildCommand?: string;
|
|
13
|
-
outputDirectory?: string;
|
|
14
|
-
envVars?: Record<string, string>;
|
|
15
|
-
target?: 'production' | 'preview';
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
export class VercelDeployTool {
|
|
19
|
-
readonly id = 'vercelDeploy';
|
|
20
|
-
readonly name = 'vercelDeploy';
|
|
21
|
-
readonly displayName = 'Deploy to Vercel';
|
|
22
|
-
readonly description = 'Deploy a project to Vercel from a Git repository URL. Creates the project if it does not exist. Supports Next.js, React, Vue, Svelte, static sites, and serverless functions with automatic framework detection.';
|
|
23
|
-
readonly category = 'cloud';
|
|
24
|
-
readonly version = '0.1.0';
|
|
25
|
-
readonly hasSideEffects = true;
|
|
26
|
-
|
|
27
|
-
readonly inputSchema = {
|
|
28
|
-
type: 'object' as const,
|
|
29
|
-
properties: {
|
|
30
|
-
gitUrl: { type: 'string', description: 'Git repository URL (e.g. "https://github.com/user/repo")' },
|
|
31
|
-
projectName: { type: 'string', description: 'Project name on Vercel (auto-generated from repo name if omitted)' },
|
|
32
|
-
framework: { type: 'string', description: 'Framework preset (e.g. "nextjs", "vite", "gatsby"). Auto-detected if omitted.' },
|
|
33
|
-
buildCommand: { type: 'string', description: 'Build command override (e.g. "npm run build")' },
|
|
34
|
-
outputDirectory: { type: 'string', description: 'Output directory override (e.g. "dist", ".next", "build")' },
|
|
35
|
-
envVars: { type: 'object', description: 'Environment variables to set on the project (key-value pairs)' },
|
|
36
|
-
target: { type: 'string', enum: ['production', 'preview'], description: 'Deployment target (default: production)' },
|
|
37
|
-
},
|
|
38
|
-
required: ['gitUrl'],
|
|
39
|
-
};
|
|
40
|
-
|
|
41
|
-
constructor(private service: VercelService) {}
|
|
42
|
-
|
|
43
|
-
async execute(args: VercelDeployInput): Promise<{ success: boolean; data?: DeployResult; error?: string }> {
|
|
44
|
-
try {
|
|
45
|
-
const result = await this.service.deployFromGit({
|
|
46
|
-
gitUrl: args.gitUrl,
|
|
47
|
-
projectName: args.projectName,
|
|
48
|
-
framework: args.framework,
|
|
49
|
-
buildCommand: args.buildCommand,
|
|
50
|
-
outputDirectory: args.outputDirectory,
|
|
51
|
-
envVars: args.envVars,
|
|
52
|
-
});
|
|
53
|
-
|
|
54
|
-
return { success: true, data: result };
|
|
55
|
-
} catch (err: any) {
|
|
56
|
-
return { success: false, error: err.message };
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
}
|
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
// @ts-nocheck
|
|
2
|
-
/**
|
|
3
|
-
* @fileoverview VercelGetDeploymentTool — check the status of a Vercel deployment.
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import type { VercelService, VercelDeployment } from '../VercelService.js';
|
|
7
|
-
|
|
8
|
-
export class VercelGetDeploymentTool {
|
|
9
|
-
readonly id = 'vercelGetDeployment';
|
|
10
|
-
readonly name = 'vercelGetDeployment';
|
|
11
|
-
readonly displayName = 'Get Deployment Status';
|
|
12
|
-
readonly description = 'Check the current status of a Vercel deployment by its ID. Returns state (BUILDING, READY, ERROR), URL, and inspector link.';
|
|
13
|
-
readonly category = 'cloud';
|
|
14
|
-
readonly version = '0.1.0';
|
|
15
|
-
readonly hasSideEffects = false;
|
|
16
|
-
|
|
17
|
-
readonly inputSchema = {
|
|
18
|
-
type: 'object' as const,
|
|
19
|
-
properties: {
|
|
20
|
-
deploymentId: { type: 'string', description: 'The deployment ID or URL to check' },
|
|
21
|
-
},
|
|
22
|
-
required: ['deploymentId'],
|
|
23
|
-
};
|
|
24
|
-
|
|
25
|
-
constructor(private service: VercelService) {}
|
|
26
|
-
|
|
27
|
-
async execute(args: { deploymentId: string }): Promise<{ success: boolean; data?: VercelDeployment; error?: string }> {
|
|
28
|
-
try {
|
|
29
|
-
const deployment = await this.service.getDeployment(args.deploymentId);
|
|
30
|
-
return { success: true, data: deployment };
|
|
31
|
-
} catch (err: any) {
|
|
32
|
-
return { success: false, error: err.message };
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
}
|
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
// @ts-nocheck
|
|
2
|
-
/**
|
|
3
|
-
* @fileoverview VercelListProjectsTool — list all projects on a Vercel account.
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import type { VercelService, VercelProject } from '../VercelService.js';
|
|
7
|
-
|
|
8
|
-
export class VercelListProjectsTool {
|
|
9
|
-
readonly id = 'vercelListProjects';
|
|
10
|
-
readonly name = 'vercelListProjects';
|
|
11
|
-
readonly displayName = 'List Vercel Projects';
|
|
12
|
-
readonly description = 'List all projects in the connected Vercel account. Shows project name, framework, linked Git repo, and latest deployment status.';
|
|
13
|
-
readonly category = 'cloud';
|
|
14
|
-
readonly version = '0.1.0';
|
|
15
|
-
readonly hasSideEffects = false;
|
|
16
|
-
|
|
17
|
-
readonly inputSchema = {
|
|
18
|
-
type: 'object' as const,
|
|
19
|
-
properties: {
|
|
20
|
-
limit: { type: 'number', description: 'Maximum number of projects to return (default: 20, max: 100)' },
|
|
21
|
-
},
|
|
22
|
-
required: [] as string[],
|
|
23
|
-
};
|
|
24
|
-
|
|
25
|
-
constructor(private service: VercelService) {}
|
|
26
|
-
|
|
27
|
-
async execute(args: { limit?: number }): Promise<{ success: boolean; data?: VercelProject[]; error?: string }> {
|
|
28
|
-
try {
|
|
29
|
-
const projects = await this.service.listProjects(args.limit ?? 20);
|
|
30
|
-
return { success: true, data: projects };
|
|
31
|
-
} catch (err: any) {
|
|
32
|
-
return { success: false, error: err.message };
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
}
|
package/src/tools/setEnvVars.ts
DELETED
|
@@ -1,65 +0,0 @@
|
|
|
1
|
-
// @ts-nocheck
|
|
2
|
-
/**
|
|
3
|
-
* @fileoverview VercelSetEnvVarsTool — set environment variables on a Vercel project.
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import type { VercelService, VercelEnvVar } from '../VercelService.js';
|
|
7
|
-
|
|
8
|
-
export interface SetEnvVarsInput {
|
|
9
|
-
projectId: string;
|
|
10
|
-
vars?: Record<string, string>;
|
|
11
|
-
action?: 'set' | 'list';
|
|
12
|
-
target?: ('production' | 'preview' | 'development')[];
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
export class VercelSetEnvVarsTool {
|
|
16
|
-
readonly id = 'vercelSetEnvVars';
|
|
17
|
-
readonly name = 'vercelSetEnvVars';
|
|
18
|
-
readonly displayName = 'Set Environment Variables';
|
|
19
|
-
readonly description = 'Set or list environment variables on a Vercel project. Variables are encrypted at rest. Targets production, preview, and development environments by default.';
|
|
20
|
-
readonly category = 'cloud';
|
|
21
|
-
readonly version = '0.1.0';
|
|
22
|
-
readonly hasSideEffects = true;
|
|
23
|
-
|
|
24
|
-
readonly inputSchema = {
|
|
25
|
-
type: 'object' as const,
|
|
26
|
-
properties: {
|
|
27
|
-
projectId: { type: 'string', description: 'Vercel project ID or name' },
|
|
28
|
-
vars: { type: 'object', description: 'Key-value pairs of environment variables to set' },
|
|
29
|
-
action: { type: 'string', enum: ['set', 'list'], description: 'Action to perform (default: set)' },
|
|
30
|
-
target: {
|
|
31
|
-
type: 'array',
|
|
32
|
-
items: { type: 'string', enum: ['production', 'preview', 'development'] },
|
|
33
|
-
description: 'Deployment targets for the variables (default: all three)',
|
|
34
|
-
},
|
|
35
|
-
},
|
|
36
|
-
required: ['projectId'],
|
|
37
|
-
};
|
|
38
|
-
|
|
39
|
-
constructor(private service: VercelService) {}
|
|
40
|
-
|
|
41
|
-
async execute(args: SetEnvVarsInput): Promise<{
|
|
42
|
-
success: boolean;
|
|
43
|
-
data?: { set: number } | VercelEnvVar[];
|
|
44
|
-
error?: string;
|
|
45
|
-
}> {
|
|
46
|
-
try {
|
|
47
|
-
const action = args.action ?? 'set';
|
|
48
|
-
|
|
49
|
-
if (action === 'list') {
|
|
50
|
-
const envVars = await this.service.listEnvVars(args.projectId);
|
|
51
|
-
return { success: true, data: envVars };
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
if (!args.vars || Object.keys(args.vars).length === 0) {
|
|
55
|
-
return { success: false, error: 'No environment variables provided. Pass vars as a key-value object.' };
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
const target = args.target ?? ['production', 'preview', 'development'];
|
|
59
|
-
await this.service.setEnvVars(args.projectId, args.vars, target);
|
|
60
|
-
return { success: true, data: { set: Object.keys(args.vars).length } };
|
|
61
|
-
} catch (err: any) {
|
|
62
|
-
return { success: false, error: err.message };
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
}
|
|
@@ -1,464 +0,0 @@
|
|
|
1
|
-
// @ts-nocheck
|
|
2
|
-
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
3
|
-
import { VercelService } from '../src/VercelService.js';
|
|
4
|
-
|
|
5
|
-
// ---------------------------------------------------------------------------
|
|
6
|
-
// Helpers
|
|
7
|
-
// ---------------------------------------------------------------------------
|
|
8
|
-
|
|
9
|
-
function mockResponse(data: unknown, status = 200): Response {
|
|
10
|
-
return {
|
|
11
|
-
ok: status >= 200 && status < 300,
|
|
12
|
-
status,
|
|
13
|
-
text: () => Promise.resolve(typeof data === 'string' ? data : JSON.stringify(data)),
|
|
14
|
-
json: () => Promise.resolve(data),
|
|
15
|
-
headers: new Headers(),
|
|
16
|
-
} as unknown as Response;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
const TOKEN = 'test-vercel-token';
|
|
20
|
-
const TEAM_ID = 'team_abc123';
|
|
21
|
-
|
|
22
|
-
function createService(teamId?: string) {
|
|
23
|
-
return new VercelService({
|
|
24
|
-
token: TOKEN,
|
|
25
|
-
teamId,
|
|
26
|
-
baseUrl: 'https://api.vercel.com',
|
|
27
|
-
});
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
// ---------------------------------------------------------------------------
|
|
31
|
-
// Tests
|
|
32
|
-
// ---------------------------------------------------------------------------
|
|
33
|
-
|
|
34
|
-
describe('VercelService', () => {
|
|
35
|
-
let mockFetch: ReturnType<typeof vi.fn>;
|
|
36
|
-
let service: VercelService;
|
|
37
|
-
|
|
38
|
-
beforeEach(() => {
|
|
39
|
-
mockFetch = vi.fn();
|
|
40
|
-
vi.stubGlobal('fetch', mockFetch);
|
|
41
|
-
service = createService();
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
afterEach(() => {
|
|
45
|
-
vi.restoreAllMocks();
|
|
46
|
-
});
|
|
47
|
-
|
|
48
|
-
// ── initialize / shutdown ─────────────────────────────────────────────
|
|
49
|
-
|
|
50
|
-
describe('initialize', () => {
|
|
51
|
-
it('should set isRunning to true on successful auth', async () => {
|
|
52
|
-
mockFetch.mockResolvedValueOnce(mockResponse({ user: { id: 'u1' } }));
|
|
53
|
-
|
|
54
|
-
await service.initialize();
|
|
55
|
-
|
|
56
|
-
expect(service.isRunning).toBe(true);
|
|
57
|
-
expect(mockFetch).toHaveBeenCalledWith(
|
|
58
|
-
'https://api.vercel.com/v2/user',
|
|
59
|
-
expect.objectContaining({
|
|
60
|
-
headers: expect.objectContaining({
|
|
61
|
-
Authorization: `Bearer ${TOKEN}`,
|
|
62
|
-
}),
|
|
63
|
-
}),
|
|
64
|
-
);
|
|
65
|
-
});
|
|
66
|
-
|
|
67
|
-
it('should throw on auth failure', async () => {
|
|
68
|
-
mockFetch.mockResolvedValueOnce(mockResponse('Unauthorized', 401));
|
|
69
|
-
|
|
70
|
-
await expect(service.initialize()).rejects.toThrow('Vercel auth failed: 401');
|
|
71
|
-
expect(service.isRunning).toBe(false);
|
|
72
|
-
});
|
|
73
|
-
});
|
|
74
|
-
|
|
75
|
-
describe('shutdown', () => {
|
|
76
|
-
it('should set isRunning to false', async () => {
|
|
77
|
-
mockFetch.mockResolvedValueOnce(mockResponse({ user: {} }));
|
|
78
|
-
await service.initialize();
|
|
79
|
-
expect(service.isRunning).toBe(true);
|
|
80
|
-
|
|
81
|
-
await service.shutdown();
|
|
82
|
-
expect(service.isRunning).toBe(false);
|
|
83
|
-
});
|
|
84
|
-
});
|
|
85
|
-
|
|
86
|
-
// ── Team query parameter ──────────────────────────────────────────────
|
|
87
|
-
|
|
88
|
-
describe('team scoping', () => {
|
|
89
|
-
it('should append teamId as query param when configured', async () => {
|
|
90
|
-
const teamService = createService(TEAM_ID);
|
|
91
|
-
mockFetch.mockResolvedValueOnce(mockResponse({ projects: [] }));
|
|
92
|
-
|
|
93
|
-
await teamService.listProjects();
|
|
94
|
-
|
|
95
|
-
const url = mockFetch.mock.calls[0][0] as string;
|
|
96
|
-
expect(url).toContain(`teamId=${TEAM_ID}`);
|
|
97
|
-
});
|
|
98
|
-
|
|
99
|
-
it('should use & separator when path already has query params', async () => {
|
|
100
|
-
const teamService = createService(TEAM_ID);
|
|
101
|
-
mockFetch.mockResolvedValueOnce(mockResponse({ projects: [] }));
|
|
102
|
-
|
|
103
|
-
await teamService.listProjects(5);
|
|
104
|
-
|
|
105
|
-
const url = mockFetch.mock.calls[0][0] as string;
|
|
106
|
-
expect(url).toContain('?limit=5&teamId=');
|
|
107
|
-
});
|
|
108
|
-
});
|
|
109
|
-
|
|
110
|
-
// ── Projects ──────────────────────────────────────────────────────────
|
|
111
|
-
|
|
112
|
-
describe('listProjects', () => {
|
|
113
|
-
it('should return mapped projects', async () => {
|
|
114
|
-
mockFetch.mockResolvedValueOnce(mockResponse({
|
|
115
|
-
projects: [
|
|
116
|
-
{ id: 'p1', name: 'my-app', framework: 'nextjs', createdAt: 1000, updatedAt: 2000 },
|
|
117
|
-
{ id: 'p2', name: 'api', framework: null, createdAt: 1500, updatedAt: 2500 },
|
|
118
|
-
],
|
|
119
|
-
}));
|
|
120
|
-
|
|
121
|
-
const projects = await service.listProjects();
|
|
122
|
-
|
|
123
|
-
expect(projects).toHaveLength(2);
|
|
124
|
-
expect(projects[0]).toEqual({
|
|
125
|
-
id: 'p1',
|
|
126
|
-
name: 'my-app',
|
|
127
|
-
framework: 'nextjs',
|
|
128
|
-
createdAt: 1000,
|
|
129
|
-
updatedAt: 2000,
|
|
130
|
-
latestDeployments: undefined,
|
|
131
|
-
link: undefined,
|
|
132
|
-
});
|
|
133
|
-
});
|
|
134
|
-
|
|
135
|
-
it('should pass limit as query parameter', async () => {
|
|
136
|
-
mockFetch.mockResolvedValueOnce(mockResponse({ projects: [] }));
|
|
137
|
-
|
|
138
|
-
await service.listProjects(5);
|
|
139
|
-
|
|
140
|
-
const url = mockFetch.mock.calls[0][0] as string;
|
|
141
|
-
expect(url).toContain('limit=5');
|
|
142
|
-
});
|
|
143
|
-
|
|
144
|
-
it('should return empty array when no projects', async () => {
|
|
145
|
-
mockFetch.mockResolvedValueOnce(mockResponse({ projects: undefined }));
|
|
146
|
-
|
|
147
|
-
const projects = await service.listProjects();
|
|
148
|
-
expect(projects).toEqual([]);
|
|
149
|
-
});
|
|
150
|
-
});
|
|
151
|
-
|
|
152
|
-
describe('getProject', () => {
|
|
153
|
-
it('should return a single mapped project', async () => {
|
|
154
|
-
mockFetch.mockResolvedValueOnce(mockResponse({
|
|
155
|
-
id: 'p1',
|
|
156
|
-
name: 'my-app',
|
|
157
|
-
framework: 'nextjs',
|
|
158
|
-
createdAt: 1000,
|
|
159
|
-
updatedAt: 2000,
|
|
160
|
-
link: { type: 'github', repo: 'my-app', org: 'acme' },
|
|
161
|
-
}));
|
|
162
|
-
|
|
163
|
-
const project = await service.getProject('my-app');
|
|
164
|
-
|
|
165
|
-
expect(project.id).toBe('p1');
|
|
166
|
-
expect(project.link).toEqual({ type: 'github', repo: 'my-app', org: 'acme' });
|
|
167
|
-
});
|
|
168
|
-
|
|
169
|
-
it('should throw on non-OK response', async () => {
|
|
170
|
-
mockFetch.mockResolvedValueOnce(mockResponse('Not Found', 404));
|
|
171
|
-
|
|
172
|
-
await expect(service.getProject('nonexistent')).rejects.toThrow('Vercel API 404');
|
|
173
|
-
});
|
|
174
|
-
});
|
|
175
|
-
|
|
176
|
-
describe('createProject', () => {
|
|
177
|
-
it('should create a project linked to a GitHub repo', async () => {
|
|
178
|
-
mockFetch.mockResolvedValueOnce(mockResponse({
|
|
179
|
-
id: 'p-new',
|
|
180
|
-
name: 'my-app',
|
|
181
|
-
framework: 'nextjs',
|
|
182
|
-
createdAt: 3000,
|
|
183
|
-
updatedAt: 3000,
|
|
184
|
-
}));
|
|
185
|
-
|
|
186
|
-
const project = await service.createProject('my-app', 'https://github.com/acme/my-app', {
|
|
187
|
-
framework: 'nextjs',
|
|
188
|
-
});
|
|
189
|
-
|
|
190
|
-
expect(project.id).toBe('p-new');
|
|
191
|
-
|
|
192
|
-
const body = JSON.parse(mockFetch.mock.calls[0][1].body);
|
|
193
|
-
expect(body.gitRepository).toEqual({ type: 'github', repo: 'acme/my-app' });
|
|
194
|
-
expect(body.framework).toBe('nextjs');
|
|
195
|
-
});
|
|
196
|
-
|
|
197
|
-
it('should detect GitLab provider', async () => {
|
|
198
|
-
mockFetch.mockResolvedValueOnce(mockResponse({
|
|
199
|
-
id: 'p-gl', name: 'gl-app', framework: null, createdAt: 0, updatedAt: 0,
|
|
200
|
-
}));
|
|
201
|
-
|
|
202
|
-
await service.createProject('gl-app', 'https://gitlab.com/org/gl-app');
|
|
203
|
-
|
|
204
|
-
const body = JSON.parse(mockFetch.mock.calls[0][1].body);
|
|
205
|
-
expect(body.gitRepository.type).toBe('gitlab');
|
|
206
|
-
});
|
|
207
|
-
|
|
208
|
-
it('should throw for unparseable git URL', async () => {
|
|
209
|
-
await expect(
|
|
210
|
-
service.createProject('bad', 'https://example.com/repo'),
|
|
211
|
-
).rejects.toThrow('Cannot parse git URL');
|
|
212
|
-
});
|
|
213
|
-
});
|
|
214
|
-
|
|
215
|
-
// ── Deployments ───────────────────────────────────────────────────────
|
|
216
|
-
|
|
217
|
-
describe('createDeployment', () => {
|
|
218
|
-
it('should trigger a production deployment by default', async () => {
|
|
219
|
-
mockFetch.mockResolvedValueOnce(mockResponse({
|
|
220
|
-
id: 'dpl_1',
|
|
221
|
-
url: 'my-app-abc.vercel.app',
|
|
222
|
-
readyState: 'BUILDING',
|
|
223
|
-
inspectorUrl: 'https://vercel.com/inspect/dpl_1',
|
|
224
|
-
projectId: 'p1',
|
|
225
|
-
}));
|
|
226
|
-
|
|
227
|
-
const result = await service.createDeployment('my-app');
|
|
228
|
-
|
|
229
|
-
expect(result.id).toBe('dpl_1');
|
|
230
|
-
expect(result.url).toBe('https://my-app-abc.vercel.app');
|
|
231
|
-
expect(result.readyState).toBe('BUILDING');
|
|
232
|
-
|
|
233
|
-
const body = JSON.parse(mockFetch.mock.calls[0][1].body);
|
|
234
|
-
expect(body.target).toBe('production');
|
|
235
|
-
});
|
|
236
|
-
|
|
237
|
-
it('should include gitRef when provided', async () => {
|
|
238
|
-
mockFetch.mockResolvedValueOnce(mockResponse({
|
|
239
|
-
id: 'dpl_2', url: 'x.vercel.app', readyState: 'QUEUED', projectId: 'p1',
|
|
240
|
-
}));
|
|
241
|
-
|
|
242
|
-
await service.createDeployment('my-app', { target: 'preview', gitRef: 'feature/xyz' });
|
|
243
|
-
|
|
244
|
-
const body = JSON.parse(mockFetch.mock.calls[0][1].body);
|
|
245
|
-
expect(body.target).toBe('preview');
|
|
246
|
-
expect(body.gitSource).toEqual({ ref: 'feature/xyz' });
|
|
247
|
-
});
|
|
248
|
-
});
|
|
249
|
-
|
|
250
|
-
describe('deployFromGit', () => {
|
|
251
|
-
it('should create project if it does not exist, set env vars, then deploy', async () => {
|
|
252
|
-
// getProject -> 404
|
|
253
|
-
mockFetch.mockResolvedValueOnce(mockResponse('Not Found', 404));
|
|
254
|
-
// createProject -> success
|
|
255
|
-
mockFetch.mockResolvedValueOnce(mockResponse({
|
|
256
|
-
id: 'p-new', name: 'my-repo', framework: null, createdAt: 0, updatedAt: 0,
|
|
257
|
-
}));
|
|
258
|
-
// setEnvVars -> POST env
|
|
259
|
-
mockFetch.mockResolvedValueOnce(mockResponse({}, 200));
|
|
260
|
-
// createDeployment
|
|
261
|
-
mockFetch.mockResolvedValueOnce(mockResponse({
|
|
262
|
-
id: 'dpl_3', url: 'my-repo.vercel.app', readyState: 'BUILDING', projectId: 'p-new',
|
|
263
|
-
}));
|
|
264
|
-
|
|
265
|
-
const result = await service.deployFromGit({
|
|
266
|
-
gitUrl: 'https://github.com/acme/my-repo',
|
|
267
|
-
envVars: { NODE_ENV: 'production' },
|
|
268
|
-
});
|
|
269
|
-
|
|
270
|
-
expect(result.id).toBe('dpl_3');
|
|
271
|
-
expect(mockFetch).toHaveBeenCalledTimes(4);
|
|
272
|
-
});
|
|
273
|
-
|
|
274
|
-
it('should reuse existing project when it exists', async () => {
|
|
275
|
-
// getProject -> success
|
|
276
|
-
mockFetch.mockResolvedValueOnce(mockResponse({
|
|
277
|
-
id: 'p-existing', name: 'my-repo', framework: null, createdAt: 0, updatedAt: 0,
|
|
278
|
-
}));
|
|
279
|
-
// createDeployment
|
|
280
|
-
mockFetch.mockResolvedValueOnce(mockResponse({
|
|
281
|
-
id: 'dpl_4', url: 'my-repo.vercel.app', readyState: 'BUILDING', projectId: 'p-existing',
|
|
282
|
-
}));
|
|
283
|
-
|
|
284
|
-
const result = await service.deployFromGit({
|
|
285
|
-
gitUrl: 'https://github.com/acme/my-repo',
|
|
286
|
-
});
|
|
287
|
-
|
|
288
|
-
expect(result.id).toBe('dpl_4');
|
|
289
|
-
// No createProject call, no setEnvVars call
|
|
290
|
-
expect(mockFetch).toHaveBeenCalledTimes(2);
|
|
291
|
-
});
|
|
292
|
-
});
|
|
293
|
-
|
|
294
|
-
describe('getDeployment', () => {
|
|
295
|
-
it('should return mapped deployment', async () => {
|
|
296
|
-
mockFetch.mockResolvedValueOnce(mockResponse({
|
|
297
|
-
uid: 'dpl_1', name: 'my-app', url: 'https://my-app.vercel.app',
|
|
298
|
-
state: 'READY', created: 5000, inspectorUrl: 'https://inspect/1',
|
|
299
|
-
}));
|
|
300
|
-
|
|
301
|
-
const deployment = await service.getDeployment('dpl_1');
|
|
302
|
-
|
|
303
|
-
expect(deployment.uid).toBe('dpl_1');
|
|
304
|
-
expect(deployment.state).toBe('READY');
|
|
305
|
-
expect(deployment.url).toBe('https://my-app.vercel.app');
|
|
306
|
-
});
|
|
307
|
-
|
|
308
|
-
it('should prefix url with https if missing', async () => {
|
|
309
|
-
mockFetch.mockResolvedValueOnce(mockResponse({
|
|
310
|
-
uid: 'dpl_2', name: 'app', url: 'app-xyz.vercel.app', state: 'BUILDING', created: 0,
|
|
311
|
-
}));
|
|
312
|
-
|
|
313
|
-
const deployment = await service.getDeployment('dpl_2');
|
|
314
|
-
expect(deployment.url).toBe('https://app-xyz.vercel.app');
|
|
315
|
-
});
|
|
316
|
-
});
|
|
317
|
-
|
|
318
|
-
describe('listDeployments', () => {
|
|
319
|
-
it('should return mapped deployments with projectId filter', async () => {
|
|
320
|
-
mockFetch.mockResolvedValueOnce(mockResponse({
|
|
321
|
-
deployments: [
|
|
322
|
-
{ uid: 'd1', name: 'a', url: 'a.vercel.app', state: 'READY', created: 100 },
|
|
323
|
-
{ uid: 'd2', name: 'b', url: 'b.vercel.app', state: 'BUILDING', created: 200 },
|
|
324
|
-
],
|
|
325
|
-
}));
|
|
326
|
-
|
|
327
|
-
const deps = await service.listDeployments('p1', 5);
|
|
328
|
-
|
|
329
|
-
expect(deps).toHaveLength(2);
|
|
330
|
-
const url = mockFetch.mock.calls[0][0] as string;
|
|
331
|
-
expect(url).toContain('projectId=p1');
|
|
332
|
-
expect(url).toContain('limit=5');
|
|
333
|
-
});
|
|
334
|
-
});
|
|
335
|
-
|
|
336
|
-
// ── Domains ───────────────────────────────────────────────────────────
|
|
337
|
-
|
|
338
|
-
describe('addDomain', () => {
|
|
339
|
-
it('should add a domain to a project', async () => {
|
|
340
|
-
mockFetch.mockResolvedValueOnce(mockResponse({
|
|
341
|
-
name: 'example.com', verified: true, configured: false,
|
|
342
|
-
}));
|
|
343
|
-
|
|
344
|
-
const domain = await service.addDomain('p1', 'example.com');
|
|
345
|
-
|
|
346
|
-
expect(domain.name).toBe('example.com');
|
|
347
|
-
expect(domain.verified).toBe(true);
|
|
348
|
-
expect(domain.configured).toBe(false);
|
|
349
|
-
});
|
|
350
|
-
|
|
351
|
-
it('should forward optional domain settings', async () => {
|
|
352
|
-
mockFetch.mockResolvedValueOnce(mockResponse({
|
|
353
|
-
name: 'old.example.com', verified: true, configured: true,
|
|
354
|
-
redirect: 'new.example.com', redirectStatusCode: 301,
|
|
355
|
-
}));
|
|
356
|
-
|
|
357
|
-
const domain = await service.addDomain('p1', 'old.example.com', {
|
|
358
|
-
redirect: 'new.example.com',
|
|
359
|
-
redirectStatusCode: 301,
|
|
360
|
-
});
|
|
361
|
-
|
|
362
|
-
expect(domain.redirect).toBe('new.example.com');
|
|
363
|
-
expect(domain.redirectStatusCode).toBe(301);
|
|
364
|
-
|
|
365
|
-
const body = JSON.parse(mockFetch.mock.calls[0][1].body);
|
|
366
|
-
expect(body.redirect).toBe('new.example.com');
|
|
367
|
-
});
|
|
368
|
-
});
|
|
369
|
-
|
|
370
|
-
describe('removeDomain', () => {
|
|
371
|
-
it('should send DELETE request', async () => {
|
|
372
|
-
mockFetch.mockResolvedValueOnce(mockResponse({}, 200));
|
|
373
|
-
|
|
374
|
-
await service.removeDomain('p1', 'example.com');
|
|
375
|
-
|
|
376
|
-
expect(mockFetch).toHaveBeenCalledWith(
|
|
377
|
-
expect.stringContaining('/v9/projects/p1/domains/example.com'),
|
|
378
|
-
expect.objectContaining({ method: 'DELETE' }),
|
|
379
|
-
);
|
|
380
|
-
});
|
|
381
|
-
});
|
|
382
|
-
|
|
383
|
-
describe('listDomains', () => {
|
|
384
|
-
it('should return mapped domain list', async () => {
|
|
385
|
-
mockFetch.mockResolvedValueOnce(mockResponse({
|
|
386
|
-
domains: [
|
|
387
|
-
{ name: 'a.com', verified: true, configured: true },
|
|
388
|
-
{ name: 'b.com', verified: false, configured: false },
|
|
389
|
-
],
|
|
390
|
-
}));
|
|
391
|
-
|
|
392
|
-
const domains = await service.listDomains('p1');
|
|
393
|
-
expect(domains).toHaveLength(2);
|
|
394
|
-
expect(domains[0].name).toBe('a.com');
|
|
395
|
-
});
|
|
396
|
-
});
|
|
397
|
-
|
|
398
|
-
describe('getDomainConfig', () => {
|
|
399
|
-
it('should return domain DNS configuration', async () => {
|
|
400
|
-
mockFetch.mockResolvedValueOnce(mockResponse({
|
|
401
|
-
configuredBy: 'CNAME',
|
|
402
|
-
nameservers: ['ns1.vercel-dns.com'],
|
|
403
|
-
misconfigured: false,
|
|
404
|
-
cnames: ['cname.vercel-dns.com'],
|
|
405
|
-
aValues: ['76.76.21.21'],
|
|
406
|
-
}));
|
|
407
|
-
|
|
408
|
-
const config = await service.getDomainConfig('example.com');
|
|
409
|
-
|
|
410
|
-
expect(config.configuredBy).toBe('CNAME');
|
|
411
|
-
expect(config.misconfigured).toBe(false);
|
|
412
|
-
expect(config.aValues).toContain('76.76.21.21');
|
|
413
|
-
});
|
|
414
|
-
});
|
|
415
|
-
|
|
416
|
-
// ── Environment Variables ─────────────────────────────────────────────
|
|
417
|
-
|
|
418
|
-
describe('setEnvVars', () => {
|
|
419
|
-
it('should create env vars via POST', async () => {
|
|
420
|
-
mockFetch.mockResolvedValueOnce(mockResponse({}, 200));
|
|
421
|
-
mockFetch.mockResolvedValueOnce(mockResponse({}, 200));
|
|
422
|
-
|
|
423
|
-
await service.setEnvVars('p1', { API_KEY: 'abc', SECRET: 'xyz' });
|
|
424
|
-
|
|
425
|
-
expect(mockFetch).toHaveBeenCalledTimes(2);
|
|
426
|
-
const body = JSON.parse(mockFetch.mock.calls[0][1].body);
|
|
427
|
-
expect(body.key).toBe('API_KEY');
|
|
428
|
-
expect(body.type).toBe('encrypted');
|
|
429
|
-
});
|
|
430
|
-
|
|
431
|
-
it('should patch env var when it already exists', async () => {
|
|
432
|
-
mockFetch.mockResolvedValueOnce(mockResponse(
|
|
433
|
-
{ error: { code: 'ENV_ALREADY_EXISTS', envId: 'env_123' } },
|
|
434
|
-
409,
|
|
435
|
-
));
|
|
436
|
-
// PATCH call
|
|
437
|
-
mockFetch.mockResolvedValueOnce(mockResponse({}, 200));
|
|
438
|
-
|
|
439
|
-
await service.setEnvVars('p1', { EXISTING_KEY: 'new-value' });
|
|
440
|
-
|
|
441
|
-
expect(mockFetch).toHaveBeenCalledTimes(2);
|
|
442
|
-
const patchUrl = mockFetch.mock.calls[1][0] as string;
|
|
443
|
-
expect(patchUrl).toContain('/env/env_123');
|
|
444
|
-
});
|
|
445
|
-
});
|
|
446
|
-
|
|
447
|
-
describe('listEnvVars', () => {
|
|
448
|
-
it('should return mapped env vars', async () => {
|
|
449
|
-
mockFetch.mockResolvedValueOnce(mockResponse({
|
|
450
|
-
envs: [
|
|
451
|
-
{ key: 'NODE_ENV', value: 'production', target: ['production'], type: 'plain' },
|
|
452
|
-
{ key: 'SECRET', target: ['production', 'preview'], type: 'encrypted' },
|
|
453
|
-
],
|
|
454
|
-
}));
|
|
455
|
-
|
|
456
|
-
const vars = await service.listEnvVars('p1');
|
|
457
|
-
|
|
458
|
-
expect(vars).toHaveLength(2);
|
|
459
|
-
expect(vars[0].key).toBe('NODE_ENV');
|
|
460
|
-
expect(vars[0].value).toBe('production');
|
|
461
|
-
expect(vars[1].value).toBe('(encrypted)');
|
|
462
|
-
});
|
|
463
|
-
});
|
|
464
|
-
});
|