emulate 0.4.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.
Files changed (50) hide show
  1. package/README.md +198 -27
  2. package/dist/api.d.ts +2 -1
  3. package/dist/api.js +675 -103
  4. package/dist/api.js.map +1 -1
  5. package/dist/chunk-WVQMFHQM.js +83 -0
  6. package/dist/chunk-WVQMFHQM.js.map +1 -0
  7. package/dist/{dist-B674PYKV.js → dist-2ZZGNPJI.js} +22 -43
  8. package/dist/dist-2ZZGNPJI.js.map +1 -0
  9. package/dist/{dist-RDFBZ5O6.js → dist-CXRPM6BK.js} +211 -48
  10. package/dist/dist-CXRPM6BK.js.map +1 -0
  11. package/dist/{dist-VVXVP5EZ.js → dist-DSJSF3GY.js} +551 -91
  12. package/dist/dist-DSJSF3GY.js.map +1 -0
  13. package/dist/{dist-RMK3BS5M.js → dist-IFULY5LE.js} +196 -33
  14. package/dist/dist-IFULY5LE.js.map +1 -0
  15. package/dist/dist-IRUBHCZU.js +1898 -0
  16. package/dist/dist-IRUBHCZU.js.map +1 -0
  17. package/dist/{dist-YOVM5HEY.js → dist-NJJLJT2N.js} +520 -61
  18. package/dist/dist-NJJLJT2N.js.map +1 -0
  19. package/dist/dist-OGSAVJ25.js +4874 -0
  20. package/dist/dist-OGSAVJ25.js.map +1 -0
  21. package/dist/{dist-H6JYGQM4.js → dist-PO4CL5SJ.js} +271 -158
  22. package/dist/dist-PO4CL5SJ.js.map +1 -0
  23. package/dist/{dist-QMOJM6DV.js → dist-R3TNKUIE.js} +238 -55
  24. package/dist/dist-R3TNKUIE.js.map +1 -0
  25. package/dist/{dist-6JFNJPUU.js → dist-WACHAAVU.js} +171 -22
  26. package/dist/dist-WACHAAVU.js.map +1 -0
  27. package/dist/{dist-OTJZRQ3Q.js → dist-XWWZVLQQ.js} +216 -75
  28. package/dist/dist-XWWZVLQQ.js.map +1 -0
  29. package/dist/{dist-6EW7SSOZ.js → dist-ZY5SZSJ2.js} +397 -223
  30. package/dist/dist-ZY5SZSJ2.js.map +1 -0
  31. package/dist/fonts/favicon.ico +0 -0
  32. package/dist/helpers-LXLP3DFE-LBOTATT5.js +17 -0
  33. package/dist/helpers-LXLP3DFE-LBOTATT5.js.map +1 -0
  34. package/dist/index.js +812 -117
  35. package/dist/index.js.map +1 -1
  36. package/package.json +17 -15
  37. package/dist/chunk-TEPNEZ63.js +0 -2143
  38. package/dist/chunk-TEPNEZ63.js.map +0 -1
  39. package/dist/dist-6EW7SSOZ.js.map +0 -1
  40. package/dist/dist-6JFNJPUU.js.map +0 -1
  41. package/dist/dist-B674PYKV.js.map +0 -1
  42. package/dist/dist-G7WQPZ3Y.js +0 -1287
  43. package/dist/dist-G7WQPZ3Y.js.map +0 -1
  44. package/dist/dist-H6JYGQM4.js.map +0 -1
  45. package/dist/dist-OTJZRQ3Q.js.map +0 -1
  46. package/dist/dist-QMOJM6DV.js.map +0 -1
  47. package/dist/dist-RDFBZ5O6.js.map +0 -1
  48. package/dist/dist-RMK3BS5M.js.map +0 -1
  49. package/dist/dist-VVXVP5EZ.js.map +0 -1
  50. package/dist/dist-YOVM5HEY.js.map +0 -1
package/dist/index.js CHANGED
@@ -3,15 +3,12 @@ import {
3
3
  importPKCS8,
4
4
  jwtVerify
5
5
  } from "./chunk-D6EKRYGP.js";
6
- import {
7
- Hono,
8
- cors
9
- } from "./chunk-TEPNEZ63.js";
10
6
 
11
7
  // src/index.ts
12
8
  import { Command } from "commander";
13
9
 
14
10
  // ../@emulators/core/dist/index.js
11
+ import { createServer as createNodeServer } from "http";
15
12
  import { createHmac } from "crypto";
16
13
  import { readFileSync } from "fs";
17
14
  import { fileURLToPath } from "url";
@@ -235,6 +232,397 @@ var Store = class {
235
232
  }
236
233
  }
237
234
  };
235
+ var HonoRequest = class {
236
+ constructor(request, params) {
237
+ this.params = params;
238
+ this.raw = request;
239
+ this.url = request.url;
240
+ this.method = request.method;
241
+ this.path = new URL(request.url).pathname;
242
+ }
243
+ raw;
244
+ url;
245
+ method;
246
+ path;
247
+ header(name) {
248
+ if (name) return this.raw.headers.get(name) ?? void 0;
249
+ const headers = {};
250
+ this.raw.headers.forEach((value, key) => {
251
+ headers[key] = value;
252
+ });
253
+ return headers;
254
+ }
255
+ query(name) {
256
+ return new URL(this.url).searchParams.get(name) ?? void 0;
257
+ }
258
+ queries(name) {
259
+ const values = new URL(this.url).searchParams.getAll(name);
260
+ return values.length > 0 ? values : void 0;
261
+ }
262
+ param(name) {
263
+ if (!name) return { ...this.params };
264
+ return this.params[name] ?? "";
265
+ }
266
+ json() {
267
+ return this.raw.json();
268
+ }
269
+ text() {
270
+ return this.raw.text();
271
+ }
272
+ arrayBuffer() {
273
+ return this.raw.arrayBuffer();
274
+ }
275
+ async parseBody() {
276
+ const contentType = this.header("Content-Type") ?? "";
277
+ if (contentType.includes("multipart/form-data")) {
278
+ return formDataToObject(await this.raw.formData());
279
+ }
280
+ if (contentType.includes("application/x-www-form-urlencoded")) {
281
+ const params = new URLSearchParams(await this.raw.text());
282
+ const out = {};
283
+ for (const [key, value] of params) {
284
+ appendBodyValue(out, key, value);
285
+ }
286
+ return out;
287
+ }
288
+ if (contentType.includes("application/json")) {
289
+ const body = await this.raw.json().catch(() => ({}));
290
+ return body && typeof body === "object" && !Array.isArray(body) ? body : {};
291
+ }
292
+ return {};
293
+ }
294
+ };
295
+ var Context = class {
296
+ constructor(request, params, notFoundHandler) {
297
+ this.notFoundHandler = notFoundHandler;
298
+ this.req = new HonoRequest(request, params);
299
+ }
300
+ req;
301
+ vars = /* @__PURE__ */ new Map();
302
+ responseHeaders = new Headers();
303
+ responseStatus = 200;
304
+ get(key) {
305
+ return this.vars.get(key);
306
+ }
307
+ set(key, value) {
308
+ this.vars.set(key, value);
309
+ }
310
+ header(name, value) {
311
+ this.responseHeaders.set(name, value);
312
+ }
313
+ status(status) {
314
+ this.responseStatus = status;
315
+ }
316
+ json(data, status, headers) {
317
+ return this.response(JSON.stringify(data), status, defaultContentType(headers, "application/json; charset=UTF-8"));
318
+ }
319
+ text(text, status, headers) {
320
+ return this.response(text, status, defaultContentType(headers, "text/plain; charset=UTF-8"));
321
+ }
322
+ html(html, status, headers) {
323
+ return this.response(html, status, defaultContentType(headers, "text/html; charset=UTF-8"));
324
+ }
325
+ body(body, status, headers) {
326
+ return this.response(body, status, headers);
327
+ }
328
+ redirect(location, status = 302) {
329
+ return this.response(null, status, { Location: location });
330
+ }
331
+ notFound() {
332
+ return this.notFoundHandler(this);
333
+ }
334
+ finalize(response) {
335
+ if (!hasHeaders(this.responseHeaders)) return response;
336
+ const headers = new Headers(response.headers);
337
+ this.responseHeaders.forEach((value, key) => {
338
+ headers.set(key, value);
339
+ });
340
+ return new Response(response.body, {
341
+ status: response.status,
342
+ statusText: response.statusText,
343
+ headers
344
+ });
345
+ }
346
+ response(body, status, headers) {
347
+ const merged = new Headers(headers);
348
+ this.responseHeaders.forEach((value, key) => {
349
+ merged.set(key, value);
350
+ });
351
+ return new Response(body, {
352
+ status: status ?? this.responseStatus,
353
+ headers: merged
354
+ });
355
+ }
356
+ };
357
+ var Hono = class {
358
+ middleware = [];
359
+ routes = [];
360
+ errorHandler = (err) => {
361
+ const message = err instanceof Error ? err.message : "Internal Server Error";
362
+ return new Response(message, { status: 500 });
363
+ };
364
+ notFoundHandler = () => new Response("404 Not Found", { status: 404 });
365
+ use(pathOrHandler, ...handlers) {
366
+ if (typeof pathOrHandler === "string") {
367
+ this.middleware.push({ method: "ALL", compiled: compilePath(pathOrHandler), handlers });
368
+ } else {
369
+ this.middleware.push({ method: "ALL", compiled: compilePath("*"), handlers: [pathOrHandler, ...handlers] });
370
+ }
371
+ return this;
372
+ }
373
+ on(method, path, ...handlers) {
374
+ this.routes.push({ method: method.toUpperCase(), compiled: compilePath(path), handlers });
375
+ return this;
376
+ }
377
+ get(path, ...handlers) {
378
+ return this.on("GET", path, ...handlers);
379
+ }
380
+ post(path, ...handlers) {
381
+ return this.on("POST", path, ...handlers);
382
+ }
383
+ put(path, ...handlers) {
384
+ return this.on("PUT", path, ...handlers);
385
+ }
386
+ patch(path, ...handlers) {
387
+ return this.on("PATCH", path, ...handlers);
388
+ }
389
+ delete(path, ...handlers) {
390
+ return this.on("DELETE", path, ...handlers);
391
+ }
392
+ onError(handler) {
393
+ this.errorHandler = handler;
394
+ return this;
395
+ }
396
+ notFound(handler) {
397
+ this.notFoundHandler = handler;
398
+ return this;
399
+ }
400
+ async request(input, init) {
401
+ if (input instanceof Request) return this.fetch(input);
402
+ const url = input.startsWith("/") ? `http://localhost${input}` : input;
403
+ return this.fetch(new Request(url, init));
404
+ }
405
+ fetch = async (request) => {
406
+ const url = new URL(request.url);
407
+ const path = url.pathname;
408
+ const method = request.method.toUpperCase();
409
+ const matched = this.match(method, path);
410
+ const context = new Context(request, matched.params, this.notFoundHandler);
411
+ try {
412
+ const response = await this.dispatch(context, matched.handlers);
413
+ return context.finalize(response ?? await this.notFoundHandler(context));
414
+ } catch (err) {
415
+ return context.finalize(await this.errorHandler(err, context));
416
+ }
417
+ };
418
+ match(method, path) {
419
+ const handlers = [];
420
+ const params = {};
421
+ for (const route2 of this.middleware) {
422
+ const match = matchPath(route2.compiled, path);
423
+ if (!match) continue;
424
+ Object.assign(params, match);
425
+ for (const handler of route2.handlers) {
426
+ handlers.push({ handler, params: match });
427
+ }
428
+ }
429
+ const route = this.routes.find((candidate) => candidate.method === method && matchPath(candidate.compiled, path) != null) ?? (method === "HEAD" ? this.routes.find((candidate) => candidate.method === "GET" && matchPath(candidate.compiled, path) != null) : void 0);
430
+ if (route) {
431
+ const match = matchPath(route.compiled, path) ?? {};
432
+ Object.assign(params, match);
433
+ for (const handler of route.handlers) {
434
+ handlers.push({ handler, params: match });
435
+ }
436
+ }
437
+ return { handlers, params };
438
+ }
439
+ async dispatch(context, handlers) {
440
+ let index = -1;
441
+ const run = async (nextIndex) => {
442
+ if (nextIndex <= index) throw new Error("next() called multiple times");
443
+ index = nextIndex;
444
+ const matched = handlers[nextIndex];
445
+ if (!matched) return void 0;
446
+ const originalParams = context.req.param();
447
+ Object.assign(originalParams, matched.params);
448
+ let nextResponse = void 0;
449
+ let nextCalled = false;
450
+ const next = async () => {
451
+ nextCalled = true;
452
+ nextResponse = await run(nextIndex + 1);
453
+ };
454
+ const response = await matched.handler(context, next);
455
+ if (response instanceof Response) return response;
456
+ if (nextCalled) return nextResponse;
457
+ return response;
458
+ };
459
+ return run(0);
460
+ }
461
+ };
462
+ function cors(options = {}) {
463
+ const origin = options.origin ?? "*";
464
+ const allowMethods = options.allowMethods ?? ["GET", "HEAD", "PUT", "POST", "DELETE", "PATCH", "OPTIONS"];
465
+ return async (c, next) => {
466
+ c.header("Access-Control-Allow-Origin", origin);
467
+ if (options.credentials) c.header("Access-Control-Allow-Credentials", "true");
468
+ if (c.req.method.toUpperCase() === "OPTIONS") {
469
+ c.header("Access-Control-Allow-Methods", allowMethods.join(","));
470
+ const allowHeaders = options.allowHeaders?.join(",") ?? c.req.header("Access-Control-Request-Headers");
471
+ if (allowHeaders) c.header("Access-Control-Allow-Headers", allowHeaders);
472
+ if (options.maxAge != null) c.header("Access-Control-Max-Age", String(options.maxAge));
473
+ return c.body(null, 204);
474
+ }
475
+ await next();
476
+ };
477
+ }
478
+ function serve(options) {
479
+ const port = options.port ?? 3e3;
480
+ const server = createNodeServer(async (req, res) => {
481
+ try {
482
+ const request = nodeRequestToFetchRequest(req);
483
+ const response = await options.fetch(request);
484
+ await writeFetchResponse(res, response, req.method?.toUpperCase() === "HEAD");
485
+ } catch (err) {
486
+ const message = err instanceof Error ? err.message : "Internal Server Error";
487
+ res.statusCode = 500;
488
+ res.setHeader("Content-Type", "text/plain; charset=UTF-8");
489
+ res.end(message);
490
+ }
491
+ });
492
+ server.listen(port, options.hostname);
493
+ return server;
494
+ }
495
+ function compilePath(pattern) {
496
+ if (pattern === "*" || pattern === "/*") {
497
+ return { pattern, regex: /^.*$/, paramNames: [] };
498
+ }
499
+ const paramNames = [];
500
+ let source = "^";
501
+ for (let i = 0; i < pattern.length; i++) {
502
+ const char = pattern[i];
503
+ if (char !== ":") {
504
+ source += escapeRegex(char);
505
+ continue;
506
+ }
507
+ let name = "";
508
+ i++;
509
+ while (i < pattern.length && /[A-Za-z0-9_]/.test(pattern[i])) {
510
+ name += pattern[i];
511
+ i++;
512
+ }
513
+ i--;
514
+ paramNames.push(name);
515
+ if (pattern[i + 1] === "{") {
516
+ const close = pattern.indexOf("}", i + 2);
517
+ if (close < 0) throw new Error(`Invalid route pattern: ${pattern}`);
518
+ const expr = pattern.slice(i + 2, close);
519
+ source += `(${expr})`;
520
+ i = close;
521
+ } else {
522
+ source += "([^/]+)";
523
+ }
524
+ }
525
+ source += "$";
526
+ return { pattern, regex: new RegExp(source), paramNames };
527
+ }
528
+ function matchPath(compiled, path) {
529
+ const match = compiled.regex.exec(path);
530
+ if (!match) return null;
531
+ const params = {};
532
+ for (let i = 0; i < compiled.paramNames.length; i++) {
533
+ params[compiled.paramNames[i]] = decodePathParam(match[i + 1] ?? "");
534
+ }
535
+ return params;
536
+ }
537
+ function decodePathParam(value) {
538
+ try {
539
+ return decodeURIComponent(value);
540
+ } catch {
541
+ return value;
542
+ }
543
+ }
544
+ function escapeRegex(value) {
545
+ return value.replace(/[|\\{}()[\]^$+*?.]/g, "\\$&");
546
+ }
547
+ function hasHeaders(headers) {
548
+ for (const _ of headers) return true;
549
+ return false;
550
+ }
551
+ function defaultContentType(headers, contentType) {
552
+ const out = new Headers(headers);
553
+ if (!out.has("Content-Type")) {
554
+ out.set("Content-Type", contentType);
555
+ }
556
+ return out;
557
+ }
558
+ function formDataToObject(formData) {
559
+ const out = {};
560
+ for (const [key, value] of formData) {
561
+ appendBodyValue(out, key, value);
562
+ }
563
+ return out;
564
+ }
565
+ function appendBodyValue(target, key, value) {
566
+ const existing = target[key];
567
+ if (existing === void 0) {
568
+ target[key] = value;
569
+ } else if (Array.isArray(existing)) {
570
+ existing.push(value);
571
+ } else {
572
+ target[key] = [existing, value];
573
+ }
574
+ }
575
+ function nodeRequestToFetchRequest(req) {
576
+ const host = req.headers.host ?? "localhost";
577
+ const url = new URL(req.url ?? "/", `http://${host}`);
578
+ const headers = new Headers();
579
+ for (const [key, value] of Object.entries(req.headers)) {
580
+ if (value == null) continue;
581
+ if (Array.isArray(value)) {
582
+ for (const item of value) headers.append(key, item);
583
+ } else {
584
+ headers.set(key, value);
585
+ }
586
+ }
587
+ const method = req.method ?? "GET";
588
+ const hasBody = method !== "GET" && method !== "HEAD";
589
+ return new Request(url.toString(), {
590
+ method,
591
+ headers,
592
+ body: hasBody ? req : void 0,
593
+ duplex: "half"
594
+ });
595
+ }
596
+ async function writeFetchResponse(res, response, headOnly) {
597
+ res.statusCode = response.status;
598
+ res.statusMessage = response.statusText;
599
+ const headersWithCookies = response.headers;
600
+ const cookies = headersWithCookies.getSetCookie?.();
601
+ response.headers.forEach((value, key) => {
602
+ if (key.toLowerCase() === "set-cookie" && cookies && cookies.length > 0) return;
603
+ res.setHeader(key, value);
604
+ });
605
+ if (cookies && cookies.length > 0) {
606
+ res.setHeader("Set-Cookie", cookies);
607
+ }
608
+ if (headOnly || !response.body) {
609
+ res.end();
610
+ return;
611
+ }
612
+ const reader = response.body.getReader();
613
+ try {
614
+ while (true) {
615
+ const { done, value } = await reader.read();
616
+ if (done) break;
617
+ if (!res.write(value)) {
618
+ await new Promise((resolve3) => res.once("drain", resolve3));
619
+ }
620
+ }
621
+ res.end();
622
+ } catch (err) {
623
+ res.destroy(err instanceof Error ? err : void 0);
624
+ }
625
+ }
238
626
  var MAX_DELIVERIES = 1e3;
239
627
  var WebhookDispatcher = class {
240
628
  subscriptions = [];
@@ -391,9 +779,7 @@ function authMiddleware(tokens, appKeyResolver, fallbackUser) {
391
779
  if (token.startsWith("eyJ") && appKeyResolver) {
392
780
  try {
393
781
  const [, payloadB64] = token.split(".");
394
- const payload = JSON.parse(
395
- Buffer.from(payloadB64, "base64url").toString()
396
- );
782
+ const payload = JSON.parse(Buffer.from(payloadB64, "base64url").toString());
397
783
  const appId = typeof payload.iss === "string" ? parseInt(payload.iss, 10) : payload.iss;
398
784
  if (typeof appId === "number" && !isNaN(appId)) {
399
785
  const appInfo = appKeyResolver(appId);
@@ -430,6 +816,7 @@ var FONTS = {
430
816
  "geist-sans.woff2": readFileSync(join(__dirname, "fonts", "geist-sans.woff2")),
431
817
  "GeistPixel-Square.woff2": readFileSync(join(__dirname, "fonts", "GeistPixel-Square.woff2"))
432
818
  };
819
+ var FAVICON = readFileSync(join(__dirname, "fonts", "favicon.ico"));
433
820
  function registerFontRoutes(app) {
434
821
  app.get("/_emulate/fonts/:name", (c) => {
435
822
  const name = c.req.param("name");
@@ -443,6 +830,14 @@ function registerFontRoutes(app) {
443
830
  }
444
831
  });
445
832
  });
833
+ app.get("/_emulate/favicon.ico", (c) => {
834
+ return new Response(FAVICON, {
835
+ headers: {
836
+ "Content-Type": "image/x-icon",
837
+ "Cache-Control": "public, max-age=31536000, immutable"
838
+ }
839
+ });
840
+ });
446
841
  }
447
842
  function createServer(plugin, options = {}) {
448
843
  const port = options.port ?? 4e3;
@@ -512,14 +907,27 @@ function createServer(plugin, options = {}) {
512
907
  }
513
908
 
514
909
  // src/registry.ts
515
- var SERVICE_NAME_LIST = ["vercel", "github", "google", "slack", "apple", "microsoft", "okta", "aws", "resend", "stripe", "mongoatlas"];
910
+ var SERVICE_NAME_LIST = [
911
+ "vercel",
912
+ "github",
913
+ "google",
914
+ "slack",
915
+ "apple",
916
+ "microsoft",
917
+ "okta",
918
+ "aws",
919
+ "resend",
920
+ "stripe",
921
+ "mongoatlas",
922
+ "clerk"
923
+ ];
516
924
  var SERVICE_NAMES = SERVICE_NAME_LIST;
517
925
  var SERVICE_REGISTRY = {
518
926
  vercel: {
519
927
  label: "Vercel REST API emulator",
520
928
  endpoints: "projects, deployments, domains, env vars, users, teams, file uploads, protection bypass",
521
929
  async load() {
522
- const mod = await import("./dist-RDFBZ5O6.js");
930
+ const mod = await import("./dist-CXRPM6BK.js");
523
931
  return { plugin: mod.vercelPlugin, seedFromConfig: mod.seedFromConfig };
524
932
  },
525
933
  defaultFallback(cfg) {
@@ -531,12 +939,14 @@ var SERVICE_REGISTRY = {
531
939
  users: [{ username: "developer", name: "Developer", email: "dev@example.com" }],
532
940
  teams: [{ slug: "my-team", name: "My Team" }],
533
941
  projects: [{ name: "my-app", team: "my-team", framework: "nextjs" }],
534
- integrations: [{
535
- client_id: "oac_example_client_id",
536
- client_secret: "example_client_secret",
537
- name: "My Vercel App",
538
- redirect_uris: ["http://localhost:3000/api/auth/callback/vercel"]
539
- }]
942
+ integrations: [
943
+ {
944
+ client_id: "oac_example_client_id",
945
+ client_secret: "example_client_secret",
946
+ name: "My Vercel App",
947
+ redirect_uris: ["http://localhost:3000/api/auth/callback/vercel"]
948
+ }
949
+ ]
540
950
  }
541
951
  }
542
952
  },
@@ -544,7 +954,7 @@ var SERVICE_REGISTRY = {
544
954
  label: "GitHub REST API emulator",
545
955
  endpoints: "users, repos, issues, PRs, comments, reviews, labels, milestones, branches, git data, orgs, teams, releases, webhooks, search, actions, checks, rate limit",
546
956
  async load() {
547
- const mod = await import("./dist-H6JYGQM4.js");
957
+ const mod = await import("./dist-PO4CL5SJ.js");
548
958
  return {
549
959
  plugin: mod.githubPlugin,
550
960
  seedFromConfig: mod.seedFromConfig,
@@ -568,25 +978,42 @@ var SERVICE_REGISTRY = {
568
978
  },
569
979
  initConfig: {
570
980
  github: {
571
- users: [{
572
- login: "octocat",
573
- name: "The Octocat",
574
- email: "octocat@github.com",
575
- bio: "I am the Octocat",
576
- company: "GitHub",
577
- location: "San Francisco"
578
- }],
981
+ users: [
982
+ {
983
+ login: "octocat",
984
+ name: "The Octocat",
985
+ email: "octocat@github.com",
986
+ bio: "I am the Octocat",
987
+ company: "GitHub",
988
+ location: "San Francisco"
989
+ }
990
+ ],
579
991
  orgs: [{ login: "my-org", name: "My Organization", description: "A test organization" }],
580
992
  repos: [
581
- { owner: "octocat", name: "hello-world", description: "My first repository", language: "JavaScript", topics: ["hello", "world"], auto_init: true },
582
- { owner: "my-org", name: "org-repo", description: "An organization repository", language: "TypeScript", auto_init: true }
993
+ {
994
+ owner: "octocat",
995
+ name: "hello-world",
996
+ description: "My first repository",
997
+ language: "JavaScript",
998
+ topics: ["hello", "world"],
999
+ auto_init: true
1000
+ },
1001
+ {
1002
+ owner: "my-org",
1003
+ name: "org-repo",
1004
+ description: "An organization repository",
1005
+ language: "TypeScript",
1006
+ auto_init: true
1007
+ }
583
1008
  ],
584
- oauth_apps: [{
585
- client_id: "Iv1.example_client_id",
586
- client_secret: "example_client_secret",
587
- name: "My App",
588
- redirect_uris: ["http://localhost:3000/api/auth/callback/github"]
589
- }]
1009
+ oauth_apps: [
1010
+ {
1011
+ client_id: "Iv1.example_client_id",
1012
+ client_secret: "example_client_secret",
1013
+ name: "My App",
1014
+ redirect_uris: ["http://localhost:3000/api/auth/callback/github"]
1015
+ }
1016
+ ]
590
1017
  }
591
1018
  }
592
1019
  },
@@ -594,7 +1021,7 @@ var SERVICE_REGISTRY = {
594
1021
  label: "Google OAuth 2.0 / OpenID Connect + Gmail, Calendar, and Drive emulator",
595
1022
  endpoints: "OAuth authorize, token exchange, userinfo, OIDC discovery, token revocation, Gmail messages/drafts/threads/labels/history/settings, Calendar lists/events/freebusy, Drive files/uploads",
596
1023
  async load() {
597
- const mod = await import("./dist-6EW7SSOZ.js");
1024
+ const mod = await import("./dist-ZY5SZSJ2.js");
598
1025
  return { plugin: mod.googlePlugin, seedFromConfig: mod.seedFromConfig };
599
1026
  },
600
1027
  defaultFallback(cfg) {
@@ -603,59 +1030,153 @@ var SERVICE_REGISTRY = {
603
1030
  },
604
1031
  initConfig: {
605
1032
  google: {
606
- users: [{ email: "testuser@example.com", name: "Test User", picture: "https://lh3.googleusercontent.com/a/default-user", email_verified: true }],
607
- oauth_clients: [{
608
- client_id: "example-client-id.apps.googleusercontent.com",
609
- client_secret: "GOCSPX-example_secret",
610
- name: "Code App (Google)",
611
- redirect_uris: ["http://localhost:3000/api/auth/callback/google"]
612
- }],
613
- labels: [{ id: "Label_ops", user_email: "testuser@example.com", name: "Ops/Review", color_background: "#DDEEFF", color_text: "#111111" }],
614
- messages: [{
615
- id: "msg_welcome",
616
- user_email: "testuser@example.com",
617
- from: "welcome@example.com",
618
- to: "testuser@example.com",
619
- subject: "Welcome to the Gmail emulator",
620
- body_text: "You can now test Gmail, Calendar, and Drive flows locally.",
621
- label_ids: ["INBOX", "UNREAD", "CATEGORY_UPDATES"],
622
- date: "2025-01-04T10:00:00.000Z"
623
- }],
624
- calendars: [{ id: "primary", user_email: "testuser@example.com", summary: "testuser@example.com", primary: true, selected: true, time_zone: "UTC" }],
625
- calendar_events: [{
626
- id: "evt_kickoff",
627
- user_email: "testuser@example.com",
628
- calendar_id: "primary",
629
- summary: "Project Kickoff",
630
- start_date_time: "2025-01-10T09:00:00.000Z",
631
- end_date_time: "2025-01-10T09:30:00.000Z"
632
- }],
633
- drive_items: [{ id: "drv_docs", user_email: "testuser@example.com", name: "Docs", mime_type: "application/vnd.google-apps.folder", parent_ids: ["root"] }]
1033
+ users: [
1034
+ {
1035
+ email: "testuser@example.com",
1036
+ name: "Test User",
1037
+ picture: "https://lh3.googleusercontent.com/a/default-user",
1038
+ email_verified: true
1039
+ }
1040
+ ],
1041
+ oauth_clients: [
1042
+ {
1043
+ client_id: "example-client-id.apps.googleusercontent.com",
1044
+ client_secret: "GOCSPX-example_secret",
1045
+ name: "Code App (Google)",
1046
+ redirect_uris: ["http://localhost:3000/api/auth/callback/google"]
1047
+ }
1048
+ ],
1049
+ labels: [
1050
+ {
1051
+ id: "Label_ops",
1052
+ user_email: "testuser@example.com",
1053
+ name: "Ops/Review",
1054
+ color_background: "#DDEEFF",
1055
+ color_text: "#111111"
1056
+ }
1057
+ ],
1058
+ messages: [
1059
+ {
1060
+ id: "msg_welcome",
1061
+ user_email: "testuser@example.com",
1062
+ from: "welcome@example.com",
1063
+ to: "testuser@example.com",
1064
+ subject: "Welcome to the Gmail emulator",
1065
+ body_text: "You can now test Gmail, Calendar, and Drive flows locally.",
1066
+ label_ids: ["INBOX", "UNREAD", "CATEGORY_UPDATES"],
1067
+ date: "2025-01-04T10:00:00.000Z"
1068
+ }
1069
+ ],
1070
+ calendars: [
1071
+ {
1072
+ id: "primary",
1073
+ user_email: "testuser@example.com",
1074
+ summary: "testuser@example.com",
1075
+ primary: true,
1076
+ selected: true,
1077
+ time_zone: "UTC"
1078
+ }
1079
+ ],
1080
+ calendar_events: [
1081
+ {
1082
+ id: "evt_kickoff",
1083
+ user_email: "testuser@example.com",
1084
+ calendar_id: "primary",
1085
+ summary: "Project Kickoff",
1086
+ start_date_time: "2025-01-10T09:00:00.000Z",
1087
+ end_date_time: "2025-01-10T09:30:00.000Z"
1088
+ }
1089
+ ],
1090
+ drive_items: [
1091
+ {
1092
+ id: "drv_docs",
1093
+ user_email: "testuser@example.com",
1094
+ name: "Docs",
1095
+ mime_type: "application/vnd.google-apps.folder",
1096
+ parent_ids: ["root"]
1097
+ }
1098
+ ]
634
1099
  }
635
1100
  }
636
1101
  },
637
1102
  slack: {
638
1103
  label: "Slack API emulator",
639
- endpoints: "auth, chat, conversations, users, reactions, team, OAuth, incoming webhooks",
1104
+ endpoints: "auth, chat, conversations, users, profiles, presence, files, pins, bookmarks, views, reactions, team, OAuth, incoming webhooks, inspector",
640
1105
  async load() {
641
- const mod = await import("./dist-G7WQPZ3Y.js");
1106
+ const mod = await import("./dist-OGSAVJ25.js");
642
1107
  return { plugin: mod.slackPlugin, seedFromConfig: mod.seedFromConfig };
643
1108
  },
644
1109
  defaultFallback() {
645
- return { login: "U000000001", id: 1, scopes: ["chat:write", "channels:read", "users:read", "reactions:write"] };
1110
+ return {
1111
+ login: "U000000001",
1112
+ id: 1,
1113
+ scopes: []
1114
+ };
646
1115
  },
647
1116
  initConfig: {
648
1117
  slack: {
649
1118
  team: { name: "My Workspace", domain: "my-workspace" },
650
- users: [{ name: "developer", real_name: "Developer", email: "dev@example.com" }],
651
- channels: [{ name: "general", topic: "General discussion" }, { name: "random", topic: "Random stuff" }],
1119
+ users: [
1120
+ {
1121
+ name: "developer",
1122
+ real_name: "Developer",
1123
+ email: "dev@example.com",
1124
+ profile: {
1125
+ title: "Local Developer",
1126
+ status_text: "Testing locally",
1127
+ status_emoji: ":computer:"
1128
+ },
1129
+ presence: "active"
1130
+ }
1131
+ ],
1132
+ channels: [
1133
+ { name: "general", topic: "General discussion" },
1134
+ { name: "random", topic: "Random stuff" }
1135
+ ],
652
1136
  bots: [{ name: "my-bot" }],
653
- oauth_apps: [{
654
- client_id: "12345.67890",
655
- client_secret: "example_client_secret",
656
- name: "My Slack App",
657
- redirect_uris: ["http://localhost:3000/api/auth/callback/slack"]
658
- }]
1137
+ oauth_apps: [
1138
+ {
1139
+ client_id: "12345.67890",
1140
+ client_secret: "example_client_secret",
1141
+ app_id: "A000000001",
1142
+ name: "My Slack App",
1143
+ redirect_uris: ["http://localhost:3000/api/auth/callback/slack"],
1144
+ scopes: [
1145
+ "chat:write",
1146
+ "channels:read",
1147
+ "channels:history",
1148
+ "channels:join",
1149
+ "channels:manage",
1150
+ "channels:write",
1151
+ "groups:read",
1152
+ "groups:history",
1153
+ "groups:write",
1154
+ "im:read",
1155
+ "im:history",
1156
+ "im:write",
1157
+ "mpim:read",
1158
+ "mpim:history",
1159
+ "mpim:write",
1160
+ "users:read",
1161
+ "users:read.email",
1162
+ "users.profile:read",
1163
+ "users.profile:write",
1164
+ "users:write",
1165
+ "files:read",
1166
+ "files:write",
1167
+ "pins:read",
1168
+ "pins:write",
1169
+ "bookmarks:read",
1170
+ "bookmarks:write",
1171
+ "reactions:read",
1172
+ "reactions:write",
1173
+ "team:read"
1174
+ ],
1175
+ user_scopes: ["users:read", "users.profile:read"],
1176
+ bot_name: "my-bot"
1177
+ }
1178
+ ],
1179
+ strict_scopes: false
659
1180
  }
660
1181
  }
661
1182
  },
@@ -663,7 +1184,7 @@ var SERVICE_REGISTRY = {
663
1184
  label: "Apple Sign In / OAuth emulator",
664
1185
  endpoints: "OAuth authorize, token exchange, JWKS",
665
1186
  async load() {
666
- const mod = await import("./dist-6JFNJPUU.js");
1187
+ const mod = await import("./dist-WACHAAVU.js");
667
1188
  return { plugin: mod.applePlugin, seedFromConfig: mod.seedFromConfig };
668
1189
  },
669
1190
  defaultFallback(cfg) {
@@ -673,12 +1194,14 @@ var SERVICE_REGISTRY = {
673
1194
  initConfig: {
674
1195
  apple: {
675
1196
  users: [{ email: "testuser@icloud.com", name: "Test User" }],
676
- oauth_clients: [{
677
- client_id: "com.example.app",
678
- team_id: "TEAM001",
679
- name: "My Apple App",
680
- redirect_uris: ["http://localhost:3000/api/auth/callback/apple"]
681
- }]
1197
+ oauth_clients: [
1198
+ {
1199
+ client_id: "com.example.app",
1200
+ team_id: "TEAM001",
1201
+ name: "My Apple App",
1202
+ redirect_uris: ["http://localhost:3000/api/auth/callback/apple"]
1203
+ }
1204
+ ]
682
1205
  }
683
1206
  }
684
1207
  },
@@ -686,7 +1209,7 @@ var SERVICE_REGISTRY = {
686
1209
  label: "Microsoft Entra ID OAuth 2.0 / OpenID Connect emulator",
687
1210
  endpoints: "OAuth authorize, token exchange, userinfo, OIDC discovery, Graph /me, logout, token revocation",
688
1211
  async load() {
689
- const mod = await import("./dist-RMK3BS5M.js");
1212
+ const mod = await import("./dist-IFULY5LE.js");
690
1213
  return { plugin: mod.microsoftPlugin, seedFromConfig: mod.seedFromConfig };
691
1214
  },
692
1215
  defaultFallback(cfg) {
@@ -696,12 +1219,14 @@ var SERVICE_REGISTRY = {
696
1219
  initConfig: {
697
1220
  microsoft: {
698
1221
  users: [{ email: "testuser@outlook.com", name: "Test User" }],
699
- oauth_clients: [{
700
- client_id: "example-client-id",
701
- client_secret: "example-client-secret",
702
- name: "My Microsoft App",
703
- redirect_uris: ["http://localhost:3000/api/auth/callback/microsoft-entra-id"]
704
- }]
1222
+ oauth_clients: [
1223
+ {
1224
+ client_id: "example-client-id",
1225
+ client_secret: "example-client-secret",
1226
+ name: "My Microsoft App",
1227
+ redirect_uris: ["http://localhost:3000/api/auth/callback/microsoft-entra-id"]
1228
+ }
1229
+ ]
705
1230
  }
706
1231
  }
707
1232
  },
@@ -709,7 +1234,7 @@ var SERVICE_REGISTRY = {
709
1234
  label: "Okta OAuth 2.0 / OpenID Connect + management API emulator",
710
1235
  endpoints: "OIDC discovery, JWKS, OAuth authorize/token/userinfo/introspect/revoke/logout, users, groups, apps, authorization servers",
711
1236
  async load() {
712
- const mod = await import("./dist-OTJZRQ3Q.js");
1237
+ const mod = await import("./dist-XWWZVLQQ.js");
713
1238
  return { plugin: mod.oktaPlugin, seedFromConfig: mod.seedFromConfig };
714
1239
  },
715
1240
  defaultFallback(cfg) {
@@ -721,13 +1246,15 @@ var SERVICE_REGISTRY = {
721
1246
  users: [{ login: "testuser@okta.local", email: "testuser@okta.local", first_name: "Test", last_name: "User" }],
722
1247
  groups: [{ name: "Everyone", description: "All users", type: "BUILT_IN", okta_id: "00g_everyone" }],
723
1248
  authorization_servers: [{ id: "default", name: "default", audiences: ["api://default"] }],
724
- oauth_clients: [{
725
- client_id: "okta-test-client",
726
- client_secret: "okta-test-secret",
727
- name: "Sample OIDC Client",
728
- redirect_uris: ["http://localhost:3000/callback"],
729
- auth_server_id: "default"
730
- }]
1249
+ oauth_clients: [
1250
+ {
1251
+ client_id: "okta-test-client",
1252
+ client_secret: "okta-test-secret",
1253
+ name: "Sample OIDC Client",
1254
+ redirect_uris: ["http://localhost:3000/callback"],
1255
+ auth_server_id: "default"
1256
+ }
1257
+ ]
731
1258
  }
732
1259
  }
733
1260
  },
@@ -735,7 +1262,7 @@ var SERVICE_REGISTRY = {
735
1262
  label: "AWS cloud service emulator",
736
1263
  endpoints: "S3 (buckets, objects), SQS (queues, messages), IAM (users, roles, access keys), STS (assume role, caller identity)",
737
1264
  async load() {
738
- const mod = await import("./dist-VVXVP5EZ.js");
1265
+ const mod = await import("./dist-DSJSF3GY.js");
739
1266
  return { plugin: mod.awsPlugin, seedFromConfig: mod.seedFromConfig };
740
1267
  },
741
1268
  defaultFallback() {
@@ -757,7 +1284,7 @@ var SERVICE_REGISTRY = {
757
1284
  label: "Resend email API emulator",
758
1285
  endpoints: "emails, domains, contacts, API keys, inbox UI",
759
1286
  async load() {
760
- const mod = await import("./dist-QMOJM6DV.js");
1287
+ const mod = await import("./dist-R3TNKUIE.js");
761
1288
  return { plugin: mod.resendPlugin, seedFromConfig: mod.seedFromConfig };
762
1289
  },
763
1290
  defaultFallback() {
@@ -772,9 +1299,9 @@ var SERVICE_REGISTRY = {
772
1299
  },
773
1300
  stripe: {
774
1301
  label: "Stripe payments emulator",
775
- endpoints: "customers, payment intents, charges, products, prices, checkout sessions, webhooks",
1302
+ endpoints: "customers, payment methods, customer sessions, payment intents, charges, products, prices, checkout sessions, webhooks",
776
1303
  async load() {
777
- const mod = await import("./dist-YOVM5HEY.js");
1304
+ const mod = await import("./dist-NJJLJT2N.js");
778
1305
  return { plugin: mod.stripePlugin, seedFromConfig: mod.seedFromConfig };
779
1306
  },
780
1307
  defaultFallback() {
@@ -792,7 +1319,7 @@ var SERVICE_REGISTRY = {
792
1319
  label: "MongoDB Atlas service emulator",
793
1320
  endpoints: "Atlas Admin API v2 (projects, clusters, database users, databases, collections), Atlas Data API v1 (findOne, find, insertOne, insertMany, updateOne, updateMany, deleteOne, deleteMany, aggregate)",
794
1321
  async load() {
795
- const mod = await import("./dist-B674PYKV.js");
1322
+ const mod = await import("./dist-2ZZGNPJI.js");
796
1323
  return { plugin: mod.mongoatlasPlugin, seedFromConfig: mod.seedFromConfig };
797
1324
  },
798
1325
  defaultFallback() {
@@ -806,15 +1333,54 @@ var SERVICE_REGISTRY = {
806
1333
  databases: [{ cluster: "Cluster0", name: "test", collections: ["items"] }]
807
1334
  }
808
1335
  }
1336
+ },
1337
+ clerk: {
1338
+ label: "Clerk authentication and user management emulator",
1339
+ endpoints: "OIDC discovery, JWKS, OAuth authorize/token/userinfo, users, email addresses, organizations, memberships, invitations, sessions",
1340
+ async load() {
1341
+ const mod = await import("./dist-IRUBHCZU.js");
1342
+ return { plugin: mod.clerkPlugin, seedFromConfig: mod.seedFromConfig };
1343
+ },
1344
+ defaultFallback(cfg) {
1345
+ const firstEmail = cfg?.users?.[0]?.email_addresses?.[0] ?? "test@example.com";
1346
+ return { login: firstEmail, id: 1, scopes: [] };
1347
+ },
1348
+ initConfig: {
1349
+ clerk: {
1350
+ users: [
1351
+ {
1352
+ first_name: "Test",
1353
+ last_name: "User",
1354
+ email_addresses: ["test@example.com"],
1355
+ password: "clerk_test_password"
1356
+ }
1357
+ ],
1358
+ organizations: [
1359
+ {
1360
+ name: "My Company",
1361
+ slug: "my-company",
1362
+ members: [{ email: "test@example.com", role: "admin" }]
1363
+ }
1364
+ ],
1365
+ oauth_applications: [
1366
+ {
1367
+ client_id: "clerk_emulate_client",
1368
+ client_secret: "clerk_emulate_secret",
1369
+ name: "Emulate App",
1370
+ redirect_uris: ["http://localhost:3000/api/auth/callback/clerk"]
1371
+ }
1372
+ ]
1373
+ }
1374
+ }
809
1375
  }
810
1376
  };
811
1377
  var DEFAULT_TOKENS = {
812
1378
  tokens: {
813
- "test_token_admin": {
1379
+ test_token_admin: {
814
1380
  login: "admin",
815
1381
  scopes: ["repo", "user", "admin:org", "admin:repo_hook"]
816
1382
  },
817
- "test_token_user1": {
1383
+ test_token_user1: {
818
1384
  login: "octocat",
819
1385
  scopes: ["repo", "user"]
820
1386
  }
@@ -822,12 +1388,110 @@ var DEFAULT_TOKENS = {
822
1388
  };
823
1389
 
824
1390
  // src/commands/start.ts
825
- import { serve } from "@hono/node-server";
826
1391
  import { readFileSync as readFileSync2, existsSync } from "fs";
827
1392
  import { resolve } from "path";
828
1393
  import { parse as parseYaml } from "yaml";
829
1394
  import pc from "picocolors";
830
- var pkg = { version: "0.4.1" };
1395
+
1396
+ // src/portless.ts
1397
+ import { execSync, spawnSync } from "child_process";
1398
+ import { createInterface } from "readline";
1399
+ function isInteractive() {
1400
+ return Boolean(process.stdin.isTTY) && !process.env.CI;
1401
+ }
1402
+ function hasPortless() {
1403
+ const result = spawnSync("portless", ["--version"], { stdio: "ignore" });
1404
+ return result.status === 0;
1405
+ }
1406
+ function promptYesNo(question) {
1407
+ return new Promise((resolve3) => {
1408
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
1409
+ rl.question(question, (answer) => {
1410
+ rl.close();
1411
+ const normalized = answer.trim().toLowerCase();
1412
+ resolve3(normalized === "" || normalized === "y" || normalized === "yes");
1413
+ });
1414
+ });
1415
+ }
1416
+ function isProxyRunning() {
1417
+ const result = spawnSync("portless", ["list"], { stdio: "ignore" });
1418
+ return result.status === 0;
1419
+ }
1420
+ async function ensurePortless() {
1421
+ if (!hasPortless()) {
1422
+ if (!isInteractive()) {
1423
+ console.error("portless is required but not installed. Run: npm i -g portless");
1424
+ process.exit(1);
1425
+ }
1426
+ const yes = await promptYesNo("portless is not installed. Install it now? (npm i -g portless) [Y/n] ");
1427
+ if (!yes) {
1428
+ console.error("Cannot continue without portless.");
1429
+ process.exit(1);
1430
+ }
1431
+ try {
1432
+ execSync("npm i -g portless", { stdio: "inherit" });
1433
+ } catch {
1434
+ console.error("Failed to install portless.");
1435
+ process.exit(1);
1436
+ }
1437
+ if (!hasPortless()) {
1438
+ console.error("portless was installed but could not be found on PATH.");
1439
+ process.exit(1);
1440
+ }
1441
+ }
1442
+ if (!isProxyRunning()) {
1443
+ console.error("portless proxy is not running. Start it with: portless proxy start");
1444
+ process.exit(1);
1445
+ }
1446
+ }
1447
+ function registerAliases(aliases) {
1448
+ const registered = [];
1449
+ for (const { name, port } of aliases) {
1450
+ const result = spawnSync("portless", ["alias", name, String(port), "--force"], {
1451
+ stdio: "inherit"
1452
+ });
1453
+ if (result.status !== 0) {
1454
+ if (registered.length > 0) {
1455
+ removeAliases(registered);
1456
+ }
1457
+ throw new Error(`Failed to register portless alias: ${name} -> ${port}`);
1458
+ }
1459
+ registered.push({ name, port });
1460
+ }
1461
+ }
1462
+ function removeAliases(aliases) {
1463
+ for (const { name } of aliases) {
1464
+ const result = spawnSync("portless", ["alias", "--remove", name], { stdio: "ignore" });
1465
+ if (result.status !== 0) {
1466
+ console.error(`Warning: failed to remove portless alias: ${name}`);
1467
+ }
1468
+ }
1469
+ }
1470
+ function portlessBaseUrl(serviceName) {
1471
+ return `https://${serviceName}.emulate.localhost`;
1472
+ }
1473
+
1474
+ // src/base-url.ts
1475
+ function resolveBaseUrl(opts) {
1476
+ if (opts.seedBaseUrl) {
1477
+ return opts.seedBaseUrl.replace(/\{service\}/g, opts.service);
1478
+ }
1479
+ if (opts.baseUrl) {
1480
+ return opts.baseUrl.replace(/\{service\}/g, opts.service);
1481
+ }
1482
+ const envBaseUrl = process.env.EMULATE_BASE_URL;
1483
+ if (envBaseUrl) {
1484
+ return envBaseUrl.replace(/\{service\}/g, opts.service);
1485
+ }
1486
+ const portlessUrl = process.env.PORTLESS_URL;
1487
+ if (portlessUrl) {
1488
+ return portlessUrl.replace(/\{service\}/g, opts.service);
1489
+ }
1490
+ return `http://localhost:${opts.port}`;
1491
+ }
1492
+
1493
+ // src/commands/start.ts
1494
+ var pkg = { version: "0.6.0" };
831
1495
  function loadSeedConfig(seedPath) {
832
1496
  if (seedPath) {
833
1497
  const fullPath = resolve(seedPath);
@@ -869,10 +1533,14 @@ function loadSeedConfig(seedPath) {
869
1533
  }
870
1534
  function inferServicesFromConfig(config) {
871
1535
  const found = SERVICE_NAMES.filter((k) => k in config);
872
- return found.length > 0 ? found : null;
1536
+ return found.length > 0 ? [...found] : null;
873
1537
  }
874
1538
  async function startCommand(options) {
875
1539
  const { port: basePort } = options;
1540
+ if (options.portless && options.baseUrl) {
1541
+ console.error("--portless and --base-url are mutually exclusive.");
1542
+ process.exit(1);
1543
+ }
876
1544
  const loaded = loadSeedConfig(options.seed);
877
1545
  const seedConfig = loaded?.config ?? null;
878
1546
  const configSource = loaded?.source ?? null;
@@ -880,9 +1548,9 @@ async function startCommand(options) {
880
1548
  if (options.service) {
881
1549
  services = options.service.split(",").map((s) => s.trim());
882
1550
  } else if (seedConfig) {
883
- services = inferServicesFromConfig(seedConfig) ?? SERVICE_NAMES;
1551
+ services = inferServicesFromConfig(seedConfig) ?? [...SERVICE_NAMES];
884
1552
  } else {
885
- services = SERVICE_NAMES;
1553
+ services = [...SERVICE_NAMES];
886
1554
  }
887
1555
  for (const svc of services) {
888
1556
  if (!SERVICE_REGISTRY[svc]) {
@@ -899,26 +1567,48 @@ async function startCommand(options) {
899
1567
  } else {
900
1568
  tokens["test_token_admin"] = { login: "admin", id: 2, scopes: ["repo", "user", "admin:org", "admin:repo_hook"] };
901
1569
  }
902
- const serviceUrls = [];
903
- const stores = [];
904
- const httpServers = [];
1570
+ if (options.portless) {
1571
+ await ensurePortless();
1572
+ }
1573
+ const portlessAliases = [];
1574
+ const prepared = [];
905
1575
  for (let i = 0; i < services.length; i++) {
906
1576
  const svc = services[i];
907
1577
  const entry = SERVICE_REGISTRY[svc];
908
1578
  const loadedSvc = await entry.load();
909
1579
  const svcSeedConfig = seedConfig?.[svc];
910
1580
  const port = svcSeedConfig?.port ?? basePort + i;
911
- const baseUrl = `http://localhost:${port}`;
1581
+ if (options.portless) {
1582
+ portlessAliases.push({ name: `${svc}.emulate`, port });
1583
+ }
1584
+ const seedBaseUrl = typeof svcSeedConfig?.baseUrl === "string" && svcSeedConfig.baseUrl.length > 0 ? svcSeedConfig.baseUrl : void 0;
1585
+ const effectiveBaseUrl = options.portless ? portlessBaseUrl(svc) : options.baseUrl;
1586
+ const baseUrl = resolveBaseUrl({ service: svc, port, baseUrl: effectiveBaseUrl, seedBaseUrl });
1587
+ prepared.push({ svc, entry, loadedSvc, svcSeedConfig, port, baseUrl });
1588
+ }
1589
+ if (portlessAliases.length > 0) {
1590
+ registerAliases(portlessAliases);
1591
+ }
1592
+ const serviceUrls = [];
1593
+ const stores = [];
1594
+ const httpServers = [];
1595
+ for (const { svc, entry, loadedSvc, svcSeedConfig, port, baseUrl } of prepared) {
912
1596
  serviceUrls.push({ name: svc, url: baseUrl });
913
1597
  let cachedResolver;
914
1598
  const appKeyResolver = loadedSvc.createAppKeyResolver ? (appId) => cachedResolver(appId) : void 0;
915
1599
  const fallbackUser = entry.defaultFallback(svcSeedConfig);
916
- const { app, store } = createServer(loadedSvc.plugin, { port, baseUrl, tokens, appKeyResolver, fallbackUser });
1600
+ const { app, store, webhooks } = createServer(loadedSvc.plugin, {
1601
+ port,
1602
+ baseUrl,
1603
+ tokens,
1604
+ appKeyResolver,
1605
+ fallbackUser
1606
+ });
917
1607
  cachedResolver = loadedSvc.createAppKeyResolver?.(store);
918
1608
  stores.push(store);
919
1609
  loadedSvc.plugin.seed?.(store, baseUrl);
920
1610
  if (svcSeedConfig && loadedSvc.seedFromConfig) {
921
- loadedSvc.seedFromConfig(store, baseUrl, svcSeedConfig);
1611
+ loadedSvc.seedFromConfig(store, baseUrl, svcSeedConfig, webhooks);
922
1612
  }
923
1613
  const httpServer = serve({ fetch: app.fetch, port });
924
1614
  httpServers.push(httpServer);
@@ -927,6 +1617,9 @@ async function startCommand(options) {
927
1617
  const shutdown = () => {
928
1618
  console.log(`
929
1619
  ${pc.dim("Shutting down...")}`);
1620
+ if (portlessAliases.length > 0) {
1621
+ removeAliases(portlessAliases);
1622
+ }
930
1623
  for (const store of stores) {
931
1624
  store.reset();
932
1625
  }
@@ -959,7 +1652,7 @@ function printBanner(services, tokens, configSource) {
959
1652
  if (configSource) {
960
1653
  lines.push(` ${pc.dim("Config:")} ${configSource}`);
961
1654
  } else {
962
- lines.push(` ${pc.dim("Config:")} defaults ${pc.dim("(run")} emulate init ${pc.dim("to customize)")}`);
1655
+ lines.push(` ${pc.dim("Config:")} defaults ${pc.dim("(run")} npx emulate init ${pc.dim("to customize)")}`);
963
1656
  }
964
1657
  lines.push("");
965
1658
  console.log(lines.join("\n"));
@@ -994,7 +1687,7 @@ function initCommand(options) {
994
1687
  writeFileSync(fullPath, content, "utf-8");
995
1688
  console.log(`Created ${filename}`);
996
1689
  console.log(`
997
- Run 'emulate' to start the emulator.`);
1690
+ Run 'npx emulate' to start the emulator.`);
998
1691
  }
999
1692
 
1000
1693
  // src/commands/list.ts
@@ -1008,11 +1701,11 @@ function listCommand() {
1008
1701
  }
1009
1702
 
1010
1703
  // src/index.ts
1011
- var pkg2 = { version: "0.4.1" };
1704
+ var pkg2 = { version: "0.6.0" };
1012
1705
  var defaultPort = process.env.EMULATE_PORT ?? process.env.PORT ?? "4000";
1013
1706
  var program = new Command();
1014
1707
  program.name("emulate").description("Local drop-in replacement services for CI and no-network sandboxes").version(pkg2.version);
1015
- 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").action(async (opts) => {
1708
+ 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)").action(async (opts) => {
1016
1709
  const port = parseInt(opts.port, 10);
1017
1710
  if (Number.isNaN(port) || port < 1 || port > 65535) {
1018
1711
  console.error(`Invalid port: ${opts.port}`);
@@ -1021,7 +1714,9 @@ program.command("start", { isDefault: true }).description("Start the emulator se
1021
1714
  await startCommand({
1022
1715
  port,
1023
1716
  service: opts.service,
1024
- seed: opts.seed
1717
+ seed: opts.seed,
1718
+ baseUrl: opts.baseUrl,
1719
+ portless: opts.portless
1025
1720
  });
1026
1721
  });
1027
1722
  program.command("init").description("Generate a starter config file").option("-s, --service <service>", "Service to generate config for", "all").action((opts) => {