wawesome 0.20.1 → 0.21.1

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 (2) hide show
  1. package/dist/index.mjs +291 -72
  2. package/package.json +1 -1
package/dist/index.mjs CHANGED
@@ -12,6 +12,20 @@ import { Readable } from "node:stream";
12
12
  import readline from "node:readline";
13
13
  import { confirm, select } from "@inquirer/prompts";
14
14
  import zlib from "node:zlib";
15
+ //#region src/brand.ts
16
+ const PLAIN = "[w] wawesome";
17
+ const TRUECOLOR_INDIGO = "\x1B[38;2;107;124;255m";
18
+ const INDIGO_256 = "\x1B[38;5;105m";
19
+ const BOLD = "\x1B[1m";
20
+ const RESET = "\x1B[0m";
21
+ function lockup(stream = process.stdout, env = process.env) {
22
+ if (!stream.isTTY || env.NO_COLOR) return PLAIN;
23
+ const indigo = /^(truecolor|24bit)$/i.test(env.COLORTERM ?? "") ? TRUECOLOR_INDIGO : INDIGO_256;
24
+ const bracket = (b) => `${indigo}${BOLD}${b}${RESET}`;
25
+ return `${bracket("[")}${BOLD}w${RESET}${bracket("]")} ${BOLD}wawesome${RESET}`;
26
+ }
27
+ const LOCKUP_HTML = "<div style=\"margin-bottom: 24px; font: 700 20px/1 ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; letter-spacing: -0.02em; color: #e8eaf0\"><span style=\"font-size: 22px; letter-spacing: -0.06em\"><span style=\"color: #6b7cff\">[</span>w<span style=\"color: #6b7cff\">]</span></span>&nbsp;wawesome</div>";
28
+ //#endregion
15
29
  //#region src/errors.ts
16
30
  /** A rejection carrying the gateway's own prose and, where given, its reason. */
17
31
  var GatewayError = class extends Error {
@@ -895,7 +909,7 @@ async function buildHandler(entryInput, config, options) {
895
909
  * that has to name this version — `--version`, the dependency a scaffolded
896
910
  * project pins — reads it here, so a release bumps one file.
897
911
  */
898
- const CLI_VERSION = "0.20.1";
912
+ const CLI_VERSION = "0.21.1";
899
913
  //#endregion
900
914
  //#region src/cli-login.ts
901
915
  const START_PATH = "/v1/cli/login/start";
@@ -1068,13 +1082,14 @@ function listen(server, port) {
1068
1082
  });
1069
1083
  });
1070
1084
  }
1071
- const STYLE = "font-family: system-ui; text-align: center; padding: 50px; background: #0f172a; color: #f8fafc;";
1085
+ const STYLE = "margin: 0; padding: 50px; text-align: center; background: #0e1016; color: #9298a8; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif;";
1072
1086
  function page(title, said) {
1073
1087
  return `<!DOCTYPE html>
1074
1088
  <html>
1075
1089
  <head><title>wawesome — ${escapeHtml(title.toLowerCase())}</title></head>
1076
1090
  <body style="${STYLE}">
1077
- <h2>${escapeHtml(title)}</h2>
1091
+ ${LOCKUP_HTML}
1092
+ <h2 style="color: #e8eaf0">${escapeHtml(title)}</h2>
1078
1093
  <p>${escapeHtml(said)}</p>
1079
1094
  </body>
1080
1095
  </html>`;
@@ -1207,7 +1222,7 @@ async function renewedSession(creds, tenantId) {
1207
1222
  if (res.status === 429) throw await asGatewayError(res, "The platform is rate limiting this session's renewals.");
1208
1223
  if (res.status >= 400 && res.status < 500) {
1209
1224
  const refused = await asGatewayError(res, "Your session could not be renewed.");
1210
- if (tenantId && refused.reason === "not-a-member") throw refused;
1225
+ if (tenantId && (refused.reason === "not-a-member" || refused.reason === "session-ended")) throw refused;
1211
1226
  endSession(refused.message);
1212
1227
  }
1213
1228
  if (!res.ok) throw new Error(`Could not renew your session (HTTP ${res.status}).`);
@@ -1372,7 +1387,7 @@ function storeSession({ gatewayUrl, dashboardUrl, session }) {
1372
1387
  tenant_slug: session.tenant_slug,
1373
1388
  tenant_name: session.tenant_name
1374
1389
  });
1375
- console.log("\n======================================================");
1390
+ console.log(`\n${lockup()}`);
1376
1391
  console.log("🎉 \x1B[32mLOGIN SUCCESSFUL!\x1B[0m");
1377
1392
  console.log("======================================================");
1378
1393
  if (session.tenant_name) console.log(`\n Workspace: ${session.tenant_name}`);
@@ -3562,6 +3577,97 @@ async function uploadAssets(creds, app, funcName, bundle, assets, declared, isVe
3562
3577
  console.log(`[wawesome] ✅ ${toUpload.length} asset(s) uploaded.`);
3563
3578
  }
3564
3579
  //#endregion
3580
+ //#region src/opens-host.ts
3581
+ /**
3582
+ * The host a variable declared with `opens_host` names, for the App's allowlist.
3583
+ * Null for a blank value. A refusal names the variable and never repeats the
3584
+ * value, since a connection string carries a password.
3585
+ */
3586
+ function hostOpenedBy(key, value) {
3587
+ const trimmed = value.trim();
3588
+ if (!trimmed) return null;
3589
+ const refuse = (why) => {
3590
+ throw new Error(`${key} ${why}`);
3591
+ };
3592
+ let url;
3593
+ try {
3594
+ url = new URL(trimmed);
3595
+ } catch {
3596
+ return refuse("is not an https:// address, so there is no host to open.");
3597
+ }
3598
+ if (url.protocol !== "https:") return refuse(`must be an https:// address, not ${url.protocol}//. Outbound calls are HTTPS only.`);
3599
+ if (url.hostname.startsWith("[")) return refuse("names an IPv6 address. The allowlist takes a host name, not an IPv6 literal.");
3600
+ if (url.port) return refuse(`names port ${url.port}. Only the default HTTPS port is opened from a variable; add the host and port under Egress in the dashboard.`);
3601
+ const host = url.hostname.toLowerCase().replace(/\.$/, "");
3602
+ if (!host) return refuse("names no host.");
3603
+ return host;
3604
+ }
3605
+ //#endregion
3606
+ //#region src/provision.ts
3607
+ /** The gateway's own words for a failure, or a plain HTTP status if it gave none. */
3608
+ async function rejected(res, fallback) {
3609
+ if (res.status === 401 || res.status === 403) return new GatewayError(unauthorizedMessage(res.status, await res.text().catch(() => "")), res.status);
3610
+ return asGatewayError(res, `${fallback} (HTTP ${res.status}).`);
3611
+ }
3612
+ function authHeaders(creds) {
3613
+ return {
3614
+ Authorization: `Bearer ${creds.tenant_jwt}`,
3615
+ "Content-Type": "application/json"
3616
+ };
3617
+ }
3618
+ /**
3619
+ * Make sure the App exists before anything is attached to it.
3620
+ *
3621
+ * Environment variables and subscriptions are both App-scoped, and neither
3622
+ * endpoint creates one — deploy would, but it runs last, and a Function that
3623
+ * goes live before its secrets do is a Function that fails its first request.
3624
+ *
3625
+ * Listing first rather than creating and reading the error keeps the common
3626
+ * case ("I already have this App") off the failure path; an App that appears in
3627
+ * between is still treated as the success it is.
3628
+ */
3629
+ async function ensureApp(creds, appSlug) {
3630
+ const listRes = await authorizedFetch(`${creds.gateway_url}/v1/apps`, { headers: { Authorization: `Bearer ${creds.tenant_jwt}` } });
3631
+ if (!listRes.ok) throw await rejected(listRes, "Could not list the Tenant's apps");
3632
+ if ((await listRes.json()).applications?.some((app) => app.slug === appSlug)) return;
3633
+ const createRes = await authorizedFetch(`${creds.gateway_url}/v1/apps`, {
3634
+ method: "POST",
3635
+ headers: authHeaders(creds),
3636
+ body: JSON.stringify({
3637
+ name: appSlug,
3638
+ slug: appSlug
3639
+ })
3640
+ });
3641
+ if (createRes.ok) return;
3642
+ const error = await rejected(createRes, `Could not create the app '${appSlug}'`);
3643
+ if (/already exists/i.test(error.message)) return;
3644
+ throw error;
3645
+ }
3646
+ /**
3647
+ * Enable one catalogue provider for the App.
3648
+ *
3649
+ * A template declares the providers it calls; without the subscription the
3650
+ * platform's egress policy denies those calls, so this is what stands between a
3651
+ * scaffolded template and an outbound request that silently fails at runtime.
3652
+ */
3653
+ async function enableEgressProvider(creds, appSlug, providerKey) {
3654
+ const res = await authorizedFetch(`${creds.gateway_url}/v1/apps/${encodeURIComponent(appSlug)}/egress/subscriptions`, {
3655
+ method: "POST",
3656
+ headers: authHeaders(creds),
3657
+ body: JSON.stringify({ provider_key: providerKey })
3658
+ });
3659
+ if (!res.ok) throw await rejected(res, `Could not enable outbound provider '${providerKey}'`);
3660
+ }
3661
+ /** Add a bare host to the App's allowlist. The gateway upserts, so a repeat is harmless. */
3662
+ async function allowHost(creds, appSlug, host) {
3663
+ const res = await authorizedFetch(`${creds.gateway_url}/v1/apps/${encodeURIComponent(appSlug)}/egress`, {
3664
+ method: "POST",
3665
+ headers: authHeaders(creds),
3666
+ body: JSON.stringify({ host_pattern: host })
3667
+ });
3668
+ if (!res.ok) throw await rejected(res, `Could not open outbound calls to '${host}'`);
3669
+ }
3670
+ //#endregion
3565
3671
  //#region src/env.ts
3566
3672
  const STANDARD_SECRET_MESSAGES = [
3567
3673
  "Encrypted at rest using AES-256",
@@ -3585,7 +3691,8 @@ function loadClientContext() {
3585
3691
  }
3586
3692
  return {
3587
3693
  creds,
3588
- app: config.app.trim()
3694
+ app: config.app.trim(),
3695
+ opensHost: config.opens_host ?? []
3589
3696
  };
3590
3697
  }
3591
3698
  /**
@@ -3623,8 +3730,18 @@ async function setEnvVar(key, value, options) {
3623
3730
  console.error("[wawesome] Error: Missing key or value. Usage: wawesome env set <KEY> <VALUE> [--secret]");
3624
3731
  process.exit(1);
3625
3732
  }
3626
- const { creds, app } = loadClientContext();
3733
+ const { creds, app, opensHost } = loadClientContext();
3627
3734
  const isSecret = Boolean(options.secret);
3735
+ const opensAHost = opensHost.includes(key);
3736
+ let host = null;
3737
+ if (opensAHost) try {
3738
+ host = hostOpenedBy(key, value);
3739
+ } catch (err) {
3740
+ console.error(`[wawesome] Error: ${errorText(err)}`);
3741
+ process.exit(1);
3742
+ return;
3743
+ }
3744
+ const previous = opensAHost ? await previousValue(creds, app, key) : void 0;
3628
3745
  if (isVerbose) {
3629
3746
  console.log(`[wawesome:verbose] POST ${creds.gateway_url}/v1/apps/${encodeURIComponent(app)}/env`);
3630
3747
  console.log(`[wawesome:verbose] Payload: key=${key}, is_secret=${isSecret}`);
@@ -3645,6 +3762,41 @@ async function setEnvVar(key, value, options) {
3645
3762
  for (const msg of messages) console.log(` • ${msg}`);
3646
3763
  console.log("");
3647
3764
  } else console.log(`[wawesome] ✅ Set variable '${key}' on app '${app}'.`);
3765
+ if (host) {
3766
+ try {
3767
+ await allowHost(creds, app, host);
3768
+ } catch (err) {
3769
+ console.error(`[wawesome] Error: ${errorText(err)}`);
3770
+ console.error(`[wawesome] '${key}' is set, but its host is not open. Run the same 'wawesome env set' again.`);
3771
+ process.exit(1);
3772
+ return;
3773
+ }
3774
+ console.log(`[wawesome] ✅ Opened outbound calls to '${host}', from ${key}.`);
3775
+ }
3776
+ if (previous?.is_secret) console.log(`[wawesome] ${key} was a secret, so the host its previous value opened cannot be read back. If that host changed, its entry stays on the allowlist; remove it under Egress in the dashboard if nothing else uses it.`);
3777
+ else if (previous) {
3778
+ const previousHost = derivedOrNull(key, previous.value);
3779
+ if (previousHost && previousHost !== host) console.log(`[wawesome] The allowlist entry '${previousHost}' no longer matches ${key}. It stays; remove it under Egress in the dashboard if nothing else uses it.`);
3780
+ }
3781
+ }
3782
+ async function previousValue(creds, app, key) {
3783
+ try {
3784
+ const res = await authorizedFetch(`${creds.gateway_url}/v1/apps/${encodeURIComponent(app)}/env`, {
3785
+ method: "GET",
3786
+ headers: { Authorization: `Bearer ${creds.tenant_jwt}` }
3787
+ });
3788
+ if (!res.ok) return void 0;
3789
+ return (await res.json()).variables?.find((variable) => variable.key === key);
3790
+ } catch {
3791
+ return;
3792
+ }
3793
+ }
3794
+ function derivedOrNull(key, value) {
3795
+ try {
3796
+ return hostOpenedBy(key, value);
3797
+ } catch {
3798
+ return null;
3799
+ }
3648
3800
  }
3649
3801
  /**
3650
3802
  * List environment variables on the current app.
@@ -3787,62 +3939,6 @@ function installDependencies(dir, options = {}) {
3787
3939
  };
3788
3940
  }
3789
3941
  //#endregion
3790
- //#region src/provision.ts
3791
- /** The gateway's own words for a failure, or a plain HTTP status if it gave none. */
3792
- async function rejected(res, fallback) {
3793
- if (res.status === 401 || res.status === 403) return new GatewayError(unauthorizedMessage(res.status, await res.text().catch(() => "")), res.status);
3794
- return asGatewayError(res, `${fallback} (HTTP ${res.status}).`);
3795
- }
3796
- function authHeaders(creds) {
3797
- return {
3798
- Authorization: `Bearer ${creds.tenant_jwt}`,
3799
- "Content-Type": "application/json"
3800
- };
3801
- }
3802
- /**
3803
- * Make sure the App exists before anything is attached to it.
3804
- *
3805
- * Environment variables and subscriptions are both App-scoped, and neither
3806
- * endpoint creates one — deploy would, but it runs last, and a Function that
3807
- * goes live before its secrets do is a Function that fails its first request.
3808
- *
3809
- * Listing first rather than creating and reading the error keeps the common
3810
- * case ("I already have this App") off the failure path; an App that appears in
3811
- * between is still treated as the success it is.
3812
- */
3813
- async function ensureApp(creds, appSlug) {
3814
- const listRes = await authorizedFetch(`${creds.gateway_url}/v1/apps`, { headers: { Authorization: `Bearer ${creds.tenant_jwt}` } });
3815
- if (!listRes.ok) throw await rejected(listRes, "Could not list the Tenant's apps");
3816
- if ((await listRes.json()).applications?.some((app) => app.slug === appSlug)) return;
3817
- const createRes = await authorizedFetch(`${creds.gateway_url}/v1/apps`, {
3818
- method: "POST",
3819
- headers: authHeaders(creds),
3820
- body: JSON.stringify({
3821
- name: appSlug,
3822
- slug: appSlug
3823
- })
3824
- });
3825
- if (createRes.ok) return;
3826
- const error = await rejected(createRes, `Could not create the app '${appSlug}'`);
3827
- if (/already exists/i.test(error.message)) return;
3828
- throw error;
3829
- }
3830
- /**
3831
- * Enable one catalogue provider for the App.
3832
- *
3833
- * A template declares the providers it calls; without the subscription the
3834
- * platform's egress policy denies those calls, so this is what stands between a
3835
- * scaffolded template and an outbound request that silently fails at runtime.
3836
- */
3837
- async function enableEgressProvider(creds, appSlug, providerKey) {
3838
- const res = await authorizedFetch(`${creds.gateway_url}/v1/apps/${encodeURIComponent(appSlug)}/egress/subscriptions`, {
3839
- method: "POST",
3840
- headers: authHeaders(creds),
3841
- body: JSON.stringify({ provider_key: providerKey })
3842
- });
3843
- if (!res.ok) throw await rejected(res, `Could not enable outbound provider '${providerKey}'`);
3844
- }
3845
- //#endregion
3846
3942
  //#region src/scaffold.ts
3847
3943
  const MANIFEST_FILE = "template.json";
3848
3944
  const PACKAGE_FILE = "package.json";
@@ -3882,6 +3978,18 @@ function applyTemplateIdentity(dir, identity) {
3882
3978
  function: identity.functionName
3883
3979
  }));
3884
3980
  }
3981
+ /**
3982
+ * Write down which variables open a host, so a later `env set` knows. Nothing is
3983
+ * written for a template that declares none.
3984
+ */
3985
+ function recordHostVariables(dir, env) {
3986
+ const keys = env.filter((variable) => variable.opens_host === true).map((variable) => variable.key);
3987
+ if (keys.length === 0) return;
3988
+ patchJson(path.join(dir, FUNCTION_CONFIG_FILE), (config) => ({
3989
+ ...config,
3990
+ opens_host: keys
3991
+ }));
3992
+ }
3885
3993
  /** The package the CLI publishes itself as, and which a template depends on. */
3886
3994
  const CLI_PACKAGE = "wawesome";
3887
3995
  /**
@@ -4481,6 +4589,19 @@ async function offerWorkspaceAddress(session, creds, tenant, appSlug, functionNa
4481
4589
  */
4482
4590
  async function wireUp(creds, appSlug, manifest, answers) {
4483
4591
  const scaffolded = "Your project is scaffolded. Fix this, then run 'wawesome deploy'.";
4592
+ const opened = [];
4593
+ for (const { declared, value } of answers) {
4594
+ if (declared.opens_host !== true) continue;
4595
+ try {
4596
+ const host = hostOpenedBy(declared.key, value);
4597
+ if (host) opened.push({
4598
+ key: declared.key,
4599
+ host
4600
+ });
4601
+ } catch (err) {
4602
+ fail$1(errorText(err), "Your project is scaffolded, and nothing was stored. Run 'wawesome init' again in an empty directory.");
4603
+ }
4604
+ }
4484
4605
  try {
4485
4606
  await ensureApp(creds, appSlug);
4486
4607
  } catch (err) {
@@ -4507,6 +4628,14 @@ async function wireUp(creds, appSlug, manifest, answers) {
4507
4628
  }
4508
4629
  console.log(`[wawesome] ✅ Enabled outbound calls to '${provider}'.`);
4509
4630
  }
4631
+ for (const { key, host } of opened) {
4632
+ try {
4633
+ await allowHost(creds, appSlug, host);
4634
+ } catch (err) {
4635
+ fail$1(errorText(err), scaffolded);
4636
+ }
4637
+ console.log(`[wawesome] ✅ Opened outbound calls to '${host}', from ${key}.`);
4638
+ }
4510
4639
  }
4511
4640
  /**
4512
4641
  * Scaffold from a catalogue template, wire up what it declared, and deploy it.
@@ -4572,6 +4701,7 @@ async function initFromTemplate(templateName, options) {
4572
4701
  app: appSlug,
4573
4702
  functionName
4574
4703
  });
4704
+ recordHostVariables(cwd, manifest.env);
4575
4705
  repinned = alignCliDependency(cwd, CLI_VERSION);
4576
4706
  } catch (err) {
4577
4707
  fail$1(errorText(err), `Some of '${template.name}' may have been written to ${cwd}.`);
@@ -5709,7 +5839,7 @@ async function renameWorkspace(slug, options) {
5709
5839
  * hearing about it.
5710
5840
  */
5711
5841
  async function listWorkspaces(options) {
5712
- const read = await requireWorkspaces(requireSession("Your workspaces would be read for that credential's Tenant alone."));
5842
+ const read = await requireWorkspaces(requireSession$1("Your workspaces would be read for that credential's Tenant alone."));
5713
5843
  console.log("\n[wawesome] Your workspaces:\n");
5714
5844
  const width = Math.max(...read.workspaces.map((workspace) => workspace.name.length));
5715
5845
  for (const workspace of read.workspaces) {
@@ -5729,7 +5859,7 @@ async function listWorkspaces(options) {
5729
5859
  * so this is where somebody is told rather than where it is decided.
5730
5860
  */
5731
5861
  async function switchWorkspace(target, options) {
5732
- const creds = requireSession("A switch would move a session that credential does not have.");
5862
+ const creds = requireSession$1("A switch would move a session that credential does not have.");
5733
5863
  if (!target || !target.trim()) {
5734
5864
  console.error("[wawesome] Error: No workspace given. Usage: wawesome workspace switch <name>");
5735
5865
  process.exit(1);
@@ -5776,7 +5906,7 @@ async function workspaceCommand(action, target, options) {
5776
5906
  * **Deploy credential**: a credential names one Tenant and holds no
5777
5907
  * **Membership** anywhere, so there is nothing for it to list or move.
5778
5908
  */
5779
- function requireSession(consequence) {
5909
+ function requireSession$1(consequence) {
5780
5910
  const credential = credentialFromEnvironment();
5781
5911
  if (credential) {
5782
5912
  reportSessionCommandRefused(credential, consequence);
@@ -5814,6 +5944,93 @@ function refuse(headline, next, lines) {
5814
5944
  process.exit(1);
5815
5945
  }
5816
5946
  //#endregion
5947
+ //#region src/export.ts
5948
+ const POLL_MS = 3e3;
5949
+ async function exportCall(creds, method) {
5950
+ const res = await authorizedFetch(`${creds.gateway_url}/v1/tenant/export`, {
5951
+ method,
5952
+ headers: { Authorization: `Bearer ${creds.tenant_jwt}` }
5953
+ });
5954
+ if (!res.ok) throw await asGatewayError(res, `Failed to ${method === "POST" ? "ask for" : "read"} the export (HTTP ${res.status}).`);
5955
+ return await res.json();
5956
+ }
5957
+ function requestExport(creds) {
5958
+ return exportCall(creds, "POST");
5959
+ }
5960
+ function readExport(creds) {
5961
+ return exportCall(creds, "GET");
5962
+ }
5963
+ function when(instant) {
5964
+ return new Date(instant).toUTCString();
5965
+ }
5966
+ function megabytes(bytes) {
5967
+ return `${(bytes / 1048576).toFixed(1)} MB`;
5968
+ }
5969
+ function describeExport(view) {
5970
+ const current = view.export;
5971
+ if (!current) return ["This workspace has not asked for an export. Run 'wawesome export' to ask for one."];
5972
+ switch (current.status) {
5973
+ case "queued": return [`Export asked for at ${when(current.requested_at)}. It is waiting to be built.`];
5974
+ case "building": return current.last_failed_at ? [`The last attempt to build it failed at ${when(current.last_failed_at)}. It is being tried again.`, "Run 'wawesome export status' later to see where it got to."] : ["The archive is being built. A large workspace takes a while."];
5975
+ case "expired": return [`The last export was deleted on ${when(current.expires_at)}, ${view.retention_days} days after it was built.`, "Run 'wawesome export' to ask for a new one."];
5976
+ case "ready": return [
5977
+ `Ready${current.size_bytes !== null ? ` (${megabytes(current.size_bytes)})` : ""}. Download it here:`,
5978
+ "",
5979
+ ` ${current.download.url}`,
5980
+ "",
5981
+ `This link works until ${when(current.download.expires_at)}. Run 'wawesome export status' for a new one.`,
5982
+ `The archive is deleted on ${when(current.expires_at)}.`,
5983
+ "Secret values are not in it. Rotate any secret that mattered."
5984
+ ];
5985
+ }
5986
+ }
5987
+ function requireSession() {
5988
+ const credential = credentialFromEnvironment();
5989
+ if (credential) {
5990
+ reportSessionCommandRefused(credential, "Only the workspace's owner, signed in, can export it.");
5991
+ process.exit(1);
5992
+ }
5993
+ const creds = readCredentials();
5994
+ if (!creds) {
5995
+ console.error("[wawesome] Error: Not logged in. Run 'wawesome login' first.");
5996
+ process.exit(1);
5997
+ }
5998
+ return creds;
5999
+ }
6000
+ function print(view) {
6001
+ for (const line of describeExport(view)) console.log(line ? `[wawesome] ${line}` : "");
6002
+ }
6003
+ const settled = (view) => view.export?.status === "ready" || view.export?.status === "expired";
6004
+ async function exportCommand(action, options) {
6005
+ if (action && action !== "status") {
6006
+ console.error(`[wawesome] Error: Unknown export action '${action}'.`);
6007
+ console.error("[wawesome] Usage: wawesome export [status] [--no-wait]");
6008
+ process.exit(1);
6009
+ }
6010
+ const creds = requireSession();
6011
+ try {
6012
+ if (action === "status") {
6013
+ print(await readExport(creds));
6014
+ return;
6015
+ }
6016
+ let view = await requestExport(creds);
6017
+ print(view);
6018
+ if (options.wait === false) return;
6019
+ let said = view.export?.status;
6020
+ while (!settled(view) && !view.export?.last_failed_at) {
6021
+ await new Promise((resolve) => setTimeout(resolve, POLL_MS));
6022
+ view = await readExport(creds);
6023
+ if (view.export?.status !== said) {
6024
+ said = view.export?.status;
6025
+ print(view);
6026
+ }
6027
+ }
6028
+ } catch (err) {
6029
+ console.error(`[wawesome] Error: ${errorText(err)}`);
6030
+ process.exit(1);
6031
+ }
6032
+ }
6033
+ //#endregion
5817
6034
  //#region ../shared/mint-surface.generated.ts
5818
6035
  const MINT_PRESETS = [
5819
6036
  {
@@ -5909,9 +6126,10 @@ function mintPreset(name) {
5909
6126
  "write:egress",
5910
6127
  "write:billing",
5911
6128
  "write:credentials",
5912
- "write:members"
6129
+ "write:members",
6130
+ "write:exports"
5913
6131
  ],
5914
- summary: "Reads the workspace, Apps, Functions, environment variables, domains, invocations, runs, schedules, previews and the egress allowlist, and can rename the workspace, create Apps, deploy Functions, set environment variables, attach domains, detach domains, run Functions, pause and resume schedules, create previews, widen the egress allowlist, change billing, manage credentials and manage members.",
6132
+ summary: "Reads the workspace, Apps, Functions, environment variables, domains, invocations, runs, schedules, previews and the egress allowlist, and can rename the workspace, create Apps, deploy Functions, set environment variables, attach domains, detach domains, run Functions, pause and resume schedules, create previews, widen the egress allowlist, change billing, manage credentials, manage members and export the workspace.",
5915
6133
  withheldFromAPerson: "A workspace has one owner, so 'owner' is not a standing anybody is put at."
5916
6134
  },
5917
6135
  {
@@ -5940,7 +6158,7 @@ function mintPreset(name) {
5940
6158
  "write:credentials",
5941
6159
  "write:members"
5942
6160
  ],
5943
- summary: "Reads the workspace, Apps, Functions, environment variables, domains, invocations, runs, schedules, previews and the egress allowlist, and can create Apps, deploy Functions, set environment variables, attach domains, detach domains, run Functions, pause and resume schedules, create previews, widen the egress allowlist, manage credentials and manage members. It does not rename the workspace or change billing.",
6161
+ summary: "Reads the workspace, Apps, Functions, environment variables, domains, invocations, runs, schedules, previews and the egress allowlist, and can create Apps, deploy Functions, set environment variables, attach domains, detach domains, run Functions, pause and resume schedules, create previews, widen the egress allowlist, manage credentials and manage members. It does not rename the workspace, change billing or export the workspace.",
5944
6162
  withheldFromAPerson: null
5945
6163
  },
5946
6164
  {
@@ -5965,7 +6183,7 @@ function mintPreset(name) {
5965
6183
  "write:schedules",
5966
6184
  "write:previews"
5967
6185
  ],
5968
- summary: "Reads the workspace, Apps, Functions, environment variables, domains, invocations, runs, schedules, previews and the egress allowlist, and can create Apps, deploy Functions, set environment variables, attach domains, run Functions, pause and resume schedules and create previews. It does not rename the workspace, detach domains, widen the egress allowlist, change billing, manage credentials or manage members.",
6186
+ summary: "Reads the workspace, Apps, Functions, environment variables, domains, invocations, runs, schedules, previews and the egress allowlist, and can create Apps, deploy Functions, set environment variables, attach domains, run Functions, pause and resume schedules and create previews. It does not rename the workspace, detach domains, widen the egress allowlist, change billing, manage credentials, manage members or export the workspace.",
5969
6187
  withheldFromAPerson: null
5970
6188
  },
5971
6189
  {
@@ -5987,7 +6205,7 @@ function mintPreset(name) {
5987
6205
  "write:env",
5988
6206
  "write:runs"
5989
6207
  ],
5990
- summary: "Reads the workspace, Apps, Functions, environment variables, domains, invocations, runs, schedules, previews and the egress allowlist, and can create Apps, deploy Functions, set environment variables and run Functions. It does not rename the workspace, attach domains, detach domains, pause and resume schedules, create previews, widen the egress allowlist, change billing, manage credentials or manage members.",
6208
+ summary: "Reads the workspace, Apps, Functions, environment variables, domains, invocations, runs, schedules, previews and the egress allowlist, and can create Apps, deploy Functions, set environment variables and run Functions. It does not rename the workspace, attach domains, detach domains, pause and resume schedules, create previews, widen the egress allowlist, change billing, manage credentials, manage members or export the workspace.",
5991
6209
  withheldFromAPerson: "'deployer' is the recipe a Deploy credential is cut from. A pipeline gets a credential of its own rather than somebody's account."
5992
6210
  },
5993
6211
  {
@@ -6473,6 +6691,7 @@ cli.command("login", "Authenticate with the wawesome.io platform").option("--api
6473
6691
  cli.command("logout", "Clear stored authentication credentials").action(async () => await logout());
6474
6692
  cli.command("whoami", "Show current login session info").action(() => whoami());
6475
6693
  cli.command("workspace [action] [name]", "Show the workspace, move between workspaces, or rename the public address").usage("workspace <action> [name]\n\nActions:\n show Show the workspace name, address, and whether it can still change\n list (ls) Show the workspaces you belong to, and which one you are in\n switch <name> Move this terminal to another workspace you belong to\n rename <name> Change the public address, while nothing live depends on it").example("wawesome workspace").example("wawesome workspace list").example("wawesome workspace switch northwind").example("wawesome workspace rename northwind").option("-v, --verbose", "Enable verbose debug output").action((action, name, options) => workspaceCommand(action, name, options));
6694
+ cli.command("export [action]", "Export the whole workspace as one archive, and print the link to it").usage("export [status]\n\nWith no action, asks for an export and waits until it is ready.\n status Show the latest export, and a new link if it is ready").example("wawesome export").example("wawesome export status").option("--no-wait", "Ask for the export and return without waiting for it").option("-v, --verbose", "Enable verbose debug output").action((action, options) => exportCommand(action, options));
6476
6695
  cli.command("templates [action]", "Browse the template catalog").usage("templates [action]\n\nActions:\n list (ls) Show every available template (default)").example("wawesome templates").example("wawesome templates list").option("--api <url>", "API URL (default: https://api.wawesome.io)").option("-v, --verbose", "Enable verbose debug output").action((action, options) => templatesCommand(action, options));
6477
6696
  cli.command("credentials [action] [target]", "Mint, list, regenerate, revoke and delete deploy credentials").alias("credential").usage(`credentials <action> [options]
6478
6697
 
@@ -6564,7 +6783,7 @@ cli.command("cron pause <schedule>", "Pause a schedule by name").option("-r, --r
6564
6783
  cli.command("cron resume <schedule>", "Resume a paused schedule").option("-f, --function <function>", "Function name override").option("-a, --app <app>", "App slug override").option("-v, --verbose", "Enable verbose debug output").action((schedule, options) => resumeScheduleCommand(schedule, void 0, options));
6565
6784
  cli.command("cron history [target]", "Read background run history").alias("cron runs").option("-s, --state <state>", "Filter run history by state (pending, running, dispatched, skipped, missed, cancelled, lost, failed)").option("--status <status>", "Alias for --state").option("-l, --limit <limit>", "Limit number of history rows (default: 50)").option("-a, --app <app>", "App slug override").option("-v, --verbose", "Enable verbose debug output").action((target, options) => runHistoryCommand(target, options));
6566
6785
  cli.command("schedules [action] [target]", "Alias for 'cron'").option("-a, --app <app>", "App slug override").option("-v, --verbose", "Enable verbose debug output").action((action, target, options) => cronCommand(action, target, void 0, options));
6567
- cli.help();
6786
+ cli.help((sections) => [{ body: `${lockup()} ${CLI_VERSION}` }, ...sections.slice(1)]);
6568
6787
  cli.version(CLI_VERSION);
6569
6788
  cli.parse();
6570
6789
  //#endregion
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wawesome",
3
- "version": "0.20.1",
3
+ "version": "0.21.1",
4
4
  "description": "CLI tool for building and deploying serverless functions on wawesome.io platform",
5
5
  "type": "module",
6
6
  "bin": {