wawesome 0.16.0 → 0.18.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 +65 -12
  2. package/dist/index.mjs +155 -36
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -478,9 +478,9 @@ concluding the platform cannot do it.
478
478
 
479
479
  | Tool | What it does | Needs |
480
480
  |:---------------------------|:------------------------------------------------------------------------|:--------------|
481
- | `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 |
482
482
  | `list_templates` | The template catalogue, with what each one is for | nothing |
483
- | `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 |
484
484
  | `list_apps` | The Apps the credential reaches, each with its address and its dashboard page | `read:apps` |
485
485
  | `list_functions` | The Functions in one App, and what their live version carries | `read:functions` |
486
486
  | `list_versions` | A Function's Versions, and which one is live | `read:functions` |
@@ -493,6 +493,8 @@ concluding the platform cannot do it.
493
493
  | `deploy_function` | Deploys code and the static files it carries | `write:functions` |
494
494
  | `deploy_status` | How a deploy went, by the handle it answered with | `read:functions` |
495
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` |
496
498
  | `set_env_var` | Sets an App's environment variable, read at the next invocation | `write:env` |
497
499
  | `attach_domain` | Attaches a domain to an App and answers the DNS records that make it work | `write:domains` |
498
500
  | `cancel_domain_claim` | Gives up a domain claim nobody has proved control of, freeing the App's place | `write:domains` |
@@ -505,8 +507,11 @@ beside it, and the presets below are named in exactly these words.
505
507
 
506
508
  `whoami` is the one to call first. It answers the workspace, every word the credential carries and
507
509
  the Apps it reaches, and it asks for nothing — so your agent can say what it is able to do before it
508
- offers, rather than finding the edge by being refused in front of you. A member's session calling the
509
- 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.
510
515
 
511
516
  A deploy is two calls. The first says which of your files the platform does not already hold, by
512
517
  content hash; your agent uploads those bytes to `PUT /v1/assets/{content_hash}` with the same
@@ -643,6 +648,36 @@ Function, so a page your root Function carries at `about/index.html` is shadowed
643
648
  named `about`. The deploy says so and names both addresses. It refuses nothing — the two Functions are
644
649
  versioned independently, and either one may deploy first.
645
650
 
651
+ ### A page for an address nothing claims
652
+
653
+ A site with no handler answers an address none of its pages claim with the platform's not-found. Add
654
+ `"fallback"` and it answers with a page of your own instead:
655
+
656
+ ```json
657
+ {
658
+ "app": "my-app",
659
+ "function": "root",
660
+ "assets": "dist",
661
+ "fallback": { "path": "404.html", "status": 404 }
662
+ }
663
+ ```
664
+
665
+ Two values rather than a mode. `404.html` at `404` is your own not-found page; `index.html` at `200`
666
+ is a single-page application's shell, which is what puts a client-routed path in front of your router
667
+ instead of a 404. Anything between those two shapes is yours to choose — the platform serves the pair
668
+ it was given.
669
+
670
+ Both halves have to be there, or the deploy stops before it uploads anything. The path names a file
671
+ this deploy carries, relative to your assets directory; the status has to be one a document can be
672
+ sent with, so `204` and `304` are refused. A fallback naming a file the deploy does not carry is
673
+ refused too, before anything is compiled or stored.
674
+
675
+ It is part of what identifies the deploy, so changing which page answers a miss, or the number it
676
+ answers with, is a new version like any other change. Where the Function carries a handler, the
677
+ handler answers that address and the fallback is never reached — and a miss beneath `assets/` stays a
678
+ 404 either way, since a page served where a browser asked for a build chunk is markup answering a
679
+ request for script.
680
+
646
681
  ### Static files
647
682
 
648
683
  Add `"assets"` to deploy static files beside your code:
@@ -681,6 +716,18 @@ Two rules to know about:
681
716
  `favicon.ico`, `robots.txt` and a `.well-known/` directory are what that is for; hashed build
682
717
  output belongs under `assets/`.
683
718
 
719
+ **A version carries exactly the files its deploy declared.** Nothing is inherited from the version
720
+ before it, so taking `"assets"` out of `wawesome-function.json` — or pointing it at a directory your
721
+ build no longer writes to — is not an edit but every file on the site going at once. A deploy that
722
+ would do that against a Function serving files today is refused, and tells you how many would go:
723
+
724
+ ```bash
725
+ npx wawesome deploy --drop-assets
726
+ ```
727
+
728
+ Removing *some* of the files is ordinary editing and needs nothing: only the wholesale drop is
729
+ refused, because that is the shape of a mistake rather than of a decision.
730
+
684
731
  **A page is a file like any other.** An `index.html` your build wrote is deployed, hashed, retained
685
732
  and billed exactly as your other files are, and it is what a directory-style address resolves to: a
686
733
  path whose last segment carries no extension is served the `index.html` beneath it, so `/`, `/about`,
@@ -790,10 +837,16 @@ Add `"domain"` and your App answers at a name you own, beside the address it alr
790
837
  }
791
838
  ```
792
839
 
793
- The deploy attaches it and prints two DNS records to add at your registrar. The TXT record proves you
794
- own the name, which is what issues the certificate. The CNAME points traffic here, and you add that
795
- one once the certificate is issued, so a site that is already live never spends a minute answering on
796
- a certificate that is not there yet.
840
+ The deploy attaches it and prints three DNS records to add at your registrar. The TXT record proves
841
+ you own the name. The `_acme-challenge` CNAME beside it issues the certificate and keeps renewing it
842
+ for as long as it stays there, so nobody comes back to your registrar at renewal time — if an
843
+ `_acme-challenge` TXT record is already at that name, delete it, because a TXT and this CNAME cannot
844
+ both sit there. The last CNAME points traffic here, and you add that one once the certificate is
845
+ issued, so a site that is already live never spends a minute answering on a certificate that is not
846
+ there yet.
847
+
848
+ Nothing you are asked to add ever changes. Copy a value into your registrar, come back an hour later,
849
+ and it is the same value.
797
850
 
798
851
  Your deploy waits for neither. It attaches the domain, prints the records and finishes, and every
799
852
  later deploy states where the domain got to: not verified, ownership verified, certificate issued, or
@@ -807,10 +860,10 @@ npx wawesome domains
807
860
 
808
861
  It prints the domain, where it got to, when the platform last checked, and the records to add. It
809
862
  changes nothing, and a deploy credential carrying `read:domains` can run it, so a pipeline reads the
810
- same thing you do. The TXT record is minted a moment after the name is claimed, and an attach that
811
- outran it prints a row saying so rather than dropping the record from the list. That row is what this
812
- command fills in, and it is where the deploy sends you: deploying again would change nothing, and a
813
- deploy that changes nothing is refused.
863
+ same thing you do. Where the platform does not hold a record yet it prints a row saying so rather
864
+ than dropping the record from the list. That row is what this command fills in, and it is where the
865
+ deploy sends you: deploying again would change nothing, and a deploy that changes nothing is
866
+ refused.
814
867
 
815
868
  A custom domain is granted from the Solo plan upwards, one per App. On a plan that grants none, the
816
869
  deploy prints the refusal and lands everything else it was doing.
package/dist/index.mjs CHANGED
@@ -905,7 +905,7 @@ async function buildHandler(entryInput, config, options) {
905
905
  * that has to name this version — `--version`, the dependency a scaffolded
906
906
  * project pins — reads it here, so a release bumps one file.
907
907
  */
908
- const CLI_VERSION = "0.16.0";
908
+ const CLI_VERSION = "0.18.0";
909
909
  //#endregion
910
910
  //#region src/prompt.ts
911
911
  /**
@@ -1873,10 +1873,9 @@ function domainsUrl(creds, app) {
1873
1873
  /**
1874
1874
  * Where a domain got to, said beside the name itself.
1875
1875
  *
1876
- * Each state names the one record that is outstanding, because the two are
1877
- * added a day apart: the TXT issues the certificate, and the CNAME is pointed
1878
- * here afterwards so a site that is already live never answers on a
1879
- * 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.
1880
1879
  *
1881
1880
  * Read off the records too, never the states alone: naming a record the payload
1882
1881
  * does not carry left a reader holding the CNAME alone (#610).
@@ -1885,22 +1884,26 @@ function whereItGotTo(domain) {
1885
1884
  const { state } = domain;
1886
1885
  if (state.serving) return "serving";
1887
1886
  if (state.certificate_active) return "certificate issued, so point the CNAME here to cut over";
1888
- if (state.ownership_verified) return "ownership verified, and the certificate is being issued";
1889
- if (domain.certificate_record_pending) return "not verified yet, and the TXT record is not ready";
1890
- 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";
1891
1890
  }
1892
1891
  /**
1893
1892
  * What a record is for, and when to add it.
1894
1893
  *
1895
- * The order is stated rather than implied. The TXT proves control and issues
1896
- * the certificate; the CNAME is pointed here afterwards, so a name already
1897
- * serving a live site never answers on a certificate that is not there yet.
1898
- * That is why the platform validates before the cutover, and a reader holding
1899
- * the CNAME alone could read the old caption as a label rather than as an
1900
- * 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).
1901
1900
  */
1902
1901
  function whatItIsFor(purpose) {
1903
- 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
+ }
1904
1907
  }
1905
1908
  /** What the dashboard's empty state shows. Kept the same by hand, not by anything. */
1906
1909
  const DOMAIN_CONFIG_LINE = "\"domain\": \"shop.client.com\"";
@@ -1948,7 +1951,7 @@ function domainLines(outcome) {
1948
1951
  * the whole of #610 and #617.
1949
1952
  */
1950
1953
  function attachedDomainLines(domain, printedBy = "deploy") {
1951
- 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;
1952
1955
  if (detaching) return [
1953
1956
  "",
1954
1957
  ` Domain: \x1b[33m${hostname}: being detached\x1b[0m`,
@@ -1964,9 +1967,13 @@ function attachedDomainLines(domain, printedBy = "deploy") {
1964
1967
  lines.push(`${VALUE_COLUMN}This app's own address answers too, so nothing pointed at it breaks.`);
1965
1968
  return lines;
1966
1969
  }
1970
+ const pending = Boolean(ownership_record_pending || certificate_record_pending);
1967
1971
  lines.push("", " DNS records to add at your registrar:");
1968
- lines.push(...recordLines(records, certificate_record_pending, " "));
1969
- lines.push("", ...closingLines(certificate_record_pending, printedBy));
1972
+ lines.push(...recordLines(records, {
1973
+ ownership_record_pending,
1974
+ certificate_record_pending
1975
+ }, " "));
1976
+ lines.push("", ...closingLines(pending, printedBy));
1970
1977
  return lines;
1971
1978
  }
1972
1979
  /**
@@ -1993,9 +2000,9 @@ function pairLines(domain, legs, printedBy) {
1993
2000
  lines.push("", " DNS records to add at your registrar:");
1994
2001
  for (const leg of outstanding) {
1995
2002
  lines.push("", ` ${leg.hostname}:`);
1996
- lines.push(...recordLines(leg.records ?? [], leg.certificate_record_pending, " "));
2003
+ lines.push(...recordLines(leg.records ?? [], leg, " "));
1997
2004
  }
1998
- const pending = outstanding.some((leg) => leg.certificate_record_pending);
2005
+ const pending = outstanding.some((leg) => leg.ownership_record_pending || leg.certificate_record_pending);
1999
2006
  lines.push("", ...closingLines(pending, printedBy));
2000
2007
  return lines;
2001
2008
  }
@@ -2009,12 +2016,16 @@ function whereTheLegGotTo(leg) {
2009
2016
  * 40-odd characters a person has to copy exactly, and a fourth column puts the
2010
2017
  * row past the width of the terminal it is being copied out of.
2011
2018
  */
2012
- function recordLines(records, certificateRecordPending, indent) {
2019
+ function recordLines(records, pending, indent) {
2013
2020
  const nameWidth = Math.max(0, ...records.map((record) => record.name.length));
2014
2021
  const row = (type, name, value = "") => `${indent} ${type.padEnd(5)} ${name.padEnd(nameWidth)} ${value}`.trimEnd();
2015
2022
  const lines = [];
2016
- if (certificateRecordPending) lines.push("", `${indent}${whatItIsFor("certificate")}:`, row("TXT", "not ready yet"));
2017
- for (const record of records) lines.push("", `${indent}${whatItIsFor(record.purpose)}:`, row(record.type, record.name, record.value));
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
+ }
2018
2029
  return lines;
2019
2030
  }
2020
2031
  /** Every hostname a claim answers at, for a block naming the claim in one line. */
@@ -2029,9 +2040,9 @@ function claimedHostnames(domain) {
2029
2040
  * nothing is refused, so the one piece of advice the reader was given was the
2030
2041
  * one they could not take.
2031
2042
  */
2032
- function closingLines(certificateRecordPending, printedBy) {
2033
- if (!certificateRecordPending) return indented(`${printedBy === "deploy" ? "The deploy is finished. " : ""}This domain comes up on its own once the records resolve.`);
2034
- 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.";
2035
2046
  if (printedBy === "domains") return indented(`${asking} Run this again in a minute.`);
2036
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`];
2037
2048
  }
@@ -2241,19 +2252,23 @@ function sha256(bytes) {
2241
2252
  }
2242
2253
  /**
2243
2254
  * What this deploy *is*: the server bundle together with the sorted set of
2244
- * asset path and content-hash pairs.
2255
+ * asset path and content-hash pairs, and the **Fallback document** it declares.
2245
2256
  *
2246
2257
  * Computed here, before anything is uploaded, so a deploy that changed nothing
2247
2258
  * is refused before a byte moves. Mirrors `deploy_digest` in
2248
2259
  * `server/crates/core/src/features/assets/identity.rs` — the gateway recomputes
2249
2260
  * it from what actually arrives, so the two have to agree exactly.
2261
+ *
2262
+ * A deploy declaring no fallback contributes nothing here rather than a stated
2263
+ * absence, so every digest computed before fallbacks existed is unmoved.
2250
2264
  */
2251
2265
  const DEPLOY_DIGEST_DOMAIN = "wawesome-deploy-v1";
2252
- function deployDigest(bundle, assets) {
2266
+ function deployDigest(bundle, assets, fallback) {
2253
2267
  const digest = crypto.createHash("sha256");
2254
2268
  digest.update(`${DEPLOY_DIGEST_DOMAIN}\n`);
2255
2269
  digest.update(`${sha256(bundle)}\n`);
2256
2270
  for (const asset of [...assets].sort((a, b) => a.path < b.path ? -1 : a.path > b.path ? 1 : 0)) digest.update(`${asset.path}\0${asset.content_hash}\n`);
2271
+ if (fallback) digest.update(`fallback\0${fallback.path}\0${fallback.status}\n`);
2257
2272
  return digest.digest("hex");
2258
2273
  }
2259
2274
  /** The manifest as the gateway reads it — the bytes on disk are the CLI's business. */
@@ -2892,11 +2907,12 @@ async function cronCommand(action, target, subtarget, options = {}) {
2892
2907
  //#region src/mount-shadow.ts
2893
2908
  /**
2894
2909
  * The address a **Document** answers at, relative to its Function's mount: a
2895
- * request path whose last segment carries no extension has `/index.html`
2896
- * appended, so the Document at `about/index.html` is the address `about`.
2910
+ * request path whose last segment carries no extension resolves to that path,
2911
+ * to it with `.html` appended and to it with `/index.html` appended, so both
2912
+ * `about.html` and `about/index.html` are the address `about`.
2897
2913
  */
2898
2914
  function documentAddressPath(assetPath) {
2899
- return assetPath.replace(/(^|\/)index\.html$/, "");
2915
+ return assetPath.replace(/(^|\/)index\.html$/, "").replace(/\.html$/, "");
2900
2916
  }
2901
2917
  /** See **Mount** in `CONTEXT.md` for why only the root Function's can be. */
2902
2918
  function mountsCanShadow(functionSlug) {
@@ -2964,14 +2980,84 @@ function shadowingLines(shadowed) {
2964
2980
  ];
2965
2981
  }
2966
2982
  //#endregion
2983
+ //#region src/document-collision.ts
2984
+ /**
2985
+ * The addresses this deploy carries more than one file for.
2986
+ *
2987
+ * An address resolves to an ordered set of keys and the first one the version
2988
+ * carries answers, so `about`, `about.html` and `about/index.html` all sit at
2989
+ * `/about` and the first of them wins. Nothing is refused: a build that emitted
2990
+ * two is a worse day when the deploy fails than when it says which one answers.
2991
+ */
2992
+ function collidingDocuments(assetPaths) {
2993
+ const byAddress = /* @__PURE__ */ new Map();
2994
+ for (const assetPath of assetPaths) {
2995
+ const address = documentAddressPath(assetPath);
2996
+ byAddress.set(address, [...byAddress.get(address) ?? [], assetPath]);
2997
+ }
2998
+ return [...byAddress].filter(([, carried]) => carried.length > 1).map(([address, carried]) => {
2999
+ const [answers, ...shadowed] = [...carried].sort((one, other) => preference(one) - preference(other));
3000
+ return {
3001
+ address,
3002
+ answers,
3003
+ shadowed
3004
+ };
3005
+ });
3006
+ }
3007
+ /** Where a file sits in the candidate order the platform resolves an address through. */
3008
+ function preference(assetPath) {
3009
+ if (assetPath === documentAddressPath(assetPath)) return 1;
3010
+ return /(^|\/)index\.html$/.test(assetPath) ? 3 : 2;
3011
+ }
3012
+ /** What the deploy says about an address it carries two pages for. */
3013
+ function collidingLines(colliding) {
3014
+ if (colliding.length === 0) return [];
3015
+ const lines = [];
3016
+ for (const [index, collision] of colliding.entries()) {
3017
+ const at = collision.address === "" ? "/" : `/${collision.address}`;
3018
+ lines.push("", ` ${index === 0 ? "Collision: " : " "} \x1b[33m${collision.answers}\x1b[0m`, ...indented(`This page and ${collision.shadowed.join(", ")} both answer ${at}. ${collision.answers} is the one served there, and ${listed(collision.shadowed)} still answer${collision.shadowed.length === 1 ? "s" : ""} at its own name.`));
3019
+ }
3020
+ return [
3021
+ ...lines,
3022
+ `${VALUE_COLUMN}Nothing was refused — every file this deploy declared is`,
3023
+ `${VALUE_COLUMN}stored and served.`
3024
+ ];
3025
+ }
3026
+ function listed(paths) {
3027
+ return paths.length === 1 ? paths[0] : `${paths.slice(0, -1).join(", ")} and ${paths[paths.length - 1]}`;
3028
+ }
3029
+ //#endregion
2967
3030
  //#region src/deploy.ts
2968
3031
  function declaredFields(declared) {
2969
3032
  return {
2970
3033
  ...declared.visibility ? { visibility: declared.visibility } : {},
2971
- ...declared.confirmPublish ? { confirm_publish: true } : {}
3034
+ ...declared.confirmPublish ? { confirm_publish: true } : {},
3035
+ ...declared.confirmAssetDrop ? { confirm_asset_drop: true } : {}
2972
3036
  };
2973
3037
  }
2974
3038
  /**
3039
+ * The fallback this project declares.
3040
+ *
3041
+ * A declaration missing either half is refused here rather than at the gateway,
3042
+ * because the pair is part of what the deploy identifies itself by and that is
3043
+ * computed before anything is sent. Which statuses a document may be sent with,
3044
+ * and whether the path names a file the deploy carries, are the platform's to
3045
+ * judge: duplicating either rule here is two places for it to drift.
3046
+ */
3047
+ function declaredFallback(value) {
3048
+ if (value === void 0 || value === null) return void 0;
3049
+ const declared = value;
3050
+ if (typeof declared.path === "string" && declared.path.trim() && Number.isInteger(declared.status)) return {
3051
+ path: declared.path.trim(),
3052
+ status: declared.status
3053
+ };
3054
+ console.error("[wawesome] Error: 'fallback' in wawesome-function.json must name a document and the");
3055
+ console.error("[wawesome] status it answers at.");
3056
+ console.error("[wawesome] A not-found page is \x1B[36m{ \"path\": \"404.html\", \"status\": 404 }\x1B[0m and a");
3057
+ console.error("[wawesome] single-page application's shell is \x1B[36m{ \"path\": \"index.html\", \"status\": 200 }\x1B[0m.");
3058
+ process.exit(1);
3059
+ }
3060
+ /**
2975
3061
  * The visibility this project declares, refused here rather than at the gateway
2976
3062
  * so a typo is a message at the keyboard.
2977
3063
  */
@@ -2982,6 +3068,15 @@ function declaredVisibility(value) {
2982
3068
  console.error("[wawesome] It must be \"public\" or \"private\".");
2983
3069
  process.exit(1);
2984
3070
  }
3071
+ function reportAssetDropRefusal(message) {
3072
+ console.error(`\n[wawesome] \x1b[31mError: ${message}\x1b[0m`);
3073
+ console.error("[wawesome] Nothing was deployed. A version carries exactly the files its");
3074
+ console.error("[wawesome] deploy declared, so this one would take every file the live");
3075
+ console.error("[wawesome] version serves off the site at once.");
3076
+ console.error("[wawesome] Point \x1B[36m\"assets\"\x1B[0m in wawesome-function.json at the directory");
3077
+ console.error("[wawesome] holding them, or re-run with \x1B[36mwawesome deploy --drop-assets\x1B[0m to");
3078
+ console.error("[wawesome] remove them.\n");
3079
+ }
2985
3080
  function reportPublishRefusal(message) {
2986
3081
  console.error(`\n[wawesome] \x1b[31mError: ${message}\x1b[0m`);
2987
3082
  console.error("[wawesome] Nothing was deployed. This Function is private, so its address");
@@ -3009,7 +3104,9 @@ async function deploy(entryInput, options) {
3009
3104
  const { app, function: funcName } = config;
3010
3105
  const declared = {
3011
3106
  visibility: declaredVisibility(config.visibility),
3012
- confirmPublish: Boolean(options.publish)
3107
+ confirmPublish: Boolean(options.publish),
3108
+ confirmAssetDrop: Boolean(options.dropAssets),
3109
+ fallback: declaredFallback(config.fallback)
3013
3110
  };
3014
3111
  const domainName = declaredDomain(config.domain);
3015
3112
  if (isVerbose) {
@@ -3054,6 +3151,7 @@ async function deploy(entryInput, options) {
3054
3151
  }
3055
3152
  }
3056
3153
  const declaresSchedules = config.schedules !== void 0;
3154
+ const declaresFallback = declared.fallback !== void 0;
3057
3155
  const assets = config.assets ? collectAssets(path.resolve(config.assets)) : [];
3058
3156
  const documents = assets.filter((asset) => isDocument(asset.path));
3059
3157
  if (jsCode === null && documents.length === 0) {
@@ -3068,7 +3166,7 @@ async function deploy(entryInput, options) {
3068
3166
  const uploadUrl = `${creds.gateway_url}/v1/apps/${encodeURIComponent(app)}/functions/${encodeURIComponent(funcName)}/code`;
3069
3167
  if (isVerbose) console.log(`[wawesome:verbose] POST ${uploadUrl}`);
3070
3168
  const declaredOnTheWire = declaredFields(declared);
3071
- const uploadRes = assets.length > 0 || declaresSchedules || Object.keys(declaredOnTheWire).length > 0 || jsCode === null ? await authorizedFetch(uploadUrl, {
3169
+ const uploadRes = assets.length > 0 || declaresSchedules || declaresFallback || Object.keys(declaredOnTheWire).length > 0 || jsCode === null ? await authorizedFetch(uploadUrl, {
3072
3170
  method: "POST",
3073
3171
  headers: {
3074
3172
  Authorization: `Bearer ${creds.tenant_jwt}`,
@@ -3078,6 +3176,7 @@ async function deploy(entryInput, options) {
3078
3176
  ...jsCode !== null ? { code: jsCode } : {},
3079
3177
  ...assets.length > 0 ? { assets: manifestOf(assets) } : {},
3080
3178
  ...declaresSchedules ? { schedules: config.schedules } : {},
3179
+ ...declaresFallback ? { fallback: declared.fallback } : {},
3081
3180
  ...declaredOnTheWire
3082
3181
  })
3083
3182
  }) : await authorizedFetch(uploadUrl, {
@@ -3100,6 +3199,10 @@ async function deploy(entryInput, options) {
3100
3199
  reportPublishRefusal(refusal.message);
3101
3200
  process.exit(1);
3102
3201
  }
3202
+ if (refusal.reason === "asset-drop-needs-confirmation") {
3203
+ reportAssetDropRefusal(refusal.message);
3204
+ process.exit(1);
3205
+ }
3103
3206
  console.error(`\n[wawesome] \x1b[31mError: ${refusal.message}\x1b[0m`);
3104
3207
  const advice = planLimitAdvice(refusal.reason);
3105
3208
  if (advice) console.error(`[wawesome] ${advice}\n`);
@@ -3197,6 +3300,7 @@ async function deploy(entryInput, options) {
3197
3300
  }
3198
3301
  if (domain) for (const line of domainLines(domain)) console.log(line);
3199
3302
  for (const line of shadowingLines(shadowed)) console.log(line);
3303
+ for (const line of collidingLines(collidingDocuments(assets.map((asset) => asset.path)))) console.log(line);
3200
3304
  if (headroom) {
3201
3305
  console.log("");
3202
3306
  for (const line of headroom) console.log(line);
@@ -3235,7 +3339,7 @@ async function uploadAssets(creds, app, funcName, bundle, assets, declared, isVe
3235
3339
  "Content-Type": "application/json"
3236
3340
  },
3237
3341
  body: JSON.stringify({
3238
- deploy_digest: deployDigest(Buffer.from(bundle, "utf-8"), assets),
3342
+ deploy_digest: deployDigest(Buffer.from(bundle, "utf-8"), assets, declared.fallback),
3239
3343
  assets: manifestOf(assets),
3240
3344
  ...declaredFields(declared)
3241
3345
  })
@@ -5597,6 +5701,16 @@ function isExpired(credential, now) {
5597
5701
  const at = new Date(credential.expires_at).getTime();
5598
5702
  return !Number.isNaN(at) && at <= now;
5599
5703
  }
5704
+ /**
5705
+ * Which rows somebody here minted and which they authorized a product to hold.
5706
+ *
5707
+ * The product is named beside the word rather than left to the label the token
5708
+ * endpoint writes: the label is a convention and this is the row's own answer.
5709
+ */
5710
+ function kindOf(credential) {
5711
+ if (credential.kind !== "oauth") return "minted";
5712
+ return credential.client ? `connector (${credential.client.name})` : "connector";
5713
+ }
5600
5714
  function statusOf(credential, now) {
5601
5715
  if (credential.revoked_at) return "revoked";
5602
5716
  if (isExpired(credential, now)) return "expired";
@@ -5719,6 +5833,7 @@ function formatCredentials(credentials, now) {
5719
5833
  const rows = credentials.map((credential) => ({
5720
5834
  name: credential.name,
5721
5835
  prefix: credential.prefix,
5836
+ kind: kindOf(credential),
5722
5837
  capabilities: credential.capabilities.join(", "),
5723
5838
  apps: namedApps(credential.apps) ?? "every App",
5724
5839
  lastUsed: asUtc(credential.last_used_at),
@@ -5734,6 +5849,10 @@ function formatCredentials(credentials, now) {
5734
5849
  header: "PREFIX",
5735
5850
  of: (row) => row.prefix
5736
5851
  },
5852
+ {
5853
+ header: "KIND",
5854
+ of: (row) => row.kind
5855
+ },
5737
5856
  {
5738
5857
  header: "CAPABILITIES",
5739
5858
  of: (row) => row.capabilities
@@ -5920,7 +6039,7 @@ async function credentialsCommand(action, target, options) {
5920
6039
  //#region src/index.ts
5921
6040
  const cli = cac("wawesome");
5922
6041
  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));
5923
- 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));
6042
+ cli.command("deploy [entry]", "Build, upload, and promote a serverless function").option("-o, --out <path>", "Output JS bundle path", { default: "dist/index.js" }).option("-v, --verbose", "Enable verbose debug output").option("--skip-build", "Skip the build step, deploy an already-built bundle").option("--publish", "Confirm putting a private function back on its public address").option("--drop-assets", "Confirm a deploy carrying no static files where the live version serves some").action((entry, options) => deploy(entry, options));
5924
6043
  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) => {
5925
6044
  if (!action || action === "list" || action === "ls") return listVersions(options);
5926
6045
  if (action === "switch" || action === "use" || action === "select") return switchVersion(target, options);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wawesome",
3
- "version": "0.16.0",
3
+ "version": "0.18.0",
4
4
  "description": "CLI tool for building and deploying serverless functions on wawesome.io platform",
5
5
  "type": "module",
6
6
  "bin": {