@wervt/cli 0.0.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/wervt.js +5 -1
- package/dist/index.mjs +542 -0
- package/package.json +34 -6
package/bin/wervt.js
CHANGED
|
@@ -1,2 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
|
|
2
|
+
// Published builds run dist/; inside the wervt repo the TypeScript source runs directly.
|
|
3
|
+
import { existsSync } from 'node:fs'
|
|
4
|
+
|
|
5
|
+
const dist = new URL('../dist/index.mjs', import.meta.url)
|
|
6
|
+
await import(existsSync(dist) ? dist.href : new URL('../src/index.ts', import.meta.url).href)
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,542 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { mkdir, readFile, readdir, stat, writeFile } from "node:fs/promises";
|
|
3
|
+
import { dirname, join, relative, resolve } from "node:path";
|
|
4
|
+
import { styleText } from "node:util";
|
|
5
|
+
import * as p from "@clack/prompts";
|
|
6
|
+
import { defineCommand, runMain } from "citty";
|
|
7
|
+
import { detectPackageManager } from "nypm";
|
|
8
|
+
import { x } from "tinyexec";
|
|
9
|
+
import { packTar } from "modern-tar";
|
|
10
|
+
import { homedir } from "node:os";
|
|
11
|
+
//#region src/bundle.ts
|
|
12
|
+
/** Files under `dir`. Follows symlinks: Nitro can leave traced dependencies in
|
|
13
|
+
* server/node_modules as links into the local pnpm store, which won't exist on the server. */
|
|
14
|
+
async function walk(dir) {
|
|
15
|
+
const entries = await readdir(dir, { withFileTypes: true });
|
|
16
|
+
return (await Promise.all(entries.map(async (e) => {
|
|
17
|
+
const path = join(dir, e.name);
|
|
18
|
+
return (e.isSymbolicLink() ? (await stat(path)).isDirectory() : e.isDirectory()) ? walk(path) : [path];
|
|
19
|
+
}))).flat();
|
|
20
|
+
}
|
|
21
|
+
async function gzip(data) {
|
|
22
|
+
const stream = new Blob([data]).stream().pipeThrough(new CompressionStream("gzip"));
|
|
23
|
+
return new Uint8Array(await new Response(stream).arrayBuffer());
|
|
24
|
+
}
|
|
25
|
+
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) {
|
|
28
|
+
const roots = [
|
|
29
|
+
[join(appDir, ".output/server"), "server"],
|
|
30
|
+
[join(appDir, ".output/public"), "public"],
|
|
31
|
+
[join(appDir, "drizzle"), "drizzle"]
|
|
32
|
+
];
|
|
33
|
+
if (!await exists(roots[0][0])) throw new Error("No .output/server. Build the app first.");
|
|
34
|
+
const files = [];
|
|
35
|
+
for (const [dir, prefix] of roots) {
|
|
36
|
+
if (!await exists(dir)) continue;
|
|
37
|
+
for (const path of await walk(dir)) {
|
|
38
|
+
if (path.endsWith(".map")) continue;
|
|
39
|
+
const data = await readFile(path);
|
|
40
|
+
files.push({
|
|
41
|
+
header: {
|
|
42
|
+
name: `${prefix}/${relative(dir, path)}`,
|
|
43
|
+
size: data.byteLength
|
|
44
|
+
},
|
|
45
|
+
body: data
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return {
|
|
50
|
+
data: await gzip(await packTar(files)),
|
|
51
|
+
files: files.length
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
//#endregion
|
|
55
|
+
//#region src/config.ts
|
|
56
|
+
const file = join(process.env.XDG_CONFIG_HOME ?? join(homedir(), ".config"), "wervt", "config.json");
|
|
57
|
+
/** Flags > WERVT_URL / WERVT_TOKEN > ~/.config/wervt/config.json. */
|
|
58
|
+
async function resolveConfig(flags) {
|
|
59
|
+
const saved = JSON.parse(await readFile(file, "utf8").catch(() => "{}"));
|
|
60
|
+
const url = flags.url ?? process.env.WERVT_URL ?? saved.url;
|
|
61
|
+
const token = flags.token ?? process.env.WERVT_TOKEN ?? saved.token;
|
|
62
|
+
if (!url || !token) throw new Error("Not logged in. Run `wervt login --url <control url> --token <token>`.");
|
|
63
|
+
return {
|
|
64
|
+
url: url.replace(/\/$/, ""),
|
|
65
|
+
token
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
async function saveConfig(config) {
|
|
69
|
+
await mkdir(dirname(file), { recursive: true });
|
|
70
|
+
await writeFile(file, `${JSON.stringify(config, null, 2)}\n`, { mode: 384 });
|
|
71
|
+
return file;
|
|
72
|
+
}
|
|
73
|
+
//#endregion
|
|
74
|
+
//#region src/index.ts
|
|
75
|
+
const connection = {
|
|
76
|
+
url: {
|
|
77
|
+
type: "string",
|
|
78
|
+
description: "Control plane URL (or WERVT_URL)"
|
|
79
|
+
},
|
|
80
|
+
token: {
|
|
81
|
+
type: "string",
|
|
82
|
+
description: "Deploy or read token (or WERVT_TOKEN)"
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
async function api(path, flags, init = {}) {
|
|
86
|
+
const { url, token } = await resolveConfig(flags);
|
|
87
|
+
const headers = new Headers(init.headers);
|
|
88
|
+
headers.set("authorization", `Bearer ${token}`);
|
|
89
|
+
const res = await fetch(`${url}${path}`, {
|
|
90
|
+
...init,
|
|
91
|
+
headers
|
|
92
|
+
});
|
|
93
|
+
if (!res.ok) {
|
|
94
|
+
const body = await res.text();
|
|
95
|
+
let message = body;
|
|
96
|
+
try {
|
|
97
|
+
message = JSON.parse(body).error ?? body;
|
|
98
|
+
} catch {}
|
|
99
|
+
console.error(styleText("red", `Error (${res.status}): ${message}`));
|
|
100
|
+
process.exit(1);
|
|
101
|
+
}
|
|
102
|
+
return res;
|
|
103
|
+
}
|
|
104
|
+
/** App directory from the argument: a path, or an app name inside the wervt repo's apps/. */
|
|
105
|
+
function resolveAppDir(arg = ".") {
|
|
106
|
+
if (existsSync(join(arg, "package.json"))) return resolve(arg);
|
|
107
|
+
if (existsSync(join("apps", arg, "package.json"))) return resolve("apps", arg);
|
|
108
|
+
throw new Error(`No app found at ${arg}`);
|
|
109
|
+
}
|
|
110
|
+
async function appName(dir, name) {
|
|
111
|
+
if (!name) {
|
|
112
|
+
const pkg = JSON.parse(await readFile(join(dir, "package.json"), "utf8"));
|
|
113
|
+
name = String(pkg.name ?? "").replace(/^@[^/]+\//, "");
|
|
114
|
+
}
|
|
115
|
+
const normalized = name.toLowerCase().replace(/[^a-z0-9-]+/g, "-").replace(/^-+|-+$/g, "");
|
|
116
|
+
if (!normalized) throw new Error("Could not determine the app name, pass --name");
|
|
117
|
+
return normalized;
|
|
118
|
+
}
|
|
119
|
+
const deploy = defineCommand({
|
|
120
|
+
meta: {
|
|
121
|
+
name: "deploy",
|
|
122
|
+
description: "Build an app and deploy it"
|
|
123
|
+
},
|
|
124
|
+
args: {
|
|
125
|
+
dir: {
|
|
126
|
+
type: "positional",
|
|
127
|
+
required: false,
|
|
128
|
+
description: "App directory (default: .)"
|
|
129
|
+
},
|
|
130
|
+
name: {
|
|
131
|
+
type: "string",
|
|
132
|
+
description: "App name (default: package.json name)"
|
|
133
|
+
},
|
|
134
|
+
build: {
|
|
135
|
+
type: "boolean",
|
|
136
|
+
default: true,
|
|
137
|
+
description: "Build before deploying"
|
|
138
|
+
},
|
|
139
|
+
...connection
|
|
140
|
+
},
|
|
141
|
+
async run({ args }) {
|
|
142
|
+
const dir = resolveAppDir(args.dir);
|
|
143
|
+
const name = await appName(dir, args.name);
|
|
144
|
+
p.intro(`wervt deploy ${name}`);
|
|
145
|
+
if (args.build) {
|
|
146
|
+
const pm = (await detectPackageManager(dir))?.name ?? "npm";
|
|
147
|
+
p.log.step(`${pm} run build`);
|
|
148
|
+
if ((await x(pm, ["run", "build"], {
|
|
149
|
+
nodeOptions: {
|
|
150
|
+
cwd: dir,
|
|
151
|
+
stdio: "inherit"
|
|
152
|
+
},
|
|
153
|
+
throwOnError: false
|
|
154
|
+
})).exitCode !== 0) throw new Error("Build failed");
|
|
155
|
+
}
|
|
156
|
+
const spinner = p.spinner();
|
|
157
|
+
spinner.start("Packing");
|
|
158
|
+
const bundle = await createBundle(dir);
|
|
159
|
+
spinner.stop(`Packed ${bundle.files} files (${(bundle.data.byteLength / 1024).toFixed(0)} kB)`);
|
|
160
|
+
spinner.start("Uploading");
|
|
161
|
+
const res = await api(`/v1/apps/${name}/deploys`, args, {
|
|
162
|
+
method: "POST",
|
|
163
|
+
headers: { "content-type": "application/gzip" },
|
|
164
|
+
body: new Uint8Array(bundle.data)
|
|
165
|
+
});
|
|
166
|
+
const labels = {
|
|
167
|
+
extract: "Extracting",
|
|
168
|
+
migrate: "Migrating database",
|
|
169
|
+
activate: "Activating"
|
|
170
|
+
};
|
|
171
|
+
const decoder = new TextDecoder();
|
|
172
|
+
let buffer = "";
|
|
173
|
+
for await (const chunk of res.body) {
|
|
174
|
+
buffer += decoder.decode(chunk, { stream: true });
|
|
175
|
+
let newline;
|
|
176
|
+
while ((newline = buffer.indexOf("\n")) >= 0) {
|
|
177
|
+
const event = JSON.parse(buffer.slice(0, newline));
|
|
178
|
+
buffer = buffer.slice(newline + 1);
|
|
179
|
+
if (event.error) {
|
|
180
|
+
spinner.error(event.error);
|
|
181
|
+
process.exitCode = 1;
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
if (event.done) {
|
|
185
|
+
spinner.stop(`Release ${event.release}`);
|
|
186
|
+
p.outro(`Deployed → ${event.url}`);
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
spinner.message(labels[event.step] ?? event.step);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
});
|
|
194
|
+
const apps = defineCommand({
|
|
195
|
+
meta: {
|
|
196
|
+
name: "apps",
|
|
197
|
+
description: "List deployed apps"
|
|
198
|
+
},
|
|
199
|
+
args: connection,
|
|
200
|
+
async run({ args }) {
|
|
201
|
+
const list = await (await api("/v1/apps", args)).json();
|
|
202
|
+
if (!list.length) return console.log("No apps deployed.");
|
|
203
|
+
for (const app of list) console.log(`${app.app.padEnd(20)} ${(app.release ?? "-").padEnd(22)} ${app.database ? "db " : " "} ${app.url}`);
|
|
204
|
+
}
|
|
205
|
+
});
|
|
206
|
+
const rollback = defineCommand({
|
|
207
|
+
meta: {
|
|
208
|
+
name: "rollback",
|
|
209
|
+
description: "Activate the previous release (migrations stay applied)"
|
|
210
|
+
},
|
|
211
|
+
args: {
|
|
212
|
+
app: {
|
|
213
|
+
type: "positional",
|
|
214
|
+
required: true,
|
|
215
|
+
description: "App name"
|
|
216
|
+
},
|
|
217
|
+
...connection
|
|
218
|
+
},
|
|
219
|
+
async run({ args }) {
|
|
220
|
+
const result = await (await api(`/v1/apps/${args.app}/rollback`, args, { method: "POST" })).json();
|
|
221
|
+
console.log(`${args.app} is now on release ${result.release}`);
|
|
222
|
+
}
|
|
223
|
+
});
|
|
224
|
+
const releases = defineCommand({
|
|
225
|
+
meta: {
|
|
226
|
+
name: "releases",
|
|
227
|
+
description: "List an app's releases, newest first"
|
|
228
|
+
},
|
|
229
|
+
args: {
|
|
230
|
+
app: {
|
|
231
|
+
type: "positional",
|
|
232
|
+
required: true,
|
|
233
|
+
description: "App name"
|
|
234
|
+
},
|
|
235
|
+
...connection
|
|
236
|
+
},
|
|
237
|
+
async run({ args }) {
|
|
238
|
+
const info = await (await api(`/v1/apps/${args.app}`, args)).json();
|
|
239
|
+
for (const release of [...info.releases].reverse()) console.log(`${release}${release === info.release ? " (active)" : ""}`);
|
|
240
|
+
}
|
|
241
|
+
});
|
|
242
|
+
const activate = defineCommand({
|
|
243
|
+
meta: {
|
|
244
|
+
name: "activate",
|
|
245
|
+
description: "Activate a release of an app (migrations stay applied)"
|
|
246
|
+
},
|
|
247
|
+
args: {
|
|
248
|
+
app: {
|
|
249
|
+
type: "positional",
|
|
250
|
+
required: true,
|
|
251
|
+
description: "App name"
|
|
252
|
+
},
|
|
253
|
+
release: {
|
|
254
|
+
type: "positional",
|
|
255
|
+
required: true,
|
|
256
|
+
description: "Release id (see `releases`)"
|
|
257
|
+
},
|
|
258
|
+
...connection
|
|
259
|
+
},
|
|
260
|
+
async run({ args }) {
|
|
261
|
+
const info = await (await api(`/v1/apps/${args.app}`, args)).json();
|
|
262
|
+
const result = await (await api(`/v1/apps/${args.app}/activate`, args, {
|
|
263
|
+
method: "POST",
|
|
264
|
+
headers: { "content-type": "application/json" },
|
|
265
|
+
body: JSON.stringify({
|
|
266
|
+
release: args.release,
|
|
267
|
+
ifActive: info.release ?? null
|
|
268
|
+
})
|
|
269
|
+
})).json();
|
|
270
|
+
console.log(`${args.app} is now on release ${result.release}`);
|
|
271
|
+
}
|
|
272
|
+
});
|
|
273
|
+
const reprovision = defineCommand({
|
|
274
|
+
meta: {
|
|
275
|
+
name: "reprovision",
|
|
276
|
+
description: "Recreate an app's database role, grants and ownership and run its migrations (after a restore)"
|
|
277
|
+
},
|
|
278
|
+
args: {
|
|
279
|
+
app: {
|
|
280
|
+
type: "positional",
|
|
281
|
+
required: false,
|
|
282
|
+
description: "App name"
|
|
283
|
+
},
|
|
284
|
+
all: {
|
|
285
|
+
type: "boolean",
|
|
286
|
+
description: "Every app"
|
|
287
|
+
},
|
|
288
|
+
...connection
|
|
289
|
+
},
|
|
290
|
+
async run({ args }) {
|
|
291
|
+
if (!args.app === !args.all) throw new Error("Pass an app name or --all");
|
|
292
|
+
const results = [await (await api(args.all ? "/v1/reprovision" : `/v1/apps/${args.app}/reprovision`, args, { method: "POST" })).json()].flat();
|
|
293
|
+
for (const r of results) console.log(`${r.app.padEnd(20)} ${r.error ?? (r.database ? "ok" : "no database")}`);
|
|
294
|
+
if (results.some((r) => r.error)) process.exitCode = 1;
|
|
295
|
+
}
|
|
296
|
+
});
|
|
297
|
+
const toList = (value) => (Array.isArray(value) ? value : value === void 0 ? [] : [value]).flatMap((v) => String(v).split(","));
|
|
298
|
+
const access = defineCommand({
|
|
299
|
+
meta: {
|
|
300
|
+
name: "access",
|
|
301
|
+
description: "Show or change who can open an app (private apps need a login)"
|
|
302
|
+
},
|
|
303
|
+
args: {
|
|
304
|
+
app: {
|
|
305
|
+
type: "positional",
|
|
306
|
+
required: true,
|
|
307
|
+
description: "App name"
|
|
308
|
+
},
|
|
309
|
+
public: {
|
|
310
|
+
type: "boolean",
|
|
311
|
+
description: "Make the app public"
|
|
312
|
+
},
|
|
313
|
+
private: {
|
|
314
|
+
type: "boolean",
|
|
315
|
+
description: "Make the app private"
|
|
316
|
+
},
|
|
317
|
+
add: {
|
|
318
|
+
type: "string",
|
|
319
|
+
description: "Allow emails or *@domain (comma separated)"
|
|
320
|
+
},
|
|
321
|
+
remove: {
|
|
322
|
+
type: "string",
|
|
323
|
+
description: "Remove emails or *@domain (comma separated)"
|
|
324
|
+
},
|
|
325
|
+
...connection
|
|
326
|
+
},
|
|
327
|
+
async run({ args }) {
|
|
328
|
+
const change = {};
|
|
329
|
+
if (args.public) change.visibility = "public";
|
|
330
|
+
if (args.private) change.visibility = "private";
|
|
331
|
+
if (args.add) change.add = toList(args.add);
|
|
332
|
+
if (args.remove) change.remove = toList(args.remove);
|
|
333
|
+
const result = await (Object.keys(change).length ? await api(`/v1/apps/${args.app}/access`, args, {
|
|
334
|
+
method: "PATCH",
|
|
335
|
+
headers: { "content-type": "application/json" },
|
|
336
|
+
body: JSON.stringify(change)
|
|
337
|
+
}) : await api(`/v1/apps/${args.app}/access`, args)).json();
|
|
338
|
+
if (args.app === "auth") return console.log("auth is the login app and always public");
|
|
339
|
+
console.log(`${args.app} is ${result.visibility}`);
|
|
340
|
+
if (result.visibility === "private") console.log(result.allow.length ? `allowed: ${result.allow.join(", ")}` : "allowed: admins only");
|
|
341
|
+
}
|
|
342
|
+
});
|
|
343
|
+
const env = defineCommand({
|
|
344
|
+
meta: {
|
|
345
|
+
name: "env",
|
|
346
|
+
description: "Set or unset app environment variables (values are write-only)"
|
|
347
|
+
},
|
|
348
|
+
args: {
|
|
349
|
+
app: {
|
|
350
|
+
type: "positional",
|
|
351
|
+
required: true,
|
|
352
|
+
description: "App name"
|
|
353
|
+
},
|
|
354
|
+
set: {
|
|
355
|
+
type: "string",
|
|
356
|
+
description: "KEY=value (repeatable)"
|
|
357
|
+
},
|
|
358
|
+
unset: {
|
|
359
|
+
type: "string",
|
|
360
|
+
description: "KEY (comma separated)"
|
|
361
|
+
},
|
|
362
|
+
...connection
|
|
363
|
+
},
|
|
364
|
+
async run({ args, rawArgs }) {
|
|
365
|
+
const pairs = rawArgs.flatMap((arg, i) => arg === "--set" ? [rawArgs[i + 1] ?? ""] : arg.startsWith("--set=") ? [arg.slice(6)] : []);
|
|
366
|
+
const set = Object.fromEntries(pairs.map((pair) => {
|
|
367
|
+
const i = pair.indexOf("=");
|
|
368
|
+
if (i < 1) throw new Error(`Expected KEY=value, got "${pair}"`);
|
|
369
|
+
return [pair.slice(0, i), pair.slice(i + 1)];
|
|
370
|
+
}));
|
|
371
|
+
const unset = toList(args.unset);
|
|
372
|
+
const { keys } = await (Object.keys(set).length || unset.length ? await api(`/v1/apps/${args.app}/env`, args, {
|
|
373
|
+
method: "PATCH",
|
|
374
|
+
headers: { "content-type": "application/json" },
|
|
375
|
+
body: JSON.stringify({
|
|
376
|
+
set,
|
|
377
|
+
unset
|
|
378
|
+
})
|
|
379
|
+
}) : await api(`/v1/apps/${args.app}/env`, args)).json();
|
|
380
|
+
console.log(keys.length ? keys.join("\n") : "No variables set.");
|
|
381
|
+
if (Object.keys(set).length || unset.length) console.log("Applies to newly started isolates (next deploy or after idle timeout).");
|
|
382
|
+
}
|
|
383
|
+
});
|
|
384
|
+
const remove = defineCommand({
|
|
385
|
+
meta: {
|
|
386
|
+
name: "delete",
|
|
387
|
+
description: "Delete an app (releases, access, env)"
|
|
388
|
+
},
|
|
389
|
+
args: {
|
|
390
|
+
app: {
|
|
391
|
+
type: "positional",
|
|
392
|
+
required: true,
|
|
393
|
+
description: "App name"
|
|
394
|
+
},
|
|
395
|
+
"drop-database": {
|
|
396
|
+
type: "boolean",
|
|
397
|
+
description: "Also drop the app's schema and all its data"
|
|
398
|
+
},
|
|
399
|
+
yes: {
|
|
400
|
+
type: "boolean",
|
|
401
|
+
description: "Skip the confirmation"
|
|
402
|
+
},
|
|
403
|
+
...connection
|
|
404
|
+
},
|
|
405
|
+
async run({ args }) {
|
|
406
|
+
const dropDb = Boolean(args["drop-database"]);
|
|
407
|
+
if (!args.yes) {
|
|
408
|
+
const ok = await p.confirm({
|
|
409
|
+
message: `Delete ${args.app}${dropDb ? " and ALL its data" : " (database is kept)"}?`,
|
|
410
|
+
initialValue: false
|
|
411
|
+
});
|
|
412
|
+
if (p.isCancel(ok) || !ok) return p.cancel("Cancelled");
|
|
413
|
+
}
|
|
414
|
+
await api(`/v1/apps/${args.app}?dropDatabase=${dropDb}`, args, { method: "DELETE" });
|
|
415
|
+
console.log(`Deleted ${args.app}${dropDb ? " and its database" : ""}`);
|
|
416
|
+
}
|
|
417
|
+
});
|
|
418
|
+
const LEVEL_COLORS = {
|
|
419
|
+
error: "red",
|
|
420
|
+
warn: "yellow",
|
|
421
|
+
info: "cyan",
|
|
422
|
+
debug: "gray"
|
|
423
|
+
};
|
|
424
|
+
/** `09:07:52.060 error <message>`, or the entry as one JSON line. */
|
|
425
|
+
function printLog(entry, asJson) {
|
|
426
|
+
if (asJson) return console.log(JSON.stringify(entry));
|
|
427
|
+
const time = new Date(entry._time).toISOString().slice(11, 23);
|
|
428
|
+
const level = String(entry.level ?? "info");
|
|
429
|
+
const color = LEVEL_COLORS[level] ?? "white";
|
|
430
|
+
console.log(`${styleText("dim", time)} ${styleText(color, level.padEnd(5))} ${entry._msg ?? ""}`);
|
|
431
|
+
}
|
|
432
|
+
const logs = defineCommand({
|
|
433
|
+
meta: {
|
|
434
|
+
name: "logs",
|
|
435
|
+
description: "Show an app's logs: console output, request events and isolate events"
|
|
436
|
+
},
|
|
437
|
+
args: {
|
|
438
|
+
app: {
|
|
439
|
+
type: "positional",
|
|
440
|
+
required: true,
|
|
441
|
+
description: "App name"
|
|
442
|
+
},
|
|
443
|
+
query: {
|
|
444
|
+
type: "positional",
|
|
445
|
+
required: false,
|
|
446
|
+
description: "LogsQL filter, e.g. 'level:error' or 'status:>=500' (default: all)"
|
|
447
|
+
},
|
|
448
|
+
since: {
|
|
449
|
+
type: "string",
|
|
450
|
+
default: "1h",
|
|
451
|
+
description: "How far back, e.g. 15m, 6h, 2d"
|
|
452
|
+
},
|
|
453
|
+
until: {
|
|
454
|
+
type: "string",
|
|
455
|
+
description: "End of the range (default: now)"
|
|
456
|
+
},
|
|
457
|
+
limit: {
|
|
458
|
+
type: "string",
|
|
459
|
+
default: "100",
|
|
460
|
+
description: "Newest entries to show (max 1000)"
|
|
461
|
+
},
|
|
462
|
+
follow: {
|
|
463
|
+
type: "boolean",
|
|
464
|
+
alias: "f",
|
|
465
|
+
description: "Keep printing new entries"
|
|
466
|
+
},
|
|
467
|
+
json: {
|
|
468
|
+
type: "boolean",
|
|
469
|
+
description: "One JSON object per line, with every field"
|
|
470
|
+
},
|
|
471
|
+
...connection
|
|
472
|
+
},
|
|
473
|
+
async run({ args }) {
|
|
474
|
+
const params = new URLSearchParams();
|
|
475
|
+
if (args.query) params.set("query", args.query);
|
|
476
|
+
if (args.follow) {
|
|
477
|
+
params.set("since", args.since);
|
|
478
|
+
const res = await api(`/v1/apps/${args.app}/logs/tail?${params.toString()}`, args);
|
|
479
|
+
let rest = "";
|
|
480
|
+
for await (const chunk of res.body.pipeThrough(new TextDecoderStream())) {
|
|
481
|
+
const lines = (rest + chunk).split("\n");
|
|
482
|
+
rest = lines.pop() ?? "";
|
|
483
|
+
for (const line of lines) if (line) printLog(JSON.parse(line), Boolean(args.json));
|
|
484
|
+
}
|
|
485
|
+
return;
|
|
486
|
+
}
|
|
487
|
+
params.set("start", args.since);
|
|
488
|
+
if (args.until) params.set("end", args.until);
|
|
489
|
+
params.set("limit", args.limit);
|
|
490
|
+
const entries = await (await api(`/v1/apps/${args.app}/logs?${params.toString()}`, args)).json();
|
|
491
|
+
if (!entries.length && !args.json) return console.error(`No logs in the last ${args.since}.`);
|
|
492
|
+
for (const entry of entries.reverse()) printLog(entry, Boolean(args.json));
|
|
493
|
+
}
|
|
494
|
+
});
|
|
495
|
+
const login = defineCommand({
|
|
496
|
+
meta: {
|
|
497
|
+
name: "login",
|
|
498
|
+
description: "Save the control plane URL and token"
|
|
499
|
+
},
|
|
500
|
+
args: {
|
|
501
|
+
url: {
|
|
502
|
+
type: "string",
|
|
503
|
+
required: true,
|
|
504
|
+
description: "Control plane URL"
|
|
505
|
+
},
|
|
506
|
+
token: {
|
|
507
|
+
type: "string",
|
|
508
|
+
required: true,
|
|
509
|
+
description: "Deploy token, or the read token"
|
|
510
|
+
}
|
|
511
|
+
},
|
|
512
|
+
async run({ args }) {
|
|
513
|
+
const res = await fetch(`${args.url.replace(/\/$/, "")}/v1/apps`, { headers: { authorization: `Bearer ${args.token}` } });
|
|
514
|
+
if (!res.ok) throw new Error(`Login failed: ${res.status} ${res.statusText}`);
|
|
515
|
+
const file = await saveConfig({
|
|
516
|
+
url: args.url,
|
|
517
|
+
token: args.token
|
|
518
|
+
});
|
|
519
|
+
console.log(`Saved to ${file}`);
|
|
520
|
+
}
|
|
521
|
+
});
|
|
522
|
+
runMain(defineCommand({
|
|
523
|
+
meta: {
|
|
524
|
+
name: "wervt",
|
|
525
|
+
description: "Deploy and manage wervt apps"
|
|
526
|
+
},
|
|
527
|
+
subCommands: {
|
|
528
|
+
deploy,
|
|
529
|
+
apps,
|
|
530
|
+
releases,
|
|
531
|
+
logs,
|
|
532
|
+
activate,
|
|
533
|
+
rollback,
|
|
534
|
+
reprovision,
|
|
535
|
+
access,
|
|
536
|
+
env,
|
|
537
|
+
delete: remove,
|
|
538
|
+
login
|
|
539
|
+
}
|
|
540
|
+
}));
|
|
541
|
+
//#endregion
|
|
542
|
+
export {};
|
package/package.json
CHANGED
|
@@ -1,11 +1,39 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wervt/cli",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "Deploy and manage apps on wervt.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"agents",
|
|
7
|
+
"cli",
|
|
8
|
+
"hosting",
|
|
9
|
+
"nitro",
|
|
10
|
+
"nuxt"
|
|
11
|
+
],
|
|
12
|
+
"homepage": "https://wervt.app",
|
|
5
13
|
"license": "MIT",
|
|
6
14
|
"author": "Niki2k1",
|
|
15
|
+
"repository": {
|
|
16
|
+
"type": "git",
|
|
17
|
+
"url": "git+https://github.com/wervt-app/wervt.git",
|
|
18
|
+
"directory": "packages/cli"
|
|
19
|
+
},
|
|
20
|
+
"bin": {
|
|
21
|
+
"wervt": "bin/wervt.js"
|
|
22
|
+
},
|
|
23
|
+
"files": [
|
|
24
|
+
"bin",
|
|
25
|
+
"dist",
|
|
26
|
+
"README.md"
|
|
27
|
+
],
|
|
7
28
|
"type": "module",
|
|
8
|
-
"
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
29
|
+
"dependencies": {
|
|
30
|
+
"@clack/prompts": "^1.8.1",
|
|
31
|
+
"citty": "^0.2.2",
|
|
32
|
+
"modern-tar": "^0.8.5",
|
|
33
|
+
"nypm": "^0.6.10",
|
|
34
|
+
"tinyexec": "^1.3.1"
|
|
35
|
+
},
|
|
36
|
+
"scripts": {
|
|
37
|
+
"build": "vp pack"
|
|
38
|
+
}
|
|
39
|
+
}
|