@wervt/cli 0.2.15 → 0.2.17

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -8,7 +8,7 @@ import { defineCommand, runMain } from "citty";
8
8
  import { detectPackageManager } from "nypm";
9
9
  import { x } from "tinyexec";
10
10
  import { packTar } from "modern-tar";
11
- //#region src/bundle.ts
11
+ // src/bundle.ts
12
12
  /** Files under `dir`. Follows symlinks: Nitro can leave traced dependencies in
13
13
  * server/node_modules as links into the local pnpm store, which won't exist on the server. */
14
14
  async function walk(dir) {
@@ -51,8 +51,7 @@ async function createBundle(appDir) {
51
51
  files: files.length
52
52
  };
53
53
  }
54
- //#endregion
55
- //#region src/config.ts
54
+ // src/config.ts
56
55
  const file = join(process.env.XDG_CONFIG_HOME ?? join(homedir(), ".config"), "wervt", "config.json");
57
56
  /** Flags > WERVT_URL / WERVT_TOKEN > ~/.config/wervt/config.json. */
58
57
  async function resolveConfig(flags) {
@@ -74,8 +73,7 @@ async function saveConfig(config) {
74
73
  await writeFile(file, `${JSON.stringify(config, null, 2)}\n`, { mode: 384 });
75
74
  return file;
76
75
  }
77
- //#endregion
78
- //#region src/format.ts
76
+ // src/format.ts
79
77
  function formatBytes(bytes) {
80
78
  if (bytes < 1024) return `${bytes} B`;
81
79
  const units = [
@@ -123,12 +121,45 @@ function auditSummary(entry) {
123
121
  ].filter(Boolean).join("; ");
124
122
  case "token.create":
125
123
  case "token.revoke": return text(d.name) || text(d.id);
124
+ case "client.create":
125
+ case "client.link":
126
+ case "client.delete": return text(d.name) || text(d.clientId);
126
127
  case "app.delete": return `database ${text(d.database)}`;
128
+ case "domains.add":
129
+ case "domains.remove":
130
+ case "domains.primary": return text(d.domain);
127
131
  default: return entry.account ?? "";
128
132
  }
129
133
  }
130
- //#endregion
131
- //#region src/source.ts
134
+ /** A saved query's alert in a few words: `alert at 5 in 5m, firing (7)`. */
135
+ function alertSummary(query) {
136
+ const { alert, state } = query;
137
+ if (!alert) return "no alert";
138
+ const rule = `alert at ${alert.threshold} in ${alert.window}`;
139
+ if (!alert.enabled) return `${rule}, off`;
140
+ if (state?.error) return `${rule}, failing: ${state.error.split("\n")[0]}`;
141
+ if (state?.firing) return `${rule}, firing (${state.lastCount ?? "?"})`;
142
+ return state?.lastFiredAt ? `${rule}, last fired ${state.lastFiredAt.slice(0, 16)}` : rule;
143
+ }
144
+ /** `GET /api/x 500 12ms 3 entries, 1 error` for a request. */
145
+ function requestSummary(group) {
146
+ const what = group.method && group.path ? [
147
+ group.method,
148
+ group.path,
149
+ group.status
150
+ ].filter(Boolean).join(" ") : group.message ?? "(no request event)";
151
+ const counts = [
152
+ `${group.entries} ${group.entries === 1 ? "entry" : "entries"}`,
153
+ group.errors && `${group.errors} ${group.errors === 1 ? "error" : "errors"}`,
154
+ group.warnings && `${group.warnings} ${group.warnings === 1 ? "warning" : "warnings"}`
155
+ ].filter(Boolean);
156
+ return [
157
+ what,
158
+ group.duration,
159
+ counts.join(", ")
160
+ ].filter(Boolean).join(" ");
161
+ }
162
+ // src/source.ts
132
163
  async function git(dir, ...args) {
133
164
  try {
134
165
  const result = await x("git", args, {
@@ -173,8 +204,7 @@ async function deploySource(dir) {
173
204
  actor: await git(dir, "config", "user.name")
174
205
  };
175
206
  }
176
- //#endregion
177
- //#region src/index.ts
207
+ // src/index.ts
178
208
  const interactive = process.stdout.isTTY === true && !process.env.CI;
179
209
  function progress() {
180
210
  if (interactive) return p.spinner();
@@ -278,7 +308,11 @@ const deploy = defineCommand({
278
308
  if ((await x(pm, ["run", "build"], {
279
309
  nodeOptions: {
280
310
  cwd: dir,
281
- stdio: "inherit"
311
+ stdio: "inherit",
312
+ env: {
313
+ ...process.env,
314
+ WERVT_APP: name
315
+ }
282
316
  },
283
317
  throwOnError: false
284
318
  })).exitCode !== 0) throw new Error("Build failed");
@@ -597,6 +631,63 @@ const access = defineCommand({
597
631
  }
598
632
  }
599
633
  });
634
+ const domains = defineCommand({
635
+ meta: {
636
+ name: "domains",
637
+ description: "Show or change an app's own domains (e.g. example.com); <app>.<base domain> then forwards to the primary one"
638
+ },
639
+ args: {
640
+ app: {
641
+ type: "positional",
642
+ required: true,
643
+ description: "App name"
644
+ },
645
+ add: {
646
+ type: "string",
647
+ description: "Add a domain; it serves the app once its DNS points here"
648
+ },
649
+ remove: {
650
+ type: "string",
651
+ description: "Remove a domain"
652
+ },
653
+ primary: {
654
+ type: "string",
655
+ description: "Make a verified domain the app's main address"
656
+ },
657
+ verify: {
658
+ type: "string",
659
+ description: "Check a pending domain's DNS now"
660
+ },
661
+ ...connection
662
+ },
663
+ async run({ args }) {
664
+ const changes = [
665
+ "add",
666
+ "remove",
667
+ "primary",
668
+ "verify"
669
+ ].filter((key) => args[key]);
670
+ if (changes.length > 1) throw new Error("Pass one of --add, --remove, --primary or --verify");
671
+ const change = changes[0];
672
+ const result = await (change ? await api(`/v1/apps/${args.app}/domains`, args, {
673
+ method: "PATCH",
674
+ headers: { "content-type": "application/json" },
675
+ body: JSON.stringify({ [change]: args[change] })
676
+ }) : await api(`/v1/apps/${args.app}/domains`, args)).json();
677
+ if (!result.configured) console.log("This server doesn't support custom domains.");
678
+ if (!result.domains.length) return console.log(`${args.app} has no domains of its own.`);
679
+ for (const d of result.domains) {
680
+ const label = d.status === "active" ? styleText("green", d.primary ? "active, primary" : "active") : styleText("yellow", "pending");
681
+ console.log(`${d.domain} ${label}${d.error ? ` ${styleText("dim", d.error)}` : ""}`);
682
+ }
683
+ const pending = result.domains.filter((d) => d.status === "pending");
684
+ if (!pending.length || !result.target) return;
685
+ console.log(`\nPoint each pending domain at ${result.target}: a CNAME (subdomains), or an A record with its addresses (a bare domain).`);
686
+ console.log("Behind a proxy of your own (Cloudflare), add a TXT record instead, the proxy has to send the traffic on:");
687
+ for (const d of pending) console.log(` ${d.txt.name} TXT ${d.txt.value}`);
688
+ console.log("Pending domains are checked every few minutes, or now with --verify <domain>.");
689
+ }
690
+ });
600
691
  const members = defineCommand({
601
692
  meta: {
602
693
  name: "members",
@@ -729,9 +820,36 @@ const accounts = defineCommand({
729
820
  type: "string",
730
821
  description: "Space all releases of their apps may take, e.g. 2GB. \"none\", \"default\""
731
822
  },
823
+ into: {
824
+ type: "string",
825
+ description: "Merge the account into this one (same person, two sign-ups): its logins, apps and access move over, apps move their data, then it is deleted"
826
+ },
732
827
  ...connection
733
828
  },
734
829
  async run({ args }) {
830
+ if (args.email && args.into) {
831
+ const result = await (await api("/v1/accounts/merge", args, {
832
+ method: "POST",
833
+ headers: { "content-type": "application/json" },
834
+ body: JSON.stringify({
835
+ from: args.email,
836
+ into: args.into
837
+ })
838
+ })).json();
839
+ console.log(`${result.from.email} merged into ${result.into.email}`);
840
+ console.log(` logins moved: ${result.logins.join(", ") || "none"}`);
841
+ if (result.owners.length) console.log(` owner or member of: ${result.owners.join(", ")}`);
842
+ if (result.access.length) console.log(` allowed in: ${result.access.join(", ")}`);
843
+ const payload = JSON.stringify({
844
+ from: result.from,
845
+ into: result.into
846
+ });
847
+ for (const [app, run] of Object.entries(result.apps)) {
848
+ console.log(` ${app}: ${run.status}${run.error ? ` (${run.error})` : ""}`);
849
+ if (run.status !== "ok") console.log(styleText("dim", ` retry: wervt tasks ${app} --run wervt:account-merged --payload '${payload}'`));
850
+ }
851
+ return;
852
+ }
735
853
  if (args.email) {
736
854
  const change = {};
737
855
  if (args.apps) change.apps = args.apps === "none" ? null : args.apps === "default" ? "default" : Number(args.apps);
@@ -805,6 +923,120 @@ const tokens = defineCommand({
805
923
  }
806
924
  }
807
925
  });
926
+ const appArg = {
927
+ type: "positional",
928
+ required: true,
929
+ description: "App name"
930
+ };
931
+ const clients = defineCommand({
932
+ meta: {
933
+ name: "clients",
934
+ description: "Manage the OAuth clients of an app's native apps (sign-in with wervt)"
935
+ },
936
+ subCommands: {
937
+ list: defineCommand({
938
+ meta: {
939
+ name: "list",
940
+ description: "List the app's native app clients"
941
+ },
942
+ args: {
943
+ app: appArg,
944
+ ...connection
945
+ },
946
+ async run({ args }) {
947
+ const list = await (await api(`/v1/apps/${args.app}/clients`, args)).json();
948
+ if (!list.length) return console.log(`No native app clients for ${args.app}.`);
949
+ for (const client of list) {
950
+ const used = client.lastUsedAt ? `used ${client.lastUsedAt.slice(0, 10)}` : "never used";
951
+ console.log(`${client.clientId} ${client.name} created ${client.createdAt.slice(0, 10)} ${used}`);
952
+ for (const uri of client.redirectUris) console.log(styleText("dim", ` ${uri}`));
953
+ }
954
+ }
955
+ }),
956
+ create: defineCommand({
957
+ meta: {
958
+ name: "create",
959
+ description: "Register a native app for sign-in, e.g. --redirect dev.example.app:/auth"
960
+ },
961
+ args: {
962
+ app: appArg,
963
+ name: {
964
+ type: "string",
965
+ required: true,
966
+ description: "Shown on the consent page"
967
+ },
968
+ redirect: {
969
+ type: "string",
970
+ required: true,
971
+ description: "Redirect URI; comma-separated for several"
972
+ },
973
+ ...connection
974
+ },
975
+ async run({ args }) {
976
+ const client = await (await api(`/v1/apps/${args.app}/clients`, args, {
977
+ method: "POST",
978
+ headers: { "content-type": "application/json" },
979
+ body: JSON.stringify({
980
+ name: args.name,
981
+ redirectUris: toList(args.redirect)
982
+ })
983
+ })).json();
984
+ console.error(`Client "${client.name}" for ${args.app}:`);
985
+ console.log(client.clientId);
986
+ console.error(styleText("dim", "A public client (PKCE, no secret): put the id in the app config."));
987
+ }
988
+ }),
989
+ delete: defineCommand({
990
+ meta: {
991
+ name: "delete",
992
+ description: "Delete a client; everyone signed in through it is signed out"
993
+ },
994
+ args: {
995
+ app: appArg,
996
+ clientId: {
997
+ type: "positional",
998
+ required: true,
999
+ description: "Client id"
1000
+ },
1001
+ yes: {
1002
+ type: "boolean",
1003
+ description: "Skip the confirmation"
1004
+ },
1005
+ ...connection
1006
+ },
1007
+ async run({ args }) {
1008
+ if (!args.yes) {
1009
+ const ok = await p.confirm({
1010
+ message: `Delete ${args.clientId}? Every install signed in through it is signed out.`,
1011
+ initialValue: false
1012
+ });
1013
+ if (p.isCancel(ok) || !ok) return p.cancel("Cancelled");
1014
+ }
1015
+ await api(`/v1/apps/${args.app}/clients/${args.clientId}`, args, { method: "DELETE" });
1016
+ console.log(`Deleted ${args.clientId}`);
1017
+ }
1018
+ }),
1019
+ link: defineCommand({
1020
+ meta: {
1021
+ name: "link",
1022
+ description: "Admins: assign a client that registered itself to the app"
1023
+ },
1024
+ args: {
1025
+ app: appArg,
1026
+ clientId: {
1027
+ type: "positional",
1028
+ required: true,
1029
+ description: "Client id"
1030
+ },
1031
+ ...connection
1032
+ },
1033
+ async run({ args }) {
1034
+ await api(`/v1/apps/${args.app}/clients/${args.clientId}/link`, args, { method: "POST" });
1035
+ console.log(`${args.clientId} belongs to ${args.app} now`);
1036
+ }
1037
+ })
1038
+ }
1039
+ });
808
1040
  const token = defineCommand({
809
1041
  meta: {
810
1042
  name: "token",
@@ -1001,7 +1233,7 @@ const remove = defineCommand({
1001
1233
  const dropDb = Boolean(args["drop-database"]);
1002
1234
  if (!args.yes) {
1003
1235
  const ok = await p.confirm({
1004
- message: `Delete ${args.app}${dropDb ? " and ALL its data" : " (database is kept)"}?`,
1236
+ message: `Delete ${args.app}${dropDb ? " and ALL its data" : " (data is kept, the name stays yours)"}?`,
1005
1237
  initialValue: false
1006
1238
  });
1007
1239
  if (p.isCancel(ok) || !ok) return p.cancel("Cancelled");
@@ -1059,6 +1291,14 @@ const logs = defineCommand({
1059
1291
  alias: "f",
1060
1292
  description: "Keep printing new entries"
1061
1293
  },
1294
+ requests: {
1295
+ type: "boolean",
1296
+ description: "One line per request (requestId): what it was and how much it logged"
1297
+ },
1298
+ saved: {
1299
+ type: "string",
1300
+ description: "Use a saved query by name (see `wervt queries`)"
1301
+ },
1062
1302
  json: {
1063
1303
  type: "boolean",
1064
1304
  description: "One JSON object per line, with every field"
@@ -1067,7 +1307,31 @@ const logs = defineCommand({
1067
1307
  },
1068
1308
  async run({ args }) {
1069
1309
  const params = new URLSearchParams();
1070
- if (args.query) params.set("query", args.query);
1310
+ if (args.saved) {
1311
+ if (args.query) throw new Error("Give either a query or --saved, not both");
1312
+ const saved = await findSavedQuery(args.app, args.saved, args);
1313
+ params.set("query", saved.query);
1314
+ if (saved.levels?.length) params.set("levels", saved.levels.join(","));
1315
+ } else if (args.query) params.set("query", args.query);
1316
+ if (args.requests) {
1317
+ if (args.follow) throw new Error("--requests and --follow do not go together");
1318
+ params.set("start", args.since);
1319
+ if (args.until) params.set("end", args.until);
1320
+ params.set("limit", args.limit);
1321
+ const groups = await (await api(`/v1/apps/${args.app}/logs/requests?${params.toString()}`, args)).json();
1322
+ if (!groups.length && !args.json) return console.error(`No requests with a requestId in the last ${args.since}.`);
1323
+ for (const group of groups.reverse()) {
1324
+ if (args.json) {
1325
+ console.log(JSON.stringify(group));
1326
+ continue;
1327
+ }
1328
+ const time = new Date(group.last).toISOString().slice(11, 23);
1329
+ const color = group.errors ? "red" : group.warnings ? "yellow" : "white";
1330
+ console.log(`${styleText("dim", time)} ${styleText(color, requestSummary(group))} ${styleText("dim", group.requestId)}`);
1331
+ }
1332
+ if (!args.json) console.error(styleText("dim", `\nOne request: wervt logs ${args.app} 'requestId:=<id>' --since ${args.since}`));
1333
+ return;
1334
+ }
1071
1335
  if (args.follow) {
1072
1336
  params.set("since", args.since);
1073
1337
  const res = await api(`/v1/apps/${args.app}/logs/tail?${params.toString()}`, args);
@@ -1087,6 +1351,111 @@ const logs = defineCommand({
1087
1351
  for (const entry of entries.reverse()) printLog(entry, Boolean(args.json));
1088
1352
  }
1089
1353
  });
1354
+ /** `/v1/apps/<app>/logs/queries`, or across apps (admins) without an app. */
1355
+ const queriesPath = (app) => app ? `/v1/apps/${app}/logs/queries` : "/v1/logs/queries";
1356
+ async function findSavedQuery(app, name, flags) {
1357
+ const list = await (await api(queriesPath(app), flags)).json();
1358
+ const found = list.find((query) => query.id === name || query.name.toLowerCase() === name.toLowerCase());
1359
+ if (!found) throw new Error(`No saved query "${name}"${list.length ? `. Saved: ${list.map((q) => q.name).join(", ")}` : ""}`);
1360
+ return found;
1361
+ }
1362
+ const queries = defineCommand({
1363
+ meta: {
1364
+ name: "queries",
1365
+ description: "List, save or delete saved log queries and the alerts on them"
1366
+ },
1367
+ args: {
1368
+ app: {
1369
+ type: "positional",
1370
+ required: false,
1371
+ description: "App name (default: queries across all apps, admins only)"
1372
+ },
1373
+ save: {
1374
+ type: "string",
1375
+ description: "Save a query under this name, or change the one that has it"
1376
+ },
1377
+ query: {
1378
+ type: "string",
1379
+ description: "LogsQL for --save, e.g. 'level:error'"
1380
+ },
1381
+ levels: {
1382
+ type: "string",
1383
+ description: "Only these levels for --save, e.g. error,warn"
1384
+ },
1385
+ alert: {
1386
+ type: "string",
1387
+ description: "on or off: mail the app owner and members when the query matches"
1388
+ },
1389
+ threshold: {
1390
+ type: "string",
1391
+ description: "Alert when at least this many entries match"
1392
+ },
1393
+ window: {
1394
+ type: "string",
1395
+ description: "Alert window, e.g. 5m (1m to 24h)"
1396
+ },
1397
+ cooldown: {
1398
+ type: "string",
1399
+ description: "Quiet time after an alert, e.g. 1h (5m to 7d)"
1400
+ },
1401
+ delete: {
1402
+ type: "string",
1403
+ description: "Delete the saved query with this name"
1404
+ },
1405
+ json: {
1406
+ type: "boolean",
1407
+ description: "Print JSON"
1408
+ },
1409
+ ...connection
1410
+ },
1411
+ async run({ args }) {
1412
+ const path = queriesPath(args.app);
1413
+ const json = { "content-type": "application/json" };
1414
+ if (args.delete) {
1415
+ const found = await findSavedQuery(args.app, args.delete, args);
1416
+ await api(`${path}/${found.id}`, args, { method: "DELETE" });
1417
+ return console.log(`Deleted ${found.name}`);
1418
+ }
1419
+ if (args.save) {
1420
+ if (args.alert !== void 0 && !["on", "off"].includes(args.alert)) throw new Error("--alert is on or off");
1421
+ const alertFields = {
1422
+ ...args.threshold ? { threshold: Number(args.threshold) } : {},
1423
+ ...args.window ? { window: args.window } : {},
1424
+ ...args.cooldown ? { cooldown: args.cooldown } : {}
1425
+ };
1426
+ const alert = args.alert === "off" ? null : args.alert === "on" || Object.keys(alertFields).length ? {
1427
+ enabled: true,
1428
+ ...alertFields
1429
+ } : void 0;
1430
+ const body = {
1431
+ name: args.save,
1432
+ ...args.query !== void 0 ? { query: args.query } : {},
1433
+ ...args.levels !== void 0 ? { levels: args.levels } : {},
1434
+ ...alert !== void 0 ? { alert } : {}
1435
+ };
1436
+ const existing = (await (await api(path, args)).json()).find((q) => q.name.toLowerCase() === args.save.toLowerCase());
1437
+ const saved = await (await api(existing ? `${path}/${existing.id}` : path, args, {
1438
+ method: existing ? "PATCH" : "POST",
1439
+ headers: json,
1440
+ body: JSON.stringify(body)
1441
+ })).json();
1442
+ if (args.json) return console.log(JSON.stringify(saved));
1443
+ return console.log(`${existing ? "Changed" : "Saved"} ${saved.name}: ${saved.query} ${styleText("dim", alertSummary(saved))}`);
1444
+ }
1445
+ const list = await (await api(path, args)).json();
1446
+ if (args.json) return console.log(JSON.stringify(list));
1447
+ if (!list.length) {
1448
+ const target = args.app ? ` ${args.app}` : "";
1449
+ return console.log(`No saved queries. Save one: wervt queries${target} --save Errors --query 'level:error' --alert on`);
1450
+ }
1451
+ for (const query of list) {
1452
+ const levels = query.levels?.length ? styleText("dim", ` [${query.levels.join(",")}]`) : "";
1453
+ const color = query.state?.error ? "red" : query.alert?.enabled && query.state?.firing ? "yellow" : "dim";
1454
+ console.log(`${styleText("bold", query.name)} ${query.query}${levels} ${styleText(color, alertSummary(query))}`);
1455
+ }
1456
+ if (args.app) console.log(styleText("dim", `\nRun one: wervt logs ${args.app} --saved '${list[0].name}'`));
1457
+ }
1458
+ });
1090
1459
  const SPARKS = "▁▂▃▄▅▆▇█";
1091
1460
  const stats = defineCommand({
1092
1461
  meta: {
@@ -1311,15 +1680,18 @@ runMain(defineCommand({
1311
1680
  releases,
1312
1681
  audit,
1313
1682
  logs,
1683
+ queries,
1314
1684
  activate,
1315
1685
  rollback,
1316
1686
  reprovision,
1317
1687
  access,
1688
+ domains,
1318
1689
  members,
1319
1690
  accounts,
1320
1691
  env,
1321
1692
  tokens,
1322
1693
  token,
1694
+ clients,
1323
1695
  identity,
1324
1696
  tasks,
1325
1697
  stats,
@@ -1329,5 +1701,4 @@ runMain(defineCommand({
1329
1701
  skill
1330
1702
  }
1331
1703
  }));
1332
- //#endregion
1333
1704
  export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wervt/cli",
3
- "version": "0.2.15",
3
+ "version": "0.2.17",
4
4
  "description": "Deploy and manage apps on wervt.",
5
5
  "keywords": [
6
6
  "agents",
@@ -31,7 +31,7 @@
31
31
  "@clack/prompts": "^1.8.1",
32
32
  "citty": "^0.2.2",
33
33
  "modern-tar": "^0.8.6",
34
- "nypm": "^0.6.10",
34
+ "nypm": "^0.6.12",
35
35
  "tinyexec": "^1.3.1"
36
36
  },
37
37
  "scripts": {
package/skill/SKILL.md CHANGED
@@ -42,6 +42,8 @@ existing app, work in its repository instead.
42
42
 
43
43
  ## 3. Build
44
44
 
45
+ <!-- build.wervt.app: from here to the end marker, also the MCP server's instructions -->
46
+
45
47
  - Keep the app small and complete: data in Postgres through `useDb()`, writes through API routes,
46
48
  lists through `useShape` so every open tab stays live. Ask only when the request is genuinely
47
49
  ambiguous; otherwise choose sensible defaults and say what you chose.
@@ -54,10 +56,13 @@ existing app, work in its repository instead.
54
56
  MCP clients at `https://<app>.wervt.app/mcp`, signed in through wervt. Details in the app's
55
57
  `AGENTS.md`.
56
58
  - After changing `server/db/schema.ts`: `pnpm db:generate`, and commit the migration.
59
+ - Finish with `pnpm exec vp check --fix` passing.
60
+
61
+ <!-- end -->
62
+
57
63
  - `pnpm dev` runs against a local wervt stack, which the user may not have. When it isn't running,
58
64
  don't set one up: `pnpm build` plus `pnpm exec vp check --fix` are the checks, and the
59
65
  deployed app is the test.
60
- - Finish with `pnpm exec vp check --fix` passing.
61
66
 
62
67
  ## 4. Ship
63
68
 
@@ -69,8 +74,12 @@ pnpm exec wervt access <name> --invite # private, and people with acc
69
74
  pnpm exec wervt access <name> --invite-by owners # only owner, members and admins may invite
70
75
  pnpm exec wervt members <name> --add a@x.com # who may deploy and manage it (the first deploy owns it)
71
76
  pnpm exec wervt env <name> --set NUXT_API_KEY=… # runtimeConfig.apiKey, applied from the next request
77
+ pnpm exec wervt domains <name> --add example.com # its own domain; prints the DNS records to set
78
+ pnpm exec wervt deploy --name <other> # the same code as another app (own data, env, domains)
72
79
  ```
73
80
 
81
+ <!-- build.wervt.app -->
82
+
74
83
  - Ask before making an app public if it shows personal or private data. "Ship it" alone doesn't
75
84
  decide that. Private, with the user's email allowed, is the safe default.
76
85
  - App configuration and secrets go through Nuxt's `runtimeConfig`, never `process.env` in app
@@ -78,6 +87,9 @@ pnpm exec wervt env <name> --set NUXT_API_KEY=… # runtimeConfig.apiKey, appl
78
87
  it with `useRuntimeConfig()` and set it as `NUXT_` + the key in SCREAMING_SNAKE_CASE
79
88
  (`apiKey` → `NUXT_API_KEY`, `public.mapStyle` → `NUXT_PUBLIC_MAP_STYLE`). Keys missing from
80
89
  `runtimeConfig` are ignored.
90
+
91
+ <!-- end -->
92
+
81
93
  - Secrets for `wervt env` (API keys) come from the user. Ask them to run the command themselves
82
94
  (`! pnpm exec wervt env <name> --set NUXT_API_KEY=…`) instead of pasting the value to you.
83
95
  - `wervt deploy` requests `/` once the release started and fails (exit 1, with the first logged
@@ -122,6 +134,8 @@ pnpm exec wervt apps # every app, active release, URL
122
134
  pnpm exec wervt releases <name> # newest first, with commit, author, failed deploys
123
135
  pnpm exec wervt audit <name> # who deployed, rolled back, changed env or access
124
136
  pnpm exec wervt logs <name> 'status:>=500' --since 1d --json
137
+ pnpm exec wervt logs <name> --requests --since 1h # one line per request
138
+ pnpm exec wervt queries <name> # saved queries and their alerts
125
139
  pnpm exec wervt tasks <name> # tasks, schedules, next runs
126
140
  pnpm exec wervt tasks <name> --run <task> # run one now
127
141
  pnpm exec wervt rollback <name> # the previous release (migrations stay)