@wervt/cli 0.2.13 → 0.2.15
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 +1 -1
- package/dist/index.mjs +309 -16
- package/package.json +2 -2
- package/skill/SKILL.md +23 -3
package/README.md
CHANGED
|
@@ -16,5 +16,5 @@ vp exec wervt logs my-app
|
|
|
16
16
|
```
|
|
17
17
|
|
|
18
18
|
Without [Vite+](https://viteplus.dev), use `npx @wervt/cli` and `pnpm exec wervt`. `wervt --help`
|
|
19
|
-
lists every command: `apps`, `releases`, `rollback`, `activate`, `env`, `tokens`, `members`,
|
|
19
|
+
lists every command: `apps`, `releases`, `audit`, `rollback`, `activate`, `env`, `tokens`, `token`, `members`,
|
|
20
20
|
`tasks`, `stats`, `delete`, `whoami`, … Docs: [wervt.app](https://wervt.app/getting-started/quickstart).
|
package/dist/index.mjs
CHANGED
|
@@ -75,6 +75,59 @@ async function saveConfig(config) {
|
|
|
75
75
|
return file;
|
|
76
76
|
}
|
|
77
77
|
//#endregion
|
|
78
|
+
//#region src/format.ts
|
|
79
|
+
function formatBytes(bytes) {
|
|
80
|
+
if (bytes < 1024) return `${bytes} B`;
|
|
81
|
+
const units = [
|
|
82
|
+
"KB",
|
|
83
|
+
"MB",
|
|
84
|
+
"GB"
|
|
85
|
+
];
|
|
86
|
+
let value = bytes / 1024;
|
|
87
|
+
let unit = 0;
|
|
88
|
+
while (value >= 1024 && unit < units.length - 1) {
|
|
89
|
+
value /= 1024;
|
|
90
|
+
unit++;
|
|
91
|
+
}
|
|
92
|
+
return `${value < 10 ? value.toFixed(1) : Math.round(value)} ${units[unit]}`;
|
|
93
|
+
}
|
|
94
|
+
const list = (value) => Array.isArray(value) ? value.map(String).join(", ") : "";
|
|
95
|
+
const text = (value) => typeof value === "string" ? value : "";
|
|
96
|
+
/** What an audit entry changed, in a few words: `env.update` → `set API_URL, TOKEN`. */
|
|
97
|
+
function auditSummary(entry) {
|
|
98
|
+
const d = entry.details ?? {};
|
|
99
|
+
switch (entry.action) {
|
|
100
|
+
case "deploy":
|
|
101
|
+
case "deploy.failed": {
|
|
102
|
+
const migrations = list(d.migrations);
|
|
103
|
+
return [
|
|
104
|
+
text(d.release),
|
|
105
|
+
migrations && `migrated ${migrations}`,
|
|
106
|
+
text(d.checkError) && `checks failed: ${text(d.checkError)}`,
|
|
107
|
+
entry.action === "deploy.failed" && text(d.error).split("\n")[0]
|
|
108
|
+
].filter(Boolean).join(" ");
|
|
109
|
+
}
|
|
110
|
+
case "rollback":
|
|
111
|
+
case "activate": return `${text(d.previous) || "none"} → ${text(d.release)}`;
|
|
112
|
+
case "env.update": return [list(d.set) && `set ${list(d.set)}`, list(d.unset) && `unset ${list(d.unset)}`].filter(Boolean).join("; ");
|
|
113
|
+
case "access.update": return [
|
|
114
|
+
text(d.visibility),
|
|
115
|
+
list(d.allow) && `allow ${list(d.allow)}`,
|
|
116
|
+
list(d.add) && `add ${list(d.add)}`,
|
|
117
|
+
list(d.remove) && `remove ${list(d.remove)}`
|
|
118
|
+
].filter(Boolean).join("; ");
|
|
119
|
+
case "owners.update": return [
|
|
120
|
+
text(d.owner) && `owner ${text(d.owner)}`,
|
|
121
|
+
list(d.added) && `added ${list(d.added)}`,
|
|
122
|
+
list(d.removed) && `removed ${list(d.removed)}`
|
|
123
|
+
].filter(Boolean).join("; ");
|
|
124
|
+
case "token.create":
|
|
125
|
+
case "token.revoke": return text(d.name) || text(d.id);
|
|
126
|
+
case "app.delete": return `database ${text(d.database)}`;
|
|
127
|
+
default: return entry.account ?? "";
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
//#endregion
|
|
78
131
|
//#region src/source.ts
|
|
79
132
|
async function git(dir, ...args) {
|
|
80
133
|
try {
|
|
@@ -294,6 +347,10 @@ const deploy = defineCommand({
|
|
|
294
347
|
process.exitCode = 1;
|
|
295
348
|
continue;
|
|
296
349
|
}
|
|
350
|
+
if (event.step === "quota-warning") {
|
|
351
|
+
p.log.warn(event.message);
|
|
352
|
+
continue;
|
|
353
|
+
}
|
|
297
354
|
if (event.step === "storage-failed") {
|
|
298
355
|
p.log.warn(`Storage isn't set up: ${event.message}. The app runs; files won't work yet.`);
|
|
299
356
|
continue;
|
|
@@ -333,6 +390,7 @@ const rollback = defineCommand({
|
|
|
333
390
|
console.log(`${args.app} is now on release ${result.release}`);
|
|
334
391
|
}
|
|
335
392
|
});
|
|
393
|
+
const firstLine = (text) => text.split("\n")[0];
|
|
336
394
|
const releases = defineCommand({
|
|
337
395
|
meta: {
|
|
338
396
|
name: "releases",
|
|
@@ -349,16 +407,75 @@ const releases = defineCommand({
|
|
|
349
407
|
async run({ args }) {
|
|
350
408
|
const info = await (await api(`/v1/apps/${args.app}`, args)).json();
|
|
351
409
|
const history = new Map(info.history?.map((entry) => [entry.release, entry]));
|
|
352
|
-
|
|
353
|
-
|
|
410
|
+
const failed = new Map(info.failedDeploys?.map((entry) => [entry.release, entry]));
|
|
411
|
+
const ids = [...info.releases, ...failed.keys()].sort((a, b) => b.localeCompare(a));
|
|
412
|
+
for (const release of ids) {
|
|
413
|
+
const failure = failed.get(release);
|
|
414
|
+
const kept = history.get(release);
|
|
415
|
+
const source = failure?.source ?? kept?.source;
|
|
416
|
+
const commit = source?.commit ? `${source.commit.slice(0, 7)}${source.dirty ? "+" : ""}` : void 0;
|
|
417
|
+
const applied = kept?.migrations?.applied.length;
|
|
354
418
|
const details = [
|
|
355
|
-
|
|
419
|
+
commit,
|
|
356
420
|
source?.branch,
|
|
357
|
-
source?.actor ?? deployedBy,
|
|
421
|
+
source?.actor ?? failure?.actor ?? kept?.deployedBy,
|
|
422
|
+
kept?.size && formatBytes(kept.size.bundle),
|
|
423
|
+
applied && `${applied} migration${applied === 1 ? "" : "s"}`,
|
|
358
424
|
source?.message && styleText("dim", source.message)
|
|
359
425
|
].filter(Boolean);
|
|
360
|
-
const
|
|
361
|
-
|
|
426
|
+
const status = release === info.release ? styleText("green", " (active)") : failure ? styleText("red", " (failed)") : kept?.bootError ? styleText("red", " (didn't start)") : "";
|
|
427
|
+
const checks = kept?.checkError ? styleText("yellow", " (checks failed)") : "";
|
|
428
|
+
console.log(`${release}${status}${checks}${details.length ? ` ${details.join(" ")}` : ""}`);
|
|
429
|
+
const error = failure?.error ?? kept?.bootError ?? kept?.checkError;
|
|
430
|
+
if (error) console.log(styleText("dim", ` ${firstLine(error)}`));
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
});
|
|
434
|
+
const audit = defineCommand({
|
|
435
|
+
meta: {
|
|
436
|
+
name: "audit",
|
|
437
|
+
description: "Show who deployed, rolled back, deleted or changed access, env and tokens"
|
|
438
|
+
},
|
|
439
|
+
args: {
|
|
440
|
+
app: {
|
|
441
|
+
type: "positional",
|
|
442
|
+
required: false,
|
|
443
|
+
description: "App name (default: all you see)"
|
|
444
|
+
},
|
|
445
|
+
action: {
|
|
446
|
+
type: "string",
|
|
447
|
+
description: "Only these actions, comma-separated; deploy* for all deploys"
|
|
448
|
+
},
|
|
449
|
+
limit: {
|
|
450
|
+
type: "string",
|
|
451
|
+
default: "50",
|
|
452
|
+
description: "Newest entries to show (max 1000)"
|
|
453
|
+
},
|
|
454
|
+
json: {
|
|
455
|
+
type: "boolean",
|
|
456
|
+
description: "One JSON object per line"
|
|
457
|
+
},
|
|
458
|
+
...connection
|
|
459
|
+
},
|
|
460
|
+
async run({ args }) {
|
|
461
|
+
const params = new URLSearchParams({ limit: args.limit });
|
|
462
|
+
if (args.action) params.set("action", args.action);
|
|
463
|
+
const entries = await (await api(`${args.app ? `/v1/apps/${args.app}/audit` : "/v1/audit"}?${params.toString()}`, args)).json();
|
|
464
|
+
if (!entries.length && !args.json) return console.error("No audit entries.");
|
|
465
|
+
for (const entry of entries.reverse()) {
|
|
466
|
+
if (args.json) {
|
|
467
|
+
console.log(JSON.stringify(entry));
|
|
468
|
+
continue;
|
|
469
|
+
}
|
|
470
|
+
const time = new Date(entry.at).toISOString().slice(0, 16).replace("T", " ");
|
|
471
|
+
const color = entry.action === "deploy.failed" ? "red" : "cyan";
|
|
472
|
+
console.log([
|
|
473
|
+
styleText("dim", time),
|
|
474
|
+
styleText(color, entry.action.padEnd(15)),
|
|
475
|
+
(entry.app ?? "-").padEnd(16),
|
|
476
|
+
entry.actor,
|
|
477
|
+
styleText("dim", auditSummary(entry))
|
|
478
|
+
].join(" "));
|
|
362
479
|
}
|
|
363
480
|
}
|
|
364
481
|
});
|
|
@@ -421,7 +538,7 @@ const toList = (value) => (Array.isArray(value) ? value : value === void 0 ? []
|
|
|
421
538
|
const access = defineCommand({
|
|
422
539
|
meta: {
|
|
423
540
|
name: "access",
|
|
424
|
-
description: "Show or change who can open an app (private apps need a login)"
|
|
541
|
+
description: "Show or change who can open an app (private and invite-only apps need a login); --remove takes back invites too"
|
|
425
542
|
},
|
|
426
543
|
args: {
|
|
427
544
|
app: {
|
|
@@ -437,6 +554,14 @@ const access = defineCommand({
|
|
|
437
554
|
type: "boolean",
|
|
438
555
|
description: "Make the app private"
|
|
439
556
|
},
|
|
557
|
+
invite: {
|
|
558
|
+
type: "boolean",
|
|
559
|
+
description: "Make the app private, and let people with access invite others in the app"
|
|
560
|
+
},
|
|
561
|
+
"invite-by": {
|
|
562
|
+
type: "string",
|
|
563
|
+
description: "Who may invite on an invite-only app: anyone (with access, the default) or owners (owner, members and admins)"
|
|
564
|
+
},
|
|
440
565
|
add: {
|
|
441
566
|
type: "string",
|
|
442
567
|
description: "Allow emails or *@domain (comma separated)"
|
|
@@ -451,6 +576,8 @@ const access = defineCommand({
|
|
|
451
576
|
const change = {};
|
|
452
577
|
if (args.public) change.visibility = "public";
|
|
453
578
|
if (args.private) change.visibility = "private";
|
|
579
|
+
if (args.invite) change.visibility = "invite";
|
|
580
|
+
if (args["invite-by"]) change.inviteBy = args["invite-by"];
|
|
454
581
|
if (args.add) change.add = toList(args.add);
|
|
455
582
|
if (args.remove) change.remove = toList(args.remove);
|
|
456
583
|
const result = await (Object.keys(change).length ? await api(`/v1/apps/${args.app}/access`, args, {
|
|
@@ -459,8 +586,15 @@ const access = defineCommand({
|
|
|
459
586
|
body: JSON.stringify(change)
|
|
460
587
|
}) : await api(`/v1/apps/${args.app}/access`, args)).json();
|
|
461
588
|
if (args.app === "auth") return console.log("auth is the login app and always public");
|
|
462
|
-
console.log(`${args.app} is ${result.visibility}`);
|
|
463
|
-
if (result.visibility === "
|
|
589
|
+
console.log(`${args.app} is ${result.visibility === "invite" ? "invite-only" : result.visibility}`);
|
|
590
|
+
if (result.visibility === "public") return;
|
|
591
|
+
console.log(result.allow.length ? `allowed: ${result.allow.join(", ")}` : "allowed: admins only");
|
|
592
|
+
if (result.visibility === "invite") {
|
|
593
|
+
console.log(result.inviteBy === "owners" ? "who invites: the owner, members and admins" : "who invites: everyone with access");
|
|
594
|
+
const invited = result.invited ?? [];
|
|
595
|
+
console.log(invited.length ? "invited:" : "invited: nobody yet");
|
|
596
|
+
for (const invite of invited) console.log(` ${invite.email} (by ${invite.by}, ${invite.at.slice(0, 10)})`);
|
|
597
|
+
}
|
|
464
598
|
}
|
|
465
599
|
});
|
|
466
600
|
const members = defineCommand({
|
|
@@ -531,7 +665,7 @@ const env = defineCommand({
|
|
|
531
665
|
return [pair.slice(0, i), pair.slice(i + 1)];
|
|
532
666
|
}));
|
|
533
667
|
const unset = toList(args.unset);
|
|
534
|
-
const { keys } = await (Object.keys(set).length || unset.length ? await api(`/v1/apps/${args.app}/env`, args, {
|
|
668
|
+
const { keys, warnings = [] } = await (Object.keys(set).length || unset.length ? await api(`/v1/apps/${args.app}/env`, args, {
|
|
535
669
|
method: "PATCH",
|
|
536
670
|
headers: { "content-type": "application/json" },
|
|
537
671
|
body: JSON.stringify({
|
|
@@ -540,13 +674,38 @@ const env = defineCommand({
|
|
|
540
674
|
})
|
|
541
675
|
}) : await api(`/v1/apps/${args.app}/env`, args)).json();
|
|
542
676
|
console.log(keys.length ? keys.join("\n") : "No variables set.");
|
|
677
|
+
for (const warning of warnings) console.error(styleText("yellow", `Warning: ${warning}`));
|
|
543
678
|
if (Object.keys(set).length || unset.length) console.log("Applies from the next request.");
|
|
544
679
|
}
|
|
545
680
|
});
|
|
681
|
+
const SIZE_FLAGS = [
|
|
682
|
+
"database",
|
|
683
|
+
"storage",
|
|
684
|
+
"releases"
|
|
685
|
+
];
|
|
686
|
+
/** Bytes as people read them: `1.5 GB`, `200 MB`; `no limit` for null. */
|
|
687
|
+
function size(bytes) {
|
|
688
|
+
if (bytes === null || bytes === void 0) return "no limit";
|
|
689
|
+
const units = [
|
|
690
|
+
"B",
|
|
691
|
+
"KB",
|
|
692
|
+
"MB",
|
|
693
|
+
"GB",
|
|
694
|
+
"TB"
|
|
695
|
+
];
|
|
696
|
+
let value = bytes;
|
|
697
|
+
let unit = 0;
|
|
698
|
+
while (value >= 1024 && unit < units.length - 1) {
|
|
699
|
+
value /= 1024;
|
|
700
|
+
unit++;
|
|
701
|
+
}
|
|
702
|
+
return `${Number(value.toFixed(value < 10 && unit ? 1 : 0))} ${units[unit]}`;
|
|
703
|
+
}
|
|
704
|
+
const describeLimits = (limits) => `${limits.apps ?? "no limit"} apps, database ${size(limits.database)} and files ${size(limits.storage)} per app, releases ${size(limits.releases)}`;
|
|
546
705
|
const accounts = defineCommand({
|
|
547
706
|
meta: {
|
|
548
707
|
name: "accounts",
|
|
549
|
-
description: "Admins: list accounts and their
|
|
708
|
+
description: "Admins: list accounts and their limits, or change them"
|
|
550
709
|
},
|
|
551
710
|
args: {
|
|
552
711
|
email: {
|
|
@@ -558,21 +717,35 @@ const accounts = defineCommand({
|
|
|
558
717
|
type: "string",
|
|
559
718
|
description: "Apps the account may own: a number, \"none\" for no limit, \"default\""
|
|
560
719
|
},
|
|
720
|
+
database: {
|
|
721
|
+
type: "string",
|
|
722
|
+
description: "Database size per app, e.g. 2GB; past it deploys are refused. \"none\", \"default\""
|
|
723
|
+
},
|
|
724
|
+
storage: {
|
|
725
|
+
type: "string",
|
|
726
|
+
description: "Files per app (bucket quota), e.g. 10GB. \"none\", \"default\""
|
|
727
|
+
},
|
|
728
|
+
releases: {
|
|
729
|
+
type: "string",
|
|
730
|
+
description: "Space all releases of their apps may take, e.g. 2GB. \"none\", \"default\""
|
|
731
|
+
},
|
|
561
732
|
...connection
|
|
562
733
|
},
|
|
563
734
|
async run({ args }) {
|
|
564
735
|
if (args.email) {
|
|
565
|
-
|
|
566
|
-
|
|
736
|
+
const change = {};
|
|
737
|
+
if (args.apps) change.apps = args.apps === "none" ? null : args.apps === "default" ? "default" : Number(args.apps);
|
|
738
|
+
for (const flag of SIZE_FLAGS) if (args[flag]) change[flag] = args[flag] === "none" ? null : args[flag];
|
|
739
|
+
if (!Object.keys(change).length) throw new Error("Pass --apps, --database, --storage or --releases (a value, none or default)");
|
|
567
740
|
const result = await (await api(`/v1/accounts/${encodeURIComponent(args.email)}`, args, {
|
|
568
741
|
method: "PATCH",
|
|
569
742
|
headers: { "content-type": "application/json" },
|
|
570
|
-
body: JSON.stringify(
|
|
743
|
+
body: JSON.stringify(change)
|
|
571
744
|
})).json();
|
|
572
|
-
return console.log(`${result.email}: ${result.limits
|
|
745
|
+
return console.log(`${result.email}: ${describeLimits(result.limits)}`);
|
|
573
746
|
}
|
|
574
747
|
const result = await (await api("/v1/accounts", args)).json();
|
|
575
|
-
console.log(`default: ${result.defaults
|
|
748
|
+
console.log(`default: ${describeLimits(result.defaults)}`);
|
|
576
749
|
for (const account of result.accounts) {
|
|
577
750
|
const list = account.apps.length ? ` ${account.apps.join(", ")}` : "";
|
|
578
751
|
if (account.admin) {
|
|
@@ -582,6 +755,7 @@ const accounts = defineCommand({
|
|
|
582
755
|
const limit = account.limits.apps ?? "no limit";
|
|
583
756
|
const own = "apps" in account.custom ? "" : " (default)";
|
|
584
757
|
console.log(`${account.email} ${account.apps.length}/${limit}${own}${list}`);
|
|
758
|
+
if (SIZE_FLAGS.some((flag) => flag in account.custom)) console.log(styleText("dim", ` database ${size(account.limits.database)}, files ${size(account.limits.storage)} per app, releases ${size(account.limits.releases)}`));
|
|
585
759
|
}
|
|
586
760
|
}
|
|
587
761
|
});
|
|
@@ -631,6 +805,122 @@ const tokens = defineCommand({
|
|
|
631
805
|
}
|
|
632
806
|
}
|
|
633
807
|
});
|
|
808
|
+
const token = defineCommand({
|
|
809
|
+
meta: {
|
|
810
|
+
name: "token",
|
|
811
|
+
description: "Mint a short-lived access token for your account on an app, to test a private app (curl, Playwright)"
|
|
812
|
+
},
|
|
813
|
+
args: {
|
|
814
|
+
app: {
|
|
815
|
+
type: "positional",
|
|
816
|
+
required: true,
|
|
817
|
+
description: "App name"
|
|
818
|
+
},
|
|
819
|
+
ttl: {
|
|
820
|
+
type: "string",
|
|
821
|
+
default: "15m",
|
|
822
|
+
description: "Lifetime, 1m to 1h (default: 15m)"
|
|
823
|
+
},
|
|
824
|
+
cookie: {
|
|
825
|
+
type: "boolean",
|
|
826
|
+
default: false,
|
|
827
|
+
description: "Print a cookie for Playwright's context.addCookies() instead of the token"
|
|
828
|
+
},
|
|
829
|
+
json: {
|
|
830
|
+
type: "boolean",
|
|
831
|
+
default: false,
|
|
832
|
+
description: "Print token, cookie and expiry as JSON"
|
|
833
|
+
},
|
|
834
|
+
...connection
|
|
835
|
+
},
|
|
836
|
+
async run({ args }) {
|
|
837
|
+
const minted = await (await api(`/v1/apps/${args.app}/agent-tokens`, args, {
|
|
838
|
+
method: "POST",
|
|
839
|
+
headers: { "content-type": "application/json" },
|
|
840
|
+
body: JSON.stringify({ ttl: args.ttl })
|
|
841
|
+
})).json();
|
|
842
|
+
if (args.json) return console.log(JSON.stringify(minted, null, 2));
|
|
843
|
+
console.log(args.cookie ? JSON.stringify(minted.cookie) : minted.token);
|
|
844
|
+
console.error(styleText("dim", `${minted.app} as ${minted.user} until ${minted.expiresAt} (token ${minted.id}). ` + (args.cookie ? `Playwright: await context.addCookies([cookie]), then open ${minted.url}` : `curl -H "Authorization: Bearer $TOKEN" ${minted.url}/api/…`)));
|
|
845
|
+
}
|
|
846
|
+
});
|
|
847
|
+
function printIdentity(status) {
|
|
848
|
+
if (!status.configured) console.log("Workload identity is not configured on this server.");
|
|
849
|
+
console.log(status.enabled ? `${status.app} has a workload identity (since ${status.enabledAt?.slice(0, 10)}, by ${status.enabledBy})` : `${status.app} has no workload identity`);
|
|
850
|
+
console.log(`issuer: ${status.issuer}`);
|
|
851
|
+
console.log(`subject: ${status.subject ?? "(the app has no owner yet)"}`);
|
|
852
|
+
console.log(`audiences: ${status.audiences.join(", ")}`);
|
|
853
|
+
}
|
|
854
|
+
const identityApp = (enabled) => defineCommand({
|
|
855
|
+
meta: {
|
|
856
|
+
name: enabled === void 0 ? "status" : enabled ? "enable" : "disable",
|
|
857
|
+
description: enabled === void 0 ? "Show an app's workload identity and the values a provider rule needs" : enabled ? "Owners: let the app get identity tokens, for provider rules that trust it" : "Owners: stop signing identity tokens for the app"
|
|
858
|
+
},
|
|
859
|
+
args: {
|
|
860
|
+
app: {
|
|
861
|
+
type: "positional",
|
|
862
|
+
required: true,
|
|
863
|
+
description: "App name"
|
|
864
|
+
},
|
|
865
|
+
...connection
|
|
866
|
+
},
|
|
867
|
+
async run({ args }) {
|
|
868
|
+
printIdentity(await (enabled === void 0 ? await api(`/v1/apps/${args.app}/identity`, args) : await api(`/v1/apps/${args.app}/identity`, args, {
|
|
869
|
+
method: "PATCH",
|
|
870
|
+
headers: { "content-type": "application/json" },
|
|
871
|
+
body: JSON.stringify({ enabled })
|
|
872
|
+
})).json());
|
|
873
|
+
}
|
|
874
|
+
});
|
|
875
|
+
const identity = defineCommand({
|
|
876
|
+
meta: {
|
|
877
|
+
name: "identity",
|
|
878
|
+
description: "Workload identity: apps authenticate to AI providers without API keys"
|
|
879
|
+
},
|
|
880
|
+
subCommands: {
|
|
881
|
+
status: identityApp(),
|
|
882
|
+
enable: identityApp(true),
|
|
883
|
+
disable: identityApp(false),
|
|
884
|
+
jwks: defineCommand({
|
|
885
|
+
meta: {
|
|
886
|
+
name: "jwks",
|
|
887
|
+
description: "Print the issuer's public keys, for providers that can't fetch them"
|
|
888
|
+
},
|
|
889
|
+
args: connection,
|
|
890
|
+
async run({ args }) {
|
|
891
|
+
const res = await api("/.well-known/jwks.json", args);
|
|
892
|
+
console.log(JSON.stringify(await res.json(), null, 2));
|
|
893
|
+
}
|
|
894
|
+
}),
|
|
895
|
+
keygen: defineCommand({
|
|
896
|
+
meta: {
|
|
897
|
+
name: "keygen",
|
|
898
|
+
description: "Print a new private signing key for WERVT_IDENTITY_KEY (runs locally)"
|
|
899
|
+
},
|
|
900
|
+
args: { kid: {
|
|
901
|
+
type: "string",
|
|
902
|
+
description: "Key id (default: wervt-<date>)"
|
|
903
|
+
} },
|
|
904
|
+
async run({ args }) {
|
|
905
|
+
const pair = await crypto.subtle.generateKey({
|
|
906
|
+
name: "ECDSA",
|
|
907
|
+
namedCurve: "P-256"
|
|
908
|
+
}, true, ["sign", "verify"]);
|
|
909
|
+
const { kty, crv, x, y, d } = await crypto.subtle.exportKey("jwk", pair.privateKey);
|
|
910
|
+
const kid = args.kid ?? `wervt-${(/* @__PURE__ */ new Date()).toISOString().slice(0, 10)}`;
|
|
911
|
+
console.log(JSON.stringify({
|
|
912
|
+
kty,
|
|
913
|
+
crv,
|
|
914
|
+
x,
|
|
915
|
+
y,
|
|
916
|
+
d,
|
|
917
|
+
kid
|
|
918
|
+
}));
|
|
919
|
+
console.error(styleText("dim", "Private: set it as WERVT_IDENTITY_KEY on control only, never in an app or a repo."));
|
|
920
|
+
}
|
|
921
|
+
})
|
|
922
|
+
}
|
|
923
|
+
});
|
|
634
924
|
const tasks = defineCommand({
|
|
635
925
|
meta: {
|
|
636
926
|
name: "tasks",
|
|
@@ -1019,6 +1309,7 @@ runMain(defineCommand({
|
|
|
1019
1309
|
deploy,
|
|
1020
1310
|
apps,
|
|
1021
1311
|
releases,
|
|
1312
|
+
audit,
|
|
1022
1313
|
logs,
|
|
1023
1314
|
activate,
|
|
1024
1315
|
rollback,
|
|
@@ -1028,6 +1319,8 @@ runMain(defineCommand({
|
|
|
1028
1319
|
accounts,
|
|
1029
1320
|
env,
|
|
1030
1321
|
tokens,
|
|
1322
|
+
token,
|
|
1323
|
+
identity,
|
|
1031
1324
|
tasks,
|
|
1032
1325
|
stats,
|
|
1033
1326
|
delete: remove,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wervt/cli",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.15",
|
|
4
4
|
"description": "Deploy and manage apps on wervt.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"agents",
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
"dependencies": {
|
|
31
31
|
"@clack/prompts": "^1.8.1",
|
|
32
32
|
"citty": "^0.2.2",
|
|
33
|
-
"modern-tar": "^0.8.
|
|
33
|
+
"modern-tar": "^0.8.6",
|
|
34
34
|
"nypm": "^0.6.10",
|
|
35
35
|
"tinyexec": "^1.3.1"
|
|
36
36
|
},
|
package/skill/SKILL.md
CHANGED
|
@@ -65,19 +65,38 @@ existing app, work in its repository instead.
|
|
|
65
65
|
pnpm exec wervt deploy # build, upload, migrate, activate
|
|
66
66
|
pnpm exec wervt access <name> --public # new apps are private (owner, members, admins)
|
|
67
67
|
pnpm exec wervt access <name> --add a@x.com,*@company.com # or let specific people in
|
|
68
|
+
pnpm exec wervt access <name> --invite # private, and people with access invite others (useWervtAccess)
|
|
69
|
+
pnpm exec wervt access <name> --invite-by owners # only owner, members and admins may invite
|
|
68
70
|
pnpm exec wervt members <name> --add a@x.com # who may deploy and manage it (the first deploy owns it)
|
|
69
|
-
pnpm exec wervt env <name> --set
|
|
71
|
+
pnpm exec wervt env <name> --set NUXT_API_KEY=… # runtimeConfig.apiKey, applied from the next request
|
|
70
72
|
```
|
|
71
73
|
|
|
72
74
|
- Ask before making an app public if it shows personal or private data. "Ship it" alone doesn't
|
|
73
75
|
decide that. Private, with the user's email allowed, is the safe default.
|
|
76
|
+
- App configuration and secrets go through Nuxt's `runtimeConfig`, never `process.env` in app
|
|
77
|
+
code: declare the key with a default in `nuxt.config.ts` (`runtimeConfig: { apiKey: '' }`), read
|
|
78
|
+
it with `useRuntimeConfig()` and set it as `NUXT_` + the key in SCREAMING_SNAKE_CASE
|
|
79
|
+
(`apiKey` → `NUXT_API_KEY`, `public.mapStyle` → `NUXT_PUBLIC_MAP_STYLE`). Keys missing from
|
|
80
|
+
`runtimeConfig` are ignored.
|
|
74
81
|
- Secrets for `wervt env` (API keys) come from the user. Ask them to run the command themselves
|
|
75
|
-
(`! pnpm exec wervt env <name> --set
|
|
82
|
+
(`! pnpm exec wervt env <name> --set NUXT_API_KEY=…`) instead of pasting the value to you.
|
|
76
83
|
- `wervt deploy` requests `/` once the release started and fails (exit 1, with the first logged
|
|
77
84
|
error) on a 5xx. Add the pages you changed: `--check /,/todos,/api/todos`. The checks are
|
|
78
85
|
anonymous, so pages behind a sign-in only show they don't crash without a user.
|
|
79
86
|
- Then open the URL `wervt deploy` printed and check
|
|
80
87
|
`pnpm exec wervt logs <name> 'level:error' --since 15m`. Fix and redeploy until both are clean.
|
|
88
|
+
- Check what's behind the login yourself, don't ask the user to. `wervt token` mints a token for
|
|
89
|
+
your CLI account on this app (15 minutes, `--ttl 1h` at most), and the app sees that account:
|
|
90
|
+
|
|
91
|
+
```bash
|
|
92
|
+
TOKEN=$(pnpm exec wervt token <name>)
|
|
93
|
+
curl -sH "Authorization: Bearer $TOKEN" <url>/api/… # protected API routes
|
|
94
|
+
pnpm exec wervt token <name> --cookie > /tmp/wervt-cookie.json # for a headless browser
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
In Playwright, `await context.addCookies([JSON.parse(cookieJson)])`, then open the URL to check
|
|
98
|
+
pages, icons and uploads. Works for owners, members and admins signed in with `wervt login`
|
|
99
|
+
(not with a deploy or read token). Never paste the token into files or commits.
|
|
81
100
|
|
|
82
101
|
## 5. Deploy on push (when the user wants a repository)
|
|
83
102
|
|
|
@@ -100,7 +119,8 @@ release lists the commit.
|
|
|
100
119
|
|
|
101
120
|
```bash
|
|
102
121
|
pnpm exec wervt apps # every app, active release, URL
|
|
103
|
-
pnpm exec wervt releases <name> # newest first, with commit
|
|
122
|
+
pnpm exec wervt releases <name> # newest first, with commit, author, failed deploys
|
|
123
|
+
pnpm exec wervt audit <name> # who deployed, rolled back, changed env or access
|
|
104
124
|
pnpm exec wervt logs <name> 'status:>=500' --since 1d --json
|
|
105
125
|
pnpm exec wervt tasks <name> # tasks, schedules, next runs
|
|
106
126
|
pnpm exec wervt tasks <name> --run <task> # run one now
|