@wervt/cli 0.2.17 → 0.2.19

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/README.md CHANGED
@@ -15,6 +15,10 @@ vp exec wervt access my-app --public # or --add friend@example.com
15
15
  vp exec wervt logs my-app
16
16
  ```
17
17
 
18
+ Nitro apps without Nuxt use `@wervt/nitro`. A static site sets `"build": "… && wervt build --static dist"`
19
+ in package.json (any `export default { fetch }` module works too, without `--static`); see
20
+ [Platform contract](https://wervt.app/api/platform-contract#wervt-build).
21
+
18
22
  Without [Vite+](https://viteplus.dev), use `npx @wervt/cli` and `pnpm exec wervt`. `wervt --help`
19
23
  lists every command: `apps`, `releases`, `audit`, `rollback`, `activate`, `env`, `tokens`, `token`, `members`,
20
24
  `tasks`, `stats`, `delete`, `whoami`, … Docs: [wervt.app](https://wervt.app/getting-started/quickstart).
package/dist/index.mjs CHANGED
@@ -1,13 +1,160 @@
1
+ import { builtinModules } from "node:module";
1
2
  import { existsSync } from "node:fs";
2
- import { appendFile, mkdir, readFile, readdir, stat, writeFile } from "node:fs/promises";
3
+ import { appendFile, cp, mkdir, readFile, readdir, rm, stat, writeFile } from "node:fs/promises";
3
4
  import { homedir, hostname } from "node:os";
4
- import { dirname, join, relative, resolve } from "node:path";
5
+ import { dirname, join, relative, resolve, sep } from "node:path";
5
6
  import { styleText } from "node:util";
6
7
  import * as p from "@clack/prompts";
7
8
  import { defineCommand, runMain } from "citty";
8
9
  import { detectPackageManager } from "nypm";
9
10
  import { x } from "tinyexec";
10
11
  import { packTar } from "modern-tar";
12
+ // ../core/src/manifest.ts
13
+ /** The bundle's `wervt-bundle.json`, next to `server/` and `public/`. */
14
+ const BUNDLE_MANIFEST = "wervt-bundle.json";
15
+ // src/static-server.ts
16
+ /** The source of `server/index.mjs`. */
17
+ function staticServer({ pages, fallback }) {
18
+ return `const pages = ${JSON.stringify(pages)}
19
+ const fallback = ${JSON.stringify(fallback ?? null)}
20
+ const html = (body, status = 200) =>
21
+ new Response(body, { status, headers: { 'content-type': 'text/html; charset=utf-8' } })
22
+
23
+ export default {
24
+ fetch(request) {
25
+ let path
26
+ try {
27
+ path = decodeURIComponent(new URL(request.url).pathname).replace(/^\\/+|\\/+$/g, '')
28
+ } catch {
29
+ return html(pages['404.html'] ?? 'Not found', 404)
30
+ }
31
+ for (const page of path ? [path + '/index.html', path + '.html'] : ['index.html'])
32
+ if (Object.hasOwn(pages, page)) return html(pages[page])
33
+ const wantsHtml = (request.headers.get('accept') ?? '').includes('text/html')
34
+ if (wantsHtml && fallback && Object.hasOwn(pages, fallback)) return html(pages[fallback])
35
+ return html(pages['404.html'] ?? 'Not found', 404)
36
+ },
37
+ }
38
+ `;
39
+ }
40
+ // src/build.ts
41
+ const ENTRIES = [
42
+ "server.ts",
43
+ "src/server.ts",
44
+ "src/index.ts",
45
+ "server.js",
46
+ "src/index.js"
47
+ ];
48
+ const exists$1 = (path) => stat(path).then(() => true, () => false);
49
+ async function walk$1(dir) {
50
+ const entries = await readdir(dir, { withFileTypes: true });
51
+ return (await Promise.all(entries.map(async (e) => e.isDirectory() ? walk$1(join(dir, e.name)) : [join(dir, e.name)]))).flat();
52
+ }
53
+ /** The app's `wervt` settings from its package.json. */
54
+ async function readBuildConfig(appDir) {
55
+ return JSON.parse(await readFile(join(appDir, "package.json"), "utf8").catch(() => "{}")).wervt ?? {};
56
+ }
57
+ async function build$1(appDir, options = {}) {
58
+ const config = {
59
+ ...await readBuildConfig(appDir),
60
+ ...definedOnly(options)
61
+ };
62
+ const out = join(appDir, options.out ?? ".output");
63
+ await rm(out, {
64
+ recursive: true,
65
+ force: true
66
+ });
67
+ await mkdir(join(out, "server"), { recursive: true });
68
+ let kind;
69
+ if (config.static) {
70
+ kind = "static";
71
+ await buildStatic(join(appDir, config.static), out, config.fallback);
72
+ } else {
73
+ kind = "server";
74
+ const entry = config.entry ?? await findEntry(appDir);
75
+ if (!entry) throw new Error(`No server entry. Add one of ${ENTRIES.join(", ")}, or set "wervt": { "entry" } in package.json (or "static" for a static site).`);
76
+ await bundleServer(join(appDir, entry), join(out, "server/index.mjs"));
77
+ const publicDir = join(appDir, config.public ?? "public");
78
+ if (await exists$1(publicDir)) await cp(publicDir, join(out, "public"), { recursive: true });
79
+ }
80
+ const manifest = bundleManifest(config, kind);
81
+ await writeFile(join(out, BUNDLE_MANIFEST), `${JSON.stringify(manifest, null, 2)}\n`);
82
+ return {
83
+ kind,
84
+ out,
85
+ manifest
86
+ };
87
+ }
88
+ async function findEntry(appDir) {
89
+ for (const entry of ENTRIES) if (await exists$1(join(appDir, entry))) return entry;
90
+ }
91
+ function bundleManifest(config, kind) {
92
+ const manifest = {
93
+ version: 2,
94
+ framework: config.framework ?? (kind === "static" ? "static" : "fetch")
95
+ };
96
+ if (config.database !== void 0) manifest.database = config.database;
97
+ if (config.immutable || config.noCache) manifest.public = {
98
+ immutable: config.immutable,
99
+ noCache: config.noCache
100
+ };
101
+ if (config.tasks || config.schedules) manifest.tasks = {
102
+ tasks: config.tasks ?? [],
103
+ schedules: config.schedules ?? [],
104
+ ...config.timezone ? { timezone: config.timezone } : {}
105
+ };
106
+ if (config.shapes) manifest.shapes = config.shapes;
107
+ if (config.preview) manifest.preview = config.preview;
108
+ if (config.brand) manifest.brand = config.brand;
109
+ return manifest;
110
+ }
111
+ async function buildStatic(siteDir, out, fallback = "index.html") {
112
+ if (!await exists$1(siteDir)) throw new Error(`No ${siteDir} to deploy. Build the site first.`);
113
+ await cp(siteDir, join(out, "public"), { recursive: true });
114
+ const pages = {};
115
+ for (const file of await walk$1(siteDir)) if (file.endsWith(".html")) pages[relative(siteDir, file).split(sep).join("/")] = await readFile(file, "utf8");
116
+ await writeFile(join(out, "server/index.mjs"), staticServer({
117
+ pages,
118
+ fallback: fallback || void 0
119
+ }));
120
+ }
121
+ const builtins = new Set(builtinModules);
122
+ /**
123
+ * One ES module for Supabase Edge Runtime (Deno): every dependency inlined, Node built-ins
124
+ * imported with `node:` (Deno rejects bare `crypto`), and the Node globals Deno lacks injected.
125
+ */
126
+ async function bundleServer(entry, file) {
127
+ const { build: rolldown } = await import("rolldown");
128
+ await rolldown({
129
+ input: entry,
130
+ platform: "node",
131
+ logLevel: "warn",
132
+ plugins: [{
133
+ name: "wervt:node-prefix",
134
+ resolveId(id) {
135
+ const bare = id.startsWith("node:") ? id.slice(5) : id;
136
+ if (builtins.has(bare) || builtins.has(bare.split("/")[0])) return {
137
+ id: `node:${bare}`,
138
+ external: true
139
+ };
140
+ return null;
141
+ }
142
+ }],
143
+ transform: { inject: {
144
+ Buffer: ["node:buffer", "Buffer"],
145
+ setImmediate: ["node:timers", "setImmediate"],
146
+ clearImmediate: ["node:timers", "clearImmediate"]
147
+ } },
148
+ output: {
149
+ file,
150
+ format: "esm",
151
+ codeSplitting: false
152
+ }
153
+ });
154
+ }
155
+ function definedOnly(value) {
156
+ return Object.fromEntries(Object.entries(value).filter(([, v]) => v !== void 0));
157
+ }
11
158
  // src/bundle.ts
12
159
  /** Files under `dir`. Follows symlinks: Nitro can leave traced dependencies in
13
160
  * server/node_modules as links into the local pnpm store, which won't exist on the server. */
@@ -23,14 +170,18 @@ async function gzip(data) {
23
170
  return new Uint8Array(await new Response(stream).arrayBuffer());
24
171
  }
25
172
  const exists = (path) => stat(path).then(() => true, () => false);
26
- /** Packs `.output/server`, `.output/public` and `drizzle/` into the bundle the control plane expects. */
27
- async function createBundle(appDir) {
173
+ /**
174
+ * Packs the build output's `server/`, `public/` and `wervt-bundle.json` (in `out`, `.output` by
175
+ * default) and the app's `drizzle/` into the bundle the control plane expects.
176
+ */
177
+ async function createBundle(appDir, out = ".output") {
178
+ const outDir = join(appDir, out);
28
179
  const roots = [
29
- [join(appDir, ".output/server"), "server"],
30
- [join(appDir, ".output/public"), "public"],
180
+ [join(outDir, "server"), "server"],
181
+ [join(outDir, "public"), "public"],
31
182
  [join(appDir, "drizzle"), "drizzle"]
32
183
  ];
33
- if (!await exists(roots[0][0])) throw new Error("No .output/server. Build the app first.");
184
+ if (!await exists(roots[0][0])) throw new Error(`No ${join(out, "server")}. Build the app first.`);
34
185
  const files = [];
35
186
  for (const [dir, prefix] of roots) {
36
187
  if (!await exists(dir)) continue;
@@ -46,6 +197,17 @@ async function createBundle(appDir) {
46
197
  });
47
198
  }
48
199
  }
200
+ const manifest = join(outDir, BUNDLE_MANIFEST);
201
+ if (await exists(manifest)) {
202
+ const data = await readFile(manifest);
203
+ files.push({
204
+ header: {
205
+ name: BUNDLE_MANIFEST,
206
+ size: data.byteLength
207
+ },
208
+ body: data
209
+ });
210
+ }
49
211
  return {
50
212
  data: await gzip(await packTar(files)),
51
213
  files: files.length
@@ -258,6 +420,13 @@ function resolveAppDir(arg = ".") {
258
420
  if (existsSync(join("apps", arg, "package.json"))) return resolve("apps", arg);
259
421
  throw new Error(`No app found at ${arg}`);
260
422
  }
423
+ const PREVIEW_RE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
424
+ /** `todos--pr-12` for `--preview pr-12`: the preview's own app name (control's previews.ts). */
425
+ function previewName(app, preview) {
426
+ if (preview === void 0) return app;
427
+ if (!PREVIEW_RE.test(preview)) throw new Error(`--preview must be lowercase letters, digits and dashes, like pr-12`);
428
+ return `${app}--${preview}`;
429
+ }
261
430
  async function appName(dir, name) {
262
431
  if (!name) {
263
432
  const pkg = JSON.parse(await readFile(join(dir, "package.json"), "utf8"));
@@ -282,11 +451,19 @@ const deploy = defineCommand({
282
451
  type: "string",
283
452
  description: "App name (default: package.json name)"
284
453
  },
454
+ preview: {
455
+ type: "string",
456
+ description: "Deploy a preview of the app instead, at <app>--<preview> (like pr-12): own empty database, the app's access and env plus its preview env"
457
+ },
285
458
  build: {
286
459
  type: "boolean",
287
460
  default: true,
288
461
  description: "Build before deploying"
289
462
  },
463
+ out: {
464
+ type: "string",
465
+ description: "Build output to pack (default: .output)"
466
+ },
290
467
  check: {
291
468
  type: "string",
292
469
  description: "Pages to request once the release started, a 5xx fails (default: /). Shape routes are checked too; --check= turns all checks off"
@@ -300,7 +477,7 @@ const deploy = defineCommand({
300
477
  },
301
478
  async run({ args }) {
302
479
  const dir = resolveAppDir(args.dir);
303
- const name = await appName(dir, args.name);
480
+ const name = previewName(await appName(dir, args.name), args.preview);
304
481
  p.intro(`wervt deploy ${name}`);
305
482
  if (args.build) {
306
483
  const pm = (await detectPackageManager(dir))?.name ?? "npm";
@@ -319,7 +496,7 @@ const deploy = defineCommand({
319
496
  }
320
497
  const spinner = progress();
321
498
  spinner.start("Packing");
322
- const bundle = await createBundle(dir);
499
+ const bundle = await createBundle(dir, args.out);
323
500
  spinner.stop(`Packed ${bundle.files} files (${(bundle.data.byteLength / 1024).toFixed(0)} kB)`);
324
501
  spinner.start("Uploading");
325
502
  const source = await deploySource(dir);
@@ -746,6 +923,10 @@ const env = defineCommand({
746
923
  type: "string",
747
924
  description: "KEY (comma separated)"
748
925
  },
926
+ preview: {
927
+ type: "boolean",
928
+ description: "The app's preview env instead: set on top of its env in every preview, from their next deploy"
929
+ },
749
930
  ...connection
750
931
  },
751
932
  async run({ args, rawArgs }) {
@@ -756,17 +937,18 @@ const env = defineCommand({
756
937
  return [pair.slice(0, i), pair.slice(i + 1)];
757
938
  }));
758
939
  const unset = toList(args.unset);
759
- const { keys, warnings = [] } = await (Object.keys(set).length || unset.length ? await api(`/v1/apps/${args.app}/env`, args, {
940
+ const path = `/v1/apps/${args.app}/${args.preview ? "preview-env" : "env"}`;
941
+ const { keys, warnings = [] } = await (Object.keys(set).length || unset.length ? await api(path, args, {
760
942
  method: "PATCH",
761
943
  headers: { "content-type": "application/json" },
762
944
  body: JSON.stringify({
763
945
  set,
764
946
  unset
765
947
  })
766
- }) : await api(`/v1/apps/${args.app}/env`, args)).json();
948
+ }) : await api(path, args)).json();
767
949
  console.log(keys.length ? keys.join("\n") : "No variables set.");
768
950
  for (const warning of warnings) console.error(styleText("yellow", `Warning: ${warning}`));
769
- if (Object.keys(set).length || unset.length) console.log("Applies from the next request.");
951
+ if (Object.keys(set).length || unset.length) console.log(args.preview ? "Previews get it with their next deploy." : "Applies from the next request.");
770
952
  }
771
953
  });
772
954
  const SIZE_FLAGS = [
@@ -908,7 +1090,7 @@ const tokens = defineCommand({
908
1090
  })).json();
909
1091
  console.error(`Deploy token "${token.name}" for ${args.app} (shown once):`);
910
1092
  console.log(token.token);
911
- console.error(styleText("dim", "It can deploy this app and nothing else. Store it as the WERVT_TOKEN secret."));
1093
+ console.error(styleText("dim", "It can deploy this app and its previews, nothing else. Store it as the WERVT_TOKEN secret."));
912
1094
  return;
913
1095
  }
914
1096
  if (args.revoke) {
@@ -1211,13 +1393,17 @@ const tasks = defineCommand({
1211
1393
  const remove = defineCommand({
1212
1394
  meta: {
1213
1395
  name: "delete",
1214
- description: "Delete an app (releases, access, env)"
1396
+ description: "Delete an app (releases, access, env) or one of its previews"
1215
1397
  },
1216
1398
  args: {
1217
1399
  app: {
1218
1400
  type: "positional",
1219
- required: true,
1220
- description: "App name"
1401
+ required: false,
1402
+ description: "App name (default: package.json name in the current directory)"
1403
+ },
1404
+ preview: {
1405
+ type: "string",
1406
+ description: "Delete the preview <app>--<preview> (like pr-12) and its data instead"
1221
1407
  },
1222
1408
  "drop-database": {
1223
1409
  type: "boolean",
@@ -1230,16 +1416,18 @@ const remove = defineCommand({
1230
1416
  ...connection
1231
1417
  },
1232
1418
  async run({ args }) {
1233
- const dropDb = Boolean(args["drop-database"]);
1419
+ const app = previewName(args.app ?? await appName(resolveAppDir()), args.preview);
1420
+ const dropDb = Boolean(args["drop-database"]) || app.includes("--");
1234
1421
  if (!args.yes) {
1235
1422
  const ok = await p.confirm({
1236
- message: `Delete ${args.app}${dropDb ? " and ALL its data" : " (data is kept, the name stays yours)"}?`,
1423
+ message: `Delete ${app}${dropDb ? " and ALL its data" : " (data is kept, the name stays yours)"}${app.includes("--") ? "" : ", and all its previews"}?`,
1237
1424
  initialValue: false
1238
1425
  });
1239
1426
  if (p.isCancel(ok) || !ok) return p.cancel("Cancelled");
1240
1427
  }
1241
- await api(`/v1/apps/${args.app}?dropDatabase=${dropDb}`, args, { method: "DELETE" });
1242
- console.log(`Deleted ${args.app}${dropDb ? " and its database" : ""}`);
1428
+ const { previews = [] } = await (await api(`/v1/apps/${app}?dropDatabase=${dropDb}`, args, { method: "DELETE" })).json();
1429
+ console.log(`Deleted ${app}${dropDb ? " and its database" : ""}`);
1430
+ if (previews.length) console.log(`Also deleted its previews: ${previews.join(", ")}`);
1243
1431
  }
1244
1432
  });
1245
1433
  const LEVEL_COLORS = {
@@ -1669,6 +1857,47 @@ const login = defineCommand({
1669
1857
  outro(`Signed in to ${url} (saved to ${file})`);
1670
1858
  }
1671
1859
  });
1860
+ const build = defineCommand({
1861
+ meta: {
1862
+ name: "build",
1863
+ description: "Build a fetch handler (Hono, plain `export default { fetch }`) or a static site into a bundle"
1864
+ },
1865
+ args: {
1866
+ dir: {
1867
+ type: "positional",
1868
+ required: false,
1869
+ description: "App directory (default: .)"
1870
+ },
1871
+ entry: {
1872
+ type: "string",
1873
+ description: "Server module (default: server.ts, src/server.ts, src/index.ts)"
1874
+ },
1875
+ static: {
1876
+ type: "string",
1877
+ description: "Folder of a static site to deploy instead of a server, e.g. dist"
1878
+ },
1879
+ fallback: {
1880
+ type: "string",
1881
+ description: "Static site: page for unknown paths (default: index.html, for SPAs); --fallback= for none"
1882
+ },
1883
+ out: {
1884
+ type: "string",
1885
+ description: "Output directory (default: .output)"
1886
+ }
1887
+ },
1888
+ async run({ args }) {
1889
+ const dir = resolveAppDir(args.dir);
1890
+ const started = performance.now();
1891
+ const result = await build$1(dir, {
1892
+ entry: args.entry,
1893
+ static: args.static,
1894
+ fallback: args.fallback === "" ? false : args.fallback,
1895
+ out: args.out
1896
+ });
1897
+ const ms = Math.round(performance.now() - started);
1898
+ console.log(`Built ${result.kind === "static" ? "a static site" : "the server"} into ${result.out} in ${ms} ms (${result.manifest.framework})`);
1899
+ }
1900
+ });
1672
1901
  runMain(defineCommand({
1673
1902
  meta: {
1674
1903
  name: "wervt",
@@ -1676,6 +1905,7 @@ runMain(defineCommand({
1676
1905
  },
1677
1906
  subCommands: {
1678
1907
  deploy,
1908
+ build,
1679
1909
  apps,
1680
1910
  releases,
1681
1911
  audit,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wervt/cli",
3
- "version": "0.2.17",
3
+ "version": "0.2.19",
4
4
  "description": "Deploy and manage apps on wervt.",
5
5
  "keywords": [
6
6
  "agents",
@@ -32,8 +32,12 @@
32
32
  "citty": "^0.2.2",
33
33
  "modern-tar": "^0.8.6",
34
34
  "nypm": "^0.6.12",
35
+ "rolldown": "^1.2.16",
35
36
  "tinyexec": "^1.3.1"
36
37
  },
38
+ "devDependencies": {
39
+ "@wervt/core": "0.0.2"
40
+ },
37
41
  "scripts": {
38
42
  "build": "vp pack"
39
43
  }
package/skill/SKILL.md CHANGED
@@ -123,7 +123,9 @@ git push -u origin main
123
123
  ```
124
124
 
125
125
  The pipe hands the deploy token straight to GitHub; it never appears in the output. A deploy token
126
- can only deploy that one app. Set the secret and variable **before** the first push, or the first
126
+ can only deploy that one app and its previews: `.github/workflows/preview.yml` deploys every pull
127
+ request to `<name>--pr-<number>` (own empty database, the app's access and env plus its preview
128
+ env, `wervt env <name> --preview --set …`) and deletes it when the pull request closes. Set the secret and variable **before** the first push, or the first
127
129
  workflow run fails. Then `gh run watch` and check `pnpm exec wervt releases <name>`: the new
128
130
  release lists the commit.
129
131