wawesome 0.22.0 → 0.24.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.
package/dist/index.mjs CHANGED
@@ -1,3 +1,4 @@
1
+ import { C as asGatewayError, E as rejectionOf, S as GatewayError, T as reasonOf, a as manifestOf, b as reportSessionCommandRefused, c as getDashboardUrl, d as readCredentials, f as readFunctionConfig, g as CREDENTIAL_ENV_VAR, h as writeCredentials, l as getFunctionConfigPath, m as resolveGatewayUrlWithoutSession, n as deployDigest, o as LOGIN_CALLBACK_PORT, p as resolveDashboardUrlForLogin, r as isDocument, s as deleteCredentials, t as collectAssets, u as getGatewayUrl, v as credentialFromEnvironment, w as errorText, x as unauthorizedMessage, y as credentialPrefix } from "./assets-Cgw3_rAq.mjs";
1
2
  import { i as polyfilledGlobals, n as declaredModules, o as unsupportedGlobals, r as methodRemedy, t as declaredMethods } from "./guest-surface-BwrNP2Cz.mjs";
2
3
  import cac from "cac";
3
4
  import fs from "node:fs";
@@ -10,7 +11,7 @@ import crypto from "node:crypto";
10
11
  import http from "node:http";
11
12
  import { Readable } from "node:stream";
12
13
  import readline from "node:readline";
13
- import { confirm, select } from "@inquirer/prompts";
14
+ import { confirm, input, select } from "@inquirer/prompts";
14
15
  import zlib from "node:zlib";
15
16
  //#region src/brand.ts
16
17
  const PLAIN = "[w] wawesome";
@@ -26,249 +27,6 @@ function lockup(stream = process.stdout, env = process.env) {
26
27
  }
27
28
  const LOCKUP_HTML = "<div style=\"margin-bottom: 24px; font: 700 20px/1 ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; letter-spacing: -0.02em; color: #e8eaf0\"><span style=\"font-size: 22px; letter-spacing: -0.06em\"><span style=\"color: #6b7cff\">[</span>w<span style=\"color: #6b7cff\">]</span></span>&nbsp;wawesome</div>";
28
29
  //#endregion
29
- //#region src/errors.ts
30
- /**
31
- * A rejection carrying the gateway's own prose and, where given, its reason.
32
- *
33
- * The call's trace id rides in the message itself, on a line of its own, so
34
- * every place that prints a refusal prints the one thing support can search by.
35
- */
36
- var GatewayError = class extends Error {
37
- status;
38
- reason;
39
- /** The raw response, for `--verbose` to print when the prose is not enough. */
40
- body;
41
- traceId;
42
- constructor(message, status, reason, body = "", traceId) {
43
- super(traceId ? `${message}\n${traceLine(traceId)}` : message);
44
- this.name = "GatewayError";
45
- this.status = status;
46
- this.reason = reason;
47
- this.body = body;
48
- this.traceId = traceId;
49
- }
50
- };
51
- function traceLine(traceId) {
52
- return `Trace ID: ${traceId}`;
53
- }
54
- /**
55
- * Read a failed response into a `GatewayError`.
56
- *
57
- * `fallback` is used only when the response carries no prose of its own — a
58
- * proxy's HTML error page, or an empty body. A 401 is deliberately *not* special
59
- * cased here: what to say about an expired session depends on what the caller
60
- * was doing, so callers handle it before reaching this.
61
- */
62
- async function asGatewayError(res, fallback) {
63
- const traceId = res.headers?.get("x-wawesome-trace-id") ?? void 0;
64
- return rejectionOf(await res.text().catch(() => ""), res.status, fallback, traceId);
65
- }
66
- /**
67
- * The same reading, for a caller that has already taken the body off the wire.
68
- * `traceId` is the header's, for a body that never made it here as JSON.
69
- */
70
- function rejectionOf(body, status, fallback, traceId) {
71
- try {
72
- const parsed = JSON.parse(body);
73
- return new GatewayError(parsed.error || fallback, status, parsed.reason, body, parsed.trace_id || traceId);
74
- } catch {
75
- return new GatewayError(fallback, status, void 0, body, traceId);
76
- }
77
- }
78
- /** Why the gateway refused, where what was thrown might not be a refusal at all. */
79
- function reasonOf(err) {
80
- return err instanceof GatewayError ? err.reason : void 0;
81
- }
82
- /** What went wrong, as text, whatever was thrown. */
83
- function errorText(err) {
84
- return err instanceof Error ? err.message : String(err);
85
- }
86
- //#endregion
87
- //#region src/credential.ts
88
- const CREDENTIAL_ENV_VAR = "WAWESOME_DEPLOY_CREDENTIAL";
89
- function credentialFromEnvironment() {
90
- const secret = process.env[CREDENTIAL_ENV_VAR]?.trim();
91
- return secret ? secret : null;
92
- }
93
- /** Enough to match a credential against the one in a CI settings page, and no more. */
94
- function credentialPrefix(secret) {
95
- return secret.slice(0, 11);
96
- }
97
- /**
98
- * A credential has no session behind it, so the platform's own reason — unknown,
99
- * revoked, expired, or a scope it does not carry — is the whole answer, and
100
- * "sign in again" names a door a CI runner cannot open.
101
- */
102
- function unauthorizedMessage(status, body) {
103
- const secret = credentialFromEnvironment();
104
- if (!secret) {
105
- const refusal = rejectionOf(body, status, `The platform refused this request (HTTP ${status}).`);
106
- if (status !== 401) return refusal.message;
107
- const expired = "Authentication expired. Run 'wawesome login' again.";
108
- return refusal.traceId ? `${expired}\n${traceLine(refusal.traceId)}` : expired;
109
- }
110
- return rejectionOf(body, status, `The deploy credential ${credentialPrefix(secret)} was refused (HTTP ${status}).`).message;
111
- }
112
- function reportSessionCommandRefused(secret, consequence) {
113
- console.error(`[wawesome] Error: ${CREDENTIAL_ENV_VAR} is set, so this CLI authenticates as deploy credential ${credentialPrefix(secret)}.`);
114
- console.error(`[wawesome] ${consequence} Unset ${CREDENTIAL_ENV_VAR} first.`);
115
- }
116
- //#endregion
117
- //#region src/config.ts
118
- /** Path to the user-level settings file (~/.wawesome/settings.json) */
119
- function getSettingsPath() {
120
- return path.join(os.homedir(), ".wawesome", "settings.json");
121
- }
122
- /**
123
- * Read stored user settings from ~/.wawesome/settings.json.
124
- */
125
- function readSettings() {
126
- const settingsPath = getSettingsPath();
127
- if (!fs.existsSync(settingsPath)) return {};
128
- try {
129
- const raw = fs.readFileSync(settingsPath, "utf-8");
130
- return JSON.parse(raw);
131
- } catch {
132
- return {};
133
- }
134
- }
135
- const GATEWAY_ENV_NAMES = ["GATEWAY_URL", "WAWESOME_GATEWAY_URL"];
136
- const DASHBOARD_ENV_NAMES = ["DASHBOARD_URL", "WAWESOME_DASHBOARD_URL"];
137
- const GATEWAY_URL_FALLBACK = "https://api.wawesome.io";
138
- const DASHBOARD_URL_FALLBACK = "https://dashboard.wawesome.io";
139
- function resolveAddress(overrideUrl, envNames, settingsKey, stored, fallback) {
140
- if (overrideUrl) return overrideUrl;
141
- for (const name of envNames) {
142
- const fromEnv = process.env[name];
143
- if (fromEnv) return fromEnv;
144
- }
145
- const configured = readSettings()[settingsKey];
146
- if (configured && typeof configured === "string") return configured;
147
- return stored || fallback;
148
- }
149
- function getGatewayUrl(overrideUrl) {
150
- return resolveAddress(overrideUrl, GATEWAY_ENV_NAMES, "gateway_url", readStoredCredentials()?.gateway_url, GATEWAY_URL_FALLBACK);
151
- }
152
- /**
153
- * A tenant token is only valid at the gateway that issued it, so every other
154
- * command has to follow the credentials. A login holds no token yet, and
155
- * reading that rung here made a local gateway outlive the removal of every
156
- * documented override, since only `logout` clears it. A **Deploy credential**
157
- * reads the same rungs, and for a sharper reason: following a stale file would
158
- * present it at a gateway that never minted it.
159
- */
160
- function resolveGatewayUrlWithoutSession(overrideUrl) {
161
- return resolveAddress(overrideUrl, GATEWAY_ENV_NAMES, "gateway_url", void 0, GATEWAY_URL_FALLBACK);
162
- }
163
- getGatewayUrl();
164
- function getDashboardUrl(overrideUrl) {
165
- return resolveAddress(overrideUrl, DASHBOARD_ENV_NAMES, "dashboard_url", readStoredCredentials()?.dashboard_url, DASHBOARD_URL_FALLBACK);
166
- }
167
- /**
168
- * The dashboard for the environment this login is going to.
169
- *
170
- * Taken from the address the platform handed back to open, so a login that
171
- * configures nothing stores the dashboard belonging to the gateway it signed
172
- * into. An explicit flag, `DASHBOARD_URL` or a settings file still outrank it,
173
- * on the rung they outrank every other address on.
174
- */
175
- function resolveDashboardUrlForLogin(overrideUrl, signInUrl) {
176
- return resolveAddress(overrideUrl, DASHBOARD_ENV_NAMES, "dashboard_url", originOf(signInUrl), DASHBOARD_URL_FALLBACK);
177
- }
178
- /** Undefined rather than throwing: an unreadable address falls to the rung below. */
179
- function originOf(url) {
180
- try {
181
- return new URL(url).origin;
182
- } catch {
183
- return;
184
- }
185
- }
186
- const LOGIN_CALLBACK_PORT = 9999;
187
- /** Path to the user-level credentials file */
188
- function getCredentialsPath() {
189
- return path.join(os.homedir(), ".wawesome", "credentials.json");
190
- }
191
- /**
192
- * What this process authenticates with: the **Deploy credential** in the
193
- * environment where one is set, and otherwise the session `login` left on disk.
194
- *
195
- * The environment wins outright rather than filling a gap, so a CI runner
196
- * carrying a stale cached home directory still authenticates as itself.
197
- */
198
- function readCredentials() {
199
- const secret = credentialFromEnvironment();
200
- return secret ? environmentCredential(secret) : readStoredCredentials();
201
- }
202
- /**
203
- * A credential wears the shape of a session so that every command reaches the
204
- * platform the same way. What it does not carry is the point: no refresh token,
205
- * because there is nothing behind it to renew, and no Tenant, because the
206
- * platform resolves that from the credential.
207
- */
208
- function environmentCredential(secret) {
209
- return {
210
- gateway_url: resolveGatewayUrlWithoutSession(void 0),
211
- tenant_jwt: secret,
212
- credential_prefix: credentialPrefix(secret)
213
- };
214
- }
215
- /** The session on disk, whatever the environment says. */
216
- function readStoredCredentials() {
217
- const credPath = getCredentialsPath();
218
- if (!fs.existsSync(credPath)) return null;
219
- try {
220
- const raw = fs.readFileSync(credPath, "utf-8");
221
- return JSON.parse(raw);
222
- } catch {
223
- return null;
224
- }
225
- }
226
- /**
227
- * Write credentials to disk, creating the directory if needed.
228
- *
229
- * Owner-only, because the file holds a refresh token. The `chmod` is what
230
- * tightens a file an older CLI left world-readable; a mode passed to
231
- * `writeFileSync` applies only where it creates the file.
232
- */
233
- function writeCredentials(creds) {
234
- if (credentialFromEnvironment()) return;
235
- const credPath = getCredentialsPath();
236
- const dir = path.dirname(credPath);
237
- fs.mkdirSync(dir, {
238
- recursive: true,
239
- mode: 448
240
- });
241
- fs.writeFileSync(credPath, JSON.stringify(creds, null, 2), {
242
- encoding: "utf-8",
243
- mode: 384
244
- });
245
- fs.chmodSync(credPath, 384);
246
- }
247
- /**
248
- * Delete stored credentials.
249
- */
250
- function deleteCredentials() {
251
- const credPath = getCredentialsPath();
252
- if (fs.existsSync(credPath)) {
253
- fs.rmSync(credPath, { force: true });
254
- return true;
255
- }
256
- return false;
257
- }
258
- function getFunctionConfigPath(projectDir) {
259
- return path.join(projectDir || process.cwd(), "wawesome-function.json");
260
- }
261
- function readFunctionConfig(projectDir) {
262
- const configPath = getFunctionConfigPath(projectDir);
263
- if (!fs.existsSync(configPath)) return null;
264
- try {
265
- const raw = fs.readFileSync(configPath, "utf-8");
266
- return JSON.parse(raw);
267
- } catch {
268
- return null;
269
- }
270
- }
271
- //#endregion
272
30
  //#region src/sourcemap.ts
273
31
  const BASE64 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
274
32
  const NO_POSITION = () => null;
@@ -929,7 +687,7 @@ async function buildHandler(entryInput, config, options) {
929
687
  * that has to name this version — `--version`, the dependency a scaffolded
930
688
  * project pins — reads it here, so a release bumps one file.
931
689
  */
932
- const CLI_VERSION = "0.22.0";
690
+ const CLI_VERSION = "0.24.0";
933
691
  //#endregion
934
692
  //#region src/cli-login.ts
935
693
  const START_PATH = "/v1/cli/login/start";
@@ -1309,6 +1067,44 @@ function endSession(said) {
1309
1067
  process.exit(1);
1310
1068
  }
1311
1069
  //#endregion
1070
+ //#region src/schedules.ts
1071
+ /**
1072
+ * When a schedule next runs, or why it does not run at all.
1073
+ *
1074
+ * The three ways of being off never render alike, and a schedule that is off
1075
+ * twice over says so: someone who reads only the pause will declare it again,
1076
+ * press resume, and file a bug when neither starts it.
1077
+ */
1078
+ function whenItRuns(schedule) {
1079
+ const undeclared = schedule.declared === false;
1080
+ if (schedule.state === "suspended") {
1081
+ const why = "The workspace owes payment, its subscription ended, or it has spent past its plan's allowance";
1082
+ return undeclared ? `suspended, and not in this config file. ${why}; declare it again too` : `suspended. ${why}`;
1083
+ }
1084
+ if (schedule.state === "paused") return undeclared ? "paused, and not in this config file. Declare it again, then resume" : "paused. Resume it to run again";
1085
+ if (undeclared) return "not in this config file, so it is disabled. Its history is kept";
1086
+ if (schedule.state !== "active") return schedule.state;
1087
+ return schedule.next_fire_at ? `next run ${asUtc$1(schedule.next_fire_at)}` : "next run unknown";
1088
+ }
1089
+ /** `2026-08-23 03:00 UTC`, which is the zone every expression is read in. */
1090
+ function asUtc$1(iso) {
1091
+ const at = new Date(iso);
1092
+ if (Number.isNaN(at.getTime())) return iso;
1093
+ const pad = (n) => String(n).padStart(2, "0");
1094
+ return `${at.getUTCFullYear()}-${pad(at.getUTCMonth() + 1)}-${pad(at.getUTCDate())} ${pad(at.getUTCHours())}:${pad(at.getUTCMinutes())} UTC`;
1095
+ }
1096
+ /**
1097
+ * The block a deploy prints for the schedules it just applied. Every schedule
1098
+ * appears, including the ones that are off, so putting recurring compute on the
1099
+ * bill — or taking it off — is never silent.
1100
+ */
1101
+ function scheduleLines(schedules) {
1102
+ if (schedules.length === 0) return [];
1103
+ const nameWidth = Math.max(...schedules.map((s) => s.name.length));
1104
+ const expressionWidth = Math.max(...schedules.map((s) => s.expression.length));
1105
+ return [" Schedules:", ...[...schedules].sort((a, b) => a.name.localeCompare(b.name)).map((schedule) => ` ${schedule.name.padEnd(nameWidth)} ${schedule.expression.padEnd(expressionWidth)} ${whenItRuns(schedule)}`)];
1106
+ }
1107
+ //#endregion
1312
1108
  //#region src/tenant.ts
1313
1109
  async function fetchTenantDetails(creds) {
1314
1110
  const res = await authorizedFetch(`${creds.gateway_url}/v1/tenant`, { headers: { Authorization: `Bearer ${creds.tenant_jwt}` } });
@@ -1344,8 +1140,12 @@ async function resolveWorkspace(creds) {
1344
1140
  * telling a user different things about the same reason — and `retryable` is
1345
1141
  * what separates a name worth retyping from one that no name would fix.
1346
1142
  */
1347
- function renameAdvice(reason) {
1348
- switch (reason) {
1143
+ function renameAdvice(err) {
1144
+ if (!(err instanceof GatewayError)) return {
1145
+ text: "",
1146
+ retryable: false
1147
+ };
1148
+ switch (err.reason) {
1349
1149
  case "taken":
1350
1150
  case "reserved": return {
1351
1151
  text: "Pick a different name.",
@@ -1360,7 +1160,7 @@ function renameAdvice(reason) {
1360
1160
  retryable: true
1361
1161
  };
1362
1162
  case "locked": return {
1363
- text: "A deploy landed while this was running, which fixed the address for good.",
1163
+ text: err.renamableAt ? nextRenameAllowed(err.renamableAt) : "",
1364
1164
  retryable: false
1365
1165
  };
1366
1166
  default: return {
@@ -1369,6 +1169,9 @@ function renameAdvice(reason) {
1369
1169
  };
1370
1170
  }
1371
1171
  }
1172
+ function nextRenameAllowed(renamableAt) {
1173
+ return `The next rename is allowed from ${asUtc$1(renamableAt)}.`;
1174
+ }
1372
1175
  /**
1373
1176
  * Ask for the workspace's public address to become `slug`.
1374
1177
  *
@@ -1548,44 +1351,6 @@ async function reportCredential(creds) {
1548
1351
  console.log("");
1549
1352
  }
1550
1353
  //#endregion
1551
- //#region src/schedules.ts
1552
- /**
1553
- * When a schedule next runs, or why it does not run at all.
1554
- *
1555
- * The three ways of being off never render alike, and a schedule that is off
1556
- * twice over says so: someone who reads only the pause will declare it again,
1557
- * press resume, and file a bug when neither starts it.
1558
- */
1559
- function whenItRuns(schedule) {
1560
- const undeclared = schedule.declared === false;
1561
- if (schedule.state === "suspended") {
1562
- const why = "The workspace owes payment, its subscription ended, or it has spent past its plan's allowance";
1563
- return undeclared ? `suspended, and not in this config file. ${why}; declare it again too` : `suspended. ${why}`;
1564
- }
1565
- if (schedule.state === "paused") return undeclared ? "paused, and not in this config file. Declare it again, then resume" : "paused. Resume it to run again";
1566
- if (undeclared) return "not in this config file, so it is disabled. Its history is kept";
1567
- if (schedule.state !== "active") return schedule.state;
1568
- return schedule.next_fire_at ? `next run ${asUtc$1(schedule.next_fire_at)}` : "next run unknown";
1569
- }
1570
- /** `2026-08-23 03:00 UTC`, which is the zone every expression is read in. */
1571
- function asUtc$1(iso) {
1572
- const at = new Date(iso);
1573
- if (Number.isNaN(at.getTime())) return iso;
1574
- const pad = (n) => String(n).padStart(2, "0");
1575
- return `${at.getUTCFullYear()}-${pad(at.getUTCMonth() + 1)}-${pad(at.getUTCDate())} ${pad(at.getUTCHours())}:${pad(at.getUTCMinutes())} UTC`;
1576
- }
1577
- /**
1578
- * The block a deploy prints for the schedules it just applied. Every schedule
1579
- * appears, including the ones that are off, so putting recurring compute on the
1580
- * bill — or taking it off — is never silent.
1581
- */
1582
- function scheduleLines(schedules) {
1583
- if (schedules.length === 0) return [];
1584
- const nameWidth = Math.max(...schedules.map((s) => s.name.length));
1585
- const expressionWidth = Math.max(...schedules.map((s) => s.expression.length));
1586
- return [" Schedules:", ...[...schedules].sort((a, b) => a.name.localeCompare(b.name)).map((schedule) => ` ${schedule.name.padEnd(nameWidth)} ${schedule.expression.padEnd(expressionWidth)} ${whenItRuns(schedule)}`)];
1587
- }
1588
- //#endregion
1589
1354
  //#region src/billing.ts
1590
1355
  function billingPageUrl() {
1591
1356
  const base = getDashboardUrl().replace(/\/+$/, "");
@@ -2315,84 +2080,6 @@ function widest(values) {
2315
2080
  return values.reduce((longest, value) => Math.max(longest, value.length), 0);
2316
2081
  }
2317
2082
  //#endregion
2318
- //#region src/assets.ts
2319
- /**
2320
- * The files under `dir`, hashed.
2321
- *
2322
- * Hashes are read off disk in a stream rather than by reading each file whole:
2323
- * a client build's largest chunk is not something to hold in memory just to
2324
- * find out the platform already has it.
2325
- */
2326
- function collectAssets(dir) {
2327
- if (!fs.existsSync(dir)) return [];
2328
- const assets = [];
2329
- const walk = (current, prefix) => {
2330
- for (const entry of fs.readdirSync(current, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name))) {
2331
- const absolute = path.join(current, entry.name);
2332
- const relative = prefix ? `${prefix}/${entry.name}` : entry.name;
2333
- if (entry.isDirectory()) walk(absolute, relative);
2334
- else if (entry.isFile()) assets.push({
2335
- path: relative,
2336
- content_hash: hashFile(absolute),
2337
- size_bytes: fs.statSync(absolute).size,
2338
- source: absolute
2339
- });
2340
- }
2341
- };
2342
- walk(dir, "");
2343
- return assets;
2344
- }
2345
- /**
2346
- * A **Static asset** a browser renders as a page. Read off the extension, as the
2347
- * content type the platform records is — mirrors `content_type_for_path` in
2348
- * `server/crates/core/src/features/assets/content_type.rs`.
2349
- */
2350
- function isDocument(assetPath) {
2351
- const name = assetPath.split("/").pop() ?? "";
2352
- const dot = name.lastIndexOf(".");
2353
- const extension = dot === -1 ? "" : name.slice(dot + 1).toLowerCase();
2354
- return extension === "html" || extension === "htm";
2355
- }
2356
- function hashFile(file) {
2357
- return sha256(fs.readFileSync(file));
2358
- }
2359
- function sha256(bytes) {
2360
- return crypto.createHash("sha256").update(bytes).digest("hex");
2361
- }
2362
- /**
2363
- * What this deploy *is*: the server bundle together with the sorted set of
2364
- * asset path and content-hash pairs, the same set for its **Private file**s
2365
- * marked apart, and the **Fallback document** it declares.
2366
- *
2367
- * Computed here, before anything is uploaded, so a deploy that changed nothing
2368
- * is refused before a byte moves. Mirrors `deploy_digest` in
2369
- * `server/crates/core/src/features/assets/identity.rs` — the gateway recomputes
2370
- * it from what actually arrives, so the two have to agree exactly.
2371
- *
2372
- * A deploy declaring no fallback and no private files contributes nothing here
2373
- * for either rather than a stated absence, so every digest computed before
2374
- * they existed is unmoved.
2375
- */
2376
- const DEPLOY_DIGEST_DOMAIN = "wawesome-deploy-v1";
2377
- function deployDigest(bundle, assets, fallback, privateFiles = []) {
2378
- const byPath = (files) => [...files].sort((a, b) => a.path < b.path ? -1 : a.path > b.path ? 1 : 0);
2379
- const digest = crypto.createHash("sha256");
2380
- digest.update(`${DEPLOY_DIGEST_DOMAIN}\n`);
2381
- digest.update(`${sha256(bundle)}\n`);
2382
- for (const asset of byPath(assets)) digest.update(`${asset.path}\0${asset.content_hash}\n`);
2383
- for (const file of byPath(privateFiles)) digest.update(`private\0${file.path}\0${file.content_hash}\n`);
2384
- if (fallback) digest.update(`fallback\0${fallback.path}\0${fallback.status}\n`);
2385
- return digest.digest("hex");
2386
- }
2387
- /** The manifest as the gateway reads it — the bytes on disk are the CLI's business. */
2388
- function manifestOf(assets) {
2389
- return assets.map(({ path, content_hash, size_bytes }) => ({
2390
- path,
2391
- content_hash,
2392
- size_bytes
2393
- }));
2394
- }
2395
- //#endregion
2396
2083
  //#region src/prompt.ts
2397
2084
  /**
2398
2085
  * Open a prompt session on stdin, queueing lines as they arrive.
@@ -3379,7 +3066,7 @@ async function deploy(entryInput, options) {
3379
3066
  if (assets.length > 0 || privateFiles.length > 0) await uploadAssets(creds, app, funcName, jsCode ?? "", {
3380
3067
  assets,
3381
3068
  privateFiles
3382
- }, declared, isVerbose, declaresSchedules);
3069
+ }, declared, isVerbose, declaresSchedules || domainName !== void 0);
3383
3070
  console.log(`[wawesome] Uploading code for ${app}/${funcName}...`);
3384
3071
  const uploadUrl = `${creds.gateway_url}/v1/apps/${encodeURIComponent(app)}/functions/${encodeURIComponent(funcName)}/code`;
3385
3072
  if (isVerbose) console.log(`[wawesome:verbose] POST ${uploadUrl}`);
@@ -3387,7 +3074,7 @@ async function deploy(entryInput, options) {
3387
3074
  ...declaredFields(declared),
3388
3075
  ...declared.message ? { message: declared.message } : {}
3389
3076
  };
3390
- const uploadRes = assets.length > 0 || privateFiles.length > 0 || declaresSchedules || declaresFallback || Object.keys(declaredOnTheWire).length > 0 || jsCode === null ? await authorizedFetch(uploadUrl, {
3077
+ const uploadRes = assets.length > 0 || privateFiles.length > 0 || declaresSchedules || declaresFallback || domainName !== void 0 || Object.keys(declaredOnTheWire).length > 0 || jsCode === null ? await authorizedFetch(uploadUrl, {
3391
3078
  method: "POST",
3392
3079
  headers: {
3393
3080
  Authorization: `Bearer ${creds.tenant_jwt}`,
@@ -3399,6 +3086,7 @@ async function deploy(entryInput, options) {
3399
3086
  ...privateFiles.length > 0 ? { files: manifestOf(privateFiles) } : {},
3400
3087
  ...declaresSchedules ? { schedules: config.schedules } : {},
3401
3088
  ...declaresFallback ? { fallback: declared.fallback } : {},
3089
+ ...domainName !== void 0 ? { domain: domainName } : {},
3402
3090
  ...declaredOnTheWire
3403
3091
  })
3404
3092
  }) : await authorizedFetch(uploadUrl, {
@@ -3573,7 +3261,7 @@ async function uploadAssets(creds, app, funcName, bundle, { assets, privateFiles
3573
3261
  if (manifestRes.status === 401 || manifestRes.status === 403) console.error(`[wawesome] Error: ${unauthorizedMessage(manifestRes.status, errorBody)}`);
3574
3262
  else if (manifestRes.status === 409) {
3575
3263
  if (continueWhenUnchanged) {
3576
- console.log("[wawesome] Code and files unchanged since the last deploy; applying schedules.");
3264
+ console.log("[wawesome] Code and files unchanged since the last deploy; applying the config file.");
3577
3265
  return;
3578
3266
  }
3579
3267
  const refusal = rejectionOf(errorBody, manifestRes.status, "This deploy is already deployed.");
@@ -4574,18 +4262,19 @@ function announceUrl(surface, slug, appSlug, functionName) {
4574
4262
  console.log(` ${SUBTREE_NOTE}\n`);
4575
4263
  }
4576
4264
  /**
4577
- * Show the URL, and offer to fix the workspace address while it can still be fixed.
4265
+ * Show the URL, and offer to change the workspace address while a rename is free.
4578
4266
  *
4579
- * This is the last moment that address is free: it locks the first time a
4580
- * version is promoted, which is what the deploy at the end of this flow does.
4581
- * Asking here rather than at signup is the whole reason naming was deferred —
4582
- * it is client-facing branding, and nobody chooses it well before seeing the
4583
- * product work. The URL comes first so the offer is about something concrete.
4267
+ * This is the last moment a rename costs nothing: after the first promotion,
4268
+ * which the deploy at the end of this flow does, the old name redirects and
4269
+ * renames wait 30 days apart. Asking here rather than at signup is the whole
4270
+ * reason naming was deferred — it is client-facing branding, and nobody chooses
4271
+ * it well before seeing the product work. The URL comes first so the offer is
4272
+ * about something concrete.
4584
4273
  *
4585
- * The lock is read from the gateway rather than guessed, so a user who cannot
4586
- * rename is told why instead of being walked into a rejection. Nothing here can
4587
- * fail the flow: the project is already on disk and the deploy still works under
4588
- * the address the workspace has.
4274
+ * A published workspace is not offered one: its rename moves live links, and
4275
+ * `wawesome workspace rename` is where that is confirmed. Nothing here can fail
4276
+ * the flow: the project is already on disk and the deploy still works under the
4277
+ * address the workspace has.
4589
4278
  */
4590
4279
  async function offerWorkspaceAddress(session, creds, tenant, appSlug, functionName) {
4591
4280
  const current = tenant?.tenant_slug ?? creds.tenant_slug;
@@ -4597,13 +4286,14 @@ async function offerWorkspaceAddress(session, creds, tenant, appSlug, functionNa
4597
4286
  console.log(" is a question for the gateway, which did not answer.\n");
4598
4287
  return;
4599
4288
  }
4600
- if (tenant.slug_locked) {
4601
- console.log(` 🔒 '${current}' is fixed. A Function version has been promoted, and live URLs already carry it.\n`);
4289
+ if (tenant.published) {
4290
+ console.log(` '${current}' is your workspace address. It is already live, so change it`);
4291
+ console.log(" with: wawesome workspace rename <name>\n");
4602
4292
  return;
4603
4293
  }
4604
4294
  console.log(` '${current}' is your workspace address, the part of that URL that is`);
4605
- console.log(" yours rather than this project's. Now is the moment to change it. It");
4606
- console.log(" locks for good the first time you deploy, because live URLs carry it.");
4295
+ console.log(" yours rather than this project's. Now is a good moment to change it.");
4296
+ console.log(" You can change it later too, once every 30 days after your first deploy.");
4607
4297
  console.log(" Press enter to keep it.\n");
4608
4298
  let refusal;
4609
4299
  for (let attempt = 0; attempt < MAX_RENAME_ATTEMPTS; attempt++) {
@@ -4620,7 +4310,7 @@ async function offerWorkspaceAddress(session, creds, tenant, appSlug, functionNa
4620
4310
  } catch (err) {
4621
4311
  console.log(`[wawesome] ${errorText(err)}`);
4622
4312
  refusal = reasonOf(err);
4623
- const { text, retryable } = renameAdvice(refusal);
4313
+ const { text, retryable } = renameAdvice(err);
4624
4314
  if (text) console.log(`[wawesome] ${text}`);
4625
4315
  if (!retryable) break;
4626
4316
  }
@@ -5840,20 +5530,27 @@ async function showWorkspace(options) {
5840
5530
  console.log(` ${SUBTREE_NOTE}`);
5841
5531
  }
5842
5532
  }
5843
- if (tenant.slug_locked) console.log("\n 🔒 The address is fixed. A Function version has been promoted, and live URLs carry it.");
5844
- else {
5845
- console.log("\n The address can still be changed: \x1B[36mwawesome workspace rename <name>\x1B[0m");
5846
- console.log(" It locks for good the first time you deploy.");
5533
+ const waitUntil = renameWaitsUntil(tenant);
5534
+ if (waitUntil) {
5535
+ console.log(`\n The address can be changed again from ${asUtc$1(waitUntil)}.`);
5536
+ console.log(" One rename is allowed every 30 days.");
5537
+ } else {
5538
+ console.log("\n The address can be changed: \x1B[36mwawesome workspace rename <name>\x1B[0m");
5539
+ console.log(tenant.published ? " The old address then redirects to the new one for 90 days. One rename every 30 days." : " Rename it freely until your first deploy. After that, once every 30 days.");
5847
5540
  }
5848
5541
  console.log("");
5849
5542
  }
5543
+ function renameWaitsUntil(tenant) {
5544
+ if (!tenant.renamable_at) return null;
5545
+ return new Date(tenant.renamable_at).getTime() > Date.now() ? tenant.renamable_at : null;
5546
+ }
5850
5547
  /**
5851
5548
  * Change the workspace's public address.
5852
5549
  *
5853
- * The lock is read from the gateway before asking, so a user who cannot rename
5854
- * is told why instead of being walked into a rejection. The rejection is still
5855
- * handled: a promotion landing between the two calls is the one case where the
5856
- * gateway knows something this command could not.
5550
+ * The 30-day wait is read from the gateway before asking, so a user who cannot
5551
+ * rename is told when they can instead of being walked into a rejection. The
5552
+ * rejection is still handled: another rename landing between the two calls is
5553
+ * something only the gateway knows.
5857
5554
  */
5858
5555
  async function renameWorkspace(slug, options) {
5859
5556
  const creds = requireCredentials$1();
@@ -5864,24 +5561,27 @@ async function renameWorkspace(slug, options) {
5864
5561
  }
5865
5562
  try {
5866
5563
  const tenant = await fetchTenantDetails(creds);
5867
- if (tenant.slug_locked) {
5868
- console.error("\n[wawesome] \x1B[31mThe workspace address can no longer be changed.\x1B[0m");
5869
- console.error(`[wawesome] '${tenant.tenant_slug}' is fixed: a Function version has been promoted, and live URLs already carry it.`);
5564
+ const waitUntil = renameWaitsUntil(tenant);
5565
+ if (waitUntil) {
5566
+ console.error("\n[wawesome] \x1B[31mThe workspace address cannot be changed yet.\x1B[0m");
5567
+ console.error(`[wawesome] It was renamed less than 30 days ago. ${nextRenameAllowed(waitUntil)}`);
5870
5568
  process.exit(1);
5871
5569
  return;
5872
5570
  }
5873
- if (options.verbose) console.log(`[wawesome:verbose] Renaming '${tenant.tenant_slug}' → '${slug}'`);
5874
- const result = await renameTenantSlug(creds, slug.trim());
5571
+ const typed = slug.trim();
5572
+ if (tenant.published && !options.yes) await confirmRename(tenant.tenant_slug, typed);
5573
+ if (options.verbose) console.log(`[wawesome:verbose] Renaming '${tenant.tenant_slug}' → '${typed}'`);
5574
+ const result = await renameTenantSlug(creds, typed);
5875
5575
  console.log("\n======================================================");
5876
5576
  console.log("✅ \x1B[32mWORKSPACE RENAMED\x1B[0m");
5877
5577
  console.log("======================================================");
5878
5578
  console.log(`\n Address: ${result.tenant_slug}`);
5879
- console.log(` Previous: ${result.previous_slug} (no longer resolves)`);
5579
+ console.log(tenant.published ? ` Previous: ${result.previous_slug} (redirects to the new address for 90 days)` : ` Previous: ${result.previous_slug} (no longer resolves)`);
5880
5580
  console.log("======================================================\n");
5881
5581
  } catch (err) {
5882
5582
  if (err instanceof GatewayError) {
5883
5583
  console.error(`\n[wawesome] \x1b[31m${err.message}\x1b[0m`);
5884
- const { text } = renameAdvice(err.reason);
5584
+ const { text } = renameAdvice(err);
5885
5585
  if (text) console.error(`[wawesome] ${text}`);
5886
5586
  console.error("");
5887
5587
  process.exit(1);
@@ -5892,6 +5592,32 @@ async function renameWorkspace(slug, options) {
5892
5592
  }
5893
5593
  }
5894
5594
  /**
5595
+ * Links are already out under a published address, so the new name is typed
5596
+ * twice, as the dashboard asks: a typo here would move every client's URL.
5597
+ */
5598
+ async function confirmRename(current, next) {
5599
+ if (!isInteractive()) {
5600
+ console.error("\n[wawesome] \x1B[31mA rename after publishing asks for confirmation, and there is nobody to ask.\x1B[0m");
5601
+ console.error(`[wawesome] Pass --yes to rename '${current}' to '${next}' without being asked.`);
5602
+ process.exit(1);
5603
+ }
5604
+ console.log(`\n Rename '${current}' to '${next}'?`);
5605
+ console.log(` - The new address is '${next}'.`);
5606
+ console.log(` - '${current}' redirects to the new address for 90 days. After that it is held`);
5607
+ console.log(" for 90 more days, and only you can take it back.");
5608
+ console.log(" - Open preview links stop working now.\n");
5609
+ let retyped;
5610
+ try {
5611
+ retyped = await input({ message: "Type the new address again to confirm:" });
5612
+ } catch {
5613
+ retyped = "";
5614
+ }
5615
+ if (retyped.trim() !== next) {
5616
+ console.error(`[wawesome] Cancelled. The address is still '${current}'.`);
5617
+ process.exit(1);
5618
+ }
5619
+ }
5620
+ /**
5895
5621
  * The workspaces this person belongs to, with the one the session is in
5896
5622
  * marked.
5897
5623
  *
@@ -6751,7 +6477,7 @@ cli.command("domains detach <hostname>", "Stop serving the current app at a doma
6751
6477
  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 to record (default: the one the platform names)").option("-v, --verbose", "Enable verbose debug output").action((options) => login(options));
6752
6478
  cli.command("logout", "Clear stored authentication credentials").action(async () => await logout());
6753
6479
  cli.command("whoami", "Show current login session info").action(() => whoami());
6754
- cli.command("workspace [action] [name]", "Show the workspace, move between workspaces, or rename the public address").usage("workspace <action> [name]\n\nActions:\n show Show the workspace name, address, and whether it can still change\n list (ls) Show the workspaces you belong to, and which one you are in\n switch <name> Move this terminal to another workspace you belong to\n rename <name> Change the public address, while nothing live depends on it").example("wawesome workspace").example("wawesome workspace list").example("wawesome workspace switch northwind").example("wawesome workspace rename northwind").option("-v, --verbose", "Enable verbose debug output").action((action, name, options) => workspaceCommand(action, name, options));
6480
+ cli.command("workspace [action] [name]", "Show the workspace, move between workspaces, or rename the public address").usage("workspace <action> [name]\n\nActions:\n show Show the workspace name, address, and whether it can still change\n list (ls) Show the workspaces you belong to, and which one you are in\n switch <name> Move this terminal to another workspace you belong to\n rename <name> Change the public address. After your first deploy, once every 30 days").example("wawesome workspace").example("wawesome workspace list").example("wawesome workspace switch northwind").example("wawesome workspace rename northwind").option("-y, --yes", "Rename without typing the new address again").option("-v, --verbose", "Enable verbose debug output").action((action, name, options) => workspaceCommand(action, name, options));
6755
6481
  cli.command("export [action]", "Export the whole workspace as one archive, and print the link to it").usage("export [status]\n\nWith no action, asks for an export and waits until it is ready.\n status Show the latest export, and a new link if it is ready").example("wawesome export").example("wawesome export status").option("--no-wait", "Ask for the export and return without waiting for it").option("-v, --verbose", "Enable verbose debug output").action((action, options) => exportCommand(action, options));
6756
6482
  cli.command("templates [action]", "Browse the template catalog").usage("templates [action]\n\nActions:\n list (ls) Show every available template (default)").example("wawesome templates").example("wawesome templates list").option("--api <url>", "API URL (default: https://api.wawesome.io)").option("-v, --verbose", "Enable verbose debug output").action((action, options) => templatesCommand(action, options));
6757
6483
  cli.command("credentials [action] [target]", "Mint, list, regenerate, revoke and delete deploy credentials").alias("credential").usage(`credentials <action> [options]