wawesome 0.18.0 → 0.19.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.
@@ -1,4 +1,4 @@
1
- import { a as unsupportedGlobals, i as shimmedGlobals, n as methodRemedy, r as polyfilledGlobals, t as declaredMethods } from "./guest-surface-CXON0L5V.mjs";
1
+ import { a as unsupportedGlobals, i as shimmedGlobals, n as methodRemedy, r as polyfilledGlobals, t as declaredMethods } from "./guest-surface-4bHyTMIy.mjs";
2
2
  //#region ../../server/guest-surface/message-channel.js
3
3
  const ENTANGLED = Symbol("entangled");
4
4
  const QUEUE = Symbol("queue");
@@ -1,2 +1,2 @@
1
- import { t as applyGuestParity } from "./guest-parity-Df4rlLDW.mjs";
1
+ import { t as applyGuestParity } from "./guest-parity-B5GLO6LA.mjs";
2
2
  export { applyGuestParity };
@@ -27,6 +27,17 @@ var globals = [
27
27
  "intl-locales-supported",
28
28
  "intl-segmenter-polyfill"
29
29
  ]
30
+ },
31
+ {
32
+ "name": "URLPattern",
33
+ "status": "unsupported",
34
+ "remedy": "bundle a URLPattern polyfill, for example urlpattern-polyfill",
35
+ "providedBy": ["urlpattern-polyfill"]
36
+ },
37
+ {
38
+ "name": "WebSocket",
39
+ "status": "unsupported",
40
+ "remedy": "the sandbox has no sockets, so nothing can supply this one; open the connection from the browser instead, or reach a realtime service over HTTPS"
30
41
  }
31
42
  ];
32
43
  var methods = [
package/dist/index.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import { a as unsupportedGlobals, n as methodRemedy, r as polyfilledGlobals, t as declaredMethods } from "./guest-surface-CXON0L5V.mjs";
1
+ import { a as unsupportedGlobals, n as methodRemedy, r as polyfilledGlobals, t as declaredMethods } from "./guest-surface-4bHyTMIy.mjs";
2
2
  import cac from "cac";
3
3
  import fs from "node:fs";
4
4
  import path from "node:path";
@@ -905,7 +905,7 @@ async function buildHandler(entryInput, config, options) {
905
905
  * that has to name this version — `--version`, the dependency a scaffolded
906
906
  * project pins — reads it here, so a release bumps one file.
907
907
  */
908
- const CLI_VERSION = "0.18.0";
908
+ const CLI_VERSION = "0.19.0";
909
909
  //#endregion
910
910
  //#region src/prompt.ts
911
911
  /**
@@ -962,6 +962,144 @@ function isInteractive() {
962
962
  return Boolean(process.stdin.isTTY);
963
963
  }
964
964
  //#endregion
965
+ //#region ../shared/public-address.ts
966
+ const INVOCATION_PREFIX = "/x";
967
+ const SLUG_SEPARATOR = "--";
968
+ const SURFACE_ROUTE = "/v1/invocation-surface";
969
+ const SUBTREE_NOTE = "Every path beneath this address reaches the Function.";
970
+ const ROOT_FUNCTION_SLUG = "root";
971
+ /**
972
+ * A gateway that serves no form of the address — and the value a client uses for
973
+ * one that did not answer at all, deliberately the same: both name no address,
974
+ * and there is nothing else either could honestly do.
975
+ */
976
+ const NO_INVOCATION_SURFACE = {
977
+ contentOrigin: null,
978
+ pathFormOrigin: null
979
+ };
980
+ async function fetchInvocationSurface(gatewayUrl) {
981
+ const origin = trimTrailingSlashes(gatewayUrl);
982
+ const res = await fetch(`${origin}${SURFACE_ROUTE}`);
983
+ if (!res.ok) throw new Error(`Failed to read the gateway's address forms (HTTP ${res.status}).`);
984
+ const body = await res.json();
985
+ return {
986
+ contentOrigin: body.content_origin ?? null,
987
+ pathFormOrigin: body.path_form ? origin : null
988
+ };
989
+ }
990
+ /** The one address the Function answers at, `null` where it has none. */
991
+ function publicAddress(surface, tenantSlug, appSlug, functionSlug) {
992
+ const base = appBase(surface, tenantSlug, appSlug);
993
+ if (base === null) return null;
994
+ if (functionSlug === "root") return base;
995
+ return `${base}/${encodeURIComponent(functionSlug)}`;
996
+ }
997
+ /**
998
+ * Where one of a Function's static files answers, `null` where none of them do.
999
+ *
1000
+ * A **Static asset** is served on the App's own hostname and on no other
1001
+ * address, so a gateway serving only the path form has no address to give for
1002
+ * one — see **Static route** in `CONTEXT.md`.
1003
+ */
1004
+ function assetAddress(surface, tenantSlug, appSlug, functionSlug, assetPath) {
1005
+ if (!surface.contentOrigin) return null;
1006
+ const mount = publicAddress(surface, tenantSlug, appSlug, functionSlug);
1007
+ if (mount === null) return null;
1008
+ return `${mount}/${assetPath.split("/").map(encodeURIComponent).join("/")}`;
1009
+ }
1010
+ /** The same address with the app and function still to be chosen. */
1011
+ function publicAddressTemplate(surface, tenantSlug) {
1012
+ if (surface.contentOrigin) {
1013
+ if (!isLegalDnsLabel(tenantSlug)) return null;
1014
+ const hostname = appHostname(surface.contentOrigin, `${tenantSlug}${SLUG_SEPARATOR}<app>`);
1015
+ return hostname === null ? null : `${hostname}/<function>`;
1016
+ }
1017
+ if (surface.pathFormOrigin) return `${pathFormBase(surface.pathFormOrigin, tenantSlug)}/<app>/<function>`;
1018
+ return null;
1019
+ }
1020
+ function appBase(surface, tenantSlug, appSlug) {
1021
+ if (surface.contentOrigin) {
1022
+ if (!isLegalDnsLabel(tenantSlug) || !isLegalDnsLabel(appSlug)) return null;
1023
+ return appHostname(surface.contentOrigin, `${tenantSlug}${SLUG_SEPARATOR}${appSlug}`);
1024
+ }
1025
+ if (surface.pathFormOrigin) return `${pathFormBase(surface.pathFormOrigin, tenantSlug)}/${encodeURIComponent(appSlug)}`;
1026
+ return null;
1027
+ }
1028
+ /**
1029
+ * The App's own hostname, built the way the gateway rebuilds it from its own
1030
+ * configuration: the label one level beneath the content domain. Callers check
1031
+ * the slugs the label is made of, since a slug the gateway would refuse to read
1032
+ * a `Host` for names no App there.
1033
+ */
1034
+ function appHostname(contentOrigin, label) {
1035
+ let origin;
1036
+ try {
1037
+ origin = new URL(contentOrigin);
1038
+ } catch {
1039
+ return null;
1040
+ }
1041
+ return `${origin.protocol}//${label}.${origin.host}`;
1042
+ }
1043
+ /** The surface, or none where the gateway did not answer. */
1044
+ async function invocationSurfaceOrNone(gatewayUrl) {
1045
+ try {
1046
+ return await fetchInvocationSurface(gatewayUrl);
1047
+ } catch {
1048
+ return NO_INVOCATION_SURFACE;
1049
+ }
1050
+ }
1051
+ function pathFormBase(origin, tenantSlug) {
1052
+ return `${trimTrailingSlashes(origin)}${INVOCATION_PREFIX}/${encodeURIComponent(tenantSlug)}`;
1053
+ }
1054
+ /**
1055
+ * The legal DNS label closest to what somebody typed, empty where nothing legal
1056
+ * survives. What a workspace name suggests as an address, and what the CLI
1057
+ * offers as a default — never applied to a typed address, which is refused
1058
+ * rather than rewritten into a name nobody chose.
1059
+ */
1060
+ function slugifyLabel(raw) {
1061
+ let label = "";
1062
+ for (const c of raw.trim().toLowerCase()) if (c >= "a" && c <= "z" || c >= "0" && c <= "9") label += c;
1063
+ else if (label.length > 0 && !label.endsWith("-")) label += "-";
1064
+ label = label.slice(0, 63);
1065
+ while (label.endsWith("-")) label = label.slice(0, -1);
1066
+ return label;
1067
+ }
1068
+ /** Must match `dns_label::is_legal` in `server/crates/core/src/value_objects.rs`. */
1069
+ function isLegalDnsLabel(candidate) {
1070
+ return candidate.length > 0 && candidate.length <= 63 && !candidate.startsWith("-") && !candidate.endsWith("-") && !candidate.includes("--") && /^[a-z0-9-]+$/.test(candidate);
1071
+ }
1072
+ function trimTrailingSlashes(origin) {
1073
+ return origin.replace(/\/+$/, "");
1074
+ }
1075
+ //#endregion
1076
+ //#region src/slug.ts
1077
+ /**
1078
+ * The character rules every slug on the platform obeys, mirrored from the
1079
+ * `dns_label` module in `server/crates/core/src/value_objects.rs`.
1080
+ *
1081
+ * A typed name is checked while the user is still standing there, rather than
1082
+ * accepted, sent, and rewritten by the gateway into something else that ends up
1083
+ * in a client's dashboard. The rules live beside the address they build, so the
1084
+ * CLI and the dashboard cannot come to disagree about what a legal name is.
1085
+ */
1086
+ /** Maximum length of a single DNS label (RFC 1035 §2.3.4). */
1087
+ const MAX_SLUG_LENGTH = 63;
1088
+ /**
1089
+ * Normalise arbitrary input into a legal DNS label, returning an empty string
1090
+ * when nothing legal survives — callers decide whether that is an error.
1091
+ */
1092
+ function sanitizeSlug(raw) {
1093
+ return slugifyLabel(raw);
1094
+ }
1095
+ /**
1096
+ * Whether `candidate` is already a legal DNS label — i.e. whether
1097
+ * {@link sanitizeSlug} would leave it untouched.
1098
+ */
1099
+ function isLegalSlug(candidate) {
1100
+ return candidate.length > 0 && candidate.length <= MAX_SLUG_LENGTH && !candidate.startsWith("-") && !candidate.endsWith("-") && !candidate.includes("--") && /^[a-z0-9-]+$/.test(candidate);
1101
+ }
1102
+ //#endregion
965
1103
  //#region src/session.ts
966
1104
  /**
967
1105
  * A gateway request, retried once against a renewed session if it comes back 401.
@@ -1170,24 +1308,36 @@ async function renameTenantSlug(creds, slug) {
1170
1308
  //#endregion
1171
1309
  //#region src/auth.ts
1172
1310
  /**
1173
- * Ask what to call the workspace being created.
1311
+ * Ask what to call the workspace being created, and what it should answer at.
1174
1312
  *
1175
- * There is no default worth offering: a name nobody typed is exactly what this
1176
- * replaced. A non-interactive login has nobody to ask, so it says what flag to
1177
- * pass rather than inventing one and minting a workspace under it.
1178
- */
1179
- async function promptForWorkspaceName(options) {
1313
+ * There is no default name worth offering: a name nobody typed is exactly what
1314
+ * this replaced. The address has one — the name, slugified — because it is a
1315
+ * DNS label and typing it a second time is work the name already did. A
1316
+ * non-interactive login has nobody to ask, so it says what flag to pass rather
1317
+ * than inventing a name and minting a workspace under it; an address it is not
1318
+ * given is still minted, which is what a pipeline that says nothing gets.
1319
+ */
1320
+ async function promptForWorkspace(options) {
1180
1321
  const supplied = options.workspace?.trim();
1181
- if (supplied) return supplied;
1322
+ if (supplied) return {
1323
+ name: supplied,
1324
+ address: options.address?.trim() || ""
1325
+ };
1182
1326
  if (!isInteractive()) throw new Error("No workspace found, and no name to create one with. Re-run with --workspace <name>.");
1183
1327
  console.log("\n[wawesome] You don't have a workspace yet. Let's create one.");
1184
- console.log(" (this is the name you'll see in the dashboard; its public address is set separately)");
1328
+ console.log(" (the name is what you'll see in the dashboard; the address is the first half of every URL you deploy)");
1185
1329
  const session = openPromptSession();
1186
1330
  try {
1187
1331
  for (let attempt = 0; attempt < 3; attempt++) {
1188
- const answer = await session.ask(" Workspace name?", "");
1189
- if (answer.trim()) return answer.trim();
1190
- console.log("[wawesome] A workspace needs a name.");
1332
+ const name = (await session.ask(" Workspace name?", "")).trim();
1333
+ if (!name) {
1334
+ console.log("[wawesome] A workspace needs a name.");
1335
+ continue;
1336
+ }
1337
+ return {
1338
+ name,
1339
+ address: (await session.ask(" Public address?", options.address?.trim() || sanitizeSlug(name))).trim()
1340
+ };
1191
1341
  }
1192
1342
  } finally {
1193
1343
  session.close();
@@ -1235,9 +1385,9 @@ async function completeLogin({ gatewayUrl, dashboardUrl, accessToken, refreshTok
1235
1385
  let workspaceName;
1236
1386
  let workspaceSlug;
1237
1387
  if (tenants.length === 0) {
1238
- const chosenName = await promptForWorkspaceName(options);
1388
+ const chosen = await promptForWorkspace(options);
1239
1389
  await confirmTermsAccepted(options);
1240
- console.log(`[wawesome] Creating workspace "${chosenName}"...`);
1390
+ console.log(`[wawesome] Creating workspace "${chosen.name}"...`);
1241
1391
  const initRes = await fetch(`${gatewayUrl}/api/v1/onboarding/init`, {
1242
1392
  method: "POST",
1243
1393
  headers: {
@@ -1245,7 +1395,8 @@ async function completeLogin({ gatewayUrl, dashboardUrl, accessToken, refreshTok
1245
1395
  "Content-Type": "application/json"
1246
1396
  },
1247
1397
  body: JSON.stringify({
1248
- tenant_name: chosenName,
1398
+ tenant_name: chosen.name,
1399
+ tenant_slug: chosen.address || void 0,
1249
1400
  accepted_terms: true
1250
1401
  })
1251
1402
  });
@@ -1253,7 +1404,7 @@ async function completeLogin({ gatewayUrl, dashboardUrl, accessToken, refreshTok
1253
1404
  const initData = await initRes.json();
1254
1405
  primaryTenantId = initData.tenant_id;
1255
1406
  workspaceSlug = initData.tenant_slug;
1256
- workspaceName = initData.tenant_name || chosenName;
1407
+ workspaceName = initData.tenant_name || chosen.name;
1257
1408
  } else primaryTenantId = tenants[0].tenant_id;
1258
1409
  if (isVerbose) console.log(`[wawesome:verbose] Using tenant: ${primaryTenantId}`);
1259
1410
  console.log("[wawesome] Exchanging for tenant-scoped credentials...");
@@ -1544,6 +1695,17 @@ function wrapped(said) {
1544
1695
  function indented(said) {
1545
1696
  return wrapped(said).map((line) => `${VALUE_COLUMN}${line}`);
1546
1697
  }
1698
+ /**
1699
+ * What fits in `room` characters, with an ellipsis where it does not. Cut
1700
+ * rather than wrapped: a row that took two lines would push the row above it
1701
+ * out of sight. A room too small for even that shows nothing, so nothing ever
1702
+ * runs past the width it was given.
1703
+ */
1704
+ function truncate(said, room) {
1705
+ if (said.length <= room) return said;
1706
+ if (room <= 1) return "";
1707
+ return `${said.slice(0, room - 1)}…`;
1708
+ }
1547
1709
  //#endregion
1548
1710
  //#region src/domains.ts
1549
1711
  /**
@@ -1883,6 +2045,7 @@ function domainsUrl(creds, app) {
1883
2045
  function whereItGotTo(domain) {
1884
2046
  const { state } = domain;
1885
2047
  if (state.serving) return "serving";
2048
+ if (domain.proxied_in_public_dns) return "the CNAME is in at Cloudflare with the proxy on, so set that record to DNS only";
1886
2049
  if (state.certificate_active) return "certificate issued, so point the CNAME here to cut over";
1887
2050
  if (state.ownership_verified) return domain.certificate_record_pending ? "name verified, and the certificate record is not ready" : "name verified, so add the certificate CNAME below";
1888
2051
  if (domain.ownership_record_pending) return "not verified yet, and the TXT record is not ready";
@@ -2280,103 +2443,6 @@ function manifestOf(assets) {
2280
2443
  }));
2281
2444
  }
2282
2445
  //#endregion
2283
- //#region ../shared/public-address.ts
2284
- const INVOCATION_PREFIX = "/x";
2285
- const SLUG_SEPARATOR = "--";
2286
- const SURFACE_ROUTE = "/v1/invocation-surface";
2287
- const SUBTREE_NOTE = "Every path beneath this address reaches the Function.";
2288
- const ROOT_FUNCTION_SLUG = "root";
2289
- /**
2290
- * A gateway that serves no form of the address — and the value a client uses for
2291
- * one that did not answer at all, deliberately the same: both name no address,
2292
- * and there is nothing else either could honestly do.
2293
- */
2294
- const NO_INVOCATION_SURFACE = {
2295
- contentOrigin: null,
2296
- pathFormOrigin: null
2297
- };
2298
- async function fetchInvocationSurface(gatewayUrl) {
2299
- const origin = trimTrailingSlashes(gatewayUrl);
2300
- const res = await fetch(`${origin}${SURFACE_ROUTE}`);
2301
- if (!res.ok) throw new Error(`Failed to read the gateway's address forms (HTTP ${res.status}).`);
2302
- const body = await res.json();
2303
- return {
2304
- contentOrigin: body.content_origin ?? null,
2305
- pathFormOrigin: body.path_form ? origin : null
2306
- };
2307
- }
2308
- /** The one address the Function answers at, `null` where it has none. */
2309
- function publicAddress(surface, tenantSlug, appSlug, functionSlug) {
2310
- const base = appBase(surface, tenantSlug, appSlug);
2311
- if (base === null) return null;
2312
- if (functionSlug === "root") return base;
2313
- return `${base}/${encodeURIComponent(functionSlug)}`;
2314
- }
2315
- /**
2316
- * Where one of a Function's static files answers, `null` where none of them do.
2317
- *
2318
- * A **Static asset** is served on the App's own hostname and on no other
2319
- * address, so a gateway serving only the path form has no address to give for
2320
- * one — see **Static route** in `CONTEXT.md`.
2321
- */
2322
- function assetAddress(surface, tenantSlug, appSlug, functionSlug, assetPath) {
2323
- if (!surface.contentOrigin) return null;
2324
- const mount = publicAddress(surface, tenantSlug, appSlug, functionSlug);
2325
- if (mount === null) return null;
2326
- return `${mount}/${assetPath.split("/").map(encodeURIComponent).join("/")}`;
2327
- }
2328
- /** The same address with the app and function still to be chosen. */
2329
- function publicAddressTemplate(surface, tenantSlug) {
2330
- if (surface.contentOrigin) {
2331
- if (!isLegalDnsLabel(tenantSlug)) return null;
2332
- const hostname = appHostname(surface.contentOrigin, `${tenantSlug}${SLUG_SEPARATOR}<app>`);
2333
- return hostname === null ? null : `${hostname}/<function>`;
2334
- }
2335
- if (surface.pathFormOrigin) return `${pathFormBase(surface.pathFormOrigin, tenantSlug)}/<app>/<function>`;
2336
- return null;
2337
- }
2338
- function appBase(surface, tenantSlug, appSlug) {
2339
- if (surface.contentOrigin) {
2340
- if (!isLegalDnsLabel(tenantSlug) || !isLegalDnsLabel(appSlug)) return null;
2341
- return appHostname(surface.contentOrigin, `${tenantSlug}${SLUG_SEPARATOR}${appSlug}`);
2342
- }
2343
- if (surface.pathFormOrigin) return `${pathFormBase(surface.pathFormOrigin, tenantSlug)}/${encodeURIComponent(appSlug)}`;
2344
- return null;
2345
- }
2346
- /**
2347
- * The App's own hostname, built the way the gateway rebuilds it from its own
2348
- * configuration: the label one level beneath the content domain. Callers check
2349
- * the slugs the label is made of, since a slug the gateway would refuse to read
2350
- * a `Host` for names no App there.
2351
- */
2352
- function appHostname(contentOrigin, label) {
2353
- let origin;
2354
- try {
2355
- origin = new URL(contentOrigin);
2356
- } catch {
2357
- return null;
2358
- }
2359
- return `${origin.protocol}//${label}.${origin.host}`;
2360
- }
2361
- /** The surface, or none where the gateway did not answer. */
2362
- async function invocationSurfaceOrNone(gatewayUrl) {
2363
- try {
2364
- return await fetchInvocationSurface(gatewayUrl);
2365
- } catch {
2366
- return NO_INVOCATION_SURFACE;
2367
- }
2368
- }
2369
- function pathFormBase(origin, tenantSlug) {
2370
- return `${trimTrailingSlashes(origin)}${INVOCATION_PREFIX}/${encodeURIComponent(tenantSlug)}`;
2371
- }
2372
- /** Must match `dns_label::is_legal` in `server/crates/core/src/value_objects.rs`. */
2373
- function isLegalDnsLabel(candidate) {
2374
- return candidate.length > 0 && candidate.length <= 63 && !candidate.startsWith("-") && !candidate.endsWith("-") && !candidate.includes("--") && /^[a-z0-9-]+$/.test(candidate);
2375
- }
2376
- function trimTrailingSlashes(origin) {
2377
- return origin.replace(/\/+$/, "");
2378
- }
2379
- //#endregion
2380
2446
  //#region src/deploy-target.ts
2381
2447
  const RECORD_PATH = path.join(".wawesome", "last-deploy.json");
2382
2448
  function readLastDeployTarget(projectDir = process.cwd()) {
@@ -2909,7 +2975,8 @@ async function cronCommand(action, target, subtarget, options = {}) {
2909
2975
  * The address a **Document** answers at, relative to its Function's mount: a
2910
2976
  * request path whose last segment carries no extension resolves to that path,
2911
2977
  * to it with `.html` appended and to it with `/index.html` appended, so both
2912
- * `about.html` and `about/index.html` are the address `about`.
2978
+ * `about.html` and `about/index.html` are the address `about`. A last segment
2979
+ * spelled `index` skips the middle one, which is what this strips first.
2913
2980
  */
2914
2981
  function documentAddressPath(assetPath) {
2915
2982
  return assetPath.replace(/(^|\/)index\.html$/, "").replace(/\.html$/, "");
@@ -3036,6 +3103,17 @@ function declaredFields(declared) {
3036
3103
  };
3037
3104
  }
3038
3105
  /**
3106
+ * What this deploy says it carries.
3107
+ *
3108
+ * Trimmed here so that a flag whose value came out of a shell variable nobody
3109
+ * set is a deploy that said nothing rather than one the gateway turns down. How
3110
+ * long a message may be and whether it is one line are the platform's rule, and
3111
+ * holding them here as well is two places for them to drift.
3112
+ */
3113
+ function declaredMessage(stated) {
3114
+ return typeof stated === "string" ? stated.trim() || void 0 : void 0;
3115
+ }
3116
+ /**
3039
3117
  * The fallback this project declares.
3040
3118
  *
3041
3119
  * A declaration missing either half is refused here rather than at the gateway,
@@ -3106,7 +3184,8 @@ async function deploy(entryInput, options) {
3106
3184
  visibility: declaredVisibility(config.visibility),
3107
3185
  confirmPublish: Boolean(options.publish),
3108
3186
  confirmAssetDrop: Boolean(options.dropAssets),
3109
- fallback: declaredFallback(config.fallback)
3187
+ fallback: declaredFallback(config.fallback),
3188
+ message: declaredMessage(options.message)
3110
3189
  };
3111
3190
  const domainName = declaredDomain(config.domain);
3112
3191
  if (isVerbose) {
@@ -3165,7 +3244,10 @@ async function deploy(entryInput, options) {
3165
3244
  console.log(`[wawesome] Uploading code for ${app}/${funcName}...`);
3166
3245
  const uploadUrl = `${creds.gateway_url}/v1/apps/${encodeURIComponent(app)}/functions/${encodeURIComponent(funcName)}/code`;
3167
3246
  if (isVerbose) console.log(`[wawesome:verbose] POST ${uploadUrl}`);
3168
- const declaredOnTheWire = declaredFields(declared);
3247
+ const declaredOnTheWire = {
3248
+ ...declaredFields(declared),
3249
+ ...declared.message ? { message: declared.message } : {}
3250
+ };
3169
3251
  const uploadRes = assets.length > 0 || declaresSchedules || declaresFallback || Object.keys(declaredOnTheWire).length > 0 || jsCode === null ? await authorizedFetch(uploadUrl, {
3170
3252
  method: "POST",
3171
3253
  headers: {
@@ -3189,7 +3271,7 @@ async function deploy(entryInput, options) {
3189
3271
  });
3190
3272
  if (!uploadRes.ok) {
3191
3273
  const errorBody = await uploadRes.text();
3192
- const refusal = rejectionOf(errorBody, uploadRes.status, "Version with this code bundle already exists.");
3274
+ const refusal = rejectionOf(errorBody, uploadRes.status, uploadRes.status === 409 ? "Version with this code bundle already exists." : `Code upload failed (HTTP ${uploadRes.status}).`);
3193
3275
  if (uploadRes.status === 401 || uploadRes.status === 403) console.error(`[wawesome] Error: ${unauthorizedMessage(uploadRes.status, errorBody)}`);
3194
3276
  else if (refusal.reason === "paid-plan-required") {
3195
3277
  console.error(`\n[wawesome] \x1b[31mError: ${refusal.message}\x1b[0m`);
@@ -3211,7 +3293,7 @@ async function deploy(entryInput, options) {
3211
3293
  console.error("[wawesome] To switch active version, run: \x1B[36mwawesome version switch\x1B[0m\n");
3212
3294
  }
3213
3295
  } else {
3214
- console.error(`[wawesome] Error: Code upload failed (HTTP ${uploadRes.status}).`);
3296
+ console.error(`\n[wawesome] \x1b[31mError: ${refusal.message}\x1b[0m\n`);
3215
3297
  if (isVerbose) console.error(`[wawesome:verbose] Response: ${errorBody}`);
3216
3298
  }
3217
3299
  process.exit(1);
@@ -3482,11 +3564,6 @@ async function setEnvVar(key, value, options) {
3482
3564
  console.log("");
3483
3565
  } else console.log(`[wawesome] ✅ Set variable '${key}' on app '${app}'.`);
3484
3566
  }
3485
- function truncate(str, maxLen) {
3486
- if (str.length <= maxLen) return str;
3487
- if (maxLen <= 3) return str.slice(0, maxLen);
3488
- return str.slice(0, maxLen - 1) + "…";
3489
- }
3490
3567
  /**
3491
3568
  * List environment variables on the current app.
3492
3569
  */
@@ -3807,26 +3884,6 @@ function renderPostDeploy(postDeploy, address) {
3807
3884
  if (!address) return postDeploy;
3808
3885
  return postDeploy.split("{{url}}").join(address);
3809
3886
  }
3810
- /**
3811
- * Normalise arbitrary input into a legal DNS label, returning an empty string
3812
- * when nothing legal survives — callers decide whether that is an error.
3813
- */
3814
- function sanitizeSlug(raw) {
3815
- let label = "";
3816
- for (const c of raw.trim().toLowerCase()) if (c >= "a" && c <= "z") label += c;
3817
- else if (c >= "0" && c <= "9") label += c;
3818
- else if (label.length > 0 && !label.endsWith("-")) label += "-";
3819
- label = label.slice(0, 63);
3820
- while (label.endsWith("-")) label = label.slice(0, -1);
3821
- return label;
3822
- }
3823
- /**
3824
- * Whether `candidate` is already a legal DNS label — i.e. whether
3825
- * {@link sanitizeSlug} would leave it untouched.
3826
- */
3827
- function isLegalSlug(candidate) {
3828
- return candidate.length > 0 && candidate.length <= 63 && !candidate.startsWith("-") && !candidate.endsWith("-") && !candidate.includes("--") && /^[a-z0-9-]+$/.test(candidate);
3829
- }
3830
3887
  //#endregion
3831
3888
  //#region src/tarball.ts
3832
3889
  /**
@@ -4576,6 +4633,24 @@ export default {
4576
4633
  //#region src/version.ts
4577
4634
  /** How both surfaces name a version nobody can switch to. */
4578
4635
  const RECLAIMED = "⚠ [assets reclaimed]";
4636
+ /** Where the message sits, under the row rather than in a column of its own. */
4637
+ const MESSAGE_INDENT = " ".repeat(10);
4638
+ const ASSUMED_WIDTH = 80;
4639
+ function terminalWidth() {
4640
+ const columns = process.stdout.columns;
4641
+ return columns && columns > 40 ? columns : ASSUMED_WIDTH;
4642
+ }
4643
+ /**
4644
+ * The lines one version takes in the listing: its row, and the words its deploy
4645
+ * stated beneath it where it stated any.
4646
+ */
4647
+ function versionLines(v, activeNumber, width) {
4648
+ const isActive = v.version_number === activeNumber;
4649
+ const row = `${`#${v.version_number}`.padEnd(7)} | ${v.cwasm_hash.slice(0, 8).padEnd(8)} | ${new Date(v.created_at).toISOString().replace("T", " ").slice(0, 19).padEnd(20)} | ${v.status.padEnd(9)} | ${isActive ? "\x1B[32m★ [active]\x1B[0m" : v.rollbackable === false ? `\x1b[33m${RECLAIMED}\x1b[0m` : ""}`;
4650
+ const message = v.message?.trim();
4651
+ if (!message) return [row];
4652
+ return [row, `${MESSAGE_INDENT}\x1b[2m${truncate(message, width - MESSAGE_INDENT.length)}\x1b[0m`];
4653
+ }
4579
4654
  async function fetchVersions(gatewayUrl, tenantJwt, app, funcName, env, isVerbose) {
4580
4655
  const url = `${gatewayUrl}/v1/apps/${encodeURIComponent(app)}/functions/${encodeURIComponent(funcName)}/versions?environment=${encodeURIComponent(env)}`;
4581
4656
  if (isVerbose) console.log(`[wawesome:verbose] GET ${url}`);
@@ -4619,15 +4694,8 @@ async function listVersions(options) {
4619
4694
  console.log(`\n📦 \x1b[1mVersions for '${app}/${funcName}' (environment: ${env})\x1b[0m\n`);
4620
4695
  console.log("VERSION | HASH | CREATED AT | STATUS | ACTIVE");
4621
4696
  console.log("--------|----------|----------------------|-----------|-------");
4622
- for (const v of data.versions) {
4623
- const isActive = v.version_number === data.active_version_number;
4624
- const verStr = `#${v.version_number}`.padEnd(7);
4625
- const hashStr = v.cwasm_hash.slice(0, 8).padEnd(8);
4626
- const dateStr = new Date(v.created_at).toISOString().replace("T", " ").slice(0, 19).padEnd(20);
4627
- const statusStr = v.status.padEnd(9);
4628
- const badge = isActive ? "\x1B[32m★ [active]\x1B[0m" : v.rollbackable === false ? `\x1b[33m${RECLAIMED}\x1b[0m` : "";
4629
- console.log(`${verStr} | ${hashStr} | ${dateStr} | ${statusStr} | ${badge}`);
4630
- }
4697
+ const width = terminalWidth();
4698
+ for (const v of data.versions) for (const line of versionLines(v, data.active_version_number, width)) console.log(line);
4631
4699
  console.log("");
4632
4700
  }
4633
4701
  /**
@@ -4675,8 +4743,11 @@ async function switchVersion(targetInput, options) {
4675
4743
  const hashShort = v.cwasm_hash.slice(0, 8);
4676
4744
  const dateStr = new Date(v.created_at).toISOString().replace("T", " ").slice(0, 19);
4677
4745
  const badge = isActive ? " ★ [active]" : v.rollbackable === false ? ` ${RECLAIMED}` : "";
4746
+ const said = v.message?.trim();
4747
+ const row = `v${v.version_number} (${hashShort}) • ${dateStr}${badge}`;
4748
+ const room = terminalWidth() - row.length - 3 - 2;
4678
4749
  return {
4679
- name: `v${v.version_number} (${hashShort}) • ${dateStr}${badge}`,
4750
+ name: said ? `${row} • ${truncate(said, room)}` : row,
4680
4751
  value: v.version_number
4681
4752
  };
4682
4753
  });
@@ -4716,6 +4787,8 @@ async function switchVersion(targetInput, options) {
4716
4787
  console.log(` Function: ${funcName}`);
4717
4788
  console.log(` Environment: ${env}`);
4718
4789
  console.log(` Active Ver: v${selectedVersionNum}`);
4790
+ const restored = data.versions.find((v) => v.version_number === selectedVersionNum)?.message?.trim();
4791
+ if (restored) console.log(` Message: ${truncate(restored, terminalWidth() - 15)}`);
4719
4792
  console.log(`======================================================\n`);
4720
4793
  }
4721
4794
  //#endregion
@@ -6039,7 +6112,7 @@ async function credentialsCommand(action, target, options) {
6039
6112
  //#region src/index.ts
6040
6113
  const cli = cac("wawesome");
6041
6114
  cli.command("build [entry]", "Bundle a serverless function to an optimized JS file").option("-o, --out <path>", "Output JS bundle path", { default: "dist/index.js" }).option("-v, --verbose", "Enable verbose debug output").action((entry, options) => buildJs(entry, options));
6042
- cli.command("deploy [entry]", "Build, upload, and promote a serverless function").option("-o, --out <path>", "Output JS bundle path", { default: "dist/index.js" }).option("-v, --verbose", "Enable verbose debug output").option("--skip-build", "Skip the build step, deploy an already-built bundle").option("--publish", "Confirm putting a private function back on its public address").option("--drop-assets", "Confirm a deploy carrying no static files where the live version serves some").action((entry, options) => deploy(entry, options));
6115
+ cli.command("deploy [entry]", "Build, upload, and promote a serverless function").option("-o, --out <path>", "Output JS bundle path", { default: "dist/index.js" }).option("-v, --verbose", "Enable verbose debug output").option("--skip-build", "Skip the build step, deploy an already-built bundle").option("--publish", "Confirm putting a private function back on its public address").option("--drop-assets", "Confirm a deploy carrying no static files where the live version serves some").option("-m, --message <text>", "One line about what this deploy carries, kept on the version it makes").action((entry, options) => deploy(entry, options));
6043
6116
  cli.command("version [action] [target]", "Manage versions (e.g. 'version list', 'version switch [version]')").option("-e, --env <environment>", "Target environment (default: production)").option("-v, --verbose", "Enable verbose debug output").action((action, target, options) => {
6044
6117
  if (!action || action === "list" || action === "ls") return listVersions(options);
6045
6118
  if (action === "switch" || action === "use" || action === "select") return switchVersion(target, options);
@@ -6054,7 +6127,7 @@ cli.command("env rm <key>", "Delete an environment variable from the current app
6054
6127
  cli.command("domains [action] [hostname]", "Show the custom domain on the current app, its state and its DNS records").usage("domains [action] [hostname]\n\nWith no action, prints the app's domain, every half of it, where each got to, when the platform last checked, and the records to add.\n\nActions:\n adopt Write the attached domain into wawesome-function.json\n detach <hostname> Stop serving the app at a domain you attached\n\nA domain is attached by declaring it in wawesome-function.json and deploying, from the dashboard, or by an agent.").example("wawesome domains").example("wawesome domains adopt").example("wawesome domains detach shop.client.com").option("-a, --app <slug>", "App to act on (default: the app in wawesome-function.json)").action((action, hostname, options) => domainsCommand(action, hostname, options));
6055
6128
  cli.command("domains adopt", "Write the domain the current app answers at into wawesome-function.json").option("-a, --app <slug>", "App to act on (default: the app in wawesome-function.json)").action((options) => adoptDomainCommand(void 0, options));
6056
6129
  cli.command("domains detach <hostname>", "Stop serving the current app at a domain you attached").option("-a, --app <slug>", "App to act on (default: the app in wawesome-function.json)").action((hostname, options) => detachDomainCommand(hostname, options));
6057
- cli.command("login", "Authenticate with the wawesome.io platform").option("--api <url>", "API URL (default: https://api.wawesome.io)").option("--gateway <url>", "Alias for --api <url>").option("--dashboard <url>", "Dashboard URL (default: https://dashboard.wawesome.io)").option("--provider <name>", "OAuth provider (default: github)").option("--workspace <name>", "Name for the workspace, when signing up without a terminal to prompt").option("--accept-terms", "Accept the terms of service and privacy policy, when signing up without a terminal to prompt").option("-v, --verbose", "Enable verbose debug output").action((options) => login(options));
6130
+ cli.command("login", "Authenticate with the wawesome.io platform").option("--api <url>", "API URL (default: https://api.wawesome.io)").option("--gateway <url>", "Alias for --api <url>").option("--dashboard <url>", "Dashboard URL (default: https://dashboard.wawesome.io)").option("--provider <name>", "OAuth provider (default: github)").option("--workspace <name>", "Name for the workspace, when signing up without a terminal to prompt").option("--address <name>", "Public address for the workspace being created (default: one is minted)").option("--accept-terms", "Accept the terms of service and privacy policy, when signing up without a terminal to prompt").option("-v, --verbose", "Enable verbose debug output").action((options) => login(options));
6058
6131
  cli.command("logout", "Clear stored authentication credentials").action(() => logout());
6059
6132
  cli.command("whoami", "Show current login session info").action(() => whoami());
6060
6133
  cli.command("workspace [action] [name]", "Show the workspace, or rename its public address").usage("workspace <action> [name]\n\nActions:\n show Show the workspace name, address, and whether it can still change\n rename <name> Change the public address, while nothing live depends on it").example("wawesome workspace").example("wawesome workspace rename northwind").option("-v, --verbose", "Enable verbose debug output").action((action, name, options) => workspaceCommand(action, name, options));
package/dist/vite.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import { a as unsupportedGlobals, n as methodRemedy, r as polyfilledGlobals, t as declaredMethods } from "./guest-surface-CXON0L5V.mjs";
1
+ import { a as unsupportedGlobals, n as methodRemedy, r as polyfilledGlobals, t as declaredMethods } from "./guest-surface-4bHyTMIy.mjs";
2
2
  import fs from "node:fs";
3
3
  import path from "node:path";
4
4
  //#region src/vite.ts
@@ -1,4 +1,4 @@
1
- import { t as applyGuestParity } from "./guest-parity-Df4rlLDW.mjs";
1
+ import { t as applyGuestParity } from "./guest-parity-B5GLO6LA.mjs";
2
2
  //#region src/vitest-setup.ts
3
3
  applyGuestParity();
4
4
  //#endregion
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wawesome",
3
- "version": "0.18.0",
3
+ "version": "0.19.0",
4
4
  "description": "CLI tool for building and deploying serverless functions on wawesome.io platform",
5
5
  "type": "module",
6
6
  "bin": {
@@ -35,6 +35,9 @@
35
35
  "dist",
36
36
  "vite-env.d.ts"
37
37
  ],
38
+ "engines": {
39
+ "node": ">=22.13.0"
40
+ },
38
41
  "scripts": {
39
42
  "build": "tsdown",
40
43
  "dev": "tsdown --watch",