api-emulator 0.5.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 +274 -0
- package/dist/api.d.ts +26 -0
- package/dist/api.js +561 -0
- package/dist/api.js.map +1 -0
- package/dist/fonts/GeistPixel-Square.woff2 +0 -0
- package/dist/fonts/favicon.ico +0 -0
- package/dist/fonts/geist-sans.woff2 +0 -0
- package/dist/index.js +887 -0
- package/dist/index.js.map +1 -0
- package/package.json +77 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,887 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// src/index.ts
|
|
3
|
+
import { Command } from "commander";
|
|
4
|
+
|
|
5
|
+
// src/external-plugin-adapter.ts
|
|
6
|
+
import { isAbsolute, resolve } from "path";
|
|
7
|
+
async function loadExternalPluginModule(specifier) {
|
|
8
|
+
const modulePath = specifier.startsWith(".") || isAbsolute(specifier) ? resolve(specifier) : specifier;
|
|
9
|
+
const mod = await import(modulePath);
|
|
10
|
+
const plugin = mod.plugin ?? mod.default;
|
|
11
|
+
if (!plugin || typeof plugin.register !== "function" || typeof plugin.name !== "string") {
|
|
12
|
+
throw new Error(`Plugin "${specifier}" must export a ServicePlugin (as "plugin" or default export)`);
|
|
13
|
+
}
|
|
14
|
+
const name = plugin.name;
|
|
15
|
+
return {
|
|
16
|
+
name,
|
|
17
|
+
label: mod.label ?? `${name} (external plugin)`,
|
|
18
|
+
endpoints: mod.endpoints ?? "",
|
|
19
|
+
async load() {
|
|
20
|
+
return {
|
|
21
|
+
plugin,
|
|
22
|
+
seedFromConfig: mod.seedFromConfig
|
|
23
|
+
};
|
|
24
|
+
},
|
|
25
|
+
defaultFallback: mod.defaultFallback ?? (() => ({ login: "admin", id: 1, scopes: [] })),
|
|
26
|
+
initConfig: mod.initConfig ?? {}
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// src/default-plugin-catalog.ts
|
|
31
|
+
var DEFAULT_PLUGIN_NAME_LIST = [
|
|
32
|
+
"vercel",
|
|
33
|
+
"github",
|
|
34
|
+
"google",
|
|
35
|
+
"slack",
|
|
36
|
+
"apple",
|
|
37
|
+
"microsoft",
|
|
38
|
+
"okta",
|
|
39
|
+
"aws",
|
|
40
|
+
"resend",
|
|
41
|
+
"stripe",
|
|
42
|
+
"mongoatlas",
|
|
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
|
+
}
|
|
457
|
+
}
|
|
458
|
+
};
|
|
459
|
+
|
|
460
|
+
// src/registry.ts
|
|
461
|
+
var DEFAULT_TOKENS = {
|
|
462
|
+
tokens: {
|
|
463
|
+
test_token_admin: {
|
|
464
|
+
login: "admin",
|
|
465
|
+
scopes: ["repo", "user", "admin:org", "admin:repo_hook"]
|
|
466
|
+
},
|
|
467
|
+
test_token_user1: {
|
|
468
|
+
login: "octocat",
|
|
469
|
+
scopes: ["repo", "user"]
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
};
|
|
473
|
+
async function resolvePluginModules(pluginSpecifiers = []) {
|
|
474
|
+
const results = await Promise.all(pluginSpecifiers.map(loadExternalPluginModule));
|
|
475
|
+
const externalEntries = {};
|
|
476
|
+
for (const pluginModule of results) {
|
|
477
|
+
if (pluginModule.name in DEFAULT_PLUGIN_REGISTRY) {
|
|
478
|
+
throw new Error(`Plugin "${pluginModule.name}" conflicts with default plugin "${pluginModule.name}"`);
|
|
479
|
+
}
|
|
480
|
+
if (pluginModule.name in externalEntries) {
|
|
481
|
+
throw new Error(`Duplicate plugin name "${pluginModule.name}"`);
|
|
482
|
+
}
|
|
483
|
+
externalEntries[pluginModule.name] = pluginModule;
|
|
484
|
+
}
|
|
485
|
+
return { ...DEFAULT_PLUGIN_REGISTRY, ...externalEntries };
|
|
486
|
+
}
|
|
487
|
+
function getDefaultPluginNames() {
|
|
488
|
+
return [...DEFAULT_PLUGIN_NAMES];
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
// src/commands/start.ts
|
|
492
|
+
import { readFileSync, existsSync } from "fs";
|
|
493
|
+
import { resolve as resolve2 } from "path";
|
|
494
|
+
import { parse as parseYaml } from "yaml";
|
|
495
|
+
import pc from "picocolors";
|
|
496
|
+
|
|
497
|
+
// src/portless.ts
|
|
498
|
+
import { execSync, spawnSync } from "child_process";
|
|
499
|
+
import { createInterface } from "readline";
|
|
500
|
+
function isInteractive() {
|
|
501
|
+
return Boolean(process.stdin.isTTY) && !process.env.CI;
|
|
502
|
+
}
|
|
503
|
+
function hasPortless() {
|
|
504
|
+
const result = spawnSync("portless", ["--version"], { stdio: "ignore" });
|
|
505
|
+
return result.status === 0;
|
|
506
|
+
}
|
|
507
|
+
function promptYesNo(question) {
|
|
508
|
+
return new Promise((resolve4) => {
|
|
509
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
510
|
+
rl.question(question, (answer) => {
|
|
511
|
+
rl.close();
|
|
512
|
+
const normalized = answer.trim().toLowerCase();
|
|
513
|
+
resolve4(normalized === "" || normalized === "y" || normalized === "yes");
|
|
514
|
+
});
|
|
515
|
+
});
|
|
516
|
+
}
|
|
517
|
+
function isProxyRunning() {
|
|
518
|
+
const result = spawnSync("portless", ["list"], { stdio: "ignore" });
|
|
519
|
+
return result.status === 0;
|
|
520
|
+
}
|
|
521
|
+
async function ensurePortless() {
|
|
522
|
+
if (!hasPortless()) {
|
|
523
|
+
if (!isInteractive()) {
|
|
524
|
+
console.error("portless is required but not installed. Run: npm i -g portless");
|
|
525
|
+
process.exit(1);
|
|
526
|
+
}
|
|
527
|
+
const yes = await promptYesNo("portless is not installed. Install it now? (npm i -g portless) [Y/n] ");
|
|
528
|
+
if (!yes) {
|
|
529
|
+
console.error("Cannot continue without portless.");
|
|
530
|
+
process.exit(1);
|
|
531
|
+
}
|
|
532
|
+
try {
|
|
533
|
+
execSync("npm i -g portless", { stdio: "inherit" });
|
|
534
|
+
} catch {
|
|
535
|
+
console.error("Failed to install portless.");
|
|
536
|
+
process.exit(1);
|
|
537
|
+
}
|
|
538
|
+
if (!hasPortless()) {
|
|
539
|
+
console.error("portless was installed but could not be found on PATH.");
|
|
540
|
+
process.exit(1);
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
if (!isProxyRunning()) {
|
|
544
|
+
console.error("portless proxy is not running. Start it with: portless proxy start");
|
|
545
|
+
process.exit(1);
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
function registerAliases(aliases) {
|
|
549
|
+
const registered = [];
|
|
550
|
+
for (const { name, port } of aliases) {
|
|
551
|
+
const result = spawnSync("portless", ["alias", name, String(port), "--force"], {
|
|
552
|
+
stdio: "inherit"
|
|
553
|
+
});
|
|
554
|
+
if (result.status !== 0) {
|
|
555
|
+
if (registered.length > 0) {
|
|
556
|
+
removeAliases(registered);
|
|
557
|
+
}
|
|
558
|
+
throw new Error(`Failed to register portless alias: ${name} -> ${port}`);
|
|
559
|
+
}
|
|
560
|
+
registered.push({ name, port });
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
function removeAliases(aliases) {
|
|
564
|
+
for (const { name } of aliases) {
|
|
565
|
+
const result = spawnSync("portless", ["alias", "--remove", name], { stdio: "ignore" });
|
|
566
|
+
if (result.status !== 0) {
|
|
567
|
+
console.error(`Warning: failed to remove portless alias: ${name}`);
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
function portlessBaseUrl(serviceName) {
|
|
572
|
+
return `https://${serviceName}.api-emulator.localhost`;
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
// src/base-url.ts
|
|
576
|
+
function resolveBaseUrl(opts) {
|
|
577
|
+
if (opts.seedBaseUrl) {
|
|
578
|
+
return opts.seedBaseUrl.replace(/\{service\}/g, opts.service);
|
|
579
|
+
}
|
|
580
|
+
if (opts.baseUrl) {
|
|
581
|
+
return opts.baseUrl.replace(/\{service\}/g, opts.service);
|
|
582
|
+
}
|
|
583
|
+
const envBaseUrl = process.env.API_EMULATOR_BASE_URL ?? process.env.EMULATE_BASE_URL;
|
|
584
|
+
if (envBaseUrl) {
|
|
585
|
+
return envBaseUrl.replace(/\{service\}/g, opts.service);
|
|
586
|
+
}
|
|
587
|
+
const portlessUrl = process.env.PORTLESS_URL;
|
|
588
|
+
if (portlessUrl) {
|
|
589
|
+
return portlessUrl.replace(/\{service\}/g, opts.service);
|
|
590
|
+
}
|
|
591
|
+
return `http://localhost:${opts.port}`;
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
// src/service-runtime.ts
|
|
595
|
+
import { createServer } from "@emulators/core";
|
|
596
|
+
import { serve } from "@hono/node-server";
|
|
597
|
+
function createAuthTokens(seedConfig) {
|
|
598
|
+
const tokens = {};
|
|
599
|
+
if (seedConfig?.tokens) {
|
|
600
|
+
let tokenId = 100;
|
|
601
|
+
for (const [token, user] of Object.entries(seedConfig.tokens)) {
|
|
602
|
+
tokens[token] = { login: user.login, id: tokenId++, scopes: user.scopes };
|
|
603
|
+
}
|
|
604
|
+
} else {
|
|
605
|
+
tokens["test_token_admin"] = { login: "admin", id: 2, scopes: ["repo", "user", "admin:org", "admin:repo_hook"] };
|
|
606
|
+
}
|
|
607
|
+
return tokens;
|
|
608
|
+
}
|
|
609
|
+
function createServiceRuntime(options) {
|
|
610
|
+
const { service, pluginModule, loadedPlugin, port, baseUrl, tokens, seedConfig } = options;
|
|
611
|
+
const resolverRef = {};
|
|
612
|
+
const appKeyResolver = loadedPlugin.createAppKeyResolver ? (appId) => resolverRef.current(appId) : void 0;
|
|
613
|
+
const fallbackUser = pluginModule.defaultFallback(seedConfig);
|
|
614
|
+
const { app, store, webhooks } = createServer(loadedPlugin.plugin, {
|
|
615
|
+
port,
|
|
616
|
+
baseUrl,
|
|
617
|
+
tokens,
|
|
618
|
+
appKeyResolver,
|
|
619
|
+
fallbackUser
|
|
620
|
+
});
|
|
621
|
+
resolverRef.current = loadedPlugin.createAppKeyResolver?.(store);
|
|
622
|
+
const seed = () => {
|
|
623
|
+
loadedPlugin.plugin.seed?.(store, baseUrl);
|
|
624
|
+
if (seedConfig && loadedPlugin.seedFromConfig) {
|
|
625
|
+
loadedPlugin.seedFromConfig(store, baseUrl, seedConfig, webhooks);
|
|
626
|
+
}
|
|
627
|
+
};
|
|
628
|
+
seed();
|
|
629
|
+
const httpServer = serve({ fetch: app.fetch, port });
|
|
630
|
+
return {
|
|
631
|
+
service,
|
|
632
|
+
url: baseUrl,
|
|
633
|
+
store,
|
|
634
|
+
reset() {
|
|
635
|
+
store.reset();
|
|
636
|
+
seed();
|
|
637
|
+
},
|
|
638
|
+
close() {
|
|
639
|
+
return new Promise((resolve4, reject) => {
|
|
640
|
+
httpServer.close((err) => {
|
|
641
|
+
if (err) reject(err);
|
|
642
|
+
else resolve4();
|
|
643
|
+
});
|
|
644
|
+
});
|
|
645
|
+
}
|
|
646
|
+
};
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
// src/commands/start.ts
|
|
650
|
+
var pkg = { version: "0.5.0" };
|
|
651
|
+
function loadSeedConfig(seedPath) {
|
|
652
|
+
if (seedPath) {
|
|
653
|
+
const fullPath = resolve2(seedPath);
|
|
654
|
+
if (!existsSync(fullPath)) {
|
|
655
|
+
console.error(`Seed file not found: ${fullPath}`);
|
|
656
|
+
process.exit(1);
|
|
657
|
+
}
|
|
658
|
+
const content = readFileSync(fullPath, "utf-8");
|
|
659
|
+
try {
|
|
660
|
+
const config = fullPath.endsWith(".json") ? JSON.parse(content) : parseYaml(content);
|
|
661
|
+
return { config, source: seedPath };
|
|
662
|
+
} catch (err) {
|
|
663
|
+
console.error(`Failed to parse ${seedPath}: ${err instanceof Error ? err.message : err}`);
|
|
664
|
+
process.exit(1);
|
|
665
|
+
}
|
|
666
|
+
}
|
|
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
|
+
];
|
|
678
|
+
for (const file of autoFiles) {
|
|
679
|
+
const fullPath = resolve2(file);
|
|
680
|
+
if (existsSync(fullPath)) {
|
|
681
|
+
const content = readFileSync(fullPath, "utf-8");
|
|
682
|
+
try {
|
|
683
|
+
const config = fullPath.endsWith(".json") ? JSON.parse(content) : parseYaml(content);
|
|
684
|
+
return { config, source: file };
|
|
685
|
+
} catch (err) {
|
|
686
|
+
console.error(`Failed to parse ${file}: ${err instanceof Error ? err.message : err}`);
|
|
687
|
+
process.exit(1);
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
return null;
|
|
692
|
+
}
|
|
693
|
+
function inferServicesFromConfig(config, availableServices) {
|
|
694
|
+
const found = availableServices.filter((k) => k in config);
|
|
695
|
+
return found.length > 0 ? [...found] : null;
|
|
696
|
+
}
|
|
697
|
+
async function startCommand(options) {
|
|
698
|
+
const { port: basePort } = options;
|
|
699
|
+
if (options.portless && options.baseUrl) {
|
|
700
|
+
console.error("--portless and --base-url are mutually exclusive.");
|
|
701
|
+
process.exit(1);
|
|
702
|
+
}
|
|
703
|
+
const loaded = loadSeedConfig(options.seed);
|
|
704
|
+
const seedConfig = loaded?.config ?? null;
|
|
705
|
+
const configSource = loaded?.source ?? null;
|
|
706
|
+
const pluginSpecifiers = options.plugin?.split(",").map((s) => s.trim()).filter(Boolean) ?? [];
|
|
707
|
+
let allPluginModules;
|
|
708
|
+
try {
|
|
709
|
+
allPluginModules = await resolvePluginModules(pluginSpecifiers);
|
|
710
|
+
} catch (err) {
|
|
711
|
+
console.error(`Failed to load plugins: ${err instanceof Error ? err.message : err}`);
|
|
712
|
+
process.exit(1);
|
|
713
|
+
}
|
|
714
|
+
const defaultPlugins = getDefaultPluginNames();
|
|
715
|
+
const externalServices = Object.keys(allPluginModules).filter((name) => !defaultPlugins.includes(name));
|
|
716
|
+
let services;
|
|
717
|
+
if (options.service) {
|
|
718
|
+
services = options.service.split(",").map((s) => s.trim());
|
|
719
|
+
} else if (seedConfig) {
|
|
720
|
+
services = inferServicesFromConfig(seedConfig, Object.keys(allPluginModules)) ?? [
|
|
721
|
+
...defaultPlugins,
|
|
722
|
+
...externalServices
|
|
723
|
+
];
|
|
724
|
+
} else {
|
|
725
|
+
services = [...defaultPlugins, ...externalServices];
|
|
726
|
+
}
|
|
727
|
+
for (const svc of services) {
|
|
728
|
+
if (!allPluginModules[svc]) {
|
|
729
|
+
console.error(`Unknown service: ${svc}`);
|
|
730
|
+
process.exit(1);
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
const tokens = createAuthTokens(seedConfig);
|
|
734
|
+
if (options.portless) {
|
|
735
|
+
await ensurePortless();
|
|
736
|
+
}
|
|
737
|
+
const portlessAliases = [];
|
|
738
|
+
const prepared = [];
|
|
739
|
+
for (let i = 0; i < services.length; i++) {
|
|
740
|
+
const svc = services[i];
|
|
741
|
+
const pluginModule = allPluginModules[svc];
|
|
742
|
+
const loadedPlugin = await pluginModule.load();
|
|
743
|
+
const svcSeedConfig = seedConfig?.[svc];
|
|
744
|
+
const port = svcSeedConfig?.port ?? basePort + i;
|
|
745
|
+
if (options.portless) {
|
|
746
|
+
portlessAliases.push({ name: `${svc}.api-emulator`, port });
|
|
747
|
+
}
|
|
748
|
+
const seedBaseUrl = typeof svcSeedConfig?.baseUrl === "string" && svcSeedConfig.baseUrl.length > 0 ? svcSeedConfig.baseUrl : void 0;
|
|
749
|
+
const effectiveBaseUrl = options.portless ? portlessBaseUrl(svc) : options.baseUrl;
|
|
750
|
+
const baseUrl = resolveBaseUrl({ service: svc, port, baseUrl: effectiveBaseUrl, seedBaseUrl });
|
|
751
|
+
prepared.push({ svc, pluginModule, loadedPlugin, svcSeedConfig, port, baseUrl });
|
|
752
|
+
}
|
|
753
|
+
if (portlessAliases.length > 0) {
|
|
754
|
+
registerAliases(portlessAliases);
|
|
755
|
+
}
|
|
756
|
+
const serviceUrls = [];
|
|
757
|
+
const runningServices = [];
|
|
758
|
+
for (const { svc, pluginModule, loadedPlugin, svcSeedConfig, port, baseUrl } of prepared) {
|
|
759
|
+
serviceUrls.push({ name: svc, url: baseUrl });
|
|
760
|
+
const running = createServiceRuntime({
|
|
761
|
+
service: svc,
|
|
762
|
+
pluginModule,
|
|
763
|
+
loadedPlugin,
|
|
764
|
+
port,
|
|
765
|
+
baseUrl,
|
|
766
|
+
tokens,
|
|
767
|
+
seedConfig: svcSeedConfig
|
|
768
|
+
});
|
|
769
|
+
runningServices.push(running);
|
|
770
|
+
}
|
|
771
|
+
printBanner(serviceUrls, tokens, configSource);
|
|
772
|
+
const shutdown = () => {
|
|
773
|
+
console.log(`
|
|
774
|
+
${pc.dim("Shutting down...")}`);
|
|
775
|
+
if (portlessAliases.length > 0) {
|
|
776
|
+
removeAliases(portlessAliases);
|
|
777
|
+
}
|
|
778
|
+
for (const running of runningServices) {
|
|
779
|
+
void running.close();
|
|
780
|
+
}
|
|
781
|
+
process.exit(0);
|
|
782
|
+
};
|
|
783
|
+
process.once("SIGINT", shutdown);
|
|
784
|
+
process.once("SIGTERM", shutdown);
|
|
785
|
+
}
|
|
786
|
+
function printBanner(services, tokens, configSource) {
|
|
787
|
+
const lines = [];
|
|
788
|
+
lines.push("");
|
|
789
|
+
lines.push(` ${pc.bold("api-emulator")} ${pc.dim(`v${pkg.version}`)}`);
|
|
790
|
+
lines.push("");
|
|
791
|
+
const maxNameLen = Math.max(...services.map((s) => s.name.length));
|
|
792
|
+
for (const { name, url } of services) {
|
|
793
|
+
lines.push(` ${pc.cyan(name.padEnd(maxNameLen + 2))}${pc.bold(url)}`);
|
|
794
|
+
}
|
|
795
|
+
lines.push("");
|
|
796
|
+
const tokenEntries = Object.entries(tokens);
|
|
797
|
+
if (tokenEntries.length > 0) {
|
|
798
|
+
lines.push(` ${pc.dim("Tokens")}`);
|
|
799
|
+
for (const [token, user] of tokenEntries) {
|
|
800
|
+
lines.push(` ${pc.dim(token)} ${pc.dim("->")} ${user.login}`);
|
|
801
|
+
}
|
|
802
|
+
lines.push("");
|
|
803
|
+
}
|
|
804
|
+
if (configSource) {
|
|
805
|
+
lines.push(` ${pc.dim("Config:")} ${configSource}`);
|
|
806
|
+
} else {
|
|
807
|
+
lines.push(` ${pc.dim("Config:")} defaults ${pc.dim("(run")} npx api-emulator init ${pc.dim("to customize)")}`);
|
|
808
|
+
}
|
|
809
|
+
lines.push("");
|
|
810
|
+
console.log(lines.join("\n"));
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
// src/commands/init.ts
|
|
814
|
+
import { writeFileSync, existsSync as existsSync2 } from "fs";
|
|
815
|
+
import { resolve as resolve3 } from "path";
|
|
816
|
+
import { stringify as yamlStringify } from "yaml";
|
|
817
|
+
async function initCommand(options) {
|
|
818
|
+
const filename = "api-emulator.config.yaml";
|
|
819
|
+
const fullPath = resolve3(filename);
|
|
820
|
+
if (existsSync2(fullPath)) {
|
|
821
|
+
console.error(`Config file already exists: ${filename}`);
|
|
822
|
+
process.exit(1);
|
|
823
|
+
}
|
|
824
|
+
const pluginSpecifiers = options.plugin?.split(",").map((s) => s.trim()).filter(Boolean) ?? [];
|
|
825
|
+
const pluginModules = await resolvePluginModules(pluginSpecifiers);
|
|
826
|
+
const availableServices = Object.keys(pluginModules);
|
|
827
|
+
let config;
|
|
828
|
+
if (options.service === "all") {
|
|
829
|
+
config = { ...DEFAULT_TOKENS };
|
|
830
|
+
for (const name of availableServices) {
|
|
831
|
+
Object.assign(config, pluginModules[name].initConfig);
|
|
832
|
+
}
|
|
833
|
+
} else {
|
|
834
|
+
const pluginModule = pluginModules[options.service];
|
|
835
|
+
if (!pluginModule) {
|
|
836
|
+
console.error(`Unknown service: ${options.service}. Available: ${availableServices.join(", ")}, all`);
|
|
837
|
+
process.exit(1);
|
|
838
|
+
}
|
|
839
|
+
config = { ...DEFAULT_TOKENS, ...pluginModule.initConfig };
|
|
840
|
+
}
|
|
841
|
+
const content = yamlStringify(config);
|
|
842
|
+
writeFileSync(fullPath, content, "utf-8");
|
|
843
|
+
console.log(`Created ${filename}`);
|
|
844
|
+
console.log(`
|
|
845
|
+
Run 'npx api-emulator' to start the emulator.`);
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
// src/commands/list.ts
|
|
849
|
+
async function listCommand(options = {}) {
|
|
850
|
+
const pluginSpecifiers = options.plugin?.split(",").map((s) => s.trim()).filter(Boolean) ?? [];
|
|
851
|
+
const pluginModules = await resolvePluginModules(pluginSpecifiers);
|
|
852
|
+
console.log("\nAvailable services:\n");
|
|
853
|
+
for (const pluginModule of Object.values(pluginModules)) {
|
|
854
|
+
console.log(` ${pluginModule.name.padEnd(10)}${pluginModule.label}`);
|
|
855
|
+
console.log(` Endpoints: ${pluginModule.endpoints}`);
|
|
856
|
+
console.log();
|
|
857
|
+
}
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
// src/index.ts
|
|
861
|
+
var pkg2 = { version: "0.5.0" };
|
|
862
|
+
var defaultPort = process.env.API_EMULATOR_PORT ?? process.env.EMULATE_PORT ?? process.env.PORT ?? "4000";
|
|
863
|
+
var program = new Command();
|
|
864
|
+
program.name("api-emulator").description("Local API emulators you can run, share, and extend with plugins").version(pkg2.version);
|
|
865
|
+
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
|
+
const port = parseInt(opts.port, 10);
|
|
867
|
+
if (Number.isNaN(port) || port < 1 || port > 65535) {
|
|
868
|
+
console.error(`Invalid port: ${opts.port}`);
|
|
869
|
+
process.exit(1);
|
|
870
|
+
}
|
|
871
|
+
await startCommand({
|
|
872
|
+
port,
|
|
873
|
+
service: opts.service,
|
|
874
|
+
seed: opts.seed,
|
|
875
|
+
baseUrl: opts.baseUrl,
|
|
876
|
+
portless: opts.portless,
|
|
877
|
+
plugin: opts.plugin
|
|
878
|
+
});
|
|
879
|
+
});
|
|
880
|
+
program.command("init").description("Generate a starter config file").option("-s, --service <service>", "Service to generate config for", "all").option("--plugin <plugins>", "Comma-separated external plugin paths or package names").action(async (opts) => {
|
|
881
|
+
await initCommand({ service: opts.service, plugin: opts.plugin });
|
|
882
|
+
});
|
|
883
|
+
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
|
+
await listCommand({ plugin: opts.plugin });
|
|
885
|
+
});
|
|
886
|
+
program.parse();
|
|
887
|
+
//# sourceMappingURL=index.js.map
|