wawesome 0.15.0 → 0.17.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 +62 -18
  2. package/dist/index.mjs +229 -37
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -118,6 +118,7 @@ npx wawesome deploy
118
118
  | `npx wawesome env set <key> <val>` | Set an environment variable (add `--secret` for write-only) |
119
119
  | `npx wawesome env rm <key>` | Delete an environment variable |
120
120
  | `npx wawesome domains` | Show the app's domain, its state, and the DNS records to add |
121
+ | `npx wawesome domains adopt` | Write the domain the app answers at into wawesome-function.json |
121
122
  | `npx wawesome domains detach <host>` | Stop serving the app at a domain you attached |
122
123
  | `npx wawesome credentials` | List deploy credentials: name, prefix, capabilities, Apps, last use |
123
124
  | `npx wawesome credentials mint <name>` | Mint a deploy credential and print its secret, once |
@@ -477,9 +478,9 @@ concluding the platform cannot do it.
477
478
 
478
479
  | Tool | What it does | Needs |
479
480
  |:---------------------------|:------------------------------------------------------------------------|:--------------|
480
- | `whoami` | The workspace, where it is administered, the words this credential carries, the Apps it reaches | nothing |
481
+ | `whoami` | The workspace, where it is administered, the words this credential carries and which of them a tool here asks for, the Apps it reaches | nothing |
481
482
  | `list_templates` | The template catalogue, with what each one is for | nothing |
482
- | `get_template` | One template's files, environment contract and outbound providers | nothing |
483
+ | `get_template` | One template's source, environment contract and outbound providers; its tests and build configuration are named, and read on request | nothing |
483
484
  | `list_apps` | The Apps the credential reaches, each with its address and its dashboard page | `read:apps` |
484
485
  | `list_functions` | The Functions in one App, and what their live version carries | `read:functions` |
485
486
  | `list_versions` | A Function's Versions, and which one is live | `read:functions` |
@@ -492,7 +493,12 @@ concluding the platform cannot do it.
492
493
  | `deploy_function` | Deploys code and the static files it carries | `write:functions` |
493
494
  | `deploy_status` | How a deploy went, by the handle it answered with | `read:functions` |
494
495
  | `rollback_function` | Puts a Version the Function already has back on its public address | `write:functions` |
496
+ | `request_upload` | Asks the person you are working with for files the conversation cannot produce, and answers a link to give them | `write:functions` |
497
+ | `list_uploads` | What the person sent against one upload session, ready to declare on the next deploy | `read:functions` |
495
498
  | `set_env_var` | Sets an App's environment variable, read at the next invocation | `write:env` |
499
+ | `attach_domain` | Attaches a domain to an App and answers the DNS records that make it work | `write:domains` |
500
+ | `cancel_domain_claim` | Gives up a domain claim nobody has proved control of, freeing the App's place | `write:domains` |
501
+ | `check_domain` | Where the domain attached to an App got to, asked at the certificate vendor | `read:domains` |
496
502
  | `invoke_function` | Runs a deployed Function once and answers with the Invocation id | `write:runs` |
497
503
  | `fetch_function` | Fetches one path from a Function's live Version and answers what it served | `write:runs` |
498
504
 
@@ -501,8 +507,11 @@ beside it, and the presets below are named in exactly these words.
501
507
 
502
508
  `whoami` is the one to call first. It answers the workspace, every word the credential carries and
503
509
  the Apps it reaches, and it asks for nothing — so your agent can say what it is able to do before it
504
- offers, rather than finding the edge by being refused in front of you. A member's session calling the
505
- endpoint gets the same answer for the role they hold.
510
+ offers, rather than finding the edge by being refused in front of you. Each word also says whether a
511
+ tool here asks for it. Your credential can carry `write:schedules` with no schedule tool on this
512
+ endpoint, and the mark is what stops an agent offering to schedule a job: a word with no tool behind
513
+ it is done on the dashboard rather than being withheld from you. A member's session calling the endpoint gets
514
+ the same answer for the role they hold.
506
515
 
507
516
  A deploy is two calls. The first says which of your files the platform does not already hold, by
508
517
  content hash; your agent uploads those bytes to `PUT /v1/assets/{content_hash}` with the same
@@ -553,7 +562,9 @@ npx wawesome credentials mint ci --preset deployer -a prod
553
562
  npx wawesome credentials mint agent --preset member -a prod
554
563
  ```
555
564
 
556
- Add `write:domains` where the project declares a custom domain the deploy should claim. Nothing gives
565
+ Add `write:domains` where the project declares a custom domain the deploy should claim, or where an
566
+ agent attaches one for somebody who has no terminal — it also gives back a claim nobody has proved
567
+ control of yet, and never takes a live name off an App. Nothing gives
557
568
  an agent billing, a plan change, credential management or the ability to rename your workspace: those
558
569
  are closed to every credential, whatever it carries. Reading the workspace's name and address is not
559
570
  among them — `whoami` answers both to any credential, which is how the agent knows where what it
@@ -675,6 +686,18 @@ Two rules to know about:
675
686
  `favicon.ico`, `robots.txt` and a `.well-known/` directory are what that is for; hashed build
676
687
  output belongs under `assets/`.
677
688
 
689
+ **A version carries exactly the files its deploy declared.** Nothing is inherited from the version
690
+ before it, so taking `"assets"` out of `wawesome-function.json` — or pointing it at a directory your
691
+ build no longer writes to — is not an edit but every file on the site going at once. A deploy that
692
+ would do that against a Function serving files today is refused, and tells you how many would go:
693
+
694
+ ```bash
695
+ npx wawesome deploy --drop-assets
696
+ ```
697
+
698
+ Removing *some* of the files is ordinary editing and needs nothing: only the wholesale drop is
699
+ refused, because that is the shape of a mistake rather than of a decision.
700
+
678
701
  **A page is a file like any other.** An `index.html` your build wrote is deployed, hashed, retained
679
702
  and billed exactly as your other files are, and it is what a directory-style address resolves to: a
680
703
  path whose last segment carries no extension is served the `index.html` beneath it, so `/`, `/about`,
@@ -784,10 +807,16 @@ Add `"domain"` and your App answers at a name you own, beside the address it alr
784
807
  }
785
808
  ```
786
809
 
787
- The deploy attaches it and prints two DNS records to add at your registrar. The TXT record proves you
788
- own the name, which is what issues the certificate. The CNAME points traffic here, and you add that
789
- one once the certificate is issued, so a site that is already live never spends a minute answering on
790
- a certificate that is not there yet.
810
+ The deploy attaches it and prints three DNS records to add at your registrar. The TXT record proves
811
+ you own the name. The `_acme-challenge` CNAME beside it issues the certificate and keeps renewing it
812
+ for as long as it stays there, so nobody comes back to your registrar at renewal time — if an
813
+ `_acme-challenge` TXT record is already at that name, delete it, because a TXT and this CNAME cannot
814
+ both sit there. The last CNAME points traffic here, and you add that one once the certificate is
815
+ issued, so a site that is already live never spends a minute answering on a certificate that is not
816
+ there yet.
817
+
818
+ Nothing you are asked to add ever changes. Copy a value into your registrar, come back an hour later,
819
+ and it is the same value.
791
820
 
792
821
  Your deploy waits for neither. It attaches the domain, prints the records and finishes, and every
793
822
  later deploy states where the domain got to: not verified, ownership verified, certificate issued, or
@@ -801,10 +830,10 @@ npx wawesome domains
801
830
 
802
831
  It prints the domain, where it got to, when the platform last checked, and the records to add. It
803
832
  changes nothing, and a deploy credential carrying `read:domains` can run it, so a pipeline reads the
804
- same thing you do. The TXT record is minted a moment after the name is claimed, and an attach that
805
- outran it prints a row saying so rather than dropping the record from the list. That row is what this
806
- command fills in, and it is where the deploy sends you: deploying again would change nothing, and a
807
- deploy that changes nothing is refused.
833
+ same thing you do. Where the platform does not hold a record yet it prints a row saying so rather
834
+ than dropping the record from the list. That row is what this command fills in, and it is where the
835
+ deploy sends you: deploying again would change nothing, and a deploy that changes nothing is
836
+ refused.
808
837
 
809
838
  A custom domain is granted from the Solo plan upwards, one per App. On a plan that grants none, the
810
839
  deploy prints the refusal and lands everything else it was doing.
@@ -812,13 +841,27 @@ deploy prints the refusal and lands everything else it was doing.
812
841
  The address derived from your workspace slug keeps serving after you attach a domain. Both names
813
842
  answer, so webhooks and integrations already pointed at the old one keep working.
814
843
 
815
- An apex domain, `client.com` with no `www`, attaches like any other name. Whether a CNAME may sit at
816
- your zone root is your DNS provider's rule, and where it cannot, attach `www.client.com` and configure
817
- the redirect at the provider.
844
+ An apex domain, `client.com` with no `www`, claims `www.client.com` with it, and attaching the `www`
845
+ claims the apex. Both halves are one domain against your App's one place, and both are printed with
846
+ their own state and their own records, because a visitor typing either one should reach your site.
847
+ Where your DNS provider cannot hold a CNAME at your zone root, that half is reported as not claimed
848
+ with the reason, and attaching the same name again picks it up once your DNS can carry it.
849
+
850
+ The file is not the only door. A domain can also be attached from the dashboard or by an agent, and
851
+ then your App answers at a name `wawesome-function.json` has never heard of. A deploy says so and
852
+ edits nothing — what was committed is what ships — so the file is written by a command of your own:
853
+
854
+ ```bash
855
+ npx wawesome domains adopt
856
+ ```
857
+
858
+ It writes the attached hostname into `wawesome-function.json` and changes nothing about the domain.
859
+ Where the file already declares a *different* name, it refuses and prints both: an App carries one
860
+ domain, and which of the two names it should be is yours to settle rather than a file to overwrite.
818
861
 
819
862
  Deleting the line detaches nothing. Detaching takes a live site dark, which is not something a
820
863
  deploy should infer from a deleted line. So the deploy reports the domain it found and left serving,
821
- and names the one command that takes it off:
864
+ names the command that writes it into the file, and names the one command that takes it off:
822
865
 
823
866
  ```bash
824
867
  npx wawesome domains detach shop.client.com
@@ -837,7 +880,7 @@ handler sees it, and off your response before the caller does. So **do not name
837
880
  own on that prefix**. It is dropped silently rather than rejected, and you will not get an error
838
881
  telling you why it vanished.
839
882
 
840
- Four headers arrive or leave on it, and the stripping is what makes them worth trusting:
883
+ Five headers arrive or leave on it, and the stripping is what makes them worth trusting:
841
884
 
842
885
  | Header | Direction | What it means |
843
886
  | --- | --- | --- |
@@ -845,6 +888,7 @@ Four headers arrive or leave on it, and the stripping is what makes them worth t
845
888
  | `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. |
846
889
  | `x-wawesome-invocation-id` | outbound | The id of this run, the key to fetch its logs with `npx wawesome logs --invocation <id>`. |
847
890
  | `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. |
891
+ | `x-wawesome-document` | outbound | Set it to the path of a document your own deploy carried (`index.html`) and the platform streams that file in place of the body you returned. Your status and your other headers stand; the content type and the length are the file's. Naming a path your deploy does not carry is a `500`, and your logs say which path you named. |
848
892
 
849
893
  ### Testing against the guest's JavaScript surface
850
894
 
package/dist/index.mjs CHANGED
@@ -232,12 +232,11 @@ function deleteCredentials() {
232
232
  }
233
233
  return false;
234
234
  }
235
- /**
236
- * Read wawesome-function.json from the project directory.
237
- */
235
+ function getFunctionConfigPath(projectDir) {
236
+ return path.join(projectDir || process.cwd(), "wawesome-function.json");
237
+ }
238
238
  function readFunctionConfig(projectDir) {
239
- const dir = projectDir || process.cwd();
240
- const configPath = path.join(dir, "wawesome-function.json");
239
+ const configPath = getFunctionConfigPath(projectDir);
241
240
  if (!fs.existsSync(configPath)) return null;
242
241
  try {
243
242
  const raw = fs.readFileSync(configPath, "utf-8");
@@ -906,7 +905,7 @@ async function buildHandler(entryInput, config, options) {
906
905
  * that has to name this version — `--version`, the dependency a scaffolded
907
906
  * project pins — reads it here, so a release bumps one file.
908
907
  */
909
- const CLI_VERSION = "0.15.0";
908
+ const CLI_VERSION = "0.17.0";
910
909
  //#endregion
911
910
  //#region src/prompt.ts
912
911
  /**
@@ -1579,7 +1578,7 @@ async function applyDeclaredDomain(creds, app, hostname) {
1579
1578
  said: whatWentWrong(err)
1580
1579
  };
1581
1580
  }
1582
- const attached = held.find((domain) => isSameHostname(domain.hostname, hostname));
1581
+ const attached = held.find((domain) => carries(domain, hostname));
1583
1582
  if (attached) return {
1584
1583
  kind: "unchanged",
1585
1584
  domain: attached
@@ -1631,6 +1630,15 @@ function whatWentWrong(err) {
1631
1630
  return errorText(err);
1632
1631
  }
1633
1632
  /**
1633
+ * Whether a claim answers at the declared name — at the leg it reads as, or at
1634
+ * the other half it carries. A file naming the `www` of an apex-led claim
1635
+ * declares a name the App already answers at, and re-attaching it every deploy
1636
+ * would report a change nothing made.
1637
+ */
1638
+ function carries(domain, declared) {
1639
+ return [domain.hostname, ...(domain.legs ?? []).filter((leg) => leg.claimed).map((leg) => leg.hostname)].some((name) => isSameHostname(name, declared));
1640
+ }
1641
+ /**
1634
1642
  * The comparison the platform makes: a name is held lowercase without its
1635
1643
  * trailing dot, so `shop.client.com.` in the file is the domain already
1636
1644
  * attached rather than a second one to ask for on every deploy.
@@ -1722,6 +1730,105 @@ async function detachDomainCommand(hostname, options = {}) {
1722
1730
  }
1723
1731
  }
1724
1732
  /**
1733
+ * The project's configuration as a file to write rather than as settings to
1734
+ * read: the raw text, so what is written back keeps the shape of what was
1735
+ * committed, and a file that does not parse is refused rather than replaced.
1736
+ */
1737
+ function projectConfig() {
1738
+ const path = getFunctionConfigPath();
1739
+ if (!fs.existsSync(path)) {
1740
+ console.error("[wawesome] Error: No wawesome-function.json found in the current directory.");
1741
+ console.error("[wawesome] Run this from the project that deploys the app.");
1742
+ process.exit(1);
1743
+ }
1744
+ const text = fs.readFileSync(path, "utf-8");
1745
+ try {
1746
+ return {
1747
+ path,
1748
+ text,
1749
+ parsed: JSON.parse(text)
1750
+ };
1751
+ } catch (err) {
1752
+ console.error(`[wawesome] Error: wawesome-function.json is not valid JSON: ${errorText(err)}`);
1753
+ console.error("[wawesome] Nothing was written. Fix the file and run this again.");
1754
+ process.exit(1);
1755
+ }
1756
+ }
1757
+ /**
1758
+ * The declared domain written back, in the indentation the file already uses so
1759
+ * the diff is the one line.
1760
+ */
1761
+ function writeDeclaredDomain(config, hostname) {
1762
+ const indent = /\n([ \t]+)"/.exec(config.text)?.[1] ?? " ";
1763
+ const written = JSON.stringify({
1764
+ ...config.parsed,
1765
+ domain: hostname
1766
+ }, null, indent);
1767
+ fs.writeFileSync(config.path, config.text.endsWith("\n") ? `${written}\n` : written, "utf-8");
1768
+ }
1769
+ /**
1770
+ * Write the domain the App carries into wawesome-function.json.
1771
+ *
1772
+ * The fix named beside the drift a deploy reports (ADR-0034). The platform row
1773
+ * is what serves and the file is a desired state, so a name attached from the
1774
+ * dashboard or by an agent is a name this file has never heard of — and a
1775
+ * deploy will not write it, because a deploy never edits a tracked file.
1776
+ *
1777
+ * A file naming a different domain is two names in play and a person's
1778
+ * question: both are printed and nothing is written.
1779
+ */
1780
+ async function adoptDomainCommand(hostname, options = {}) {
1781
+ if (hostname?.trim()) {
1782
+ console.error(`[wawesome] Error: 'wawesome domains adopt' takes no hostname, and '${hostname.trim()}' was given.`);
1783
+ console.error("[wawesome] It writes the name the app already answers at, which it reads from the platform.");
1784
+ process.exit(1);
1785
+ }
1786
+ const config = projectConfig();
1787
+ const { creds, app } = credentialsAndApp(options);
1788
+ const deploysTo = typeof config.parsed.app === "string" ? config.parsed.app.trim() : void 0;
1789
+ if (deploysTo && deploysTo !== app) {
1790
+ console.error(`[wawesome] Error: wawesome-function.json deploys to app '${deploysTo}', and --app names '${app}'.`);
1791
+ console.error("[wawesome] Nothing was written: a name off another app does not belong in this file.");
1792
+ process.exit(1);
1793
+ }
1794
+ let held;
1795
+ try {
1796
+ held = await listDomains(creds, app);
1797
+ } catch (err) {
1798
+ console.error(`[wawesome] Error: ${whatWentWrong(err)}`);
1799
+ process.exit(1);
1800
+ }
1801
+ const attached = held.find((domain) => !domain.detaching);
1802
+ if (!attached) {
1803
+ console.log(`[wawesome] No domain is attached to app '${app}', so there is nothing to adopt.`);
1804
+ console.log(held.length > 0 ? "[wawesome] The one it carried is being detached." : "[wawesome] Attach one from the dashboard, from an agent, or by declaring it here and deploying.");
1805
+ return;
1806
+ }
1807
+ const declared = declaredDomain(config.parsed.domain);
1808
+ if (declared && carries(attached, declared)) {
1809
+ console.log(`[wawesome] wawesome-function.json already declares '${declared}'.`);
1810
+ console.log(`[wawesome] App '${app}' answers at it, so nothing was written.`);
1811
+ return;
1812
+ }
1813
+ if (declared) {
1814
+ console.error(`[wawesome] Error: Two domains are in play on app '${app}'.`);
1815
+ console.error(`[wawesome] wawesome-function.json declares '${declared}'`);
1816
+ console.error(`[wawesome] the app answers at '${attached.hostname}'`);
1817
+ console.error("[wawesome] Nothing was written. An App carries one domain, so which of the two");
1818
+ console.error("[wawesome] it is is yours to settle: edit the line yourself, or take the attached");
1819
+ console.error("[wawesome] one off with");
1820
+ console.error(`[wawesome] \x1b[36mwawesome domains detach ${attached.hostname}\x1b[0m`);
1821
+ console.error("[wawesome] and deploy to attach the declared one.");
1822
+ process.exit(1);
1823
+ }
1824
+ writeDeclaredDomain(config, attached.hostname);
1825
+ console.log(`[wawesome] wawesome-function.json now declares '${attached.hostname}'.`);
1826
+ const answersAt = claimedHostnames(attached);
1827
+ if (answersAt.length > 1) console.log(`[wawesome] The app answers at both halves of the name: ${answersAt.join(", ")}.`);
1828
+ console.log("[wawesome] Nothing about the domain changed: it was attached already, and the file");
1829
+ console.log("[wawesome] now says so. Commit it, and every deploy from here agrees.");
1830
+ }
1831
+ /**
1725
1832
  * What this App answers at, read and printed.
1726
1833
  *
1727
1834
  * The one place a Tenant can ask. The records used to exist only in the output
@@ -1752,10 +1859,12 @@ async function showDomainCommand(options = {}) {
1752
1859
  async function domainsCommand(action, target, options = {}) {
1753
1860
  const verb = (action || "").toLowerCase().trim();
1754
1861
  if (verb === "detach") return detachDomainCommand(target, options);
1862
+ if (verb === "adopt") return adoptDomainCommand(target, options);
1755
1863
  if (!verb) return showDomainCommand(options);
1756
1864
  console.error(`[wawesome] Error: Unknown command '${["domains", action].filter(Boolean).join(" ")}'.`);
1757
- console.error("[wawesome] Usage: wawesome domains [detach <hostname>]");
1758
- console.error("[wawesome] A domain is attached by declaring it in wawesome-function.json.");
1865
+ console.error("[wawesome] Usage: wawesome domains [adopt | detach <hostname>]");
1866
+ console.error("[wawesome] A domain is attached by declaring it in wawesome-function.json, from the");
1867
+ console.error("[wawesome] dashboard, or by an agent.");
1759
1868
  process.exit(1);
1760
1869
  }
1761
1870
  function domainsUrl(creds, app) {
@@ -1764,10 +1873,9 @@ function domainsUrl(creds, app) {
1764
1873
  /**
1765
1874
  * Where a domain got to, said beside the name itself.
1766
1875
  *
1767
- * Each state names the one record that is outstanding, because the two are
1768
- * added a day apart: the TXT issues the certificate, and the CNAME is pointed
1769
- * here afterwards so a site that is already live never answers on a
1770
- * certificate that is not there yet.
1876
+ * Each state names the one record that is outstanding. The two that prove the
1877
+ * name go in first and the traffic CNAME a day later, so a site that is already
1878
+ * live never answers on a certificate that is not there yet.
1771
1879
  *
1772
1880
  * Read off the records too, never the states alone: naming a record the payload
1773
1881
  * does not carry left a reader holding the CNAME alone (#610).
@@ -1776,22 +1884,26 @@ function whereItGotTo(domain) {
1776
1884
  const { state } = domain;
1777
1885
  if (state.serving) return "serving";
1778
1886
  if (state.certificate_active) return "certificate issued, so point the CNAME here to cut over";
1779
- if (state.ownership_verified) return "ownership verified, and the certificate is being issued";
1780
- if (domain.certificate_record_pending) return "not verified yet, and the TXT record is not ready";
1781
- return "not verified yet, so add the TXT record below";
1887
+ if (state.ownership_verified) return domain.certificate_record_pending ? "name verified, and the certificate record is not ready" : "name verified, so add the certificate CNAME below";
1888
+ if (domain.ownership_record_pending) return "not verified yet, and the TXT record is not ready";
1889
+ return "not verified yet, so add the records below";
1782
1890
  }
1783
1891
  /**
1784
1892
  * What a record is for, and when to add it.
1785
1893
  *
1786
- * The order is stated rather than implied. The TXT proves control and issues
1787
- * the certificate; the CNAME is pointed here afterwards, so a name already
1788
- * serving a live site never answers on a certificate that is not there yet.
1789
- * That is why the platform validates before the cutover, and a reader holding
1790
- * the CNAME alone could read the old caption as a label rather than as an
1791
- * instruction to wait (#617).
1894
+ * The order is stated rather than implied. The TXT proves the name is theirs
1895
+ * and the CNAME beside it issues the certificate; the traffic record is pointed
1896
+ * here afterwards, so a name already serving a live site never answers on a
1897
+ * certificate that is not there yet. That is why the platform validates before
1898
+ * the cutover, and a reader holding the traffic record alone could read the old
1899
+ * caption as a label rather than as an instruction to wait (#617).
1792
1900
  */
1793
1901
  function whatItIsFor(purpose) {
1794
- return purpose === "certificate" ? "Add this first. It proves you own the name and issues the certificate" : "Add this once the certificate is active. It points traffic here";
1902
+ switch (purpose) {
1903
+ case "ownership": return "Add this first. It proves you own the name";
1904
+ case "certificate": return "Add this first too. It issues the certificate and keeps it renewed";
1905
+ default: return "Add this once the certificate is active. It points traffic here";
1906
+ }
1795
1907
  }
1796
1908
  /** What the dashboard's empty state shows. Kept the same by hand, not by anything. */
1797
1909
  const DOMAIN_CONFIG_LINE = "\"domain\": \"shop.client.com\"";
@@ -1809,10 +1921,14 @@ function domainLines(outcome) {
1809
1921
  if (outcome.kind === "undeclared") {
1810
1922
  const { hostname, state } = outcome.domain;
1811
1923
  const left = state.serving ? "it is still serving" : "nothing changed";
1924
+ const answersAt = claimedHostnames(outcome.domain);
1812
1925
  return [
1813
1926
  "",
1814
1927
  ` Domain: \x1b[33m${hostname}: attached, and this project declares no domain\x1b[0m`,
1815
- ...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}`),
1928
+ ...answersAt.length > 1 ? indented(`Both halves of the name are attached: ${answersAt.join(", ")}.`) : [],
1929
+ ...indented(`Nothing was detached and ${left}. A domain comes off an App when somebody names it, never because a line was deleted. To write it into wawesome-function.json, run`),
1930
+ `${VALUE_COLUMN} \x1b[36mwawesome domains adopt\x1b[0m`,
1931
+ ...indented("To take it off the App instead, run"),
1816
1932
  `${VALUE_COLUMN} \x1b[36mwawesome domains detach ${hostname}\x1b[0m`
1817
1933
  ];
1818
1934
  }
@@ -1835,7 +1951,7 @@ function domainLines(outcome) {
1835
1951
  * the whole of #610 and #617.
1836
1952
  */
1837
1953
  function attachedDomainLines(domain, printedBy = "deploy") {
1838
- const { hostname, records, state, last_checked_at, detaching, certificate_record_pending } = domain;
1954
+ const { hostname, records, state, last_checked_at, detaching, ownership_record_pending, certificate_record_pending } = domain;
1839
1955
  if (detaching) return [
1840
1956
  "",
1841
1957
  ` Domain: \x1b[33m${hostname}: being detached\x1b[0m`,
@@ -1843,20 +1959,80 @@ function attachedDomainLines(domain, printedBy = "deploy") {
1843
1959
  `${VALUE_COLUMN}Remove 'domain' from wawesome-function.json, or deploy again`,
1844
1960
  `${VALUE_COLUMN}once it is gone to attach it afresh.`
1845
1961
  ];
1962
+ const legs = domain.legs ?? [];
1963
+ if (legs.length > 1) return pairLines(domain, legs, printedBy);
1846
1964
  const lines = ["", ` Domain: \x1b[36m${hostname}\x1b[0m: ${whereItGotTo(domain)}`];
1847
1965
  if (last_checked_at) lines.push(`${VALUE_COLUMN}Last checked ${asUtc$1(last_checked_at)}.`);
1848
1966
  if (state.serving) {
1849
1967
  lines.push(`${VALUE_COLUMN}This app's own address answers too, so nothing pointed at it breaks.`);
1850
1968
  return lines;
1851
1969
  }
1852
- const nameWidth = Math.max(...records.map((record) => record.name.length));
1853
- const row = (type, name, value = "") => ` ${type.padEnd(5)} ${name.padEnd(nameWidth)} ${value}`.trimEnd();
1970
+ const pending = Boolean(ownership_record_pending || certificate_record_pending);
1971
+ lines.push("", " DNS records to add at your registrar:");
1972
+ lines.push(...recordLines(records, {
1973
+ ownership_record_pending,
1974
+ certificate_record_pending
1975
+ }, " "));
1976
+ lines.push("", ...closingLines(pending, printedBy));
1977
+ return lines;
1978
+ }
1979
+ /**
1980
+ * A claim carrying both halves of a name: where the claim got to, then each leg
1981
+ * with its own state and its own records.
1982
+ *
1983
+ * Every leg is named, the half nothing claimed included. Four records to add
1984
+ * rather than two is the cost of a name that answers at both halves, and a leg
1985
+ * left out of this block is a record nobody adds on the name a visitor typed.
1986
+ */
1987
+ function pairLines(domain, legs, printedBy) {
1988
+ const lines = ["", ` Domain: \x1b[36m${domain.hostname}\x1b[0m: ${domain.state.serving ? "serving at both halves of the name" : "coming up, and each half of the name is below"}`];
1989
+ if (domain.last_checked_at) lines.push(`${VALUE_COLUMN}Last checked ${asUtc$1(domain.last_checked_at)}.`);
1990
+ lines.push("");
1991
+ for (const leg of legs) {
1992
+ lines.push(`${VALUE_COLUMN}${leg.hostname}: ${whereTheLegGotTo(leg)}`);
1993
+ if (leg.unclaimed_reason) lines.push(...wrapped(leg.unclaimed_reason).map((line) => `${VALUE_COLUMN} ${line}`));
1994
+ }
1995
+ const outstanding = legs.filter((leg) => leg.claimed && !leg.state.serving);
1996
+ if (outstanding.length === 0) {
1997
+ lines.push(`${VALUE_COLUMN}This app's own address answers too, so nothing pointed at it breaks.`);
1998
+ return lines;
1999
+ }
1854
2000
  lines.push("", " DNS records to add at your registrar:");
1855
- if (certificate_record_pending) lines.push("", ` ${whatItIsFor("certificate")}:`, row("TXT", "not ready yet"));
1856
- for (const record of records) lines.push("", ` ${whatItIsFor(record.purpose)}:`, row(record.type, record.name, record.value));
1857
- lines.push("", ...closingLines(certificate_record_pending, printedBy));
2001
+ for (const leg of outstanding) {
2002
+ lines.push("", ` ${leg.hostname}:`);
2003
+ lines.push(...recordLines(leg.records ?? [], leg, " "));
2004
+ }
2005
+ const pending = outstanding.some((leg) => leg.ownership_record_pending || leg.certificate_record_pending);
2006
+ lines.push("", ...closingLines(pending, printedBy));
1858
2007
  return lines;
1859
2008
  }
2009
+ function whereTheLegGotTo(leg) {
2010
+ return leg.claimed ? whereItGotTo(leg) : "not claimed";
2011
+ }
2012
+ /**
2013
+ * The records one leg is waiting on, each under what it is for.
2014
+ *
2015
+ * What each record is for goes above it rather than beside it. A DCV value is
2016
+ * 40-odd characters a person has to copy exactly, and a fourth column puts the
2017
+ * row past the width of the terminal it is being copied out of.
2018
+ */
2019
+ function recordLines(records, pending, indent) {
2020
+ const nameWidth = Math.max(0, ...records.map((record) => record.name.length));
2021
+ const row = (type, name, value = "") => `${indent} ${type.padEnd(5)} ${name.padEnd(nameWidth)} ${value}`.trimEnd();
2022
+ const lines = [];
2023
+ if (pending.ownership_record_pending) lines.push("", `${indent}${whatItIsFor("ownership")}:`, row("TXT", "not ready yet"));
2024
+ if (pending.certificate_record_pending) lines.push("", `${indent}${whatItIsFor("certificate")}:`, row("CNAME", "not ready yet"));
2025
+ for (const record of records) {
2026
+ lines.push("", `${indent}${whatItIsFor(record.purpose)}:`, row(record.type, record.name, record.value));
2027
+ if (record.purpose === "certificate") lines.push(`${indent} Delete any _acme-challenge TXT record already at that name.`);
2028
+ }
2029
+ return lines;
2030
+ }
2031
+ /** Every hostname a claim answers at, for a block naming the claim in one line. */
2032
+ function claimedHostnames(domain) {
2033
+ const claimed = (domain.legs ?? []).filter((leg) => leg.claimed).map((leg) => leg.hostname);
2034
+ return claimed.length > 0 ? claimed : [domain.hostname];
2035
+ }
1860
2036
  /**
1861
2037
  * What is left to do, which is nothing.
1862
2038
  *
@@ -1864,9 +2040,9 @@ function attachedDomainLines(domain, printedBy = "deploy") {
1864
2040
  * nothing is refused, so the one piece of advice the reader was given was the
1865
2041
  * one they could not take.
1866
2042
  */
1867
- function closingLines(certificateRecordPending, printedBy) {
1868
- if (!certificateRecordPending) return indented(`${printedBy === "deploy" ? "The deploy is finished. " : ""}This domain comes up on its own once the records resolve.`);
1869
- const asking = "The TXT record is not ready yet. The platform asks the certificate vendor again on its own.";
2043
+ function closingLines(aRecordIsPending, printedBy) {
2044
+ if (!aRecordIsPending) return indented(`${printedBy === "deploy" ? "The deploy is finished. " : ""}This domain comes up on its own once the records resolve.`);
2045
+ const asking = "A record is not ready yet. The platform asks the certificate vendor again on its own.";
1870
2046
  if (printedBy === "domains") return indented(`${asking} Run this again in a minute.`);
1871
2047
  return [...indented(`${asking} The deploy is finished either way. To read the record when it lands, run`), `${VALUE_COLUMN} \x1b[36mwawesome domains\x1b[0m`];
1872
2048
  }
@@ -2803,7 +2979,8 @@ function shadowingLines(shadowed) {
2803
2979
  function declaredFields(declared) {
2804
2980
  return {
2805
2981
  ...declared.visibility ? { visibility: declared.visibility } : {},
2806
- ...declared.confirmPublish ? { confirm_publish: true } : {}
2982
+ ...declared.confirmPublish ? { confirm_publish: true } : {},
2983
+ ...declared.confirmAssetDrop ? { confirm_asset_drop: true } : {}
2807
2984
  };
2808
2985
  }
2809
2986
  /**
@@ -2817,6 +2994,15 @@ function declaredVisibility(value) {
2817
2994
  console.error("[wawesome] It must be \"public\" or \"private\".");
2818
2995
  process.exit(1);
2819
2996
  }
2997
+ function reportAssetDropRefusal(message) {
2998
+ console.error(`\n[wawesome] \x1b[31mError: ${message}\x1b[0m`);
2999
+ console.error("[wawesome] Nothing was deployed. A version carries exactly the files its");
3000
+ console.error("[wawesome] deploy declared, so this one would take every file the live");
3001
+ console.error("[wawesome] version serves off the site at once.");
3002
+ console.error("[wawesome] Point \x1B[36m\"assets\"\x1B[0m in wawesome-function.json at the directory");
3003
+ console.error("[wawesome] holding them, or re-run with \x1B[36mwawesome deploy --drop-assets\x1B[0m to");
3004
+ console.error("[wawesome] remove them.\n");
3005
+ }
2820
3006
  function reportPublishRefusal(message) {
2821
3007
  console.error(`\n[wawesome] \x1b[31mError: ${message}\x1b[0m`);
2822
3008
  console.error("[wawesome] Nothing was deployed. This Function is private, so its address");
@@ -2844,7 +3030,8 @@ async function deploy(entryInput, options) {
2844
3030
  const { app, function: funcName } = config;
2845
3031
  const declared = {
2846
3032
  visibility: declaredVisibility(config.visibility),
2847
- confirmPublish: Boolean(options.publish)
3033
+ confirmPublish: Boolean(options.publish),
3034
+ confirmAssetDrop: Boolean(options.dropAssets)
2848
3035
  };
2849
3036
  const domainName = declaredDomain(config.domain);
2850
3037
  if (isVerbose) {
@@ -2935,6 +3122,10 @@ async function deploy(entryInput, options) {
2935
3122
  reportPublishRefusal(refusal.message);
2936
3123
  process.exit(1);
2937
3124
  }
3125
+ if (refusal.reason === "asset-drop-needs-confirmation") {
3126
+ reportAssetDropRefusal(refusal.message);
3127
+ process.exit(1);
3128
+ }
2938
3129
  console.error(`\n[wawesome] \x1b[31mError: ${refusal.message}\x1b[0m`);
2939
3130
  const advice = planLimitAdvice(refusal.reason);
2940
3131
  if (advice) console.error(`[wawesome] ${advice}\n`);
@@ -5755,7 +5946,7 @@ async function credentialsCommand(action, target, options) {
5755
5946
  //#region src/index.ts
5756
5947
  const cli = cac("wawesome");
5757
5948
  cli.command("build [entry]", "Bundle a serverless function to an optimized JS file").option("-o, --out <path>", "Output JS bundle path", { default: "dist/index.js" }).option("-v, --verbose", "Enable verbose debug output").action((entry, options) => buildJs(entry, options));
5758
- cli.command("deploy [entry]", "Build, upload, and promote a serverless function").option("-o, --out <path>", "Output JS bundle path", { default: "dist/index.js" }).option("-v, --verbose", "Enable verbose debug output").option("--skip-build", "Skip the build step, deploy an already-built bundle").option("--publish", "Confirm putting a private function back on its public address").action((entry, options) => deploy(entry, options));
5949
+ cli.command("deploy [entry]", "Build, upload, and promote a serverless function").option("-o, --out <path>", "Output JS bundle path", { default: "dist/index.js" }).option("-v, --verbose", "Enable verbose debug output").option("--skip-build", "Skip the build step, deploy an already-built bundle").option("--publish", "Confirm putting a private function back on its public address").option("--drop-assets", "Confirm a deploy carrying no static files where the live version serves some").action((entry, options) => deploy(entry, options));
5759
5950
  cli.command("version [action] [target]", "Manage versions (e.g. 'version list', 'version switch [version]')").option("-e, --env <environment>", "Target environment (default: production)").option("-v, --verbose", "Enable verbose debug output").action((action, target, options) => {
5760
5951
  if (!action || action === "list" || action === "ls") return listVersions(options);
5761
5952
  if (action === "switch" || action === "use" || action === "select") return switchVersion(target, options);
@@ -5767,7 +5958,8 @@ cli.command("env [action] [key] [value]", "Manage environment variables (set, li
5767
5958
  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));
5768
5959
  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));
5769
5960
  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));
5770
- cli.command("domains [action] [hostname]", "Show the custom domain on the current app, its state and its DNS records").usage("domains [action] [hostname]\n\nWith no action, prints the app's domain, where it got to, when the platform last checked, and the records to add.\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").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));
5961
+ cli.command("domains [action] [hostname]", "Show the custom domain on the current app, its state and its DNS records").usage("domains [action] [hostname]\n\nWith no action, prints the app's domain, every half of it, where each got to, when the platform last checked, and the records to add.\n\nActions:\n adopt Write the attached domain into wawesome-function.json\n detach <hostname> Stop serving the app at a domain you attached\n\nA domain is attached by declaring it in wawesome-function.json and deploying, from the dashboard, or by an agent.").example("wawesome domains").example("wawesome domains adopt").example("wawesome domains detach shop.client.com").option("-a, --app <slug>", "App to act on (default: the app in wawesome-function.json)").action((action, hostname, options) => domainsCommand(action, hostname, options));
5962
+ cli.command("domains adopt", "Write the domain the current app answers at into wawesome-function.json").option("-a, --app <slug>", "App to act on (default: the app in wawesome-function.json)").action((options) => adoptDomainCommand(void 0, options));
5771
5963
  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));
5772
5964
  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));
5773
5965
  cli.command("logout", "Clear stored authentication credentials").action(() => logout());
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wawesome",
3
- "version": "0.15.0",
3
+ "version": "0.17.0",
4
4
  "description": "CLI tool for building and deploying serverless functions on wawesome.io platform",
5
5
  "type": "module",
6
6
  "bin": {