@wervt/cli 0.2.16 → 0.2.18
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 +4 -0
- package/dist/index.mjs +476 -10
- package/package.json +5 -1
- package/skill/SKILL.md +15 -1
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
|
-
/**
|
|
27
|
-
|
|
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(
|
|
30
|
-
[join(
|
|
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(
|
|
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
|
|
@@ -125,9 +287,40 @@ function auditSummary(entry) {
|
|
|
125
287
|
case "client.link":
|
|
126
288
|
case "client.delete": return text(d.name) || text(d.clientId);
|
|
127
289
|
case "app.delete": return `database ${text(d.database)}`;
|
|
290
|
+
case "domains.add":
|
|
291
|
+
case "domains.remove":
|
|
292
|
+
case "domains.primary": return text(d.domain);
|
|
128
293
|
default: return entry.account ?? "";
|
|
129
294
|
}
|
|
130
295
|
}
|
|
296
|
+
/** A saved query's alert in a few words: `alert at 5 in 5m, firing (7)`. */
|
|
297
|
+
function alertSummary(query) {
|
|
298
|
+
const { alert, state } = query;
|
|
299
|
+
if (!alert) return "no alert";
|
|
300
|
+
const rule = `alert at ${alert.threshold} in ${alert.window}`;
|
|
301
|
+
if (!alert.enabled) return `${rule}, off`;
|
|
302
|
+
if (state?.error) return `${rule}, failing: ${state.error.split("\n")[0]}`;
|
|
303
|
+
if (state?.firing) return `${rule}, firing (${state.lastCount ?? "?"})`;
|
|
304
|
+
return state?.lastFiredAt ? `${rule}, last fired ${state.lastFiredAt.slice(0, 16)}` : rule;
|
|
305
|
+
}
|
|
306
|
+
/** `GET /api/x 500 12ms 3 entries, 1 error` for a request. */
|
|
307
|
+
function requestSummary(group) {
|
|
308
|
+
const what = group.method && group.path ? [
|
|
309
|
+
group.method,
|
|
310
|
+
group.path,
|
|
311
|
+
group.status
|
|
312
|
+
].filter(Boolean).join(" ") : group.message ?? "(no request event)";
|
|
313
|
+
const counts = [
|
|
314
|
+
`${group.entries} ${group.entries === 1 ? "entry" : "entries"}`,
|
|
315
|
+
group.errors && `${group.errors} ${group.errors === 1 ? "error" : "errors"}`,
|
|
316
|
+
group.warnings && `${group.warnings} ${group.warnings === 1 ? "warning" : "warnings"}`
|
|
317
|
+
].filter(Boolean);
|
|
318
|
+
return [
|
|
319
|
+
what,
|
|
320
|
+
group.duration,
|
|
321
|
+
counts.join(", ")
|
|
322
|
+
].filter(Boolean).join(" ");
|
|
323
|
+
}
|
|
131
324
|
// src/source.ts
|
|
132
325
|
async function git(dir, ...args) {
|
|
133
326
|
try {
|
|
@@ -256,6 +449,10 @@ const deploy = defineCommand({
|
|
|
256
449
|
default: true,
|
|
257
450
|
description: "Build before deploying"
|
|
258
451
|
},
|
|
452
|
+
out: {
|
|
453
|
+
type: "string",
|
|
454
|
+
description: "Build output to pack (default: .output)"
|
|
455
|
+
},
|
|
259
456
|
check: {
|
|
260
457
|
type: "string",
|
|
261
458
|
description: "Pages to request once the release started, a 5xx fails (default: /). Shape routes are checked too; --check= turns all checks off"
|
|
@@ -277,14 +474,18 @@ const deploy = defineCommand({
|
|
|
277
474
|
if ((await x(pm, ["run", "build"], {
|
|
278
475
|
nodeOptions: {
|
|
279
476
|
cwd: dir,
|
|
280
|
-
stdio: "inherit"
|
|
477
|
+
stdio: "inherit",
|
|
478
|
+
env: {
|
|
479
|
+
...process.env,
|
|
480
|
+
WERVT_APP: name
|
|
481
|
+
}
|
|
281
482
|
},
|
|
282
483
|
throwOnError: false
|
|
283
484
|
})).exitCode !== 0) throw new Error("Build failed");
|
|
284
485
|
}
|
|
285
486
|
const spinner = progress();
|
|
286
487
|
spinner.start("Packing");
|
|
287
|
-
const bundle = await createBundle(dir);
|
|
488
|
+
const bundle = await createBundle(dir, args.out);
|
|
288
489
|
spinner.stop(`Packed ${bundle.files} files (${(bundle.data.byteLength / 1024).toFixed(0)} kB)`);
|
|
289
490
|
spinner.start("Uploading");
|
|
290
491
|
const source = await deploySource(dir);
|
|
@@ -596,6 +797,63 @@ const access = defineCommand({
|
|
|
596
797
|
}
|
|
597
798
|
}
|
|
598
799
|
});
|
|
800
|
+
const domains = defineCommand({
|
|
801
|
+
meta: {
|
|
802
|
+
name: "domains",
|
|
803
|
+
description: "Show or change an app's own domains (e.g. example.com); <app>.<base domain> then forwards to the primary one"
|
|
804
|
+
},
|
|
805
|
+
args: {
|
|
806
|
+
app: {
|
|
807
|
+
type: "positional",
|
|
808
|
+
required: true,
|
|
809
|
+
description: "App name"
|
|
810
|
+
},
|
|
811
|
+
add: {
|
|
812
|
+
type: "string",
|
|
813
|
+
description: "Add a domain; it serves the app once its DNS points here"
|
|
814
|
+
},
|
|
815
|
+
remove: {
|
|
816
|
+
type: "string",
|
|
817
|
+
description: "Remove a domain"
|
|
818
|
+
},
|
|
819
|
+
primary: {
|
|
820
|
+
type: "string",
|
|
821
|
+
description: "Make a verified domain the app's main address"
|
|
822
|
+
},
|
|
823
|
+
verify: {
|
|
824
|
+
type: "string",
|
|
825
|
+
description: "Check a pending domain's DNS now"
|
|
826
|
+
},
|
|
827
|
+
...connection
|
|
828
|
+
},
|
|
829
|
+
async run({ args }) {
|
|
830
|
+
const changes = [
|
|
831
|
+
"add",
|
|
832
|
+
"remove",
|
|
833
|
+
"primary",
|
|
834
|
+
"verify"
|
|
835
|
+
].filter((key) => args[key]);
|
|
836
|
+
if (changes.length > 1) throw new Error("Pass one of --add, --remove, --primary or --verify");
|
|
837
|
+
const change = changes[0];
|
|
838
|
+
const result = await (change ? await api(`/v1/apps/${args.app}/domains`, args, {
|
|
839
|
+
method: "PATCH",
|
|
840
|
+
headers: { "content-type": "application/json" },
|
|
841
|
+
body: JSON.stringify({ [change]: args[change] })
|
|
842
|
+
}) : await api(`/v1/apps/${args.app}/domains`, args)).json();
|
|
843
|
+
if (!result.configured) console.log("This server doesn't support custom domains.");
|
|
844
|
+
if (!result.domains.length) return console.log(`${args.app} has no domains of its own.`);
|
|
845
|
+
for (const d of result.domains) {
|
|
846
|
+
const label = d.status === "active" ? styleText("green", d.primary ? "active, primary" : "active") : styleText("yellow", "pending");
|
|
847
|
+
console.log(`${d.domain} ${label}${d.error ? ` ${styleText("dim", d.error)}` : ""}`);
|
|
848
|
+
}
|
|
849
|
+
const pending = result.domains.filter((d) => d.status === "pending");
|
|
850
|
+
if (!pending.length || !result.target) return;
|
|
851
|
+
console.log(`\nPoint each pending domain at ${result.target}: a CNAME (subdomains), or an A record with its addresses (a bare domain).`);
|
|
852
|
+
console.log("Behind a proxy of your own (Cloudflare), add a TXT record instead, the proxy has to send the traffic on:");
|
|
853
|
+
for (const d of pending) console.log(` ${d.txt.name} TXT ${d.txt.value}`);
|
|
854
|
+
console.log("Pending domains are checked every few minutes, or now with --verify <domain>.");
|
|
855
|
+
}
|
|
856
|
+
});
|
|
599
857
|
const members = defineCommand({
|
|
600
858
|
meta: {
|
|
601
859
|
name: "members",
|
|
@@ -728,9 +986,36 @@ const accounts = defineCommand({
|
|
|
728
986
|
type: "string",
|
|
729
987
|
description: "Space all releases of their apps may take, e.g. 2GB. \"none\", \"default\""
|
|
730
988
|
},
|
|
989
|
+
into: {
|
|
990
|
+
type: "string",
|
|
991
|
+
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"
|
|
992
|
+
},
|
|
731
993
|
...connection
|
|
732
994
|
},
|
|
733
995
|
async run({ args }) {
|
|
996
|
+
if (args.email && args.into) {
|
|
997
|
+
const result = await (await api("/v1/accounts/merge", args, {
|
|
998
|
+
method: "POST",
|
|
999
|
+
headers: { "content-type": "application/json" },
|
|
1000
|
+
body: JSON.stringify({
|
|
1001
|
+
from: args.email,
|
|
1002
|
+
into: args.into
|
|
1003
|
+
})
|
|
1004
|
+
})).json();
|
|
1005
|
+
console.log(`${result.from.email} merged into ${result.into.email}`);
|
|
1006
|
+
console.log(` logins moved: ${result.logins.join(", ") || "none"}`);
|
|
1007
|
+
if (result.owners.length) console.log(` owner or member of: ${result.owners.join(", ")}`);
|
|
1008
|
+
if (result.access.length) console.log(` allowed in: ${result.access.join(", ")}`);
|
|
1009
|
+
const payload = JSON.stringify({
|
|
1010
|
+
from: result.from,
|
|
1011
|
+
into: result.into
|
|
1012
|
+
});
|
|
1013
|
+
for (const [app, run] of Object.entries(result.apps)) {
|
|
1014
|
+
console.log(` ${app}: ${run.status}${run.error ? ` (${run.error})` : ""}`);
|
|
1015
|
+
if (run.status !== "ok") console.log(styleText("dim", ` retry: wervt tasks ${app} --run wervt:account-merged --payload '${payload}'`));
|
|
1016
|
+
}
|
|
1017
|
+
return;
|
|
1018
|
+
}
|
|
734
1019
|
if (args.email) {
|
|
735
1020
|
const change = {};
|
|
736
1021
|
if (args.apps) change.apps = args.apps === "none" ? null : args.apps === "default" ? "default" : Number(args.apps);
|
|
@@ -1172,6 +1457,14 @@ const logs = defineCommand({
|
|
|
1172
1457
|
alias: "f",
|
|
1173
1458
|
description: "Keep printing new entries"
|
|
1174
1459
|
},
|
|
1460
|
+
requests: {
|
|
1461
|
+
type: "boolean",
|
|
1462
|
+
description: "One line per request (requestId): what it was and how much it logged"
|
|
1463
|
+
},
|
|
1464
|
+
saved: {
|
|
1465
|
+
type: "string",
|
|
1466
|
+
description: "Use a saved query by name (see `wervt queries`)"
|
|
1467
|
+
},
|
|
1175
1468
|
json: {
|
|
1176
1469
|
type: "boolean",
|
|
1177
1470
|
description: "One JSON object per line, with every field"
|
|
@@ -1180,7 +1473,31 @@ const logs = defineCommand({
|
|
|
1180
1473
|
},
|
|
1181
1474
|
async run({ args }) {
|
|
1182
1475
|
const params = new URLSearchParams();
|
|
1183
|
-
if (args.
|
|
1476
|
+
if (args.saved) {
|
|
1477
|
+
if (args.query) throw new Error("Give either a query or --saved, not both");
|
|
1478
|
+
const saved = await findSavedQuery(args.app, args.saved, args);
|
|
1479
|
+
params.set("query", saved.query);
|
|
1480
|
+
if (saved.levels?.length) params.set("levels", saved.levels.join(","));
|
|
1481
|
+
} else if (args.query) params.set("query", args.query);
|
|
1482
|
+
if (args.requests) {
|
|
1483
|
+
if (args.follow) throw new Error("--requests and --follow do not go together");
|
|
1484
|
+
params.set("start", args.since);
|
|
1485
|
+
if (args.until) params.set("end", args.until);
|
|
1486
|
+
params.set("limit", args.limit);
|
|
1487
|
+
const groups = await (await api(`/v1/apps/${args.app}/logs/requests?${params.toString()}`, args)).json();
|
|
1488
|
+
if (!groups.length && !args.json) return console.error(`No requests with a requestId in the last ${args.since}.`);
|
|
1489
|
+
for (const group of groups.reverse()) {
|
|
1490
|
+
if (args.json) {
|
|
1491
|
+
console.log(JSON.stringify(group));
|
|
1492
|
+
continue;
|
|
1493
|
+
}
|
|
1494
|
+
const time = new Date(group.last).toISOString().slice(11, 23);
|
|
1495
|
+
const color = group.errors ? "red" : group.warnings ? "yellow" : "white";
|
|
1496
|
+
console.log(`${styleText("dim", time)} ${styleText(color, requestSummary(group))} ${styleText("dim", group.requestId)}`);
|
|
1497
|
+
}
|
|
1498
|
+
if (!args.json) console.error(styleText("dim", `\nOne request: wervt logs ${args.app} 'requestId:=<id>' --since ${args.since}`));
|
|
1499
|
+
return;
|
|
1500
|
+
}
|
|
1184
1501
|
if (args.follow) {
|
|
1185
1502
|
params.set("since", args.since);
|
|
1186
1503
|
const res = await api(`/v1/apps/${args.app}/logs/tail?${params.toString()}`, args);
|
|
@@ -1200,6 +1517,111 @@ const logs = defineCommand({
|
|
|
1200
1517
|
for (const entry of entries.reverse()) printLog(entry, Boolean(args.json));
|
|
1201
1518
|
}
|
|
1202
1519
|
});
|
|
1520
|
+
/** `/v1/apps/<app>/logs/queries`, or across apps (admins) without an app. */
|
|
1521
|
+
const queriesPath = (app) => app ? `/v1/apps/${app}/logs/queries` : "/v1/logs/queries";
|
|
1522
|
+
async function findSavedQuery(app, name, flags) {
|
|
1523
|
+
const list = await (await api(queriesPath(app), flags)).json();
|
|
1524
|
+
const found = list.find((query) => query.id === name || query.name.toLowerCase() === name.toLowerCase());
|
|
1525
|
+
if (!found) throw new Error(`No saved query "${name}"${list.length ? `. Saved: ${list.map((q) => q.name).join(", ")}` : ""}`);
|
|
1526
|
+
return found;
|
|
1527
|
+
}
|
|
1528
|
+
const queries = defineCommand({
|
|
1529
|
+
meta: {
|
|
1530
|
+
name: "queries",
|
|
1531
|
+
description: "List, save or delete saved log queries and the alerts on them"
|
|
1532
|
+
},
|
|
1533
|
+
args: {
|
|
1534
|
+
app: {
|
|
1535
|
+
type: "positional",
|
|
1536
|
+
required: false,
|
|
1537
|
+
description: "App name (default: queries across all apps, admins only)"
|
|
1538
|
+
},
|
|
1539
|
+
save: {
|
|
1540
|
+
type: "string",
|
|
1541
|
+
description: "Save a query under this name, or change the one that has it"
|
|
1542
|
+
},
|
|
1543
|
+
query: {
|
|
1544
|
+
type: "string",
|
|
1545
|
+
description: "LogsQL for --save, e.g. 'level:error'"
|
|
1546
|
+
},
|
|
1547
|
+
levels: {
|
|
1548
|
+
type: "string",
|
|
1549
|
+
description: "Only these levels for --save, e.g. error,warn"
|
|
1550
|
+
},
|
|
1551
|
+
alert: {
|
|
1552
|
+
type: "string",
|
|
1553
|
+
description: "on or off: mail the app owner and members when the query matches"
|
|
1554
|
+
},
|
|
1555
|
+
threshold: {
|
|
1556
|
+
type: "string",
|
|
1557
|
+
description: "Alert when at least this many entries match"
|
|
1558
|
+
},
|
|
1559
|
+
window: {
|
|
1560
|
+
type: "string",
|
|
1561
|
+
description: "Alert window, e.g. 5m (1m to 24h)"
|
|
1562
|
+
},
|
|
1563
|
+
cooldown: {
|
|
1564
|
+
type: "string",
|
|
1565
|
+
description: "Quiet time after an alert, e.g. 1h (5m to 7d)"
|
|
1566
|
+
},
|
|
1567
|
+
delete: {
|
|
1568
|
+
type: "string",
|
|
1569
|
+
description: "Delete the saved query with this name"
|
|
1570
|
+
},
|
|
1571
|
+
json: {
|
|
1572
|
+
type: "boolean",
|
|
1573
|
+
description: "Print JSON"
|
|
1574
|
+
},
|
|
1575
|
+
...connection
|
|
1576
|
+
},
|
|
1577
|
+
async run({ args }) {
|
|
1578
|
+
const path = queriesPath(args.app);
|
|
1579
|
+
const json = { "content-type": "application/json" };
|
|
1580
|
+
if (args.delete) {
|
|
1581
|
+
const found = await findSavedQuery(args.app, args.delete, args);
|
|
1582
|
+
await api(`${path}/${found.id}`, args, { method: "DELETE" });
|
|
1583
|
+
return console.log(`Deleted ${found.name}`);
|
|
1584
|
+
}
|
|
1585
|
+
if (args.save) {
|
|
1586
|
+
if (args.alert !== void 0 && !["on", "off"].includes(args.alert)) throw new Error("--alert is on or off");
|
|
1587
|
+
const alertFields = {
|
|
1588
|
+
...args.threshold ? { threshold: Number(args.threshold) } : {},
|
|
1589
|
+
...args.window ? { window: args.window } : {},
|
|
1590
|
+
...args.cooldown ? { cooldown: args.cooldown } : {}
|
|
1591
|
+
};
|
|
1592
|
+
const alert = args.alert === "off" ? null : args.alert === "on" || Object.keys(alertFields).length ? {
|
|
1593
|
+
enabled: true,
|
|
1594
|
+
...alertFields
|
|
1595
|
+
} : void 0;
|
|
1596
|
+
const body = {
|
|
1597
|
+
name: args.save,
|
|
1598
|
+
...args.query !== void 0 ? { query: args.query } : {},
|
|
1599
|
+
...args.levels !== void 0 ? { levels: args.levels } : {},
|
|
1600
|
+
...alert !== void 0 ? { alert } : {}
|
|
1601
|
+
};
|
|
1602
|
+
const existing = (await (await api(path, args)).json()).find((q) => q.name.toLowerCase() === args.save.toLowerCase());
|
|
1603
|
+
const saved = await (await api(existing ? `${path}/${existing.id}` : path, args, {
|
|
1604
|
+
method: existing ? "PATCH" : "POST",
|
|
1605
|
+
headers: json,
|
|
1606
|
+
body: JSON.stringify(body)
|
|
1607
|
+
})).json();
|
|
1608
|
+
if (args.json) return console.log(JSON.stringify(saved));
|
|
1609
|
+
return console.log(`${existing ? "Changed" : "Saved"} ${saved.name}: ${saved.query} ${styleText("dim", alertSummary(saved))}`);
|
|
1610
|
+
}
|
|
1611
|
+
const list = await (await api(path, args)).json();
|
|
1612
|
+
if (args.json) return console.log(JSON.stringify(list));
|
|
1613
|
+
if (!list.length) {
|
|
1614
|
+
const target = args.app ? ` ${args.app}` : "";
|
|
1615
|
+
return console.log(`No saved queries. Save one: wervt queries${target} --save Errors --query 'level:error' --alert on`);
|
|
1616
|
+
}
|
|
1617
|
+
for (const query of list) {
|
|
1618
|
+
const levels = query.levels?.length ? styleText("dim", ` [${query.levels.join(",")}]`) : "";
|
|
1619
|
+
const color = query.state?.error ? "red" : query.alert?.enabled && query.state?.firing ? "yellow" : "dim";
|
|
1620
|
+
console.log(`${styleText("bold", query.name)} ${query.query}${levels} ${styleText(color, alertSummary(query))}`);
|
|
1621
|
+
}
|
|
1622
|
+
if (args.app) console.log(styleText("dim", `\nRun one: wervt logs ${args.app} --saved '${list[0].name}'`));
|
|
1623
|
+
}
|
|
1624
|
+
});
|
|
1203
1625
|
const SPARKS = "▁▂▃▄▅▆▇█";
|
|
1204
1626
|
const stats = defineCommand({
|
|
1205
1627
|
meta: {
|
|
@@ -1413,6 +1835,47 @@ const login = defineCommand({
|
|
|
1413
1835
|
outro(`Signed in to ${url} (saved to ${file})`);
|
|
1414
1836
|
}
|
|
1415
1837
|
});
|
|
1838
|
+
const build = defineCommand({
|
|
1839
|
+
meta: {
|
|
1840
|
+
name: "build",
|
|
1841
|
+
description: "Build a fetch handler (Hono, plain `export default { fetch }`) or a static site into a bundle"
|
|
1842
|
+
},
|
|
1843
|
+
args: {
|
|
1844
|
+
dir: {
|
|
1845
|
+
type: "positional",
|
|
1846
|
+
required: false,
|
|
1847
|
+
description: "App directory (default: .)"
|
|
1848
|
+
},
|
|
1849
|
+
entry: {
|
|
1850
|
+
type: "string",
|
|
1851
|
+
description: "Server module (default: server.ts, src/server.ts, src/index.ts)"
|
|
1852
|
+
},
|
|
1853
|
+
static: {
|
|
1854
|
+
type: "string",
|
|
1855
|
+
description: "Folder of a static site to deploy instead of a server, e.g. dist"
|
|
1856
|
+
},
|
|
1857
|
+
fallback: {
|
|
1858
|
+
type: "string",
|
|
1859
|
+
description: "Static site: page for unknown paths (default: index.html, for SPAs); --fallback= for none"
|
|
1860
|
+
},
|
|
1861
|
+
out: {
|
|
1862
|
+
type: "string",
|
|
1863
|
+
description: "Output directory (default: .output)"
|
|
1864
|
+
}
|
|
1865
|
+
},
|
|
1866
|
+
async run({ args }) {
|
|
1867
|
+
const dir = resolveAppDir(args.dir);
|
|
1868
|
+
const started = performance.now();
|
|
1869
|
+
const result = await build$1(dir, {
|
|
1870
|
+
entry: args.entry,
|
|
1871
|
+
static: args.static,
|
|
1872
|
+
fallback: args.fallback === "" ? false : args.fallback,
|
|
1873
|
+
out: args.out
|
|
1874
|
+
});
|
|
1875
|
+
const ms = Math.round(performance.now() - started);
|
|
1876
|
+
console.log(`Built ${result.kind === "static" ? "a static site" : "the server"} into ${result.out} in ${ms} ms (${result.manifest.framework})`);
|
|
1877
|
+
}
|
|
1878
|
+
});
|
|
1416
1879
|
runMain(defineCommand({
|
|
1417
1880
|
meta: {
|
|
1418
1881
|
name: "wervt",
|
|
@@ -1420,14 +1883,17 @@ runMain(defineCommand({
|
|
|
1420
1883
|
},
|
|
1421
1884
|
subCommands: {
|
|
1422
1885
|
deploy,
|
|
1886
|
+
build,
|
|
1423
1887
|
apps,
|
|
1424
1888
|
releases,
|
|
1425
1889
|
audit,
|
|
1426
1890
|
logs,
|
|
1891
|
+
queries,
|
|
1427
1892
|
activate,
|
|
1428
1893
|
rollback,
|
|
1429
1894
|
reprovision,
|
|
1430
1895
|
access,
|
|
1896
|
+
domains,
|
|
1431
1897
|
members,
|
|
1432
1898
|
accounts,
|
|
1433
1899
|
env,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wervt/cli",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.18",
|
|
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
|
@@ -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)
|