wawesome 0.0.8 → 0.0.10

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 (3) hide show
  1. package/README.md +22 -0
  2. package/dist/index.mjs +163 -51
  3. package/package.json +2 -1
package/README.md CHANGED
@@ -67,6 +67,13 @@ dlx`, `bun x`), so the types are there when you open the project and the bundler
67
67
  template's imports. Pass `--no-install` to do it yourself. A failed install never stops the flow;
68
68
  the command to re-run is printed.
69
69
 
70
+ Before any of that is sent anywhere, the CLI shows you the **public URL your Function will have**
71
+ and offers to change your workspace address — the part of that URL that is yours rather than this
72
+ project's, and the same one `wawesome workspace show` prints. It is randomly minted at signup and
73
+ stays changeable until your first deploy, at which point it locks for good, because live URLs carry
74
+ it. Press enter to keep it. If it is already locked, the offer is not made and the reason is said
75
+ plainly.
76
+
70
77
  Being logged in is checked **before** the first question, not at the deploy: an expired session
71
78
  offers you a login there and then, and declining still leaves you the project plus the two commands
72
79
  that finish it.
@@ -193,6 +200,21 @@ Every project directory includes a `wawesome-function.json` file generated durin
193
200
  `app` is the App this Function is deployed into, and it is client-facing — every deploy from this
194
201
  directory is scoped to it.
195
202
 
203
+ ### Reserved headers
204
+
205
+ `x-wawesome-*` belongs to the platform in both directions. It is stripped off the request before your
206
+ handler sees it, and off your response before the caller does — so **do not name a header of your own
207
+ on that prefix**: it is dropped silently rather than rejected, and you will not get an error telling
208
+ you why it vanished.
209
+
210
+ Three headers arrive or leave on it, and the stripping is what makes them worth trusting:
211
+
212
+ | Header | Direction | What it means |
213
+ | --- | --- | --- |
214
+ | `x-wawesome-forwarded-prefix` | inbound | The mount that was stripped from the path. Join it to the path you observe to rebuild the caller's URL. |
215
+ | `x-wawesome-invocation-id` | outbound | The id of this run — the key to fetch its logs with `npx wawesome logs --invocation <id>`. |
216
+ | `x-wawesome-error` | outbound | Present only when the platform failed, never when your Function did. Its *absence* means the status on the wire is yours. |
217
+
196
218
  ### Local Development / Gateway Overrides
197
219
 
198
220
  If you are running a local gateway or self-hosted instance, you can configure your CLI Gateway URL using any of the
package/dist/index.mjs CHANGED
@@ -161,7 +161,7 @@ async function buildJs(entryInput, options) {
161
161
  * that has to name this version — `--version`, the dependency a scaffolded
162
162
  * project pins — reads it here, so a release bumps one file.
163
163
  */
164
- const CLI_VERSION = "0.0.8";
164
+ const CLI_VERSION = "0.0.10";
165
165
  //#endregion
166
166
  //#region src/prompt.ts
167
167
  /**
@@ -284,6 +284,34 @@ async function resolveWorkspace(creds) {
284
284
  };
285
285
  }
286
286
  /**
287
+ * What a refused rename means for whoever asked for it.
288
+ *
289
+ * Shared by the two places that offer a rename, so they cannot drift into
290
+ * telling a user different things about the same reason — and `retryable` is
291
+ * what separates a name worth retyping from one that no name would fix.
292
+ */
293
+ function renameAdvice(reason) {
294
+ switch (reason) {
295
+ case "taken":
296
+ case "reserved": return {
297
+ text: "Pick a different name.",
298
+ retryable: true
299
+ };
300
+ case "malformed": return {
301
+ text: "Use lowercase letters, numbers and single hyphens.",
302
+ retryable: true
303
+ };
304
+ case "locked": return {
305
+ text: "A deploy landed while this was running, which fixed the address for good.",
306
+ retryable: false
307
+ };
308
+ default: return {
309
+ text: "",
310
+ retryable: false
311
+ };
312
+ }
313
+ }
314
+ /**
287
315
  * Ask for the workspace's public address to become `slug`.
288
316
  *
289
317
  * The slug is sent exactly as typed. Sanitizing it the way an App slug is
@@ -301,17 +329,12 @@ async function renameTenantSlug(creds, slug) {
301
329
  });
302
330
  if (!res.ok) throw await asGatewayError(res, `Rename failed (HTTP ${res.status}).`);
303
331
  const result = await res.json();
304
- const stored = readCredentials();
305
- if (stored) writeCredentials({
306
- ...stored,
332
+ writeCredentials({
333
+ ...readCredentials() ?? creds,
307
334
  tenant_slug: result.tenant_slug
308
335
  });
309
336
  return result;
310
337
  }
311
- /** The address a deployed Function answers on, for a workspace and app. */
312
- function publicInvokeUrl(gatewayUrl, tenantSlug, appSlug, functionName) {
313
- return `${gatewayUrl}/v1/s/${encodeURIComponent(tenantSlug)}/apps/${encodeURIComponent(appSlug)}/functions/${encodeURIComponent(functionName)}/invoke`;
314
- }
315
338
  //#endregion
316
339
  //#region src/auth.ts
317
340
  /**
@@ -521,6 +544,20 @@ async function whoami() {
521
544
  console.log(` Gateway: ${creds.gateway_url}\n`);
522
545
  }
523
546
  //#endregion
547
+ //#region ../shared/public-address.ts
548
+ const INVOCATION_PREFIX = "/x";
549
+ const SUBTREE_NOTE = "Every path beneath this address reaches the Function.";
550
+ function mountBase(origin, tenantSlug) {
551
+ return `${origin.replace(/\/+$/, "")}${INVOCATION_PREFIX}/${encodeURIComponent(tenantSlug)}`;
552
+ }
553
+ function publicAddress(origin, tenantSlug, appSlug, functionSlug) {
554
+ return `${mountBase(origin, tenantSlug)}/${encodeURIComponent(appSlug)}/${encodeURIComponent(functionSlug)}`;
555
+ }
556
+ /** The same address with the app and function still to be chosen. */
557
+ function publicAddressTemplate(origin, tenantSlug) {
558
+ return `${mountBase(origin, tenantSlug)}/<app>/<function>`;
559
+ }
560
+ //#endregion
524
561
  //#region src/deploy.ts
525
562
  /**
526
563
  * Deploy a function: build → upload JS to gateway → promote.
@@ -617,10 +654,10 @@ async function deploy(entryInput, options) {
617
654
  }
618
655
  process.exit(1);
619
656
  }
620
- let invokeUrl = null;
657
+ let address = null;
621
658
  try {
622
659
  const { slug } = await resolveWorkspace(creds);
623
- invokeUrl = publicInvokeUrl(creds.gateway_url, slug, app, funcName);
660
+ address = publicAddress(creds.gateway_url, slug, app, funcName);
624
661
  } catch (err) {
625
662
  if (isVerbose) console.log(`[wawesome:verbose] Could not resolve the workspace address: ${err instanceof Error ? err.message : err}`);
626
663
  }
@@ -630,13 +667,16 @@ async function deploy(entryInput, options) {
630
667
  console.log(`\n App: ${app}`);
631
668
  console.log(` Function: ${funcName}`);
632
669
  if (version !== void 0) console.log(` Version: ${version}`);
633
- if (invokeUrl) console.log(`\n URL: \x1b[36m${invokeUrl}\x1b[0m`);
670
+ if (address) {
671
+ console.log(`\n URL: \x1b[36m${address}\x1b[0m`);
672
+ console.log(` ${SUBTREE_NOTE}`);
673
+ }
634
674
  console.log("======================================================\n");
635
675
  return {
636
676
  app,
637
677
  functionName: funcName,
638
678
  version,
639
- invokeUrl
679
+ address
640
680
  };
641
681
  }
642
682
  //#endregion
@@ -987,11 +1027,9 @@ function alignCliDependency(dir, version) {
987
1027
  const file = path.join(dir, PACKAGE_FILE);
988
1028
  const pkg = readJson(file);
989
1029
  const wanted = `^${version}`;
990
- for (const field of ["dependencies", "devDependencies"]) {
1030
+ for (const { field, range } of declaredCliRanges(pkg)) {
1031
+ if (range === wanted) continue;
991
1032
  const deps = pkg[field];
992
- if (!isRecord(deps)) continue;
993
- const current = deps[CLI_PACKAGE];
994
- if (typeof current !== "string" || current === wanted) continue;
995
1033
  writeJson(file, {
996
1034
  ...pkg,
997
1035
  [field]: {
@@ -1000,12 +1038,22 @@ function alignCliDependency(dir, version) {
1000
1038
  }
1001
1039
  });
1002
1040
  return {
1003
- from: current,
1041
+ from: range,
1004
1042
  to: wanted
1005
1043
  };
1006
1044
  }
1007
1045
  return null;
1008
1046
  }
1047
+ /** Where a template declares the CLI, and at which range. */
1048
+ function declaredCliRanges(pkg) {
1049
+ return ["dependencies", "devDependencies"].flatMap((field) => {
1050
+ const deps = pkg[field];
1051
+ return isRecord(deps) && typeof deps[CLI_PACKAGE] === "string" ? [{
1052
+ field,
1053
+ range: deps[CLI_PACKAGE]
1054
+ }] : [];
1055
+ });
1056
+ }
1009
1057
  function isRecord(value) {
1010
1058
  return typeof value === "object" && value !== null && !Array.isArray(value);
1011
1059
  }
@@ -1039,9 +1087,9 @@ function collidingPaths(dir, relativePaths) {
1039
1087
  * not be resolved the message still prints, with the placeholder left visible
1040
1088
  * rather than a confident "undefined".
1041
1089
  */
1042
- function renderPostDeploy(postDeploy, invokeUrl) {
1043
- if (!invokeUrl) return postDeploy;
1044
- return postDeploy.split("{{url}}").join(invokeUrl);
1090
+ function renderPostDeploy(postDeploy, address) {
1091
+ if (!address) return postDeploy;
1092
+ return postDeploy.split("{{url}}").join(address);
1045
1093
  }
1046
1094
  /**
1047
1095
  * Normalise arbitrary input into a legal DNS label, returning an empty string
@@ -1410,18 +1458,25 @@ async function promptForDeclaredEnv(session, declared) {
1410
1458
  return answers;
1411
1459
  }
1412
1460
  /**
1413
- * Whether the stored session is one the platform still accepts.
1461
+ * The stored credentials, if the platform still accepts them, with whatever it
1462
+ * said about the Tenant while answering.
1414
1463
  *
1415
- * Only a refusal counts as an answer. An unreachable gateway says nothing about
1416
- * whether the credentials are good, and treating it as a dead session would send
1417
- * a user who is merely offline through a login they do not need.
1464
+ * Only a refusal counts as an answer — that is the null. An unreachable gateway
1465
+ * says nothing about whether the credentials are good, and treating it as a dead
1466
+ * session would send a user who is merely offline through a login they do not
1467
+ * need, so it comes back as a session with nothing known about the Tenant.
1418
1468
  */
1419
- async function sessionIsLive(creds) {
1469
+ async function probeSession(creds) {
1420
1470
  try {
1421
- await fetchTenantDetails(creds);
1422
- return true;
1471
+ return {
1472
+ creds,
1473
+ tenant: await fetchTenantDetails(creds)
1474
+ };
1423
1475
  } catch (err) {
1424
- return !(err instanceof GatewayError && (err.status === 401 || err.status === 403));
1476
+ return err instanceof GatewayError && (err.status === 401 || err.status === 403) ? null : {
1477
+ creds,
1478
+ tenant: null
1479
+ };
1425
1480
  }
1426
1481
  }
1427
1482
  /**
@@ -1438,7 +1493,10 @@ async function sessionIsLive(creds) {
1438
1493
  */
1439
1494
  async function ensureSession(options) {
1440
1495
  const stored = readCredentials();
1441
- if (stored && await sessionIsLive(stored)) return stored;
1496
+ if (stored) {
1497
+ const probed = await probeSession(stored);
1498
+ if (probed) return probed;
1499
+ }
1442
1500
  console.log(stored ? "\n[wawesome] Your session has expired." : "\n[wawesome] You're not logged in yet — that's what a deploy needs.");
1443
1501
  if (!isInteractive()) {
1444
1502
  console.log("[wawesome] Run 'wawesome login', then 'wawesome deploy'.");
@@ -1464,7 +1522,69 @@ async function ensureSession(options) {
1464
1522
  console.error(`[wawesome] Login failed: ${errorText(err)}`);
1465
1523
  return null;
1466
1524
  }
1467
- return readCredentials();
1525
+ const fresh = readCredentials();
1526
+ return fresh ? probeSession(fresh) : null;
1527
+ }
1528
+ /** Bound on the rename loop, so a name the gateway keeps refusing ends the offer. */
1529
+ const MAX_RENAME_ATTEMPTS = 3;
1530
+ /** Show the address the Function will answer on, once it is deployed. */
1531
+ function announceUrl(creds, slug, appSlug, functionName) {
1532
+ console.log("\n[wawesome] Your Function will answer on:\n");
1533
+ console.log(` \x1b[36m${publicAddress(creds.gateway_url, slug, appSlug, functionName)}\x1b[0m`);
1534
+ console.log(` ${SUBTREE_NOTE}\n`);
1535
+ }
1536
+ /**
1537
+ * Show the URL, and offer to fix the workspace address while it can still be fixed.
1538
+ *
1539
+ * This is the last moment that address is free: it locks the first time a
1540
+ * version is promoted, which is what the deploy at the end of this flow does.
1541
+ * Asking here rather than at signup is the whole reason naming was deferred —
1542
+ * it is client-facing branding, and nobody chooses it well before seeing the
1543
+ * product work. The URL comes first so the offer is about something concrete.
1544
+ *
1545
+ * The lock is read from the gateway rather than guessed, so a user who cannot
1546
+ * rename is told why instead of being walked into a rejection. Nothing here can
1547
+ * fail the flow: the project is already on disk and the deploy still works under
1548
+ * the address the workspace has.
1549
+ */
1550
+ async function offerWorkspaceAddress(session, creds, tenant, appSlug, functionName) {
1551
+ const current = tenant?.tenant_slug ?? creds.tenant_slug;
1552
+ if (!current) return;
1553
+ announceUrl(creds, current, appSlug, functionName);
1554
+ if (!tenant) {
1555
+ console.log(` '${current}' is your workspace address. Whether it can still be changed`);
1556
+ console.log(" is a question for the gateway, which did not answer.\n");
1557
+ return;
1558
+ }
1559
+ if (tenant.slug_locked) {
1560
+ console.log(` 🔒 '${current}' is fixed — a Function version has been promoted, and live URLs already carry it.\n`);
1561
+ return;
1562
+ }
1563
+ console.log(` '${current}' is your workspace address — the part of that URL that is yours`);
1564
+ console.log(" rather than this project's, and this is the moment to change it: it locks");
1565
+ console.log(" for good the first time you deploy, because live URLs carry it.");
1566
+ console.log(" Press enter to keep it.\n");
1567
+ let refusal;
1568
+ for (let attempt = 0; attempt < MAX_RENAME_ATTEMPTS; attempt++) {
1569
+ const answer = await session.ask(" Workspace address?", current);
1570
+ if (answer === current) {
1571
+ console.log(`[wawesome] Keeping '${current}'.`);
1572
+ return;
1573
+ }
1574
+ try {
1575
+ const renamed = await renameTenantSlug(creds, answer);
1576
+ console.log(`\n[wawesome] ✅ Workspace renamed to '${renamed.tenant_slug}'.`);
1577
+ announceUrl(creds, renamed.tenant_slug, appSlug, functionName);
1578
+ return;
1579
+ } catch (err) {
1580
+ console.log(`[wawesome] ${errorText(err)}`);
1581
+ refusal = err instanceof GatewayError ? err.reason : void 0;
1582
+ const { text, retryable } = renameAdvice(refusal);
1583
+ if (text) console.log(`[wawesome] ${text}`);
1584
+ if (!retryable) break;
1585
+ }
1586
+ }
1587
+ console.log(refusal === "locked" ? `[wawesome] Keeping '${current}'.` : `[wawesome] Keeping '${current}'. Change it later with: wawesome workspace rename <name>`);
1468
1588
  }
1469
1589
  /**
1470
1590
  * Satisfy the template's declarations against the platform.
@@ -1538,7 +1658,7 @@ async function initFromTemplate(templateName, options) {
1538
1658
  let functionName;
1539
1659
  let appSlug;
1540
1660
  let manifest;
1541
- let creds;
1661
+ let tenantSession;
1542
1662
  let answers = [];
1543
1663
  try {
1544
1664
  let files;
@@ -1553,7 +1673,7 @@ async function initFromTemplate(templateName, options) {
1553
1673
  }
1554
1674
  const collisions = collidingPaths(cwd, files);
1555
1675
  if (collisions.length > 0) fail(`${collisions.join(", ")} already exist${collisions.length === 1 ? "s" : ""} here.`, "Run this in an empty directory, or move those files aside first. Nothing was written.");
1556
- creds = await ensureSession(options);
1676
+ tenantSession = await ensureSession(options);
1557
1677
  const session = openPromptSession();
1558
1678
  try {
1559
1679
  functionName = await promptForSlug(session, "Function name", readFunctionConfig(staging)?.function || dirName);
@@ -1572,7 +1692,8 @@ async function initFromTemplate(templateName, options) {
1572
1692
  }
1573
1693
  console.log(`\n[wawesome] ✅ Scaffolded ${files.length} files from '${template.name}'.`);
1574
1694
  if (repinned) console.log(`[wawesome] Using wawesome ${repinned.to} — the template pinned ${repinned.from}.`);
1575
- answers = creds ? await promptForDeclaredEnv(session, manifest.env) : [];
1695
+ if (tenantSession) await offerWorkspaceAddress(session, tenantSession.creds, tenantSession.tenant, appSlug, functionName);
1696
+ answers = tenantSession ? await promptForDeclaredEnv(session, manifest.env) : [];
1576
1697
  } finally {
1577
1698
  session.close();
1578
1699
  }
@@ -1580,7 +1701,7 @@ async function initFromTemplate(templateName, options) {
1580
1701
  sweepStaging();
1581
1702
  process.off("exit", sweepStaging);
1582
1703
  }
1583
- if (!creds) {
1704
+ if (!tenantSession) {
1584
1705
  const installed = options.install === false ? null : installDependencies(cwd, { verbose: options.verbose });
1585
1706
  console.log("\n[wawesome] 🎉 Project ready. Next steps:");
1586
1707
  [
@@ -1591,14 +1712,14 @@ async function initFromTemplate(templateName, options) {
1591
1712
  console.log("");
1592
1713
  return;
1593
1714
  }
1594
- await wireUp(creds, appSlug, manifest, answers);
1715
+ await wireUp(tenantSession.creds, appSlug, manifest, answers);
1595
1716
  if (options.install !== false) installDependencies(cwd, { verbose: options.verbose });
1596
1717
  const result = await deploy(void 0, {
1597
1718
  out: "dist/index.js",
1598
1719
  verbose: options.verbose
1599
1720
  });
1600
1721
  if (manifest.post_deploy) {
1601
- console.log(renderPostDeploy(manifest.post_deploy, result.invokeUrl));
1722
+ console.log(renderPostDeploy(manifest.post_deploy, result.address));
1602
1723
  console.log("");
1603
1724
  }
1604
1725
  }
@@ -2366,7 +2487,10 @@ async function showWorkspace(options) {
2366
2487
  console.log(` Name: ${tenant.name}`);
2367
2488
  console.log(` Address: ${tenant.tenant_slug}`);
2368
2489
  console.log(` Tenant: ${tenant.id}`);
2369
- if (options.verbose) console.log(` Invoke: ${creds.gateway_url}/v1/s/${tenant.tenant_slug}/apps/<app>/functions/<function>/invoke`);
2490
+ if (options.verbose) {
2491
+ console.log(` URLs: ${publicAddressTemplate(creds.gateway_url, tenant.tenant_slug)}`);
2492
+ console.log(` ${SUBTREE_NOTE}`);
2493
+ }
2370
2494
  if (tenant.slug_locked) console.log("\n 🔒 The address is fixed — a Function version has been promoted and live URLs carry it.");
2371
2495
  else {
2372
2496
  console.log("\n The address can still be changed: \x1B[36mwawesome workspace rename <name>\x1B[0m");
@@ -2408,20 +2532,8 @@ async function renameWorkspace(slug, options) {
2408
2532
  } catch (err) {
2409
2533
  if (err instanceof GatewayError) {
2410
2534
  console.error(`\n[wawesome] \x1b[31m${err.message}\x1b[0m`);
2411
- switch (err.reason) {
2412
- case "taken":
2413
- console.error("[wawesome] Pick a different name.");
2414
- break;
2415
- case "reserved":
2416
- console.error("[wawesome] Pick a different name.");
2417
- break;
2418
- case "malformed":
2419
- console.error("[wawesome] Use lowercase letters, numbers and single hyphens.");
2420
- break;
2421
- case "locked":
2422
- console.error("[wawesome] A deploy landed while this was running, which fixed the address for good.");
2423
- break;
2424
- }
2535
+ const { text } = renameAdvice(err.reason);
2536
+ if (text) console.error(`[wawesome] ${text}`);
2425
2537
  console.error("");
2426
2538
  process.exit(1);
2427
2539
  return;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wawesome",
3
- "version": "0.0.8",
3
+ "version": "0.0.10",
4
4
  "description": "CLI tool for building and deploying serverless functions on wawesome.io platform",
5
5
  "type": "module",
6
6
  "bin": {
@@ -16,6 +16,7 @@
16
16
  "build": "tsdown",
17
17
  "dev": "tsdown --watch",
18
18
  "test": "vitest run",
19
+ "test:templates": "vitest run --config vitest.live.config.ts",
19
20
  "check": "publint --pack npm",
20
21
  "changeset": "changeset"
21
22
  },