wasper-cli 0.3.1 → 0.3.3

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 (4) hide show
  1. package/README.md +77 -83
  2. package/dist/cli.js +1845 -834
  3. package/dist/index.js +90 -31
  4. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -2786,6 +2786,11 @@ function parseSpecText(text, url, name) {
2786
2786
  const info = doc.info ?? {};
2787
2787
  const servers = doc.servers ?? [];
2788
2788
  let baseUrl = servers[0]?.url ?? "";
2789
+ if (!baseUrl && doc.swagger && doc.host) {
2790
+ const scheme = doc.schemes?.[0] ?? "https";
2791
+ const basePath = typeof doc.basePath === "string" ? doc.basePath.replace(/\/$/, "") : "";
2792
+ baseUrl = `${scheme}://${doc.host}${basePath}`;
2793
+ }
2789
2794
  if (!baseUrl && url) {
2790
2795
  try {
2791
2796
  baseUrl = new URL(url).origin;
@@ -3607,7 +3612,7 @@ var package_default;
3607
3612
  var init_package = __esm(() => {
3608
3613
  package_default = {
3609
3614
  name: "wasper-cli",
3610
- version: "0.3.1",
3615
+ version: "0.3.3",
3611
3616
  description: "Host an MCP server + API proxy from any OpenAPI spec. Like Drizzle Studio, but for APIs.",
3612
3617
  type: "module",
3613
3618
  homepage: "https://wasper.site",
@@ -5316,6 +5321,7 @@ async function handleSpecUpload(req) {
5316
5321
  try {
5317
5322
  const state = loadSpecFromText(content, filename);
5318
5323
  const suggestedVars = extractSuggestedVars(content, state.spec.baseUrl);
5324
+ logBus.broadcastServerEvent({ kind: "spec_changed" });
5319
5325
  return json({
5320
5326
  ok: true,
5321
5327
  spec: { title: state.spec.title, version: state.spec.version, baseUrl: state.spec.baseUrl },
@@ -5338,6 +5344,7 @@ async function handleSpecReloadUrl(req) {
5338
5344
  try {
5339
5345
  const state = await loadSpec(body.url);
5340
5346
  const suggestedVars = extractSuggestedVars(state.spec.raw, state.spec.baseUrl);
5347
+ logBus.broadcastServerEvent({ kind: "spec_changed" });
5341
5348
  return json({
5342
5349
  ok: true,
5343
5350
  spec: { title: state.spec.title, version: state.spec.version, baseUrl: state.spec.baseUrl },
@@ -5349,7 +5356,8 @@ async function handleSpecReloadUrl(req) {
5349
5356
  }
5350
5357
  }
5351
5358
  function handleGetLogs(searchParams) {
5352
- const limit = Math.min(parseInt(searchParams.get("limit") ?? "500"), 2000);
5359
+ const raw = parseInt(searchParams.get("limit") ?? "500", 10);
5360
+ const limit = Math.min(Number.isFinite(raw) ? raw : 500, 2000);
5353
5361
  return json(dbQueries.getRecentLogs(limit));
5354
5362
  }
5355
5363
  function handleClearLogs() {
@@ -5371,6 +5379,8 @@ async function handleSetAuth(req) {
5371
5379
  return json({ type: body.type, config: body.config });
5372
5380
  }
5373
5381
  async function handleTestAuth() {
5382
+ if (!hasState())
5383
+ return badRequest("No spec loaded");
5374
5384
  const { spec } = getState();
5375
5385
  const authRow = dbQueries.getAuthConfig();
5376
5386
  const authConfig = authRow ? JSON.parse(authRow.config) : { type: "none" };
@@ -5384,6 +5394,8 @@ async function handleTestAuth() {
5384
5394
  }
5385
5395
  }
5386
5396
  function handleGetEndpoints() {
5397
+ if (!hasState())
5398
+ return json([]);
5387
5399
  return json(getState().operations);
5388
5400
  }
5389
5401
  function handleGetSettings() {
@@ -5405,6 +5417,8 @@ async function handleSetSettings(req) {
5405
5417
  return json(body);
5406
5418
  }
5407
5419
  async function executeTool(name, args, cache = new Map) {
5420
+ if (!hasState())
5421
+ return { text: "No spec loaded.", isError: true };
5408
5422
  const { operations, spec } = getState();
5409
5423
  if (name === "search_endpoints") {
5410
5424
  const cacheKey2 = `search:${String(args.query ?? "").toLowerCase()}`;
@@ -6064,10 +6078,11 @@ async function handleExplorerRequest(req) {
6064
6078
  };
6065
6079
  if (timeoutMs > 0)
6066
6080
  fetchOpts.signal = AbortSignal.timeout(timeoutMs);
6081
+ const parsedUrl = new URL(authedUrl);
6067
6082
  let dnsMs = 0;
6068
6083
  let resolvedAddr = "";
6069
6084
  try {
6070
- const u = new URL(authedUrl);
6085
+ const u = parsedUrl;
6071
6086
  const h = u.hostname;
6072
6087
  const defaultPort = u.protocol === "https:" ? 443 : 80;
6073
6088
  const port = u.port ? Number(u.port) : defaultPort;
@@ -6086,7 +6101,7 @@ async function handleExplorerRequest(req) {
6086
6101
  const waitMs = Math.round(performance.now() - fetchStart);
6087
6102
  const resHeaders = Object.fromEntries(res.headers.entries());
6088
6103
  const ct = res.headers.get("content-type") ?? "";
6089
- const u = new URL(authedUrl);
6104
+ const u = parsedUrl;
6090
6105
  const networkInfo = {
6091
6106
  scheme: u.protocol.replace(":", ""),
6092
6107
  host: u.host,
@@ -6581,25 +6596,69 @@ var init_routes = __esm(() => {
6581
6596
  // src/daemon.ts
6582
6597
  import { join as join2 } from "path";
6583
6598
  import { homedir as homedir2 } from "os";
6584
- import { mkdir, readFile, writeFile, unlink } from "fs/promises";
6599
+ import { mkdir, readFile, readdir, writeFile, unlink } from "fs/promises";
6585
6600
  async function ensureDir() {
6586
- await mkdir(DIR, { recursive: true });
6601
+ await mkdir(WASPER_DIR, { recursive: true });
6602
+ }
6603
+ function stateFile(port) {
6604
+ return join2(WASPER_DIR, `server-${port}.json`);
6605
+ }
6606
+ function logFile(port) {
6607
+ return join2(WASPER_DIR, `server-${port}.log`);
6587
6608
  }
6588
6609
  async function writeDaemonState(s) {
6589
6610
  await ensureDir();
6590
- await writeFile(STATE_FILE, JSON.stringify(s, null, 2), "utf-8");
6611
+ await writeFile(stateFile(s.port), JSON.stringify(s, null, 2), "utf-8");
6591
6612
  }
6592
- async function readDaemonState() {
6613
+ async function readAllDaemonStates() {
6593
6614
  try {
6594
- const raw = await readFile(STATE_FILE, "utf-8");
6595
- return JSON.parse(raw);
6615
+ const files = await readdir(WASPER_DIR);
6616
+ const states = [];
6617
+ for (const f of files) {
6618
+ if (!f.match(/^server(-\d+)?\.json$/))
6619
+ continue;
6620
+ const filePath = join2(WASPER_DIR, f);
6621
+ try {
6622
+ const raw = await readFile(filePath, "utf-8");
6623
+ const state = JSON.parse(raw);
6624
+ if (isProcessAlive(state.pid)) {
6625
+ states.push(state);
6626
+ } else {
6627
+ await unlink(filePath).catch(() => {});
6628
+ }
6629
+ } catch {}
6630
+ }
6631
+ return states.sort((a, b) => a.port - b.port);
6596
6632
  } catch {
6597
- return null;
6633
+ return [];
6634
+ }
6635
+ }
6636
+ async function readDaemonState(port) {
6637
+ if (port !== undefined) {
6638
+ try {
6639
+ const raw = await readFile(stateFile(port), "utf-8");
6640
+ const state = JSON.parse(raw);
6641
+ return isProcessAlive(state.pid) ? state : null;
6642
+ } catch {
6643
+ return null;
6644
+ }
6598
6645
  }
6646
+ const all = await readAllDaemonStates();
6647
+ if (all.length === 0)
6648
+ return null;
6649
+ if (all.length === 1)
6650
+ return all[0];
6651
+ return all.find((s) => s.port === DEFAULT_PORT) ?? all[0];
6599
6652
  }
6600
- async function clearDaemonState() {
6653
+ async function clearDaemonState(port) {
6601
6654
  try {
6602
- await unlink(STATE_FILE);
6655
+ await unlink(stateFile(port));
6656
+ } catch {}
6657
+ try {
6658
+ const raw = await readFile(join2(WASPER_DIR, "server.json"), "utf-8");
6659
+ const state = JSON.parse(raw);
6660
+ if (state.port === port)
6661
+ await unlink(join2(WASPER_DIR, "server.json")).catch(() => {});
6603
6662
  } catch {}
6604
6663
  }
6605
6664
  function isProcessAlive(pid) {
@@ -6633,22 +6692,19 @@ async function spawnDaemon(specUrl, port, opts = {}) {
6633
6692
  args.push("--readonly");
6634
6693
  }
6635
6694
  args.push("--_daemon");
6636
- const logDir = DIR;
6637
6695
  await ensureDir();
6638
- const logPath = join2(logDir, "server.log");
6639
6696
  const child = Bun.spawn([process.execPath, Bun.main, ...args], {
6640
6697
  detached: true,
6641
6698
  cwd: process.cwd(),
6642
6699
  env: { ...process.env },
6643
- stdio: ["ignore", Bun.file(logPath), Bun.file(logPath)]
6700
+ stdio: ["ignore", Bun.file(logFile(port)), Bun.file(logFile(port))]
6644
6701
  });
6645
6702
  child.unref();
6646
6703
  return child.pid;
6647
6704
  }
6648
- var DIR, STATE_FILE;
6705
+ var WASPER_DIR, DEFAULT_PORT = 3388;
6649
6706
  var init_daemon = __esm(() => {
6650
- DIR = join2(homedir2(), ".wasper");
6651
- STATE_FILE = join2(DIR, "server.json");
6707
+ WASPER_DIR = join2(homedir2(), ".wasper");
6652
6708
  });
6653
6709
 
6654
6710
  // src/ui.ts
@@ -7508,7 +7564,7 @@ async function run2(overrideOpts) {
7508
7564
  ${paint.dim("shutting down")}
7509
7565
 
7510
7566
  `);
7511
- clearDaemonState().finally(() => {
7567
+ clearDaemonState(PORT).finally(() => {
7512
7568
  db.close();
7513
7569
  server.stop();
7514
7570
  process.exit(0);
@@ -7836,16 +7892,19 @@ function printInteractiveHelp() {
7836
7892
  }
7837
7893
  function printHelp() {
7838
7894
  console.log(`
7839
- Usage: wasper [start] [options]
7895
+ Usage: wasper start [options]
7896
+
7897
+ Starts wasper in the foreground with an interactive REPL.
7898
+ For background (daemon) mode \u2014 the default \u2014 use: wasper up
7840
7899
 
7841
- wasper [--url <spec-url>] [--port <port>] Start in foreground (auto-resumes last spec)
7842
- wasper start --background Start in background
7843
- wasper stop Stop background server
7844
- wasper status Show server status
7900
+ wasper up [--url <spec>] Start daemon in background (default)
7901
+ wasper start [--url <spec>] Start in foreground with REPL
7902
+ wasper down Stop the daemon
7903
+ wasper status Show daemon status
7904
+ wasper logs [-f] Tail server logs
7905
+ wasper service install Install as system service (auto-start)
7845
7906
  wasper reload Hot-reload the spec
7846
7907
  wasper ls List saved specs (history)
7847
- wasper use <number|url> Start with a saved spec
7848
- wasper rm <number|url> Remove a spec from history
7849
7908
 
7850
7909
  Options:
7851
7910
  --url, -u OpenAPI spec URL or local path
@@ -7860,17 +7919,17 @@ Options:
7860
7919
  --no-proxy Start with the HTTP proxy disabled
7861
7920
  --no-ai Start with the AI chat endpoint disabled
7862
7921
  --readonly Block all non-GET upstream requests (agent guardrail)
7863
- --background, -b Start detached in background
7922
+ --background, -b Start detached in background (same as wasper up)
7864
7923
  --daemon, -d Same as --background
7865
7924
  -h, --help Show this help
7866
7925
 
7867
- Interactive mode supports slash commands \u2014 press / and type:
7926
+ Interactive REPL slash commands (foreground mode):
7868
7927
  /mcp on|off \xB7 /proxy on|off \xB7 /ai on|off \xB7 /readonly on|off
7869
7928
  /auth use <role> \xB7 /token new \xB7 /spec <url> \xB7 /tail \xB7 /help
7870
7929
 
7871
7930
  Self-hosting:
7872
- wasper start --url <spec> --origin https://api.example.com --token <secret> -b
7873
- Then open the studio with ?server=https://api.example.com&token=<secret>
7931
+ wasper up --url <spec> --origin https://api.example.com --token <secret>
7932
+ wasper service install --url <spec> --port 3388
7874
7933
  `);
7875
7934
  }
7876
7935
  function buildScalarHtml(title, req) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wasper-cli",
3
- "version": "0.3.1",
3
+ "version": "0.3.3",
4
4
  "description": "Host an MCP server + API proxy from any OpenAPI spec. Like Drizzle Studio, but for APIs.",
5
5
  "type": "module",
6
6
  "homepage": "https://wasper.site",