api-emulator 0.5.1 → 0.6.0
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/README.md +79 -185
- package/dist/api.d.ts +8 -2
- package/dist/api.js +77 -422
- package/dist/api.js.map +1 -1
- package/dist/index.js +285 -472
- package/dist/index.js.map +1 -1
- package/package.json +8 -18
package/dist/index.js
CHANGED
|
@@ -4,6 +4,24 @@ import { Command } from "commander";
|
|
|
4
4
|
|
|
5
5
|
// src/external-plugin-adapter.ts
|
|
6
6
|
import { isAbsolute, resolve } from "path";
|
|
7
|
+
|
|
8
|
+
// src/plugin-manifest.ts
|
|
9
|
+
function readPluginManifest(mod) {
|
|
10
|
+
return mod.manifest ?? {};
|
|
11
|
+
}
|
|
12
|
+
function validatePluginManifest(manifest, pluginName) {
|
|
13
|
+
if (manifest.name && manifest.name !== pluginName) {
|
|
14
|
+
throw new Error(`Plugin manifest name "${manifest.name}" does not match plugin name "${pluginName}"`);
|
|
15
|
+
}
|
|
16
|
+
return {
|
|
17
|
+
...manifest,
|
|
18
|
+
label: manifest.label ?? `${pluginName} (external plugin)`,
|
|
19
|
+
endpoints: manifest.endpoints ?? "",
|
|
20
|
+
initConfig: manifest.initConfig ?? {}
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// src/external-plugin-adapter.ts
|
|
7
25
|
async function loadExternalPluginModule(specifier) {
|
|
8
26
|
const modulePath = specifier.startsWith(".") || isAbsolute(specifier) ? resolve(specifier) : specifier;
|
|
9
27
|
const mod = await import(modulePath);
|
|
@@ -12,10 +30,12 @@ async function loadExternalPluginModule(specifier) {
|
|
|
12
30
|
throw new Error(`Plugin "${specifier}" must export a ServicePlugin (as "plugin" or default export)`);
|
|
13
31
|
}
|
|
14
32
|
const name = plugin.name;
|
|
33
|
+
const manifest = validatePluginManifest(readPluginManifest(mod), name);
|
|
15
34
|
return {
|
|
16
35
|
name,
|
|
17
|
-
label:
|
|
18
|
-
endpoints:
|
|
36
|
+
label: manifest.label,
|
|
37
|
+
endpoints: manifest.endpoints,
|
|
38
|
+
manifest,
|
|
19
39
|
async load() {
|
|
20
40
|
return {
|
|
21
41
|
plugin,
|
|
@@ -23,439 +43,39 @@ async function loadExternalPluginModule(specifier) {
|
|
|
23
43
|
};
|
|
24
44
|
},
|
|
25
45
|
defaultFallback: mod.defaultFallback ?? (() => ({ login: "admin", id: 1, scopes: [] })),
|
|
26
|
-
initConfig:
|
|
46
|
+
initConfig: manifest.initConfig
|
|
27
47
|
};
|
|
28
48
|
}
|
|
29
49
|
|
|
30
|
-
// src/
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
"
|
|
41
|
-
"
|
|
42
|
-
|
|
43
|
-
"clerk"
|
|
44
|
-
];
|
|
45
|
-
var DEFAULT_PLUGIN_NAMES = DEFAULT_PLUGIN_NAME_LIST;
|
|
46
|
-
var DEFAULT_PLUGIN_REGISTRY = {
|
|
47
|
-
vercel: {
|
|
48
|
-
name: "vercel",
|
|
49
|
-
label: "Vercel REST API emulator",
|
|
50
|
-
endpoints: "projects, deployments, domains, env vars, users, teams, file uploads, protection bypass",
|
|
51
|
-
async load() {
|
|
52
|
-
const mod = await import("@emulators/vercel");
|
|
53
|
-
return { plugin: mod.vercelPlugin, seedFromConfig: mod.seedFromConfig };
|
|
54
|
-
},
|
|
55
|
-
defaultFallback(cfg) {
|
|
56
|
-
const firstLogin = cfg?.users?.[0]?.username ?? "admin";
|
|
57
|
-
return { login: firstLogin, id: 1, scopes: [] };
|
|
58
|
-
},
|
|
59
|
-
initConfig: {
|
|
60
|
-
vercel: {
|
|
61
|
-
users: [{ username: "developer", name: "Developer", email: "dev@example.com" }],
|
|
62
|
-
teams: [{ slug: "my-team", name: "My Team" }],
|
|
63
|
-
projects: [{ name: "my-app", team: "my-team", framework: "nextjs" }],
|
|
64
|
-
integrations: [
|
|
65
|
-
{
|
|
66
|
-
client_id: "oac_example_client_id",
|
|
67
|
-
client_secret: "example_client_secret",
|
|
68
|
-
name: "My Vercel App",
|
|
69
|
-
redirect_uris: ["http://localhost:3000/api/auth/callback/vercel"]
|
|
70
|
-
}
|
|
71
|
-
]
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
},
|
|
75
|
-
github: {
|
|
76
|
-
name: "github",
|
|
77
|
-
label: "GitHub REST API emulator",
|
|
78
|
-
endpoints: "users, repos, issues, PRs, comments, reviews, labels, milestones, branches, git data, orgs, teams, releases, webhooks, search, actions, checks, rate limit",
|
|
79
|
-
async load() {
|
|
80
|
-
const mod = await import("@emulators/github");
|
|
81
|
-
return {
|
|
82
|
-
plugin: mod.githubPlugin,
|
|
83
|
-
seedFromConfig: mod.seedFromConfig,
|
|
84
|
-
createAppKeyResolver(store) {
|
|
85
|
-
return (appId) => {
|
|
86
|
-
try {
|
|
87
|
-
const gh = mod.getGitHubStore(store);
|
|
88
|
-
const ghApp = gh.apps.all().find((a) => a.app_id === appId);
|
|
89
|
-
if (!ghApp) return null;
|
|
90
|
-
return { privateKey: ghApp.private_key, slug: ghApp.slug, name: ghApp.name };
|
|
91
|
-
} catch {
|
|
92
|
-
return null;
|
|
93
|
-
}
|
|
94
|
-
};
|
|
95
|
-
}
|
|
96
|
-
};
|
|
97
|
-
},
|
|
98
|
-
defaultFallback(cfg) {
|
|
99
|
-
const firstLogin = cfg?.users?.[0]?.login ?? "admin";
|
|
100
|
-
return { login: firstLogin, id: 1, scopes: ["repo", "user", "admin:org", "admin:repo_hook"] };
|
|
101
|
-
},
|
|
102
|
-
initConfig: {
|
|
103
|
-
github: {
|
|
104
|
-
users: [
|
|
105
|
-
{
|
|
106
|
-
login: "octocat",
|
|
107
|
-
name: "The Octocat",
|
|
108
|
-
email: "octocat@github.com",
|
|
109
|
-
bio: "I am the Octocat",
|
|
110
|
-
company: "GitHub",
|
|
111
|
-
location: "San Francisco"
|
|
112
|
-
}
|
|
113
|
-
],
|
|
114
|
-
orgs: [{ login: "my-org", name: "My Organization", description: "A test organization" }],
|
|
115
|
-
repos: [
|
|
116
|
-
{
|
|
117
|
-
owner: "octocat",
|
|
118
|
-
name: "hello-world",
|
|
119
|
-
description: "My first repository",
|
|
120
|
-
language: "JavaScript",
|
|
121
|
-
topics: ["hello", "world"],
|
|
122
|
-
auto_init: true
|
|
123
|
-
},
|
|
124
|
-
{
|
|
125
|
-
owner: "my-org",
|
|
126
|
-
name: "org-repo",
|
|
127
|
-
description: "An organization repository",
|
|
128
|
-
language: "TypeScript",
|
|
129
|
-
auto_init: true
|
|
130
|
-
}
|
|
131
|
-
],
|
|
132
|
-
oauth_apps: [
|
|
133
|
-
{
|
|
134
|
-
client_id: "Iv1.example_client_id",
|
|
135
|
-
client_secret: "example_client_secret",
|
|
136
|
-
name: "My App",
|
|
137
|
-
redirect_uris: ["http://localhost:3000/api/auth/callback/github"]
|
|
138
|
-
}
|
|
139
|
-
]
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
},
|
|
143
|
-
google: {
|
|
144
|
-
name: "google",
|
|
145
|
-
label: "Google OAuth 2.0 / OpenID Connect + Gmail, Calendar, and Drive emulator",
|
|
146
|
-
endpoints: "OAuth authorize, token exchange, userinfo, OIDC discovery, token revocation, Gmail messages/drafts/threads/labels/history/settings, Calendar lists/events/freebusy, Drive files/uploads",
|
|
147
|
-
async load() {
|
|
148
|
-
const mod = await import("@emulators/google");
|
|
149
|
-
return { plugin: mod.googlePlugin, seedFromConfig: mod.seedFromConfig };
|
|
150
|
-
},
|
|
151
|
-
defaultFallback(cfg) {
|
|
152
|
-
const firstEmail = cfg?.users?.[0]?.email ?? "testuser@gmail.com";
|
|
153
|
-
return { login: firstEmail, id: 1, scopes: ["openid", "email", "profile"] };
|
|
154
|
-
},
|
|
155
|
-
initConfig: {
|
|
156
|
-
google: {
|
|
157
|
-
users: [
|
|
158
|
-
{
|
|
159
|
-
email: "testuser@example.com",
|
|
160
|
-
name: "Test User",
|
|
161
|
-
picture: "https://lh3.googleusercontent.com/a/default-user",
|
|
162
|
-
email_verified: true
|
|
163
|
-
}
|
|
164
|
-
],
|
|
165
|
-
oauth_clients: [
|
|
166
|
-
{
|
|
167
|
-
client_id: "example-client-id.apps.googleusercontent.com",
|
|
168
|
-
client_secret: "GOCSPX-example_secret",
|
|
169
|
-
name: "Code App (Google)",
|
|
170
|
-
redirect_uris: ["http://localhost:3000/api/auth/callback/google"]
|
|
171
|
-
}
|
|
172
|
-
],
|
|
173
|
-
labels: [
|
|
174
|
-
{
|
|
175
|
-
id: "Label_ops",
|
|
176
|
-
user_email: "testuser@example.com",
|
|
177
|
-
name: "Ops/Review",
|
|
178
|
-
color_background: "#DDEEFF",
|
|
179
|
-
color_text: "#111111"
|
|
180
|
-
}
|
|
181
|
-
],
|
|
182
|
-
messages: [
|
|
183
|
-
{
|
|
184
|
-
id: "msg_welcome",
|
|
185
|
-
user_email: "testuser@example.com",
|
|
186
|
-
from: "welcome@example.com",
|
|
187
|
-
to: "testuser@example.com",
|
|
188
|
-
subject: "Welcome to the Gmail emulator",
|
|
189
|
-
body_text: "You can now test Gmail, Calendar, and Drive flows locally.",
|
|
190
|
-
label_ids: ["INBOX", "UNREAD", "CATEGORY_UPDATES"],
|
|
191
|
-
date: "2025-01-04T10:00:00.000Z"
|
|
192
|
-
}
|
|
193
|
-
],
|
|
194
|
-
calendars: [
|
|
195
|
-
{
|
|
196
|
-
id: "primary",
|
|
197
|
-
user_email: "testuser@example.com",
|
|
198
|
-
summary: "testuser@example.com",
|
|
199
|
-
primary: true,
|
|
200
|
-
selected: true,
|
|
201
|
-
time_zone: "UTC"
|
|
202
|
-
}
|
|
203
|
-
],
|
|
204
|
-
calendar_events: [
|
|
205
|
-
{
|
|
206
|
-
id: "evt_kickoff",
|
|
207
|
-
user_email: "testuser@example.com",
|
|
208
|
-
calendar_id: "primary",
|
|
209
|
-
summary: "Project Kickoff",
|
|
210
|
-
start_date_time: "2025-01-10T09:00:00.000Z",
|
|
211
|
-
end_date_time: "2025-01-10T09:30:00.000Z"
|
|
212
|
-
}
|
|
213
|
-
],
|
|
214
|
-
drive_items: [
|
|
215
|
-
{
|
|
216
|
-
id: "drv_docs",
|
|
217
|
-
user_email: "testuser@example.com",
|
|
218
|
-
name: "Docs",
|
|
219
|
-
mime_type: "application/vnd.google-apps.folder",
|
|
220
|
-
parent_ids: ["root"]
|
|
221
|
-
}
|
|
222
|
-
]
|
|
223
|
-
}
|
|
224
|
-
}
|
|
225
|
-
},
|
|
226
|
-
slack: {
|
|
227
|
-
name: "slack",
|
|
228
|
-
label: "Slack API emulator",
|
|
229
|
-
endpoints: "auth, chat, conversations, users, reactions, team, OAuth, incoming webhooks",
|
|
230
|
-
async load() {
|
|
231
|
-
const mod = await import("@emulators/slack");
|
|
232
|
-
return { plugin: mod.slackPlugin, seedFromConfig: mod.seedFromConfig };
|
|
233
|
-
},
|
|
234
|
-
defaultFallback() {
|
|
235
|
-
return { login: "U000000001", id: 1, scopes: ["chat:write", "channels:read", "users:read", "reactions:write"] };
|
|
236
|
-
},
|
|
237
|
-
initConfig: {
|
|
238
|
-
slack: {
|
|
239
|
-
team: { name: "My Workspace", domain: "my-workspace" },
|
|
240
|
-
users: [{ name: "developer", real_name: "Developer", email: "dev@example.com" }],
|
|
241
|
-
channels: [
|
|
242
|
-
{ name: "general", topic: "General discussion" },
|
|
243
|
-
{ name: "random", topic: "Random stuff" }
|
|
244
|
-
],
|
|
245
|
-
bots: [{ name: "my-bot" }],
|
|
246
|
-
oauth_apps: [
|
|
247
|
-
{
|
|
248
|
-
client_id: "12345.67890",
|
|
249
|
-
client_secret: "example_client_secret",
|
|
250
|
-
name: "My Slack App",
|
|
251
|
-
redirect_uris: ["http://localhost:3000/api/auth/callback/slack"]
|
|
252
|
-
}
|
|
253
|
-
]
|
|
254
|
-
}
|
|
255
|
-
}
|
|
256
|
-
},
|
|
257
|
-
apple: {
|
|
258
|
-
name: "apple",
|
|
259
|
-
label: "Apple Sign In / OAuth emulator",
|
|
260
|
-
endpoints: "OAuth authorize, token exchange, JWKS",
|
|
261
|
-
async load() {
|
|
262
|
-
const mod = await import("@emulators/apple");
|
|
263
|
-
return { plugin: mod.applePlugin, seedFromConfig: mod.seedFromConfig };
|
|
264
|
-
},
|
|
265
|
-
defaultFallback(cfg) {
|
|
266
|
-
const firstEmail = cfg?.users?.[0]?.email ?? "testuser@icloud.com";
|
|
267
|
-
return { login: firstEmail, id: 1, scopes: ["openid", "email", "name"] };
|
|
268
|
-
},
|
|
269
|
-
initConfig: {
|
|
270
|
-
apple: {
|
|
271
|
-
users: [{ email: "testuser@icloud.com", name: "Test User" }],
|
|
272
|
-
oauth_clients: [
|
|
273
|
-
{
|
|
274
|
-
client_id: "com.example.app",
|
|
275
|
-
team_id: "TEAM001",
|
|
276
|
-
name: "My Apple App",
|
|
277
|
-
redirect_uris: ["http://localhost:3000/api/auth/callback/apple"]
|
|
278
|
-
}
|
|
279
|
-
]
|
|
280
|
-
}
|
|
281
|
-
}
|
|
282
|
-
},
|
|
283
|
-
microsoft: {
|
|
284
|
-
name: "microsoft",
|
|
285
|
-
label: "Microsoft Entra ID OAuth 2.0 / OpenID Connect emulator",
|
|
286
|
-
endpoints: "OAuth authorize, token exchange, userinfo, OIDC discovery, Graph /me, logout, token revocation",
|
|
287
|
-
async load() {
|
|
288
|
-
const mod = await import("@emulators/microsoft");
|
|
289
|
-
return { plugin: mod.microsoftPlugin, seedFromConfig: mod.seedFromConfig };
|
|
290
|
-
},
|
|
291
|
-
defaultFallback(cfg) {
|
|
292
|
-
const firstEmail = cfg?.users?.[0]?.email ?? "testuser@outlook.com";
|
|
293
|
-
return { login: firstEmail, id: 1, scopes: ["openid", "email", "profile", "User.Read"] };
|
|
294
|
-
},
|
|
295
|
-
initConfig: {
|
|
296
|
-
microsoft: {
|
|
297
|
-
users: [{ email: "testuser@outlook.com", name: "Test User" }],
|
|
298
|
-
oauth_clients: [
|
|
299
|
-
{
|
|
300
|
-
client_id: "example-client-id",
|
|
301
|
-
client_secret: "example-client-secret",
|
|
302
|
-
name: "My Microsoft App",
|
|
303
|
-
redirect_uris: ["http://localhost:3000/api/auth/callback/microsoft-entra-id"]
|
|
304
|
-
}
|
|
305
|
-
]
|
|
306
|
-
}
|
|
307
|
-
}
|
|
308
|
-
},
|
|
309
|
-
okta: {
|
|
310
|
-
name: "okta",
|
|
311
|
-
label: "Okta OAuth 2.0 / OpenID Connect + management API emulator",
|
|
312
|
-
endpoints: "OIDC discovery, JWKS, OAuth authorize/token/userinfo/introspect/revoke/logout, users, groups, apps, authorization servers",
|
|
313
|
-
async load() {
|
|
314
|
-
const mod = await import("@emulators/okta");
|
|
315
|
-
return { plugin: mod.oktaPlugin, seedFromConfig: mod.seedFromConfig };
|
|
316
|
-
},
|
|
317
|
-
defaultFallback(cfg) {
|
|
318
|
-
const firstLogin = cfg?.users?.[0]?.login ?? cfg?.users?.[0]?.email ?? "testuser@okta.local";
|
|
319
|
-
return { login: firstLogin, id: 1, scopes: ["openid", "profile", "email", "groups"] };
|
|
320
|
-
},
|
|
321
|
-
initConfig: {
|
|
322
|
-
okta: {
|
|
323
|
-
users: [{ login: "testuser@okta.local", email: "testuser@okta.local", first_name: "Test", last_name: "User" }],
|
|
324
|
-
groups: [{ name: "Everyone", description: "All users", type: "BUILT_IN", okta_id: "00g_everyone" }],
|
|
325
|
-
authorization_servers: [{ id: "default", name: "default", audiences: ["api://default"] }],
|
|
326
|
-
oauth_clients: [
|
|
327
|
-
{
|
|
328
|
-
client_id: "okta-test-client",
|
|
329
|
-
client_secret: "okta-test-secret",
|
|
330
|
-
name: "Sample OIDC Client",
|
|
331
|
-
redirect_uris: ["http://localhost:3000/callback"],
|
|
332
|
-
auth_server_id: "default"
|
|
333
|
-
}
|
|
334
|
-
]
|
|
335
|
-
}
|
|
336
|
-
}
|
|
337
|
-
},
|
|
338
|
-
aws: {
|
|
339
|
-
name: "aws",
|
|
340
|
-
label: "AWS cloud service emulator",
|
|
341
|
-
endpoints: "S3 (buckets, objects), SQS (queues, messages), IAM (users, roles, access keys), STS (assume role, caller identity)",
|
|
342
|
-
async load() {
|
|
343
|
-
const mod = await import("@emulators/aws");
|
|
344
|
-
return { plugin: mod.awsPlugin, seedFromConfig: mod.seedFromConfig };
|
|
345
|
-
},
|
|
346
|
-
defaultFallback() {
|
|
347
|
-
return { login: "admin", id: 1, scopes: ["s3:*", "sqs:*", "iam:*", "sts:*"] };
|
|
348
|
-
},
|
|
349
|
-
initConfig: {
|
|
350
|
-
aws: {
|
|
351
|
-
region: "us-east-1",
|
|
352
|
-
s3: { buckets: [{ name: "my-app-bucket" }, { name: "my-app-uploads" }] },
|
|
353
|
-
sqs: { queues: [{ name: "my-app-events" }, { name: "my-app-dlq" }] },
|
|
354
|
-
iam: {
|
|
355
|
-
users: [{ user_name: "developer", create_access_key: true }],
|
|
356
|
-
roles: [{ role_name: "lambda-execution-role", description: "Role for Lambda function execution" }]
|
|
357
|
-
}
|
|
358
|
-
}
|
|
359
|
-
}
|
|
360
|
-
},
|
|
361
|
-
resend: {
|
|
362
|
-
name: "resend",
|
|
363
|
-
label: "Resend email API emulator",
|
|
364
|
-
endpoints: "emails, domains, contacts, API keys, inbox UI",
|
|
365
|
-
async load() {
|
|
366
|
-
const mod = await import("@emulators/resend");
|
|
367
|
-
return { plugin: mod.resendPlugin, seedFromConfig: mod.seedFromConfig };
|
|
368
|
-
},
|
|
369
|
-
defaultFallback() {
|
|
370
|
-
return { login: "re_test_admin", id: 1, scopes: [] };
|
|
371
|
-
},
|
|
372
|
-
initConfig: {
|
|
373
|
-
resend: {
|
|
374
|
-
domains: [{ name: "example.com", region: "us-east-1" }],
|
|
375
|
-
contacts: [{ email: "test@example.com", first_name: "Test", last_name: "User" }]
|
|
376
|
-
}
|
|
377
|
-
}
|
|
378
|
-
},
|
|
379
|
-
stripe: {
|
|
380
|
-
name: "stripe",
|
|
381
|
-
label: "Stripe payments emulator",
|
|
382
|
-
endpoints: "customers, payment methods, customer sessions, payment intents, charges, products, prices, checkout sessions, webhooks",
|
|
383
|
-
async load() {
|
|
384
|
-
const mod = await import("@emulators/stripe");
|
|
385
|
-
return { plugin: mod.stripePlugin, seedFromConfig: mod.seedFromConfig };
|
|
386
|
-
},
|
|
387
|
-
defaultFallback() {
|
|
388
|
-
return { login: "sk_test_admin", id: 1, scopes: [] };
|
|
389
|
-
},
|
|
390
|
-
initConfig: {
|
|
391
|
-
stripe: {
|
|
392
|
-
customers: [{ email: "test@example.com", name: "Test Customer" }],
|
|
393
|
-
products: [{ name: "Pro Plan", description: "Monthly pro subscription" }],
|
|
394
|
-
prices: [{ product_name: "Pro Plan", currency: "usd", unit_amount: 2e3 }]
|
|
395
|
-
}
|
|
396
|
-
}
|
|
397
|
-
},
|
|
398
|
-
mongoatlas: {
|
|
399
|
-
name: "mongoatlas",
|
|
400
|
-
label: "MongoDB Atlas service emulator",
|
|
401
|
-
endpoints: "Atlas Admin API v2 (projects, clusters, database users, databases, collections), Atlas Data API v1 (findOne, find, insertOne, insertMany, updateOne, updateMany, deleteOne, deleteMany, aggregate)",
|
|
402
|
-
async load() {
|
|
403
|
-
const mod = await import("@emulators/mongoatlas");
|
|
404
|
-
return { plugin: mod.mongoatlasPlugin, seedFromConfig: mod.seedFromConfig };
|
|
405
|
-
},
|
|
406
|
-
defaultFallback() {
|
|
407
|
-
return { login: "admin", id: 1, scopes: [] };
|
|
408
|
-
},
|
|
409
|
-
initConfig: {
|
|
410
|
-
mongoatlas: {
|
|
411
|
-
projects: [{ name: "Project0" }],
|
|
412
|
-
clusters: [{ name: "Cluster0", project: "Project0" }],
|
|
413
|
-
database_users: [{ username: "admin", project: "Project0" }],
|
|
414
|
-
databases: [{ cluster: "Cluster0", name: "test", collections: ["items"] }]
|
|
415
|
-
}
|
|
416
|
-
}
|
|
417
|
-
},
|
|
418
|
-
clerk: {
|
|
419
|
-
name: "clerk",
|
|
420
|
-
label: "Clerk authentication and user management emulator",
|
|
421
|
-
endpoints: "OIDC discovery, JWKS, OAuth authorize/token/userinfo, users, email addresses, organizations, memberships, invitations, sessions",
|
|
422
|
-
async load() {
|
|
423
|
-
const mod = await import("@emulators/clerk");
|
|
424
|
-
return { plugin: mod.clerkPlugin, seedFromConfig: mod.seedFromConfig };
|
|
425
|
-
},
|
|
426
|
-
defaultFallback(cfg) {
|
|
427
|
-
const firstEmail = cfg?.users?.[0]?.email_addresses?.[0] ?? "test@example.com";
|
|
428
|
-
return { login: firstEmail, id: 1, scopes: [] };
|
|
429
|
-
},
|
|
430
|
-
initConfig: {
|
|
431
|
-
clerk: {
|
|
432
|
-
users: [
|
|
433
|
-
{
|
|
434
|
-
first_name: "Test",
|
|
435
|
-
last_name: "User",
|
|
436
|
-
email_addresses: ["test@example.com"],
|
|
437
|
-
password: "clerk_test_password"
|
|
438
|
-
}
|
|
439
|
-
],
|
|
440
|
-
organizations: [
|
|
441
|
-
{
|
|
442
|
-
name: "My Company",
|
|
443
|
-
slug: "my-company",
|
|
444
|
-
members: [{ email: "test@example.com", role: "admin" }]
|
|
445
|
-
}
|
|
446
|
-
],
|
|
447
|
-
oauth_applications: [
|
|
448
|
-
{
|
|
449
|
-
client_id: "clerk_emulate_client",
|
|
450
|
-
client_secret: "clerk_emulate_secret",
|
|
451
|
-
name: "api-emulator App",
|
|
452
|
-
redirect_uris: ["http://localhost:3000/api/auth/callback/clerk"]
|
|
453
|
-
}
|
|
454
|
-
]
|
|
455
|
-
}
|
|
456
|
-
}
|
|
50
|
+
// src/plugin-lock.ts
|
|
51
|
+
import { existsSync, readFileSync, writeFileSync } from "fs";
|
|
52
|
+
import { resolve as resolve2 } from "path";
|
|
53
|
+
var PLUGIN_LOCK_FILE = "api-emulator.lock";
|
|
54
|
+
function createEmptyPluginLock() {
|
|
55
|
+
return { version: 1, plugins: {} };
|
|
56
|
+
}
|
|
57
|
+
function readPluginLock(cwd = process.cwd()) {
|
|
58
|
+
const path = resolve2(cwd, PLUGIN_LOCK_FILE);
|
|
59
|
+
if (!existsSync(path)) return createEmptyPluginLock();
|
|
60
|
+
const parsed = JSON.parse(readFileSync(path, "utf-8"));
|
|
61
|
+
if (parsed.version !== 1 || typeof parsed.plugins !== "object" || parsed.plugins === null) {
|
|
62
|
+
throw new Error(`Invalid ${PLUGIN_LOCK_FILE}`);
|
|
457
63
|
}
|
|
458
|
-
|
|
64
|
+
return parsed;
|
|
65
|
+
}
|
|
66
|
+
function writePluginLock(lock, cwd = process.cwd()) {
|
|
67
|
+
const sortedPlugins = Object.fromEntries(Object.entries(lock.plugins).sort(([a], [b]) => a.localeCompare(b)));
|
|
68
|
+
const content = `${JSON.stringify({ version: 1, plugins: sortedPlugins }, null, 2)}
|
|
69
|
+
`;
|
|
70
|
+
writeFileSync(resolve2(cwd, PLUGIN_LOCK_FILE), content, "utf-8");
|
|
71
|
+
}
|
|
72
|
+
function getLockedPluginSpecifiers(cwd = process.cwd()) {
|
|
73
|
+
return Object.values(readPluginLock(cwd).plugins).map((entry) => entry.specifier);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// src/default-plugin-catalog.ts
|
|
77
|
+
var DEFAULT_PLUGIN_NAMES = [];
|
|
78
|
+
var DEFAULT_PLUGIN_REGISTRY = {};
|
|
459
79
|
|
|
460
80
|
// src/registry.ts
|
|
461
81
|
var DEFAULT_TOKENS = {
|
|
@@ -470,8 +90,10 @@ var DEFAULT_TOKENS = {
|
|
|
470
90
|
}
|
|
471
91
|
}
|
|
472
92
|
};
|
|
473
|
-
async function resolvePluginModules(pluginSpecifiers = []) {
|
|
474
|
-
const
|
|
93
|
+
async function resolvePluginModules(pluginSpecifiers = [], options = {}) {
|
|
94
|
+
const installedSpecifiers = options.includeInstalled ? getLockedPluginSpecifiers() : [];
|
|
95
|
+
const allSpecifiers = [...installedSpecifiers, ...pluginSpecifiers];
|
|
96
|
+
const results = await Promise.all(allSpecifiers.map(loadExternalPluginModule));
|
|
475
97
|
const externalEntries = {};
|
|
476
98
|
for (const pluginModule of results) {
|
|
477
99
|
if (pluginModule.name in DEFAULT_PLUGIN_REGISTRY) {
|
|
@@ -489,8 +111,8 @@ function getDefaultPluginNames() {
|
|
|
489
111
|
}
|
|
490
112
|
|
|
491
113
|
// src/commands/start.ts
|
|
492
|
-
import { readFileSync, existsSync } from "fs";
|
|
493
|
-
import { resolve as
|
|
114
|
+
import { readFileSync as readFileSync2, existsSync as existsSync2 } from "fs";
|
|
115
|
+
import { resolve as resolve3 } from "path";
|
|
494
116
|
import { parse as parseYaml } from "yaml";
|
|
495
117
|
import pc from "picocolors";
|
|
496
118
|
|
|
@@ -505,12 +127,12 @@ function hasPortless() {
|
|
|
505
127
|
return result.status === 0;
|
|
506
128
|
}
|
|
507
129
|
function promptYesNo(question) {
|
|
508
|
-
return new Promise((
|
|
130
|
+
return new Promise((resolve7) => {
|
|
509
131
|
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
510
132
|
rl.question(question, (answer) => {
|
|
511
133
|
rl.close();
|
|
512
134
|
const normalized = answer.trim().toLowerCase();
|
|
513
|
-
|
|
135
|
+
resolve7(normalized === "" || normalized === "y" || normalized === "yes");
|
|
514
136
|
});
|
|
515
137
|
});
|
|
516
138
|
}
|
|
@@ -580,7 +202,7 @@ function resolveBaseUrl(opts) {
|
|
|
580
202
|
if (opts.baseUrl) {
|
|
581
203
|
return opts.baseUrl.replace(/\{service\}/g, opts.service);
|
|
582
204
|
}
|
|
583
|
-
const envBaseUrl = process.env.API_EMULATOR_BASE_URL
|
|
205
|
+
const envBaseUrl = process.env.API_EMULATOR_BASE_URL;
|
|
584
206
|
if (envBaseUrl) {
|
|
585
207
|
return envBaseUrl.replace(/\{service\}/g, opts.service);
|
|
586
208
|
}
|
|
@@ -592,7 +214,11 @@ function resolveBaseUrl(opts) {
|
|
|
592
214
|
}
|
|
593
215
|
|
|
594
216
|
// src/service-runtime.ts
|
|
595
|
-
import {
|
|
217
|
+
import {
|
|
218
|
+
createServer,
|
|
219
|
+
createStoreFixture,
|
|
220
|
+
fixtureStoreSnapshot
|
|
221
|
+
} from "@api-emulator/core";
|
|
596
222
|
import { serve } from "@hono/node-server";
|
|
597
223
|
function createAuthTokens(seedConfig) {
|
|
598
224
|
const tokens = {};
|
|
@@ -631,15 +257,32 @@ function createServiceRuntime(options) {
|
|
|
631
257
|
service,
|
|
632
258
|
url: baseUrl,
|
|
633
259
|
store,
|
|
260
|
+
snapshot() {
|
|
261
|
+
return store.snapshot();
|
|
262
|
+
},
|
|
263
|
+
restore(fixture) {
|
|
264
|
+
store.restore(fixtureStoreSnapshot(fixture));
|
|
265
|
+
},
|
|
266
|
+
exportFixture(options2 = {}) {
|
|
267
|
+
const interactions = store.getData("api-emulator:interactions");
|
|
268
|
+
return createStoreFixture(service, store.snapshot(), {
|
|
269
|
+
...options2,
|
|
270
|
+
interactions: options2.interactions ?? interactions
|
|
271
|
+
});
|
|
272
|
+
},
|
|
273
|
+
resetToFixture(fixture) {
|
|
274
|
+
store.reset();
|
|
275
|
+
store.restore(fixtureStoreSnapshot(fixture));
|
|
276
|
+
},
|
|
634
277
|
reset() {
|
|
635
278
|
store.reset();
|
|
636
279
|
seed();
|
|
637
280
|
},
|
|
638
281
|
close() {
|
|
639
|
-
return new Promise((
|
|
282
|
+
return new Promise((resolve7, reject) => {
|
|
640
283
|
httpServer.close((err) => {
|
|
641
284
|
if (err) reject(err);
|
|
642
|
-
else
|
|
285
|
+
else resolve7();
|
|
643
286
|
});
|
|
644
287
|
});
|
|
645
288
|
}
|
|
@@ -647,15 +290,15 @@ function createServiceRuntime(options) {
|
|
|
647
290
|
}
|
|
648
291
|
|
|
649
292
|
// src/commands/start.ts
|
|
650
|
-
var pkg = { version: "0.
|
|
293
|
+
var pkg = { version: "0.6.0" };
|
|
651
294
|
function loadSeedConfig(seedPath) {
|
|
652
295
|
if (seedPath) {
|
|
653
|
-
const fullPath =
|
|
654
|
-
if (!
|
|
296
|
+
const fullPath = resolve3(seedPath);
|
|
297
|
+
if (!existsSync2(fullPath)) {
|
|
655
298
|
console.error(`Seed file not found: ${fullPath}`);
|
|
656
299
|
process.exit(1);
|
|
657
300
|
}
|
|
658
|
-
const content =
|
|
301
|
+
const content = readFileSync2(fullPath, "utf-8");
|
|
659
302
|
try {
|
|
660
303
|
const config = fullPath.endsWith(".json") ? JSON.parse(content) : parseYaml(content);
|
|
661
304
|
return { config, source: seedPath };
|
|
@@ -664,21 +307,11 @@ function loadSeedConfig(seedPath) {
|
|
|
664
307
|
process.exit(1);
|
|
665
308
|
}
|
|
666
309
|
}
|
|
667
|
-
const autoFiles = [
|
|
668
|
-
"api-emulator.config.yaml",
|
|
669
|
-
"api-emulator.config.yml",
|
|
670
|
-
"api-emulator.config.json",
|
|
671
|
-
"emulate.config.yaml",
|
|
672
|
-
"emulate.config.yml",
|
|
673
|
-
"emulate.config.json",
|
|
674
|
-
"service-emulator.config.yaml",
|
|
675
|
-
"service-emulator.config.yml",
|
|
676
|
-
"service-emulator.config.json"
|
|
677
|
-
];
|
|
310
|
+
const autoFiles = ["api-emulator.config.yaml", "api-emulator.config.yml", "api-emulator.config.json"];
|
|
678
311
|
for (const file of autoFiles) {
|
|
679
|
-
const fullPath =
|
|
680
|
-
if (
|
|
681
|
-
const content =
|
|
312
|
+
const fullPath = resolve3(file);
|
|
313
|
+
if (existsSync2(fullPath)) {
|
|
314
|
+
const content = readFileSync2(fullPath, "utf-8");
|
|
682
315
|
try {
|
|
683
316
|
const config = fullPath.endsWith(".json") ? JSON.parse(content) : parseYaml(content);
|
|
684
317
|
return { config, source: file };
|
|
@@ -706,7 +339,7 @@ async function startCommand(options) {
|
|
|
706
339
|
const pluginSpecifiers = options.plugin?.split(",").map((s) => s.trim()).filter(Boolean) ?? [];
|
|
707
340
|
let allPluginModules;
|
|
708
341
|
try {
|
|
709
|
-
allPluginModules = await resolvePluginModules(pluginSpecifiers);
|
|
342
|
+
allPluginModules = await resolvePluginModules(pluginSpecifiers, { includeInstalled: true });
|
|
710
343
|
} catch (err) {
|
|
711
344
|
console.error(`Failed to load plugins: ${err instanceof Error ? err.message : err}`);
|
|
712
345
|
process.exit(1);
|
|
@@ -804,25 +437,27 @@ function printBanner(services, tokens, configSource) {
|
|
|
804
437
|
if (configSource) {
|
|
805
438
|
lines.push(` ${pc.dim("Config:")} ${configSource}`);
|
|
806
439
|
} else {
|
|
807
|
-
lines.push(
|
|
440
|
+
lines.push(
|
|
441
|
+
` ${pc.dim("Config:")} defaults ${pc.dim("(run")} npx -p api-emulator api init ${pc.dim("to customize)")}`
|
|
442
|
+
);
|
|
808
443
|
}
|
|
809
444
|
lines.push("");
|
|
810
445
|
console.log(lines.join("\n"));
|
|
811
446
|
}
|
|
812
447
|
|
|
813
448
|
// src/commands/init.ts
|
|
814
|
-
import { writeFileSync, existsSync as
|
|
815
|
-
import { resolve as
|
|
449
|
+
import { writeFileSync as writeFileSync2, existsSync as existsSync3 } from "fs";
|
|
450
|
+
import { resolve as resolve4 } from "path";
|
|
816
451
|
import { stringify as yamlStringify } from "yaml";
|
|
817
452
|
async function initCommand(options) {
|
|
818
453
|
const filename = "api-emulator.config.yaml";
|
|
819
|
-
const fullPath =
|
|
820
|
-
if (
|
|
454
|
+
const fullPath = resolve4(filename);
|
|
455
|
+
if (existsSync3(fullPath)) {
|
|
821
456
|
console.error(`Config file already exists: ${filename}`);
|
|
822
457
|
process.exit(1);
|
|
823
458
|
}
|
|
824
459
|
const pluginSpecifiers = options.plugin?.split(",").map((s) => s.trim()).filter(Boolean) ?? [];
|
|
825
|
-
const pluginModules = await resolvePluginModules(pluginSpecifiers);
|
|
460
|
+
const pluginModules = await resolvePluginModules(pluginSpecifiers, { includeInstalled: true });
|
|
826
461
|
const availableServices = Object.keys(pluginModules);
|
|
827
462
|
let config;
|
|
828
463
|
if (options.service === "all") {
|
|
@@ -839,16 +474,16 @@ async function initCommand(options) {
|
|
|
839
474
|
config = { ...DEFAULT_TOKENS, ...pluginModule.initConfig };
|
|
840
475
|
}
|
|
841
476
|
const content = yamlStringify(config);
|
|
842
|
-
|
|
477
|
+
writeFileSync2(fullPath, content, "utf-8");
|
|
843
478
|
console.log(`Created ${filename}`);
|
|
844
479
|
console.log(`
|
|
845
|
-
Run 'npx api-emulator' to start the emulator.`);
|
|
480
|
+
Run 'npx -p api-emulator api' to start the emulator.`);
|
|
846
481
|
}
|
|
847
482
|
|
|
848
483
|
// src/commands/list.ts
|
|
849
484
|
async function listCommand(options = {}) {
|
|
850
485
|
const pluginSpecifiers = options.plugin?.split(",").map((s) => s.trim()).filter(Boolean) ?? [];
|
|
851
|
-
const pluginModules = await resolvePluginModules(pluginSpecifiers);
|
|
486
|
+
const pluginModules = await resolvePluginModules(pluginSpecifiers, { includeInstalled: true });
|
|
852
487
|
console.log("\nAvailable services:\n");
|
|
853
488
|
for (const pluginModule of Object.values(pluginModules)) {
|
|
854
489
|
console.log(` ${pluginModule.name.padEnd(10)}${pluginModule.label}`);
|
|
@@ -857,11 +492,179 @@ async function listCommand(options = {}) {
|
|
|
857
492
|
}
|
|
858
493
|
}
|
|
859
494
|
|
|
495
|
+
// src/commands/install.ts
|
|
496
|
+
import { spawnSync as spawnSync2 } from "child_process";
|
|
497
|
+
import { existsSync as existsSync5 } from "fs";
|
|
498
|
+
import { resolve as resolve6 } from "path";
|
|
499
|
+
|
|
500
|
+
// src/plugin-source-registry.ts
|
|
501
|
+
import { existsSync as existsSync4, readdirSync, readFileSync as readFileSync3, statSync } from "fs";
|
|
502
|
+
import { dirname, join, resolve as resolve5 } from "path";
|
|
503
|
+
var PLUGIN_SOURCES = {
|
|
504
|
+
cloudflare: {
|
|
505
|
+
name: "cloudflare",
|
|
506
|
+
sourceId: "public",
|
|
507
|
+
kind: "package",
|
|
508
|
+
packageName: "@api-emulator/cloudflare",
|
|
509
|
+
specifier: "@api-emulator/cloudflare",
|
|
510
|
+
description: "Workers-style bindings, D1, KV, R2, queues, durable objects, and service bindings"
|
|
511
|
+
}
|
|
512
|
+
};
|
|
513
|
+
var DEFAULT_CATALOG_DIRS = ["api-emulator-plugins", "api-emulator-internal"];
|
|
514
|
+
var CATALOG_MANIFEST_FILES = ["api-emulator.catalog.json"];
|
|
515
|
+
var PLUGIN_ENTRY_FILES = ["api-emulator.mjs", "api-emulator/index.mjs"];
|
|
516
|
+
var PACKAGE_ENTRY_DIRS = ["api-emulator", "trading-emulator"];
|
|
517
|
+
function candidateCatalogRoots(cwd = process.cwd()) {
|
|
518
|
+
const envRoots = process.env.API_EMULATOR_PLUGIN_CATALOGS?.split(",").map((value) => value.trim()).filter(Boolean) ?? [];
|
|
519
|
+
const roots = [...envRoots];
|
|
520
|
+
let current = resolve5(cwd);
|
|
521
|
+
for (; ; ) {
|
|
522
|
+
for (const dir of DEFAULT_CATALOG_DIRS) {
|
|
523
|
+
roots.push(join(current, dir));
|
|
524
|
+
roots.push(join(dirname(current), dir));
|
|
525
|
+
}
|
|
526
|
+
const parent = dirname(current);
|
|
527
|
+
if (parent === current) break;
|
|
528
|
+
current = parent;
|
|
529
|
+
}
|
|
530
|
+
return [...new Set(roots.map((root) => resolve5(root)))];
|
|
531
|
+
}
|
|
532
|
+
function sourceIdForRoot(root) {
|
|
533
|
+
const base = root.split(/[\\/]/).pop() ?? "local";
|
|
534
|
+
if (base === "api-emulator-plugins") return "public";
|
|
535
|
+
if (base === "api-emulator-internal") return "internal";
|
|
536
|
+
return base;
|
|
537
|
+
}
|
|
538
|
+
function packageEntrySpecifier(packageRoot, pkg3) {
|
|
539
|
+
if (typeof pkg3.exports === "string") return resolve5(packageRoot, pkg3.exports);
|
|
540
|
+
if (typeof pkg3.exports === "object" && pkg3.exports !== null && "." in pkg3.exports && typeof pkg3.exports["."] === "string") {
|
|
541
|
+
return resolve5(packageRoot, pkg3.exports["."]);
|
|
542
|
+
}
|
|
543
|
+
if (typeof pkg3.exports === "object" && pkg3.exports !== null && "." in pkg3.exports && typeof pkg3.exports["."] === "object" && pkg3.exports["."] !== null && "import" in pkg3.exports["."] && typeof pkg3.exports["."].import === "string") {
|
|
544
|
+
return resolve5(packageRoot, pkg3.exports["."].import);
|
|
545
|
+
}
|
|
546
|
+
if (typeof pkg3.main === "string") return resolve5(packageRoot, pkg3.main);
|
|
547
|
+
return packageRoot;
|
|
548
|
+
}
|
|
549
|
+
function discoverManifest(root, sourceId) {
|
|
550
|
+
const sources = [];
|
|
551
|
+
for (const manifestFile of CATALOG_MANIFEST_FILES) {
|
|
552
|
+
const manifestPath = join(root, manifestFile);
|
|
553
|
+
if (!existsSync4(manifestPath)) continue;
|
|
554
|
+
const manifest = JSON.parse(readFileSync3(manifestPath, "utf-8"));
|
|
555
|
+
for (const [name, entry] of Object.entries(manifest.plugins ?? {})) {
|
|
556
|
+
sources.push({
|
|
557
|
+
name,
|
|
558
|
+
sourceId,
|
|
559
|
+
kind: entry.kind ?? (entry.packageName ? "package" : "file"),
|
|
560
|
+
packageName: entry.packageName,
|
|
561
|
+
specifier: entry.specifier.startsWith(".") ? resolve5(root, entry.specifier) : entry.specifier,
|
|
562
|
+
description: entry.description ?? `${name} plugin from ${sourceId} catalog`
|
|
563
|
+
});
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
return sources;
|
|
567
|
+
}
|
|
568
|
+
function discoverCatalog(root) {
|
|
569
|
+
if (!existsSync4(root) || !statSync(root).isDirectory()) return [];
|
|
570
|
+
const sourceId = sourceIdForRoot(root);
|
|
571
|
+
const sources = discoverManifest(root, sourceId);
|
|
572
|
+
for (const entry of readdirSync(root, { withFileTypes: true })) {
|
|
573
|
+
if (!entry.isDirectory() || !entry.name.startsWith("@")) continue;
|
|
574
|
+
const name = entry.name.slice(1);
|
|
575
|
+
const pluginRoot = join(root, entry.name);
|
|
576
|
+
for (const entryFile of PLUGIN_ENTRY_FILES) {
|
|
577
|
+
const specifier = join(pluginRoot, entryFile);
|
|
578
|
+
if (existsSync4(specifier)) {
|
|
579
|
+
sources.push({
|
|
580
|
+
name,
|
|
581
|
+
sourceId,
|
|
582
|
+
kind: "file",
|
|
583
|
+
specifier,
|
|
584
|
+
description: `${name} plugin from ${sourceId} catalog`
|
|
585
|
+
});
|
|
586
|
+
break;
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
for (const entryDir of PACKAGE_ENTRY_DIRS) {
|
|
590
|
+
const packageRoot = join(pluginRoot, entryDir);
|
|
591
|
+
const packageJsonPath = join(packageRoot, "package.json");
|
|
592
|
+
if (!existsSync4(packageJsonPath)) continue;
|
|
593
|
+
const pkg3 = JSON.parse(readFileSync3(packageJsonPath, "utf-8"));
|
|
594
|
+
sources.push({
|
|
595
|
+
name,
|
|
596
|
+
sourceId,
|
|
597
|
+
kind: "package",
|
|
598
|
+
packageName: pkg3.name,
|
|
599
|
+
packageRoot,
|
|
600
|
+
specifier: packageEntrySpecifier(packageRoot, pkg3),
|
|
601
|
+
description: pkg3.description ?? `${name} package from ${sourceId} catalog`
|
|
602
|
+
});
|
|
603
|
+
break;
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
return sources;
|
|
607
|
+
}
|
|
608
|
+
function pluginSources() {
|
|
609
|
+
const sources = { ...PLUGIN_SOURCES };
|
|
610
|
+
for (const root of candidateCatalogRoots()) {
|
|
611
|
+
for (const source of discoverCatalog(root)) {
|
|
612
|
+
sources[source.name] ??= source;
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
return sources;
|
|
616
|
+
}
|
|
617
|
+
function resolvePluginSource(name) {
|
|
618
|
+
return pluginSources()[name] ?? null;
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
// src/commands/install.ts
|
|
622
|
+
function detectPackageManager() {
|
|
623
|
+
if (existsSync5(resolve6("bun.lock")) || existsSync5(resolve6("bun.lockb"))) return "bun";
|
|
624
|
+
if (existsSync5(resolve6("pnpm-lock.yaml"))) return "pnpm";
|
|
625
|
+
if (existsSync5(resolve6("yarn.lock"))) return "yarn";
|
|
626
|
+
return "npm";
|
|
627
|
+
}
|
|
628
|
+
function installPackage(packageManager, packageName) {
|
|
629
|
+
const args = packageManager === "bun" ? ["add", "-D", packageName] : packageManager === "pnpm" ? ["add", "-D", packageName] : packageManager === "yarn" ? ["add", "-D", packageName] : ["install", "-D", packageName];
|
|
630
|
+
const result = spawnSync2(packageManager, args, { stdio: "inherit" });
|
|
631
|
+
if (result.error) {
|
|
632
|
+
throw result.error;
|
|
633
|
+
}
|
|
634
|
+
if (result.status !== 0) {
|
|
635
|
+
throw new Error(`${packageManager} ${args.join(" ")} failed`);
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
async function installCommand(name, options = {}) {
|
|
639
|
+
const source = resolvePluginSource(name);
|
|
640
|
+
if (!source) {
|
|
641
|
+
throw new Error(`Unknown plugin source: ${name}`);
|
|
642
|
+
}
|
|
643
|
+
const packageManager = options.packageManager === void 0 ? detectPackageManager() : options.packageManager;
|
|
644
|
+
if (packageManager && source.packageName) {
|
|
645
|
+
installPackage(packageManager, source.packageName);
|
|
646
|
+
} else if (packageManager && source.kind === "package" && !source.packageName) {
|
|
647
|
+
throw new Error(`Plugin source "${name}" is a local package without a package name`);
|
|
648
|
+
}
|
|
649
|
+
const lock = readPluginLock();
|
|
650
|
+
lock.plugins[source.name] = {
|
|
651
|
+
name: source.name,
|
|
652
|
+
source: source.kind === "file" ? "specifier" : "registry",
|
|
653
|
+
sourceId: source.sourceId,
|
|
654
|
+
packageName: source.packageName,
|
|
655
|
+
specifier: packageManager && source.packageName ? source.packageName : source.specifier,
|
|
656
|
+
version: "latest"
|
|
657
|
+
};
|
|
658
|
+
writePluginLock(lock);
|
|
659
|
+
console.log(`Installed ${source.name} from ${source.sourceId}`);
|
|
660
|
+
console.log(`Recorded plugin in ${PLUGIN_LOCK_FILE}`);
|
|
661
|
+
}
|
|
662
|
+
|
|
860
663
|
// src/index.ts
|
|
861
|
-
var pkg2 = { version: "0.
|
|
862
|
-
var defaultPort = process.env.API_EMULATOR_PORT ?? process.env.
|
|
664
|
+
var pkg2 = { version: "0.6.0" };
|
|
665
|
+
var defaultPort = process.env.API_EMULATOR_PORT ?? process.env.PORT ?? "4000";
|
|
863
666
|
var program = new Command();
|
|
864
|
-
program.name("api
|
|
667
|
+
program.name("api").description("Local API emulators you can run, share, and extend with plugins").version(pkg2.version);
|
|
865
668
|
program.command("start", { isDefault: true }).description("Start the emulator server").option("-p, --port <port>", "Base port", defaultPort).option("-s, --service <services>", "Comma-separated services to enable").option("--seed <file>", "Path to seed config file").option("--base-url <url>", "Override advertised base URL (supports {service} template)").option("--portless", "Serve over HTTPS via portless (auto-registers aliases)").option("--plugin <plugins>", "Comma-separated external plugin paths or package names").action(async (opts) => {
|
|
866
669
|
const port = parseInt(opts.port, 10);
|
|
867
670
|
if (Number.isNaN(port) || port < 1 || port > 65535) {
|
|
@@ -883,5 +686,15 @@ program.command("init").description("Generate a starter config file").option("-s
|
|
|
883
686
|
program.command("list").alias("list-services").description("List available services").option("--plugin <plugins>", "Comma-separated external plugin paths or package names").action(async (opts) => {
|
|
884
687
|
await listCommand({ plugin: opts.plugin });
|
|
885
688
|
});
|
|
689
|
+
program.command("install <plugin>").description("Install a provider plugin by name").option("--package-manager <name>", "Package manager to use").option("--no-package-manager", "Only record the plugin in api-emulator.lock").action(async (plugin, opts) => {
|
|
690
|
+
try {
|
|
691
|
+
await installCommand(plugin, {
|
|
692
|
+
packageManager: opts.packageManager === false ? false : opts.packageManager
|
|
693
|
+
});
|
|
694
|
+
} catch (err) {
|
|
695
|
+
console.error(err instanceof Error ? err.message : err);
|
|
696
|
+
process.exit(1);
|
|
697
|
+
}
|
|
698
|
+
});
|
|
886
699
|
program.parse();
|
|
887
700
|
//# sourceMappingURL=index.js.map
|