wawesome 0.18.1 → 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.1";
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
  /**
@@ -2281,103 +2443,6 @@ function manifestOf(assets) {
2281
2443
  }));
2282
2444
  }
2283
2445
  //#endregion
2284
- //#region ../shared/public-address.ts
2285
- const INVOCATION_PREFIX = "/x";
2286
- const SLUG_SEPARATOR = "--";
2287
- const SURFACE_ROUTE = "/v1/invocation-surface";
2288
- const SUBTREE_NOTE = "Every path beneath this address reaches the Function.";
2289
- const ROOT_FUNCTION_SLUG = "root";
2290
- /**
2291
- * A gateway that serves no form of the address — and the value a client uses for
2292
- * one that did not answer at all, deliberately the same: both name no address,
2293
- * and there is nothing else either could honestly do.
2294
- */
2295
- const NO_INVOCATION_SURFACE = {
2296
- contentOrigin: null,
2297
- pathFormOrigin: null
2298
- };
2299
- async function fetchInvocationSurface(gatewayUrl) {
2300
- const origin = trimTrailingSlashes(gatewayUrl);
2301
- const res = await fetch(`${origin}${SURFACE_ROUTE}`);
2302
- if (!res.ok) throw new Error(`Failed to read the gateway's address forms (HTTP ${res.status}).`);
2303
- const body = await res.json();
2304
- return {
2305
- contentOrigin: body.content_origin ?? null,
2306
- pathFormOrigin: body.path_form ? origin : null
2307
- };
2308
- }
2309
- /** The one address the Function answers at, `null` where it has none. */
2310
- function publicAddress(surface, tenantSlug, appSlug, functionSlug) {
2311
- const base = appBase(surface, tenantSlug, appSlug);
2312
- if (base === null) return null;
2313
- if (functionSlug === "root") return base;
2314
- return `${base}/${encodeURIComponent(functionSlug)}`;
2315
- }
2316
- /**
2317
- * Where one of a Function's static files answers, `null` where none of them do.
2318
- *
2319
- * A **Static asset** is served on the App's own hostname and on no other
2320
- * address, so a gateway serving only the path form has no address to give for
2321
- * one — see **Static route** in `CONTEXT.md`.
2322
- */
2323
- function assetAddress(surface, tenantSlug, appSlug, functionSlug, assetPath) {
2324
- if (!surface.contentOrigin) return null;
2325
- const mount = publicAddress(surface, tenantSlug, appSlug, functionSlug);
2326
- if (mount === null) return null;
2327
- return `${mount}/${assetPath.split("/").map(encodeURIComponent).join("/")}`;
2328
- }
2329
- /** The same address with the app and function still to be chosen. */
2330
- function publicAddressTemplate(surface, tenantSlug) {
2331
- if (surface.contentOrigin) {
2332
- if (!isLegalDnsLabel(tenantSlug)) return null;
2333
- const hostname = appHostname(surface.contentOrigin, `${tenantSlug}${SLUG_SEPARATOR}<app>`);
2334
- return hostname === null ? null : `${hostname}/<function>`;
2335
- }
2336
- if (surface.pathFormOrigin) return `${pathFormBase(surface.pathFormOrigin, tenantSlug)}/<app>/<function>`;
2337
- return null;
2338
- }
2339
- function appBase(surface, tenantSlug, appSlug) {
2340
- if (surface.contentOrigin) {
2341
- if (!isLegalDnsLabel(tenantSlug) || !isLegalDnsLabel(appSlug)) return null;
2342
- return appHostname(surface.contentOrigin, `${tenantSlug}${SLUG_SEPARATOR}${appSlug}`);
2343
- }
2344
- if (surface.pathFormOrigin) return `${pathFormBase(surface.pathFormOrigin, tenantSlug)}/${encodeURIComponent(appSlug)}`;
2345
- return null;
2346
- }
2347
- /**
2348
- * The App's own hostname, built the way the gateway rebuilds it from its own
2349
- * configuration: the label one level beneath the content domain. Callers check
2350
- * the slugs the label is made of, since a slug the gateway would refuse to read
2351
- * a `Host` for names no App there.
2352
- */
2353
- function appHostname(contentOrigin, label) {
2354
- let origin;
2355
- try {
2356
- origin = new URL(contentOrigin);
2357
- } catch {
2358
- return null;
2359
- }
2360
- return `${origin.protocol}//${label}.${origin.host}`;
2361
- }
2362
- /** The surface, or none where the gateway did not answer. */
2363
- async function invocationSurfaceOrNone(gatewayUrl) {
2364
- try {
2365
- return await fetchInvocationSurface(gatewayUrl);
2366
- } catch {
2367
- return NO_INVOCATION_SURFACE;
2368
- }
2369
- }
2370
- function pathFormBase(origin, tenantSlug) {
2371
- return `${trimTrailingSlashes(origin)}${INVOCATION_PREFIX}/${encodeURIComponent(tenantSlug)}`;
2372
- }
2373
- /** Must match `dns_label::is_legal` in `server/crates/core/src/value_objects.rs`. */
2374
- function isLegalDnsLabel(candidate) {
2375
- return candidate.length > 0 && candidate.length <= 63 && !candidate.startsWith("-") && !candidate.endsWith("-") && !candidate.includes("--") && /^[a-z0-9-]+$/.test(candidate);
2376
- }
2377
- function trimTrailingSlashes(origin) {
2378
- return origin.replace(/\/+$/, "");
2379
- }
2380
- //#endregion
2381
2446
  //#region src/deploy-target.ts
2382
2447
  const RECORD_PATH = path.join(".wawesome", "last-deploy.json");
2383
2448
  function readLastDeployTarget(projectDir = process.cwd()) {
@@ -2910,7 +2975,8 @@ async function cronCommand(action, target, subtarget, options = {}) {
2910
2975
  * The address a **Document** answers at, relative to its Function's mount: a
2911
2976
  * request path whose last segment carries no extension resolves to that path,
2912
2977
  * to it with `.html` appended and to it with `/index.html` appended, so both
2913
- * `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.
2914
2980
  */
2915
2981
  function documentAddressPath(assetPath) {
2916
2982
  return assetPath.replace(/(^|\/)index\.html$/, "").replace(/\.html$/, "");
@@ -3037,6 +3103,17 @@ function declaredFields(declared) {
3037
3103
  };
3038
3104
  }
3039
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
+ /**
3040
3117
  * The fallback this project declares.
3041
3118
  *
3042
3119
  * A declaration missing either half is refused here rather than at the gateway,
@@ -3107,7 +3184,8 @@ async function deploy(entryInput, options) {
3107
3184
  visibility: declaredVisibility(config.visibility),
3108
3185
  confirmPublish: Boolean(options.publish),
3109
3186
  confirmAssetDrop: Boolean(options.dropAssets),
3110
- fallback: declaredFallback(config.fallback)
3187
+ fallback: declaredFallback(config.fallback),
3188
+ message: declaredMessage(options.message)
3111
3189
  };
3112
3190
  const domainName = declaredDomain(config.domain);
3113
3191
  if (isVerbose) {
@@ -3166,7 +3244,10 @@ async function deploy(entryInput, options) {
3166
3244
  console.log(`[wawesome] Uploading code for ${app}/${funcName}...`);
3167
3245
  const uploadUrl = `${creds.gateway_url}/v1/apps/${encodeURIComponent(app)}/functions/${encodeURIComponent(funcName)}/code`;
3168
3246
  if (isVerbose) console.log(`[wawesome:verbose] POST ${uploadUrl}`);
3169
- const declaredOnTheWire = declaredFields(declared);
3247
+ const declaredOnTheWire = {
3248
+ ...declaredFields(declared),
3249
+ ...declared.message ? { message: declared.message } : {}
3250
+ };
3170
3251
  const uploadRes = assets.length > 0 || declaresSchedules || declaresFallback || Object.keys(declaredOnTheWire).length > 0 || jsCode === null ? await authorizedFetch(uploadUrl, {
3171
3252
  method: "POST",
3172
3253
  headers: {
@@ -3190,7 +3271,7 @@ async function deploy(entryInput, options) {
3190
3271
  });
3191
3272
  if (!uploadRes.ok) {
3192
3273
  const errorBody = await uploadRes.text();
3193
- 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}).`);
3194
3275
  if (uploadRes.status === 401 || uploadRes.status === 403) console.error(`[wawesome] Error: ${unauthorizedMessage(uploadRes.status, errorBody)}`);
3195
3276
  else if (refusal.reason === "paid-plan-required") {
3196
3277
  console.error(`\n[wawesome] \x1b[31mError: ${refusal.message}\x1b[0m`);
@@ -3212,7 +3293,7 @@ async function deploy(entryInput, options) {
3212
3293
  console.error("[wawesome] To switch active version, run: \x1B[36mwawesome version switch\x1B[0m\n");
3213
3294
  }
3214
3295
  } else {
3215
- console.error(`[wawesome] Error: Code upload failed (HTTP ${uploadRes.status}).`);
3296
+ console.error(`\n[wawesome] \x1b[31mError: ${refusal.message}\x1b[0m\n`);
3216
3297
  if (isVerbose) console.error(`[wawesome:verbose] Response: ${errorBody}`);
3217
3298
  }
3218
3299
  process.exit(1);
@@ -3483,11 +3564,6 @@ async function setEnvVar(key, value, options) {
3483
3564
  console.log("");
3484
3565
  } else console.log(`[wawesome] ✅ Set variable '${key}' on app '${app}'.`);
3485
3566
  }
3486
- function truncate(str, maxLen) {
3487
- if (str.length <= maxLen) return str;
3488
- if (maxLen <= 3) return str.slice(0, maxLen);
3489
- return str.slice(0, maxLen - 1) + "…";
3490
- }
3491
3567
  /**
3492
3568
  * List environment variables on the current app.
3493
3569
  */
@@ -3808,26 +3884,6 @@ function renderPostDeploy(postDeploy, address) {
3808
3884
  if (!address) return postDeploy;
3809
3885
  return postDeploy.split("{{url}}").join(address);
3810
3886
  }
3811
- /**
3812
- * Normalise arbitrary input into a legal DNS label, returning an empty string
3813
- * when nothing legal survives — callers decide whether that is an error.
3814
- */
3815
- function sanitizeSlug(raw) {
3816
- let label = "";
3817
- for (const c of raw.trim().toLowerCase()) if (c >= "a" && c <= "z") label += c;
3818
- else if (c >= "0" && c <= "9") label += c;
3819
- else if (label.length > 0 && !label.endsWith("-")) label += "-";
3820
- label = label.slice(0, 63);
3821
- while (label.endsWith("-")) label = label.slice(0, -1);
3822
- return label;
3823
- }
3824
- /**
3825
- * Whether `candidate` is already a legal DNS label — i.e. whether
3826
- * {@link sanitizeSlug} would leave it untouched.
3827
- */
3828
- function isLegalSlug(candidate) {
3829
- return candidate.length > 0 && candidate.length <= 63 && !candidate.startsWith("-") && !candidate.endsWith("-") && !candidate.includes("--") && /^[a-z0-9-]+$/.test(candidate);
3830
- }
3831
3887
  //#endregion
3832
3888
  //#region src/tarball.ts
3833
3889
  /**
@@ -4577,6 +4633,24 @@ export default {
4577
4633
  //#region src/version.ts
4578
4634
  /** How both surfaces name a version nobody can switch to. */
4579
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
+ }
4580
4654
  async function fetchVersions(gatewayUrl, tenantJwt, app, funcName, env, isVerbose) {
4581
4655
  const url = `${gatewayUrl}/v1/apps/${encodeURIComponent(app)}/functions/${encodeURIComponent(funcName)}/versions?environment=${encodeURIComponent(env)}`;
4582
4656
  if (isVerbose) console.log(`[wawesome:verbose] GET ${url}`);
@@ -4620,15 +4694,8 @@ async function listVersions(options) {
4620
4694
  console.log(`\n📦 \x1b[1mVersions for '${app}/${funcName}' (environment: ${env})\x1b[0m\n`);
4621
4695
  console.log("VERSION | HASH | CREATED AT | STATUS | ACTIVE");
4622
4696
  console.log("--------|----------|----------------------|-----------|-------");
4623
- for (const v of data.versions) {
4624
- const isActive = v.version_number === data.active_version_number;
4625
- const verStr = `#${v.version_number}`.padEnd(7);
4626
- const hashStr = v.cwasm_hash.slice(0, 8).padEnd(8);
4627
- const dateStr = new Date(v.created_at).toISOString().replace("T", " ").slice(0, 19).padEnd(20);
4628
- const statusStr = v.status.padEnd(9);
4629
- const badge = isActive ? "\x1B[32m★ [active]\x1B[0m" : v.rollbackable === false ? `\x1b[33m${RECLAIMED}\x1b[0m` : "";
4630
- console.log(`${verStr} | ${hashStr} | ${dateStr} | ${statusStr} | ${badge}`);
4631
- }
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);
4632
4699
  console.log("");
4633
4700
  }
4634
4701
  /**
@@ -4676,8 +4743,11 @@ async function switchVersion(targetInput, options) {
4676
4743
  const hashShort = v.cwasm_hash.slice(0, 8);
4677
4744
  const dateStr = new Date(v.created_at).toISOString().replace("T", " ").slice(0, 19);
4678
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;
4679
4749
  return {
4680
- name: `v${v.version_number} (${hashShort}) • ${dateStr}${badge}`,
4750
+ name: said ? `${row} • ${truncate(said, room)}` : row,
4681
4751
  value: v.version_number
4682
4752
  };
4683
4753
  });
@@ -4717,6 +4787,8 @@ async function switchVersion(targetInput, options) {
4717
4787
  console.log(` Function: ${funcName}`);
4718
4788
  console.log(` Environment: ${env}`);
4719
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)}`);
4720
4792
  console.log(`======================================================\n`);
4721
4793
  }
4722
4794
  //#endregion
@@ -6040,7 +6112,7 @@ async function credentialsCommand(action, target, options) {
6040
6112
  //#region src/index.ts
6041
6113
  const cli = cac("wawesome");
6042
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));
6043
- 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));
6044
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) => {
6045
6117
  if (!action || action === "list" || action === "ls") return listVersions(options);
6046
6118
  if (action === "switch" || action === "use" || action === "select") return switchVersion(target, options);
@@ -6055,7 +6127,7 @@ cli.command("env rm <key>", "Delete an environment variable from the current app
6055
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));
6056
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));
6057
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));
6058
- 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));
6059
6131
  cli.command("logout", "Clear stored authentication credentials").action(() => logout());
6060
6132
  cli.command("whoami", "Show current login session info").action(() => whoami());
6061
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.1",
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": {