wawesome 0.10.0 → 0.12.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.
Files changed (3) hide show
  1. package/README.md +126 -111
  2. package/dist/index.mjs +153 -47
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -1,23 +1,23 @@
1
- # 🚀 wawesome
1
+ # wawesome
2
2
 
3
- > Official CLI for building, bundling, and deploying serverless WebAssembly functions on the
4
- > **[wawesome.io](https://wawesome.io)** platform.
3
+ > Official CLI for bundling and deploying serverless WebAssembly functions on the
4
+ > [wawesome.io](https://wawesome.io) platform.
5
5
 
6
6
  [![npm version](https://img.shields.io/npm/v/wawesome.svg?color=cyan)](https://www.npmjs.com/package/wawesome)
7
7
 
8
- Deploy ultra-fast, lightweight serverless WebAssembly functions directly from your terminal in under 60 seconds.
8
+ Write a TypeScript function, deploy it from your terminal, and get back the address it answers on.
9
9
 
10
10
  ---
11
11
 
12
- ## Quick Start
12
+ ## Quick start
13
13
 
14
14
  ### 1. Create an account
15
15
 
16
16
  Sign up for free at **[https://wawesome.io](https://wawesome.io)** to set up your workspace.
17
17
 
18
- ### 2. Authenticate CLI
18
+ ### 2. Authenticate the CLI
19
19
 
20
- Login via browser OAuth:
20
+ Log in through your browser:
21
21
 
22
22
  ```bash
23
23
  npx wawesome login
@@ -32,14 +32,14 @@ mkdir my-wasm-app && cd my-wasm-app
32
32
  npx wawesome init
33
33
  ```
34
34
 
35
- `init` asks for the function name and for the **App slug** the App groups the Functions of one
35
+ `init` asks for the function name and for the **App slug**. An App groups the Functions of one
36
36
  project, and its slug is part of the public URL your client sees. Both default to the directory
37
37
  name, so naming is usually a matter of pressing enter.
38
38
 
39
39
  An App slug must be a legal hostname label: lowercase letters, numbers, and single hyphens between
40
- them (no leading or trailing hyphen, 63 characters at most). Type something else `My Client` —
41
- and the CLI shows you the slug it would become (`my-client`) and asks again, rather than rewriting
42
- your answer behind your back.
40
+ them (no leading or trailing hyphen, 63 characters at most). Type something else, such as
41
+ `My Client`, and the CLI shows you the slug it would become (`my-client`) and asks again, rather
42
+ than rewriting your answer behind your back.
43
43
 
44
44
  #### Starting from a template
45
45
 
@@ -51,39 +51,40 @@ npx wawesome init --template stripe-webhook
51
51
  ```
52
52
 
53
53
  This goes from nothing to a deployed Function in one command. The template is downloaded over
54
- plain HTTP and unpacked **git is not required**, on any platform and its files land exactly as
55
- they are published. Only the `name` in `package.json`, the App and Function names in
56
- `wawesome-function.json`, and the `wawesome` dependency are touched the last so the project you
57
- get depends on the CLI that scaffolded it rather than the one the template was released against.
58
-
59
- Templates declare what they need rather than shipping placeholders, so the CLI then asks for each
60
- environment variable the template requires showing where in the provider's own dashboard to find
61
- the value stores them (secrets write-only), enables any outbound providers the template calls,
54
+ plain HTTP and unpacked, so **git is not required** on any platform, and its files land exactly as
55
+ they are published. Only three things are touched: the `name` in `package.json`, the App and
56
+ Function names in `wawesome-function.json`, and the `wawesome` dependency. That last one is so the
57
+ project you get depends on the CLI that scaffolded it rather than the one the template was released
58
+ against.
59
+
60
+ Templates declare what they need rather than shipping placeholders. The CLI asks for each
61
+ environment variable the template requires, showing where in the provider's own dashboard to find
62
+ the value. It stores them (secrets write-only), enables any outbound providers the template calls,
62
63
  and deploys. If a value does not exist yet, leave it blank; the CLI tells you the `env set` command
63
64
  to run once it does.
64
65
 
65
- Dependencies are installed for you (with whatever package manager launched the CLI — `npx`, `pnpm
66
- dlx`, `bun x`), so the types are there when you open the project and the bundler can resolve the
67
- template's imports. Pass `--no-install` to do it yourself. A failed install never stops the flow;
68
- the command to re-run is printed.
66
+ The CLI installs your dependencies with whatever package manager launched it (`npx`, `pnpm dlx`,
67
+ `bun x`), so the types are there when you open the project and the bundler can resolve the
68
+ template's imports. Pass `--no-install` to do it yourself. A failed install never stops the flow,
69
+ and the CLI prints the command to re-run.
69
70
 
70
71
  Before any of that is sent anywhere, the CLI shows you the **public URL your Function will have**
71
- and offers to change your workspace address the part of that URL that is yours rather than this
72
- project's, and the same one `wawesome workspace show` prints. It is randomly minted at signup and
73
- stays changeable until your first deploy, at which point it locks for good, because live URLs carry
74
- it. Press enter to keep it. If it is already locked, the offer is not made and the reason is said
75
- plainly.
72
+ and offers to change your workspace address. That is the part of the URL which is yours rather
73
+ than this project's, and it is the same one `wawesome workspace show` prints. Signup mints it at
74
+ random and it stays changeable until your first deploy, at which point it locks for good, because
75
+ live URLs carry it. Press enter to keep it. If it is already locked, the CLI does not make the
76
+ offer and says why.
76
77
 
77
- Being logged in is checked **before** the first question, not at the deploy: an expired session
78
- offers you a login there and then, and declining still leaves you the project plus the two commands
79
- that finish it.
78
+ The CLI checks you are logged in **before** the first question rather than at the deploy. An
79
+ expired session offers you a login there and then, and declining still leaves you the project plus
80
+ the two commands that finish it.
80
81
 
81
- Run it in an empty directory: if any file would be overwritten, nothing is written at all and the
82
- collision is named.
82
+ Run it in an empty directory. If any file would be overwritten, nothing is written at all and the
83
+ CLI names the collision.
83
84
 
84
- ### 4. Build & Deploy
85
+ ### 4. Build and deploy
85
86
 
86
- Deploy your serverless function to Wawesome Cloud instantly:
87
+ Deploy your serverless function to the platform:
87
88
 
88
89
  ```bash
89
90
  npx wawesome deploy
@@ -91,7 +92,7 @@ npx wawesome deploy
91
92
 
92
93
  ---
93
94
 
94
- ## 📖 Command Reference
95
+ ## Command reference
95
96
 
96
97
  | Command | Description |
97
98
  |:----------------------------------------|:--------------------------------------------------------------------|
@@ -116,37 +117,37 @@ npx wawesome deploy
116
117
  | `npx wawesome env list` | View environment variables for the current app |
117
118
  | `npx wawesome env set <key> <val>` | Set an environment variable (add `--secret` for write-only) |
118
119
  | `npx wawesome env rm <key>` | Delete an environment variable |
119
- | `npx wawesome credentials` | List deploy credentials name, prefix, capabilities, Apps, last use |
120
+ | `npx wawesome credentials` | List deploy credentials: name, prefix, capabilities, Apps, last use |
120
121
  | `npx wawesome credentials mint <name>` | Mint a deploy credential and print its secret, once |
121
122
  | `npx wawesome credentials revoke <name>`| Revoke a deploy credential by name or prefix |
122
123
 
123
124
  ---
124
125
 
125
- ## 🧭 Unsupported Globals
126
+ ## Unsupported globals
126
127
 
127
128
  Every build scans the bundle it just produced against the platform's declared guest surface, and says
128
129
  nothing unless it finds something.
129
130
 
130
- - **`Intl` is not provided.** Where the bundle reaches it as it loads your own module scope, or a
131
- dependency's the build is refused before a deploy uploads anything: that bundle would not
132
- evaluate on the platform. Where the reference sits inside a function that may never be called,
131
+ - **`Intl` is not provided.** Where the bundle reaches it as it loads, in your own module scope or
132
+ a dependency's, the build is refused before a deploy uploads anything, because that bundle would
133
+ not evaluate on the platform. Where the reference sits inside a function that may never be called,
133
134
  behind a `typeof` check, or inside a `try`, you get a warning and the deploy proceeds.
134
135
  - **`toLocaleString`, `toLocaleDateString`, `toLocaleTimeString`, `toLocaleLowerCase` and
135
136
  `toLocaleUpperCase` ignore their locale argument.** They run and return an unlocalised answer, so
136
137
  these warn.
137
138
 
138
139
  Each message names the global, the file and line in *your* source, and what to do about it. Declaring
139
- an `Intl` polyfill in your `package.json` the same declaration [local parity
140
- reads](#testing-against-the-guests-javascript-surface) silences the report, as does installing one
140
+ an `Intl` polyfill in your `package.json`, the same declaration [local parity
141
+ reads](#testing-against-the-guests-javascript-surface), silences the report, as does installing one
141
142
  on `globalThis` in the bundle itself. The scan reads static references only: a global reached through
142
143
  `globalThis['Intl']` is invisible to it, so it never refuses a deploy on a guess. `deploy
143
144
  --skip-build` scans the bundle it found on disk before uploading it.
144
145
 
145
- ## 📜 Invocation Logs
146
+ ## Invocation logs
146
147
 
147
148
  Inspect past function runs or view raw `stdout` / `stderr` log outputs directly in your terminal.
148
149
 
149
- ### 1. List Recent Invocations
150
+ ### 1. List recent invocations
150
151
 
151
152
  List past executions (including status, trigger type, timestamp, and duration) for the function in the
152
153
  current directory:
@@ -161,7 +162,7 @@ Or list invocations for a specific function by name:
161
162
  npx wawesome logs my-function
162
163
  ```
163
164
 
164
- Filter by invocation status only show errors, timeouts, etc.:
165
+ Filter by invocation status, to show only errors or timeouts:
165
166
 
166
167
  ```bash
167
168
  npx wawesome logs my-function --error
@@ -169,7 +170,7 @@ npx wawesome logs my-function --status timeout
169
170
  npx wawesome logs my-function --running
170
171
  ```
171
172
 
172
- ### 2. View Invocation Log Body (`stdout`/`stderr`)
173
+ ### 2. View the invocation log body (`stdout`/`stderr`)
173
174
 
174
175
  Fetch and print the captured `console.log` / `console.error` text for a specific invocation:
175
176
 
@@ -183,9 +184,9 @@ Or pass the UUID directly as the target:
183
184
  npx wawesome logs 019fb344-ea0c-78f2-8a9b-d04e188b9823
184
185
  ```
185
186
 
186
- ### 3. Follow Live Output (`--follow`)
187
+ ### 3. Follow live output (`--follow`)
187
188
 
188
- Stream an invocation's output as it runs like `tail -f` for your serverless function.
189
+ Stream an invocation's output as it runs, like `tail -f` for your serverless function.
189
190
 
190
191
  #### Follow by function name (recommended)
191
192
 
@@ -205,13 +206,13 @@ npx wawesome logs 019fb344-ea0c-78f2-8a9b-d04e188b9823 --follow
205
206
 
206
207
  #### Reconnection
207
208
 
208
- On transient network errors or server issues (5xx), the CLI automatically reconnects with
209
- exponential back-off (up to 3 retries). Non-recoverable errors like authentication failures
210
- (401) or unknown invocations (404) exit immediately with a clear message.
209
+ On a transient network error or a 5xx, the CLI reconnects with exponential back-off, up to three
210
+ retries. It does not retry an authentication failure (401) or an unknown invocation (404). Those
211
+ exit at once, naming what happened.
211
212
 
212
213
  ---
213
214
 
214
- ## Manual Invocation
215
+ ## Manual invocation
215
216
 
216
217
  Fire a background run of a Function immediately without waiting for a schedule tick or deploying code:
217
218
 
@@ -231,11 +232,11 @@ npx wawesome invoke --no-follow
231
232
 
232
233
  ---
233
234
 
234
- ## Schedules & Cron Management
235
+ ## Schedules and cron management
235
236
 
236
237
  Manage recurring Schedules and inspect background run history directly from your terminal.
237
238
 
238
- ### 1. List Schedules
239
+ ### 1. List schedules
239
240
 
240
241
  List a Function's Schedules with expression, state, and next run in UTC:
241
242
 
@@ -250,12 +251,12 @@ npx wawesome cron list my-function
250
251
  npx wawesome cron list --app my-app
251
252
  ```
252
253
 
253
- The output clearly distinguishes the three off-states:
254
+ The output tells the three off-states apart:
254
255
  - `paused`: stopped by a user, resumable with `wawesome cron resume <name>`
255
256
  - `not in this config file`: disabled because it was removed from configuration, resumable only by declaring it again in code
256
257
  - `suspended`: suspended by the non-payment ladder, resumable only after settling workspace balance
257
258
 
258
- ### 2. Pause and Resume Schedules
259
+ ### 2. Pause and resume schedules
259
260
 
260
261
  ```bash
261
262
  # Pause a schedule by name (stops queued ticks and survives future deploys)
@@ -268,7 +269,7 @@ npx wawesome cron pause nightly-reconcile --reason "database maintenance"
268
269
  npx wawesome cron resume nightly-reconcile
269
270
  ```
270
271
 
271
- ### 3. Read Run History
272
+ ### 3. Read run history
272
273
 
273
274
  Inspect past scheduled and manual runs, showing when each run was due, when it started, pool delay, and how it ended:
274
275
 
@@ -282,15 +283,15 @@ npx wawesome cron history my-function --state failed
282
283
 
283
284
  ---
284
285
 
285
- ## 🔑 Deploy Credentials
286
+ ## Deploy credentials
286
287
 
287
288
  A deploy credential is what a CI pipeline or an agent authenticates with, where there is nobody at a
288
289
  keyboard to log in. It belongs to the workspace rather than to you: it keeps working after you leave,
289
- and revoking it does not end your own session. Once you have one, [Deploying from CI](#-deploying-from-ci)
290
+ and revoking it does not end your own session. Once you have one, [Deploying from CI](#deploying-from-ci)
290
291
  is what to do with it.
291
292
 
292
- Minting, listing and revoking are the workspace owner's, and are closed to deploy credentials
293
- themselves whatever they carry a leaked credential cannot mint another. So these three run on your
293
+ Minting, listing and revoking belong to the workspace owner. They are closed to deploy credentials
294
+ themselves, whatever those carry, so a leaked credential cannot mint another. All three run on your
294
295
  ordinary `npx wawesome login` session.
295
296
 
296
297
  ### Mint one
@@ -299,8 +300,8 @@ ordinary `npx wawesome login` session.
299
300
  npx wawesome credentials mint ci-pipeline
300
301
  ```
301
302
 
302
- The secret is printed once and never again the platform stores a digest of it, so no later read can
303
- rebuild it. It goes to stdout on a line of its own and everything else the command prints goes to
303
+ The secret is printed once and never again. The platform stores a digest of it, so no later read
304
+ can rebuild it. It goes to stdout on a line of its own and everything else the command prints goes to
304
305
  stderr, so a redirect catches the secret and not one character besides:
305
306
 
306
307
  ```bash
@@ -313,17 +314,20 @@ you say otherwise:
313
314
  ```bash
314
315
  # reads logs and cannot deploy, restricted to one App, never expires
315
316
  npx wawesome credentials mint agent -c logs:read -a prod --expires never
317
+
318
+ # ships code and attaches the domain the project declares
319
+ npx wawesome credentials mint ci -c deploy,domains:attach -a prod
316
320
  ```
317
321
 
318
322
  | Flag | Meaning |
319
323
  |:---------------------|:------------------------------------------------------------------------------|
320
- | `-c, --capability` | `deploy`, `logs:read`, or both. Repeatable or comma-separated. Default `deploy` |
324
+ | `-c, --capability` | `deploy`, `logs:read`, `domains:attach`, or any mix. Repeatable or comma-separated. Default `deploy` |
321
325
  | `-a, --app` | Restrict to these Apps, by slug. Repeatable. Default: every App in the workspace |
322
326
  | `--expires` | Days, or `never`. Default: 90 |
323
327
 
324
- An App named in a restriction does not have to exist yet a pipeline whose first deploy creates the
328
+ An App named in a restriction does not have to exist yet. A pipeline whose first deploy creates the
325
329
  App it was minted for is the ordinary case. A restriction bounds what a credential can disturb, not
326
- what it can read: a Function deployed into one App still reads the whole workspace's environment at
330
+ what it can read. A Function deployed into one App still reads the whole workspace's environment at
327
331
  runtime.
328
332
 
329
333
  ### List them
@@ -332,7 +336,7 @@ runtime.
332
336
  npx wawesome credentials
333
337
  ```
334
338
 
335
- Name, displayable prefix, capabilities, Apps, last use and expiry never the secret, which the
339
+ Name, displayable prefix, capabilities, Apps, last use and expiry. Never the secret, which the
336
340
  platform no longer holds. `LAST USED` lags by a few minutes and reads `never` for a credential
337
341
  nothing has ever presented, which is a better reason to revoke one than any calendar date.
338
342
 
@@ -350,7 +354,7 @@ it.
350
354
 
351
355
  ---
352
356
 
353
- ## ⚙️ Configuration & Custom Gateway
357
+ ## Configuration and a custom gateway
354
358
 
355
359
  ### `wawesome-function.json`
356
360
 
@@ -364,7 +368,7 @@ Every project directory includes a `wawesome-function.json` file generated durin
364
368
  }
365
369
  ```
366
370
 
367
- `app` is the App this Function is deployed into, and it is client-facing every deploy from this
371
+ `app` is the App this Function is deployed into, and it is client-facing. Every deploy from this
368
372
  directory is scoped to it.
369
373
 
370
374
  `function` is the address. Changing it does not rename anything: your Function's URL is built from
@@ -385,40 +389,40 @@ Add `"assets"` to deploy static files beside your code:
385
389
  ```
386
390
 
387
391
  Everything under that directory is deployed with the version and served at its path beneath your
388
- Function's URL `dist/client/assets/index-a1.js` answers at `https://<app-host>/<function>/assets/index-a1.js`.
392
+ Function's URL, so `dist/client/assets/index-a1.js` answers at `https://<app-host>/<function>/assets/index-a1.js`.
389
393
  The CLI hashes each file and asks the platform which of them it does not already hold, so a redeploy
390
- that changed one chunk uploads one chunk and a deploy that changed nothing at all is refused
391
- before a byte moves.
394
+ that changed one chunk uploads one chunk. A deploy that changed nothing at all is refused before a
395
+ byte moves.
392
396
 
393
397
  Files are served straight from object storage; your Function is never invoked for one, and no
394
- invocation is recorded. They answer on your App's own hostname and nowhere else on the
398
+ invocation is recorded. They answer on your App's own hostname and nowhere else. On the
395
399
  development path form (`/x/<tenant>/<app>/<function>/...`) the same address reaches your handler
396
400
  as it always has, because a file on an origin every workspace shares would be same-origin with
397
401
  all of them. Each carries `Cache-Control: public, max-age=31536000, immutable` and an
398
- `ETag`, so name your build output by content hash a file's bytes must never change under a name a
402
+ `ETag`, so name your build output by content hash. A file's bytes must never change under a name a
399
403
  browser has already cached for a year. The content type comes from the extension against a fixed
400
404
  allowlist and is never sniffed; anything off it is served as a download.
401
405
 
402
406
  Two rules to know about:
403
407
 
404
408
  - **Everything beneath `assets/` is static**, whatever the deploy carries. A request there never
405
- reaches your handler an unknown path under it is a 404, not a route for you to answer.
409
+ reaches your handler, and an unknown path under it is a 404 rather than a route for you to answer.
406
410
  - **At most 100 files may sit outside `assets/`.** Those paths travel on the version record so a
407
411
  request can be routed without a lookup per file. Put bulk output under `assets/`, where a file
408
412
  costs nothing; `favicon.ico`, `robots.txt` and a `.well-known/` directory are what the rest is
409
413
  for.
410
414
 
411
415
  **HTML is refused at deploy time.** Your Function renders its own markup, and a document served from
412
- your App's own origin is the sharpest same-origin vector a static file has.
416
+ your App's own origin is the sharpest same-origin risk a static file carries.
413
417
 
414
418
  **An SVG is served inert.** The rule behind the refusal above is that nothing you deploy as a file
415
- becomes a page on your App's own origin, and an SVG opened directly in a browser would it runs
416
- script, and it renders whatever HTML a `<foreignObject>` holds. It is served rather than refused
417
- because making it inert costs the file nothing: an `<img src="logo.svg">` never ran that script and
418
- is never checked against the policy, so your drawings render as they always did. Every SVG and XML
419
- file carries `Content-Security-Policy: script-src 'none'; sandbox`, and only navigating straight to
420
- one loses anything such a file is sandboxed onto an origin of its own, so its links no longer
421
- navigate and a page embedding it through `<object>` or `<iframe>` cannot reach into its DOM.
419
+ becomes a page on your App's own origin, and an SVG opened directly in a browser would become one.
420
+ It runs script, and it renders whatever HTML a `<foreignObject>` holds. It is served rather than
421
+ refused because making it inert costs the file nothing. An `<img src="logo.svg">` never ran that
422
+ script and is never checked against the policy, so your drawings render as they always did. Every
423
+ SVG and XML file carries `Content-Security-Policy: script-src 'none'; sandbox`, and only navigating
424
+ straight to one loses anything. Such a file is sandboxed onto an origin of its own, so its links no
425
+ longer navigate and a page embedding it through `<object>` or `<iframe>` cannot reach into its DOM.
422
426
 
423
427
  ### Schedules
424
428
 
@@ -433,12 +437,12 @@ Add `"schedules"` to run a Function on a recurring timer, with no caller:
433
437
  }
434
438
  ```
435
439
 
436
- An expression is **five fields, read in UTC** minute, hour, day of month, month, day of week. There
437
- is no seconds field, and no timezone: a local zone would make one night a year fire a job twice and
440
+ An expression is **five fields, read in UTC**: minute, hour, day of month, month, day of week. There
441
+ is no seconds field and no timezone. A local zone would make one night a year fire a job twice and
438
442
  another night not at all.
439
443
 
440
444
  The name is yours to choose and is what the platform keys the schedule by, so editing an expression
441
- is a change to the same schedule rather than the deletion of one and the creation of another — its
445
+ is a change to the same schedule rather than the deletion of one and the creation of another. Its
442
446
  history and its paused state stay attached to it.
443
447
 
444
448
  Your deploy applies them and prints each one with the time it will next run. It is refused, before
@@ -448,7 +452,7 @@ five minutes, or if one Function declares more than five schedules.
448
452
  Two rules worth knowing before you edit the file:
449
453
 
450
454
  - **A schedule you delete from the file is disabled, not deleted.** Its history stays, and declaring
451
- it again is what turns it back on so a typo costs you a deploy rather than a job's record.
455
+ it again is what turns it back on, so a typo costs you a deploy rather than a job's record.
452
456
  - **A deploy never resumes a schedule a person paused.** When it runs is code; whether it is running
453
457
  is not, and an unrelated commit the next morning must not restart what you stopped at 3am.
454
458
 
@@ -469,7 +473,7 @@ A job on a timer should not also be sitting at a guessable URL where a stranger
469
473
  }
470
474
  ```
471
475
 
472
- A private Function has **no public address at all** in production not a hidden one, not one
476
+ A private Function has **no public address at all** in production, not a hidden one and not one
473
477
  behind a credential. A request for it against your App's own hostname is answered with the same 404
474
478
  as a Function that was never deployed. This is how you deploy a job with side effects without leaving
475
479
  it where a stranger who guesses the slug can fire it.
@@ -497,7 +501,7 @@ the line is refused, and the deploy tells you so having written nothing.
497
501
  npx wawesome deploy --publish
498
502
  ```
499
503
 
500
- That is deliberate the line that keeps a job off the internet is one line, and a deploy that
504
+ That is deliberate. The line that keeps a job off the internet is one line, and a deploy that
501
505
  quietly honoured its deletion would put the job back on the open internet with nothing said. Every
502
506
  deploy prints the visibility it landed, beside the URL or in place of it.
503
507
 
@@ -529,19 +533,30 @@ deploy prints the refusal and lands everything else it was doing.
529
533
  The address derived from your workspace slug keeps serving after you attach a domain. Both names
530
534
  answer, so webhooks and integrations already pointed at the old one keep working.
531
535
 
532
- An apex domain `client.com` with no `www` attaches like any other name. Whether a CNAME may sit at
536
+ An apex domain, `client.com` with no `www`, attaches like any other name. Whether a CNAME may sit at
533
537
  your zone root is your DNS provider's rule, and where it cannot, attach `www.client.com` and configure
534
538
  the redirect at the provider.
535
539
 
536
- Deleting the line detaches nothing. Detaching takes a live site dark, which is not something a deploy
537
- should infer from a deleted line.
540
+ Deleting the line detaches nothing. Detaching takes a live site dark, which is not something a
541
+ deploy should infer from a deleted line. So the deploy reports the domain it found and left serving,
542
+ and names the one command that takes it off:
543
+
544
+ ```bash
545
+ npx wawesome domains detach shop.client.com
546
+ ```
547
+
548
+ The hostname is typed because an App carries exactly one domain, and the command that removes it
549
+ should not be one you can run by reflex. It says what it is about to do first: the domain stops
550
+ resolving here, this App's own address keeps serving, and the certificate is given back. Afterwards
551
+ the name is free to attach again by declaring it and deploying, which is also what the next deploy
552
+ does if you leave the line in the file.
538
553
 
539
554
  ### Reserved headers
540
555
 
541
556
  `x-wawesome-*` belongs to the platform in both directions. It is stripped off the request before your
542
- handler sees it, and off your response before the caller does so **do not name a header of your own
543
- on that prefix**: it is dropped silently rather than rejected, and you will not get an error telling
544
- you why it vanished.
557
+ handler sees it, and off your response before the caller does. So **do not name a header of your
558
+ own on that prefix**. It is dropped silently rather than rejected, and you will not get an error
559
+ telling you why it vanished.
545
560
 
546
561
  Four headers arrive or leave on it, and the stripping is what makes them worth trusting:
547
562
 
@@ -549,13 +564,13 @@ Four headers arrive or leave on it, and the stripping is what makes them worth t
549
564
  | --- | --- | --- |
550
565
  | `x-wawesome-forwarded-prefix` | inbound | The mount that was stripped from the path. Join it to the path you observe to rebuild the caller's URL. |
551
566
  | `x-wawesome-trigger` | inbound | How this run started: `caller` when someone called your address, `schedule` when fired by a Schedule. A caller cannot forge it in production (stripped inbound). On the local development surface, pass `x-wawesome-trigger: schedule` to exercise a background run by hand with the collapsed budget. |
552
- | `x-wawesome-invocation-id` | outbound | The id of this run the key to fetch its logs with `npx wawesome logs --invocation <id>`. |
553
- | `x-wawesome-error` | outbound | Present only when the platform failed, never when your Function did. Its *absence* means the status on the wire is yours up to the moment your response is committed, and no further. |
567
+ | `x-wawesome-invocation-id` | outbound | The id of this run, the key to fetch its logs with `npx wawesome logs --invocation <id>`. |
568
+ | `x-wawesome-error` | outbound | Present only when the platform failed, never when your Function did. Its *absence* means the status on the wire is yours, up to the moment your response is committed and no further. |
554
569
 
555
570
  ### Testing against the guest's JavaScript surface
556
571
 
557
572
  Your Function does not run on Node. The engine has no `Intl`, and its `toLocaleString` ignores the
558
- locale you pass `(1234.5).toLocaleString('en-US')` comes back as `"1234.5"`, not `"1,234.50"`. On
573
+ locale you pass. `(1234.5).toLocaleString('en-US')` comes back as `"1234.5"`, not `"1,234.50"`. On
559
574
  Node both work, which is how a green suite ships a Function that throws in production, or renders
560
575
  markup the browser then refuses to hydrate.
561
576
 
@@ -572,19 +587,19 @@ export default defineConfig({
572
587
 
573
588
  Templates scaffolded with `wawesome init --template` ship this already. With it in place `Intl` is
574
589
  gone, `MessageChannel` is the platform's own implementation rather than Node's, and the
575
- locale-sensitive methods throw with a message naming the remedy they throw rather than return the
590
+ locale-sensitive methods throw with a message naming the remedy. They throw rather than return the
576
591
  engine's unlocalised answer because the platform declares them unsupported, and a wrong string that
577
592
  fails nowhere is the thing this is here to stop you shipping.
578
593
 
579
- If you bundle an `Intl` polyfill, declare it in your `package.json` as you normally would — a
594
+ If you bundle an `Intl` polyfill, declare it in your `package.json` as you normally would. A
580
595
  dependency that provides `Intl` is left in place rather than stripped out from under you.
581
596
 
582
- ### Local Development / Gateway Overrides
597
+ ### Local development and gateway overrides
583
598
 
584
599
  If you are running a local gateway or self-hosted instance, you can configure your CLI Gateway URL using any of the
585
600
  following:
586
601
 
587
- #### 1. Custom Settings (`~/.wawesome/settings.json`)
602
+ #### 1. Custom settings (`~/.wawesome/settings.json`)
588
603
 
589
604
  Create `~/.wawesome/settings.json`:
590
605
 
@@ -594,13 +609,13 @@ Create `~/.wawesome/settings.json`:
594
609
  }
595
610
  ```
596
611
 
597
- #### 2. Environment Variables
612
+ #### 2. Environment variables
598
613
 
599
614
  ```bash
600
615
  export WAWESOME_GATEWAY_URL="http://localhost:3000"
601
616
  ```
602
617
 
603
- #### 3. CLI Flag
618
+ #### 3. CLI flag
604
619
 
605
620
  ```bash
606
621
  npx wawesome login --gateway http://localhost:3000
@@ -613,10 +628,10 @@ gateway along with the credentials.
613
628
 
614
629
  ---
615
630
 
616
- ## 🤖 Deploying from CI
631
+ ## Deploying from CI
617
632
 
618
633
  A **deploy credential** is what deploys without a person at a keyboard. Your workspace mints one, and the CLI reads it
619
- from a single environment variable there is no login, and no credentials file is involved at any point:
634
+ from a single environment variable. There is no login, and no credentials file is involved at any point:
620
635
 
621
636
  ```bash
622
637
  export WAWESOME_DEPLOY_CREDENTIAL="wawe_..."
@@ -633,8 +648,8 @@ While it is set:
633
648
  - it wins over any `~/.wawesome/credentials.json` on the machine, so a runner with a stale cached home directory still
634
649
  deploys as the credential rather than as whoever last logged in there;
635
650
  - nothing is written to disk, and the secret stays in the environment where you put it;
636
- - a refusal says which of four things happened the credential is unknown, revoked, expired, or does not carry what the
637
- command needs instead of asking you to sign in again, which a pipeline cannot do;
651
+ - a refusal names which of four things happened: the credential is unknown, revoked, expired, or does not carry what
652
+ the command needs. It does not ask you to sign in again, which a pipeline cannot do;
638
653
  - `login` and `logout` refuse, because neither would change what the next command authenticates as;
639
654
  - `credentials` is refused by the platform, because minting, listing and revoking are closed to
640
655
  credentials whatever they carry;
@@ -642,7 +657,7 @@ While it is set:
642
657
 
643
658
  ---
644
659
 
645
- ## 🔒 Security & Secrets
660
+ ## Security and secrets
646
661
 
647
662
  Wawesome encrypts environment variables at rest using two-tier envelope encryption (AES-256-GCM with per-app data keys
648
663
  and AAD context binding). Use `--secret` when setting sensitive keys:
@@ -653,7 +668,7 @@ npx wawesome env set STRIPE_SECRET_KEY sk_live_xxx --secret
653
668
 
654
669
  ---
655
670
 
656
- ## 🌐 Resources & Support
671
+ ## Resources and support
657
672
 
658
673
  - **Platform Homepage**: [https://wawesome.io](https://wawesome.io)
659
674
  - **Documentation**: [https://docs.wawesome.io](https://docs.wawesome.io)
package/dist/index.mjs CHANGED
@@ -331,10 +331,10 @@ function decodeVlq(encoded) {
331
331
  //#endregion
332
332
  //#region src/guest-surface-scan.ts
333
333
  const MAX_LOCATIONS = 3;
334
- const ABSENT_PROBLEM = "the guest does not define it evaluating this reference throws a ReferenceError";
335
- const METHOD_PROBLEM = "the guest carries no ICU, so it ignores the locale argument";
336
- const GUARDED_PROBLEM = "the guest does not define it this bundle guards the reference, so the path it takes when the global is missing is the one that runs";
337
- const DEFERRED_PROBLEM = "the guest does not define it the code holding this reference throws a ReferenceError if it ever runs";
334
+ const ABSENT_PROBLEM = "The guest does not define it, so evaluating this reference throws a ReferenceError.";
335
+ const METHOD_PROBLEM = "The guest carries no ICU, so it ignores the locale argument.";
336
+ const GUARDED_PROBLEM = "The guest does not define it. This bundle guards the reference, so what runs is the path it takes when the global is missing.";
337
+ const DEFERRED_PROBLEM = "The guest does not define it. The code holding this reference throws a ReferenceError if it ever runs.";
338
338
  const GLOBAL_OBJECTS = /* @__PURE__ */ new Set([
339
339
  "globalThis",
340
340
  "window",
@@ -472,7 +472,7 @@ function surfaceReportLines(findings) {
472
472
  const lines = [];
473
473
  for (const finding of findings) {
474
474
  const headline = finding.severity === "refuse" ? "Refusing to deploy" : "Warning";
475
- lines.push(`[wawesome] ${headline}: \`${finding.name}\` ${finding.problem}.`);
475
+ lines.push(`[wawesome] ${headline}: \`${finding.name}\`. ${finding.problem}`);
476
476
  for (const location of finding.locations) lines.push(`[wawesome] at ${location.file}:${location.line}:${location.column}`);
477
477
  if (finding.undisplayedLocations > 0) lines.push(`[wawesome] and ${finding.undisplayedLocations} more reference${finding.undisplayedLocations === 1 ? "" : "s"}`);
478
478
  lines.push(`[wawesome] Remedy: ${finding.remedy}`);
@@ -883,7 +883,7 @@ async function buildJs(entryInput, options) {
883
883
  * that has to name this version — `--version`, the dependency a scaffolded
884
884
  * project pins — reads it here, so a release bumps one file.
885
885
  */
886
- const CLI_VERSION = "0.10.0";
886
+ const CLI_VERSION = "0.12.0";
887
887
  //#endregion
888
888
  //#region src/prompt.ts
889
889
  /**
@@ -1158,7 +1158,7 @@ async function promptForWorkspaceName(options) {
1158
1158
  const supplied = options.workspace?.trim();
1159
1159
  if (supplied) return supplied;
1160
1160
  if (!isInteractive()) throw new Error("No workspace found, and no name to create one with. Re-run with --workspace <name>.");
1161
- console.log("\n[wawesome] You don't have a workspace yet — let's create one.");
1161
+ console.log("\n[wawesome] You don't have a workspace yet. Let's create one.");
1162
1162
  console.log(" (this is the name you'll see in the dashboard; its public address is set separately)");
1163
1163
  const session = openPromptSession();
1164
1164
  try {
@@ -1454,11 +1454,11 @@ async function reportCredential(creds) {
1454
1454
  function whenItRuns(schedule) {
1455
1455
  const undeclared = schedule.declared === false;
1456
1456
  if (schedule.state === "suspended") {
1457
- const why = "the workspace owes payment, its subscription ended, or it has spent past its plan's allowance";
1458
- return undeclared ? `suspended, and not in this config file ${why}; declare it again too` : `suspended ${why}`;
1457
+ const why = "The workspace owes payment, its subscription ended, or it has spent past its plan's allowance";
1458
+ return undeclared ? `suspended, and not in this config file. ${why}; declare it again too` : `suspended. ${why}`;
1459
1459
  }
1460
- if (schedule.state === "paused") return undeclared ? "paused, and not in this config file declare it again, then resume" : "paused resume it to run again";
1461
- if (undeclared) return "not in this config file disabled, kept";
1460
+ if (schedule.state === "paused") return undeclared ? "paused, and not in this config file. Declare it again, then resume" : "paused. Resume it to run again";
1461
+ if (undeclared) return "not in this config file, so it is disabled. Its history is kept";
1462
1462
  if (schedule.state !== "active") return schedule.state;
1463
1463
  return schedule.next_fire_at ? `next run ${asUtc$1(schedule.next_fire_at)}` : "next run unknown";
1464
1464
  }
@@ -1553,11 +1553,33 @@ async function applyDeclaredDomain(creds, app, hostname) {
1553
1553
  kind: "refused",
1554
1554
  hostname,
1555
1555
  said: whatWentWrong(err),
1556
- advice: planLimitAdvice(refusal?.reason)
1556
+ advice: planLimitAdvice(refusal?.reason),
1557
+ held: held.find((domain) => !domain.detaching)?.hostname
1557
1558
  };
1558
1559
  }
1559
1560
  }
1560
1561
  /**
1562
+ * What the App carries that its configuration does not, for a deploy that
1563
+ * declares no domain at all.
1564
+ *
1565
+ * Reports and never acts. A read that fails says nothing rather than guessing:
1566
+ * a project with no domain in its file has nothing to be told about one, and a
1567
+ * false alarm here is one about a client's site that is up.
1568
+ */
1569
+ async function findUndeclaredDomain(creds, app) {
1570
+ let held;
1571
+ try {
1572
+ held = await listDomains(creds, app);
1573
+ } catch {
1574
+ return null;
1575
+ }
1576
+ const domain = held.find((candidate) => !candidate.detaching);
1577
+ return domain ? {
1578
+ kind: "undeclared",
1579
+ domain
1580
+ } : null;
1581
+ }
1582
+ /**
1561
1583
  * The platform's own words where it has any. A credential refusal is read
1562
1584
  * through the same reading every other command gives one, so a CI runner is
1563
1585
  * told what its credential lacks rather than to sign in again.
@@ -1597,6 +1619,62 @@ async function attachDomain(creds, app, hostname) {
1597
1619
  const { domain } = await res.json();
1598
1620
  return domain;
1599
1621
  }
1622
+ /**
1623
+ * Take a domain off the App, naming it. The name is the route's requirement and
1624
+ * ADR-0025's argument, not this function's.
1625
+ */
1626
+ async function detachDomain(creds, app, hostname) {
1627
+ const res = await authorizedFetch(`${domainsUrl(creds, app)}/${encodeURIComponent(hostname)}`, {
1628
+ method: "DELETE",
1629
+ headers: { Authorization: `Bearer ${creds.tenant_jwt}` }
1630
+ });
1631
+ if (!res.ok) throw await asGatewayError(res, `'${hostname}' could not be detached (HTTP ${res.status}).`);
1632
+ }
1633
+ /** A missing name is refused here rather than sent, so the reason reads at the keyboard. */
1634
+ async function detachDomainCommand(hostname, options = {}) {
1635
+ if (!hostname || !hostname.trim()) {
1636
+ console.error("[wawesome] Error: Missing domain. Usage: wawesome domains detach <hostname>");
1637
+ console.error("[wawesome] The domain is typed out because detaching one takes it dark.");
1638
+ process.exit(1);
1639
+ }
1640
+ const creds = readCredentials();
1641
+ if (!creds) {
1642
+ console.error("[wawesome] Error: Not logged in. Run 'wawesome login' first.");
1643
+ process.exit(1);
1644
+ }
1645
+ const app = options.app?.trim() || readFunctionConfig()?.app?.trim();
1646
+ if (!app) {
1647
+ console.error("[wawesome] Error: No wawesome-function.json found in the current directory.");
1648
+ console.error("[wawesome] Run this from your project, or name the app with --app <slug>.");
1649
+ process.exit(1);
1650
+ }
1651
+ const name = hostname.trim();
1652
+ console.log(`[wawesome] Detaching '${name}' from app '${app}':`);
1653
+ console.log("[wawesome] • the domain stops resolving here, so anything pointed at it stops");
1654
+ console.log("[wawesome] reaching this app");
1655
+ console.log("[wawesome] • this app's own address is unaffected and keeps serving");
1656
+ console.log("[wawesome] • the certificate is given back, and none is held for the name");
1657
+ try {
1658
+ await detachDomain(creds, app, name);
1659
+ } catch (err) {
1660
+ console.error(`[wawesome] Error: ${whatWentWrong(err)}`);
1661
+ process.exit(1);
1662
+ }
1663
+ console.log(`[wawesome] ✅ Detached '${name}' from app '${app}'.`);
1664
+ console.log("[wawesome] The domain is free to attach again by declaring it and deploying.");
1665
+ const declared = readFunctionConfig()?.domain;
1666
+ if (typeof declared === "string" && isSameHostname(declared, name)) {
1667
+ console.log(`[wawesome] wawesome-function.json still declares '${declared}'. Remove the line,`);
1668
+ console.log("[wawesome] or the next deploy attaches the domain again.");
1669
+ }
1670
+ }
1671
+ async function domainsCommand(action, target, options = {}) {
1672
+ if ((action || "").toLowerCase().trim() === "detach") return detachDomainCommand(target, options);
1673
+ console.error(`[wawesome] Error: Unknown command '${["domains", action].filter(Boolean).join(" ")}'.`);
1674
+ console.error("[wawesome] Usage: wawesome domains detach <hostname>");
1675
+ console.error("[wawesome] A domain is attached by declaring it in wawesome-function.json.");
1676
+ process.exit(1);
1677
+ }
1600
1678
  function domainsUrl(creds, app) {
1601
1679
  return `${creds.gateway_url}/v1/apps/${encodeURIComponent(app)}/domains`;
1602
1680
  }
@@ -1607,11 +1685,16 @@ function domainsUrl(creds, app) {
1607
1685
  * added a day apart: the TXT issues the certificate, and the CNAME is pointed
1608
1686
  * here afterwards so a site that is already live never answers on a
1609
1687
  * certificate that is not there yet.
1688
+ *
1689
+ * Read off the records too, never the states alone: naming a record the payload
1690
+ * does not carry left a reader holding the CNAME alone (#610).
1610
1691
  */
1611
- function whereItGotTo(state) {
1692
+ function whereItGotTo(domain) {
1693
+ const { state } = domain;
1612
1694
  if (state.serving) return "serving";
1613
1695
  if (state.certificate_active) return "certificate issued, so point the CNAME here to cut over";
1614
1696
  if (state.ownership_verified) return "ownership verified, and the certificate is being issued";
1697
+ if (domain.certificate_record_pending) return "not verified yet, and the TXT record is not ready";
1615
1698
  return "not verified yet, so add the TXT record below";
1616
1699
  }
1617
1700
  function whatItIsFor(record) {
@@ -1625,19 +1708,37 @@ const VALUE_COLUMN = " ".repeat(14);
1625
1708
  function domainLines(outcome) {
1626
1709
  if (outcome.kind === "unread") return [
1627
1710
  "",
1628
- ` Domain: \x1b[33m${outcome.hostname} could not be read\x1b[0m`,
1711
+ ` Domain: \x1b[33m${outcome.hostname}: where it got to is unknown\x1b[0m`,
1629
1712
  ...wrapped(outcome.said).map((line) => `${VALUE_COLUMN}${line}`),
1630
1713
  `${VALUE_COLUMN}Nothing about the domain changed, and the deploy landed.`
1631
1714
  ];
1715
+ if (outcome.kind === "undeclared") {
1716
+ const { hostname, state } = outcome.domain;
1717
+ const left = state.serving ? "it is still serving" : "nothing changed";
1718
+ return [
1719
+ "",
1720
+ ` Domain: \x1b[33m${hostname}: attached, and this project declares no domain\x1b[0m`,
1721
+ ...wrapped(`Nothing was detached and ${left}. A domain comes off an App when somebody names it, never because a line was deleted. Declare it again in wawesome-function.json, or take it off with`).map((line) => `${VALUE_COLUMN}${line}`),
1722
+ `${VALUE_COLUMN} \x1b[36mwawesome domains detach ${hostname}\x1b[0m`
1723
+ ];
1724
+ }
1632
1725
  if (outcome.kind === "refused") return [
1633
1726
  "",
1634
- ` Domain: \x1b[33m${outcome.hostname} not attached\x1b[0m`,
1727
+ ` Domain: \x1b[33m${outcome.hostname}: not attached\x1b[0m`,
1635
1728
  ...wrapped(outcome.said).map((line) => `${VALUE_COLUMN}${line}`),
1636
1729
  ...outcome.advice ? [`${VALUE_COLUMN}${outcome.advice}`] : [],
1730
+ ...outcome.held ? [`${VALUE_COLUMN}Nothing was detached. Take '${outcome.held}' off first with`, `${VALUE_COLUMN} \x1b[36mwawesome domains detach ${outcome.held}\x1b[0m`] : [],
1637
1731
  `${VALUE_COLUMN}The deploy landed, and this App's other address still answers.`
1638
1732
  ];
1639
- const { hostname, records, state, last_checked_at } = outcome.domain;
1640
- const lines = ["", ` Domain: \x1b[36m${hostname}\x1b[0m — ${whereItGotTo(state)}`];
1733
+ const { hostname, records, state, last_checked_at, detaching, certificate_record_pending } = outcome.domain;
1734
+ if (detaching) return [
1735
+ "",
1736
+ ` Domain: \x1b[33m${hostname}: being detached\x1b[0m`,
1737
+ `${VALUE_COLUMN}It has stopped answering, and the certificate is being given back.`,
1738
+ `${VALUE_COLUMN}Remove 'domain' from wawesome-function.json, or deploy again`,
1739
+ `${VALUE_COLUMN}once it is gone to attach it afresh.`
1740
+ ];
1741
+ const lines = ["", ` Domain: \x1b[36m${hostname}\x1b[0m: ${whereItGotTo(outcome.domain)}`];
1641
1742
  if (state.serving) {
1642
1743
  lines.push(`${VALUE_COLUMN}The address above answers too, so nothing pointed at it breaks.`);
1643
1744
  return lines;
@@ -1646,7 +1747,9 @@ function domainLines(outcome) {
1646
1747
  const nameWidth = Math.max(...records.map((record) => record.name.length));
1647
1748
  lines.push("", " DNS records to add at your registrar:");
1648
1749
  for (const record of records) lines.push("", ` ${whatItIsFor(record)}:`, ` ${record.type.padEnd(5)} ${record.name.padEnd(nameWidth)} ${record.value}`);
1649
- lines.push("", `${VALUE_COLUMN}The deploy is finished. This domain comes up on its own`, `${VALUE_COLUMN}once the records resolve.`);
1750
+ lines.push("");
1751
+ if (certificate_record_pending) lines.push(`${VALUE_COLUMN}The TXT record that proves you own the name is not ready`, `${VALUE_COLUMN}yet. Deploy again in a minute and it prints here. The`, `${VALUE_COLUMN}deploy is finished either way.`);
1752
+ else lines.push(`${VALUE_COLUMN}The deploy is finished. This domain comes up on its own`, `${VALUE_COLUMN}once the records resolve.`);
1650
1753
  return lines;
1651
1754
  }
1652
1755
  /** The platform's own prose, kept inside the block it is printed in. */
@@ -1738,7 +1841,7 @@ const MONTHS = [
1738
1841
  ];
1739
1842
  /** Indented to sit inside the receipt the deploy already prints. */
1740
1843
  function headroomLines(usage) {
1741
- const lines = [` Plan: ${usage.plan.name} this deploy does not change your bill.`, ` Apps: ${usage.occupied_app_slots} / ${usage.plan.limits.app_slots} slots`];
1844
+ const lines = [` Plan: ${usage.plan.name}. This deploy does not change your bill.`, ` Apps: ${usage.occupied_app_slots} / ${usage.plan.limits.app_slots} slots`];
1742
1845
  const stored = storedLine(usage);
1743
1846
  if (stored !== null) lines.push(stored);
1744
1847
  lines.push(` Usage: ${periodLabel(usage.period.start, usage.period.end)}`);
@@ -2020,8 +2123,8 @@ function divergenceLines(previous, target, addresses) {
2020
2123
  `[wawesome] wawesome-function.json now says ${qualifiedName(target)}.`,
2021
2124
  "",
2022
2125
  "[wawesome] Deploying will not rename it. A Function's address is built from its",
2023
- "[wawesome] name, so this deploy lands on a Function of its own and leaves the old",
2024
- "[wawesome] one exactly as it is — promoted, and still answering the code on it:",
2126
+ "[wawesome] name, so this deploy lands on a Function of its own. The old one stays",
2127
+ "[wawesome] promoted, and still answers the code on it:",
2025
2128
  "",
2026
2129
  `[wawesome] stays live: ${addresses.previous ?? qualifiedName(previous)}`,
2027
2130
  `[wawesome] gets this deploy: ${addresses.next ?? qualifiedName(target)}`,
@@ -2075,7 +2178,7 @@ function declaredVisibility(value) {
2075
2178
  function reportPublishRefusal(message) {
2076
2179
  console.error(`\n[wawesome] \x1b[31mError: ${message}\x1b[0m`);
2077
2180
  console.error("[wawesome] Nothing was deployed. This Function is private, so its address");
2078
- console.error("[wawesome] resolves for nobody publishing it puts it back on the open");
2181
+ console.error("[wawesome] resolves for nobody. Publishing it puts it back on the open");
2079
2182
  console.error("[wawesome] internet.");
2080
2183
  console.error("[wawesome] Re-run with \x1B[36mwawesome deploy --publish\x1B[0m to publish it, or put");
2081
2184
  console.error("[wawesome] \x1B[36m\"visibility\": \"private\"\x1B[0m back in wawesome-function.json.\n");
@@ -2236,7 +2339,7 @@ async function deploy(entryInput, options) {
2236
2339
  } catch (err) {
2237
2340
  if (isVerbose) console.log(`[wawesome:verbose] Could not resolve the workspace address: ${err instanceof Error ? err.message : err}`);
2238
2341
  }
2239
- const domain = domainName ? await applyDeclaredDomain(creds, app, domainName) : null;
2342
+ const domain = domainName ? await applyDeclaredDomain(creds, app, domainName) : await findUndeclaredDomain(creds, app);
2240
2343
  let headroom = null;
2241
2344
  try {
2242
2345
  headroom = headroomLines(await fetchTenantUsage(creds));
@@ -2256,13 +2359,13 @@ async function deploy(entryInput, options) {
2256
2359
  for (const line of scheduleLines(schedules)) console.log(line);
2257
2360
  }
2258
2361
  if (visibility === "private") {
2259
- console.log("\n URL: none this Function is private, so it is not reachable");
2362
+ console.log("\n URL: none. This Function is private, so it is not reachable");
2260
2363
  console.log(" from the web at all.");
2261
2364
  } else if (address) {
2262
2365
  console.log(`\n URL: \x1b[36m${address}\x1b[0m`);
2263
2366
  console.log(` ${SUBTREE_NOTE}`);
2264
2367
  } else if (surface) {
2265
- console.log("\n URL: none this gateway serves no public address form.");
2368
+ console.log("\n URL: none. This gateway serves no public address form.");
2266
2369
  console.log(" Set CONTENT_ORIGIN on it, or ALLOW_PATH_INVOCATION_FORM");
2267
2370
  console.log(" for local development.");
2268
2371
  }
@@ -2358,8 +2461,8 @@ async function uploadAssets(creds, app, funcName, bundle, assets, declared, isVe
2358
2461
  //#region src/env.ts
2359
2462
  const STANDARD_SECRET_MESSAGES = [
2360
2463
  "Encrypted at rest using AES-256",
2361
- "This value can't be viewed again after you save it only updated or deleted",
2362
- "Only decrypted at the moment your function runs never returned by the CLI, dashboard, or API after creation."
2464
+ "This value can't be viewed again after you save it, only updated or deleted",
2465
+ "Only decrypted at the moment your function runs, and never returned by the CLI, dashboard, or API after creation."
2363
2466
  ];
2364
2467
  /**
2365
2468
  * Load credentials and validate project config.
@@ -3048,7 +3151,7 @@ async function listTemplates(options) {
3048
3151
  }
3049
3152
  console.log("\n📦 \x1B[1mTemplates\x1B[0m\n");
3050
3153
  for (const template of templates) {
3051
- console.log(` \x1b[36m${template.name}\x1b[0m ${template.title}`);
3154
+ console.log(` \x1b[36m${template.name}\x1b[0m: ${template.title}`);
3052
3155
  console.log(` ${template.description}`);
3053
3156
  if (template.tags.length > 0) console.log(` \x1b[90m${template.tags.join(", ")}\x1b[0m`);
3054
3157
  console.log(` \x1b[90m${template.source_url}\x1b[0m`);
@@ -3170,7 +3273,7 @@ async function ensureSession(options) {
3170
3273
  const probed = await probeSession(stored);
3171
3274
  if (probed) return probed;
3172
3275
  }
3173
- console.log(stored ? "\n[wawesome] Your session has expired." : "\n[wawesome] You're not logged in yet that's what a deploy needs.");
3276
+ console.log(stored ? "\n[wawesome] Your session has expired." : "\n[wawesome] You're not logged in yet, which is what a deploy needs.");
3174
3277
  if (!isInteractive()) {
3175
3278
  console.log("[wawesome] Run 'wawesome login', then 'wawesome deploy'.");
3176
3279
  return null;
@@ -3234,12 +3337,12 @@ async function offerWorkspaceAddress(session, creds, tenant, appSlug, functionNa
3234
3337
  return;
3235
3338
  }
3236
3339
  if (tenant.slug_locked) {
3237
- console.log(` 🔒 '${current}' is fixed a Function version has been promoted, and live URLs already carry it.\n`);
3340
+ console.log(` 🔒 '${current}' is fixed. A Function version has been promoted, and live URLs already carry it.\n`);
3238
3341
  return;
3239
3342
  }
3240
- console.log(` '${current}' is your workspace address the part of that URL that is yours`);
3241
- console.log(" rather than this project's, and this is the moment to change it: it locks");
3242
- console.log(" for good the first time you deploy, because live URLs carry it.");
3343
+ console.log(` '${current}' is your workspace address, the part of that URL that is`);
3344
+ console.log(" yours rather than this project's. Now is the moment to change it. It");
3345
+ console.log(" locks for good the first time you deploy, because live URLs carry it.");
3243
3346
  console.log(" Press enter to keep it.\n");
3244
3347
  let refusal;
3245
3348
  for (let attempt = 0; attempt < MAX_RENAME_ATTEMPTS; attempt++) {
@@ -3271,7 +3374,7 @@ async function offerWorkspaceAddress(session, creds, tenant, appSlug, functionNa
3271
3374
  * template from needing a CLI release.
3272
3375
  */
3273
3376
  async function wireUp(creds, appSlug, manifest, answers) {
3274
- const scaffolded = "Your project is scaffolded fix this, then run 'wawesome deploy'.";
3377
+ const scaffolded = "Your project is scaffolded. Fix this, then run 'wawesome deploy'.";
3275
3378
  try {
3276
3379
  await ensureApp(creds, appSlug);
3277
3380
  } catch (err) {
@@ -3354,7 +3457,7 @@ async function initFromTemplate(templateName, options) {
3354
3457
  const session = openPromptSession();
3355
3458
  try {
3356
3459
  functionName = await promptForSlug(session, "Function name", readFunctionConfig(staging)?.function || dirName);
3357
- console.log(" (an App groups the Functions of one project its slug is part of the public URL)");
3460
+ console.log(" (an App groups the Functions of one project, and its slug is part of the public URL)");
3358
3461
  appSlug = await promptForSlug(session, "App slug", dirName);
3359
3462
  let repinned = null;
3360
3463
  try {
@@ -3368,7 +3471,7 @@ async function initFromTemplate(templateName, options) {
3368
3471
  fail$1(errorText(err), `Some of '${template.name}' may have been written to ${cwd}.`);
3369
3472
  }
3370
3473
  console.log(`\n[wawesome] ✅ Scaffolded ${files.length} files from '${template.name}'.`);
3371
- if (repinned) console.log(`[wawesome] Using wawesome ${repinned.to} the template pinned ${repinned.from}.`);
3474
+ if (repinned) console.log(`[wawesome] Using wawesome ${repinned.to}. The template pinned ${repinned.from}.`);
3372
3475
  if (tenantSession) await offerWorkspaceAddress(session, tenantSession.creds, tenantSession.tenant, appSlug, functionName);
3373
3476
  answers = tenantSession ? await promptForDeclaredEnv(session, manifest.env) : [];
3374
3477
  } finally {
@@ -3417,7 +3520,7 @@ async function init(options) {
3417
3520
  functionName = "root";
3418
3521
  console.log(" Function name? root (locked by --root)");
3419
3522
  } else functionName = await promptForSlug(session, "Function name", dirName);
3420
- console.log(" (an App groups the Functions of one project its slug is part of the public URL)");
3523
+ console.log(" (an App groups the Functions of one project, and its slug is part of the public URL)");
3421
3524
  appSlug = await promptForSlug(session, "App slug", dirName);
3422
3525
  } finally {
3423
3526
  session.close();
@@ -4424,7 +4527,7 @@ async function showWorkspace(options) {
4424
4527
  console.log(` ${SUBTREE_NOTE}`);
4425
4528
  }
4426
4529
  }
4427
- if (tenant.slug_locked) console.log("\n 🔒 The address is fixed a Function version has been promoted and live URLs carry it.");
4530
+ if (tenant.slug_locked) console.log("\n 🔒 The address is fixed. A Function version has been promoted, and live URLs carry it.");
4428
4531
  else {
4429
4532
  console.log("\n The address can still be changed: \x1B[36mwawesome workspace rename <name>\x1B[0m");
4430
4533
  console.log(" It locks for good the first time you deploy.");
@@ -5196,6 +5299,8 @@ cli.command("env [action] [key] [value]", "Manage environment variables (set, li
5196
5299
  cli.command("env set <key> <value>", "Set or overwrite an environment variable on the current app").option("-s, --secret", "Flag variable as secret (write-only)").option("-v, --verbose", "Enable verbose debug output").action((key, value, options) => setEnvVar(key, value, options));
5197
5300
  cli.command("env list", "List environment variables for the current app").alias("env ls").option("-v, --verbose", "Enable verbose debug output").action((options) => listEnvVars(options));
5198
5301
  cli.command("env rm <key>", "Delete an environment variable from the current app").alias("env remove").alias("env delete").alias("env unset").option("-v, --verbose", "Enable verbose debug output").action((key, options) => removeEnvVar(key, options));
5302
+ cli.command("domains [action] [hostname]", "Manage the custom domain on the current app (detach)").usage("domains <action> [hostname]\n\nActions:\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.").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));
5303
+ 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));
5199
5304
  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));
5200
5305
  cli.command("logout", "Clear stored authentication credentials").action(() => logout());
5201
5306
  cli.command("whoami", "Show current login session info").action(() => whoami());
@@ -5217,34 +5322,35 @@ cli.command("credentials [action] [target]", "Mint, list and revoke deploy crede
5217
5322
  wawesome credentials mint ci-pipeline
5218
5323
  wawesome credentials mint agent -c logs:read --expires never
5219
5324
  wawesome credentials mint prod-deploy -c deploy -a prod --expires 30
5325
+ wawesome credentials mint ci -c deploy,domains:attach -a prod
5220
5326
  wawesome credentials mint ci-pipeline > /tmp/secret
5221
5327
  wawesome credentials revoke ci-pipeline
5222
- wawesome credentials revoke wawe_ab3k9x --yes`).option("-c, --capability <capability>", "Capability to grant: deploy, logs:read (repeatable or comma-separated, default: deploy)").option("-a, --app <app>", "Restrict the credential to this App (repeatable or comma-separated, default: every App)").option("--expires <days|never>", "Days until the credential expires, or 'never' (default: 90)").option("-y, --yes", "Skip the revoke confirmation").option("-v, --verbose", "Enable verbose debug output").action((action, target, options) => credentialsCommand(action, target, options));
5223
- cli.command("credentials mint <name>", "Mint a deploy credential, printing its secret exactly once").option("-c, --capability <capability>", "Capability to grant: deploy, logs:read (repeatable or comma-separated)").option("-a, --app <app>", "Restrict the credential to this App (repeatable or comma-separated)").option("--expires <days|never>", "Days until the credential expires, or 'never' (default: 90)").option("-v, --verbose", "Enable verbose debug output").action((name, options) => mintCredentialCommand(name, options));
5224
- cli.command("credentials list", "List deploy credentials never their secrets").alias("credentials ls").option("-v, --verbose", "Enable verbose debug output").action((options) => listCredentialsCommand(options));
5328
+ wawesome credentials revoke wawe_ab3k9x --yes`).option("-c, --capability <capability>", "Capability to grant: deploy, logs:read, domains:attach (repeatable or comma-separated, default: deploy)").option("-a, --app <app>", "Restrict the credential to this App (repeatable or comma-separated, default: every App)").option("--expires <days|never>", "Days until the credential expires, or 'never' (default: 90)").option("-y, --yes", "Skip the revoke confirmation").option("-v, --verbose", "Enable verbose debug output").action((action, target, options) => credentialsCommand(action, target, options));
5329
+ cli.command("credentials mint <name>", "Mint a deploy credential, printing its secret exactly once").option("-c, --capability <capability>", "Capability to grant: deploy, logs:read, domains:attach (repeatable or comma-separated)").option("-a, --app <app>", "Restrict the credential to this App (repeatable or comma-separated)").option("--expires <days|never>", "Days until the credential expires, or 'never' (default: 90)").option("-v, --verbose", "Enable verbose debug output").action((name, options) => mintCredentialCommand(name, options));
5330
+ cli.command("credentials list", "List deploy credentials, never their secrets").alias("credentials ls").option("-v, --verbose", "Enable verbose debug output").action((options) => listCredentialsCommand(options));
5225
5331
  cli.command("credentials revoke <name-or-prefix>", "Revoke a deploy credential, immediately and for good").option("-y, --yes", "Revoke without being asked to confirm").option("-v, --verbose", "Enable verbose debug output").action((target, options) => revokeCredentialCommand(target, options));
5226
5332
  cli.command("init", "Scaffold a new function project in the current directory").usage("init [options]\n\nWith --template, the project is fetched from the template catalog, wired up\nfrom what the template declares it needs, and deployed. Run 'wawesome templates'\nto see what is available.").example("wawesome init").example("wawesome init --template stripe-webhook").option("-t, --template <name>", "Scaffold from a catalog template and deploy it").option("--api <url>", "API URL (default: https://api.wawesome.io)").option("--dashboard <url>", "Dashboard URL (default: https://dashboard.wawesome.io)").option("--no-install", "Skip installing dependencies after scaffolding").option("--root", "Generate a root function router template").option("-v, --verbose", "Enable verbose debug output").action((options) => init(options));
5227
5333
  cli.command("logs [function-name-or-invocation-id]", "View invocation history, fetch log output, or follow live").usage(`logs [target] [options]
5228
5334
 
5229
5335
  The target argument determines what the command does:
5230
5336
 
5231
- MODE 1 List invocations (no UUID target)
5337
+ MODE 1: list invocations (no UUID target)
5232
5338
  wawesome logs # list invocations for the function in the current directory
5233
5339
  wawesome logs my-function # list invocations for 'my-function'
5234
5340
  wawesome logs my-function --error # only show failed invocations
5235
5341
  wawesome logs my-function --running # only show currently running invocations
5236
5342
 
5237
- MODE 2 Fetch captured log body (UUID target or --invocation)
5343
+ MODE 2: fetch the captured log body (UUID target or --invocation)
5238
5344
  wawesome logs <invocation-id> # print stdout/stderr for a specific invocation
5239
5345
  wawesome logs my-function --invocation <id> # same, explicit flag form
5240
5346
 
5241
- MODE 3 Follow live output (--follow / -f)
5347
+ MODE 3: follow live output (--follow / -f)
5242
5348
  wawesome logs my-function --follow # follow the function: stream output from every invocation as it runs
5243
5349
  wawesome logs <invocation-id> --follow # follow one specific in-flight invocation by ID
5244
5350
 
5245
- With a function name, --follow streams the function's output continuously across
5246
- invocations — new output appears each time the function runs, no need to catch a
5247
- specific invocation. Press Ctrl-C to stop at any time.`).option("-f, --follow", "Stream live output (tail -f style). Follows a running invocation or waits for the next one. Ctrl-C to stop").option("-i, --invocation <id>", "Fetch stdout/stderr log body for a specific invocation ID").option("-a, --app <app>", "App slug override (defaults to wawesome-function.json)").option("-s, --status <status>", "Filter invocations by status (success, error, timeout, running)").option("--success", "Shorthand for --status success").option("--error", "Shorthand for --status error").option("--timeout", "Shorthand for --status timeout").option("--running", "Shorthand for --status running").option("-v, --verbose", "Enable verbose debug output").action((target, options) => logsCommand(target, options));
5351
+ With a function name, --follow streams the function's output across invocations.
5352
+ New output appears each time the function runs, so you never have to catch a
5353
+ specific one. Press Ctrl-C to stop at any time.`).option("-f, --follow", "Stream live output (tail -f style). Follows a running invocation or waits for the next one. Ctrl-C to stop").option("-i, --invocation <id>", "Fetch stdout/stderr log body for a specific invocation ID").option("-a, --app <app>", "App slug override (defaults to wawesome-function.json)").option("-s, --status <status>", "Filter invocations by status (success, error, timeout, running)").option("--success", "Shorthand for --status success").option("--error", "Shorthand for --status error").option("--timeout", "Shorthand for --status timeout").option("--running", "Shorthand for --status running").option("-v, --verbose", "Enable verbose debug output").action((target, options) => logsCommand(target, options));
5248
5354
  cli.command("invoke [function]", "Fire a background run of a serverless function").usage(`invoke [function] [options]
5249
5355
 
5250
5356
  Fire a Function run now through the authenticated API, without deploying anything
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wawesome",
3
- "version": "0.10.0",
3
+ "version": "0.12.0",
4
4
  "description": "CLI tool for building and deploying serverless functions on wawesome.io platform",
5
5
  "type": "module",
6
6
  "bin": {