@wervt/cli 0.2.14 → 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 +295 -13
- package/package.json +2 -2
- package/skill/SKILL.md +22 -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
|
});
|
|
@@ -441,6 +558,10 @@ const access = defineCommand({
|
|
|
441
558
|
type: "boolean",
|
|
442
559
|
description: "Make the app private, and let people with access invite others in the app"
|
|
443
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
|
+
},
|
|
444
565
|
add: {
|
|
445
566
|
type: "string",
|
|
446
567
|
description: "Allow emails or *@domain (comma separated)"
|
|
@@ -456,6 +577,7 @@ const access = defineCommand({
|
|
|
456
577
|
if (args.public) change.visibility = "public";
|
|
457
578
|
if (args.private) change.visibility = "private";
|
|
458
579
|
if (args.invite) change.visibility = "invite";
|
|
580
|
+
if (args["invite-by"]) change.inviteBy = args["invite-by"];
|
|
459
581
|
if (args.add) change.add = toList(args.add);
|
|
460
582
|
if (args.remove) change.remove = toList(args.remove);
|
|
461
583
|
const result = await (Object.keys(change).length ? await api(`/v1/apps/${args.app}/access`, args, {
|
|
@@ -468,6 +590,7 @@ const access = defineCommand({
|
|
|
468
590
|
if (result.visibility === "public") return;
|
|
469
591
|
console.log(result.allow.length ? `allowed: ${result.allow.join(", ")}` : "allowed: admins only");
|
|
470
592
|
if (result.visibility === "invite") {
|
|
593
|
+
console.log(result.inviteBy === "owners" ? "who invites: the owner, members and admins" : "who invites: everyone with access");
|
|
471
594
|
const invited = result.invited ?? [];
|
|
472
595
|
console.log(invited.length ? "invited:" : "invited: nobody yet");
|
|
473
596
|
for (const invite of invited) console.log(` ${invite.email} (by ${invite.by}, ${invite.at.slice(0, 10)})`);
|
|
@@ -542,7 +665,7 @@ const env = defineCommand({
|
|
|
542
665
|
return [pair.slice(0, i), pair.slice(i + 1)];
|
|
543
666
|
}));
|
|
544
667
|
const unset = toList(args.unset);
|
|
545
|
-
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, {
|
|
546
669
|
method: "PATCH",
|
|
547
670
|
headers: { "content-type": "application/json" },
|
|
548
671
|
body: JSON.stringify({
|
|
@@ -551,13 +674,38 @@ const env = defineCommand({
|
|
|
551
674
|
})
|
|
552
675
|
}) : await api(`/v1/apps/${args.app}/env`, args)).json();
|
|
553
676
|
console.log(keys.length ? keys.join("\n") : "No variables set.");
|
|
677
|
+
for (const warning of warnings) console.error(styleText("yellow", `Warning: ${warning}`));
|
|
554
678
|
if (Object.keys(set).length || unset.length) console.log("Applies from the next request.");
|
|
555
679
|
}
|
|
556
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)}`;
|
|
557
705
|
const accounts = defineCommand({
|
|
558
706
|
meta: {
|
|
559
707
|
name: "accounts",
|
|
560
|
-
description: "Admins: list accounts and their
|
|
708
|
+
description: "Admins: list accounts and their limits, or change them"
|
|
561
709
|
},
|
|
562
710
|
args: {
|
|
563
711
|
email: {
|
|
@@ -569,21 +717,35 @@ const accounts = defineCommand({
|
|
|
569
717
|
type: "string",
|
|
570
718
|
description: "Apps the account may own: a number, \"none\" for no limit, \"default\""
|
|
571
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
|
+
},
|
|
572
732
|
...connection
|
|
573
733
|
},
|
|
574
734
|
async run({ args }) {
|
|
575
735
|
if (args.email) {
|
|
576
|
-
|
|
577
|
-
|
|
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)");
|
|
578
740
|
const result = await (await api(`/v1/accounts/${encodeURIComponent(args.email)}`, args, {
|
|
579
741
|
method: "PATCH",
|
|
580
742
|
headers: { "content-type": "application/json" },
|
|
581
|
-
body: JSON.stringify(
|
|
743
|
+
body: JSON.stringify(change)
|
|
582
744
|
})).json();
|
|
583
|
-
return console.log(`${result.email}: ${result.limits
|
|
745
|
+
return console.log(`${result.email}: ${describeLimits(result.limits)}`);
|
|
584
746
|
}
|
|
585
747
|
const result = await (await api("/v1/accounts", args)).json();
|
|
586
|
-
console.log(`default: ${result.defaults
|
|
748
|
+
console.log(`default: ${describeLimits(result.defaults)}`);
|
|
587
749
|
for (const account of result.accounts) {
|
|
588
750
|
const list = account.apps.length ? ` ${account.apps.join(", ")}` : "";
|
|
589
751
|
if (account.admin) {
|
|
@@ -593,6 +755,7 @@ const accounts = defineCommand({
|
|
|
593
755
|
const limit = account.limits.apps ?? "no limit";
|
|
594
756
|
const own = "apps" in account.custom ? "" : " (default)";
|
|
595
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)}`));
|
|
596
759
|
}
|
|
597
760
|
}
|
|
598
761
|
});
|
|
@@ -642,6 +805,122 @@ const tokens = defineCommand({
|
|
|
642
805
|
}
|
|
643
806
|
}
|
|
644
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
|
+
});
|
|
645
924
|
const tasks = defineCommand({
|
|
646
925
|
meta: {
|
|
647
926
|
name: "tasks",
|
|
@@ -1030,6 +1309,7 @@ runMain(defineCommand({
|
|
|
1030
1309
|
deploy,
|
|
1031
1310
|
apps,
|
|
1032
1311
|
releases,
|
|
1312
|
+
audit,
|
|
1033
1313
|
logs,
|
|
1034
1314
|
activate,
|
|
1035
1315
|
rollback,
|
|
@@ -1039,6 +1319,8 @@ runMain(defineCommand({
|
|
|
1039
1319
|
accounts,
|
|
1040
1320
|
env,
|
|
1041
1321
|
tokens,
|
|
1322
|
+
token,
|
|
1323
|
+
identity,
|
|
1042
1324
|
tasks,
|
|
1043
1325
|
stats,
|
|
1044
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
|
@@ -66,19 +66,37 @@ pnpm exec wervt deploy # build, upload, migrate, acti
|
|
|
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
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
|
|
69
70
|
pnpm exec wervt members <name> --add a@x.com # who may deploy and manage it (the first deploy owns it)
|
|
70
|
-
pnpm exec wervt env <name> --set
|
|
71
|
+
pnpm exec wervt env <name> --set NUXT_API_KEY=… # runtimeConfig.apiKey, applied from the next request
|
|
71
72
|
```
|
|
72
73
|
|
|
73
74
|
- Ask before making an app public if it shows personal or private data. "Ship it" alone doesn't
|
|
74
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.
|
|
75
81
|
- Secrets for `wervt env` (API keys) come from the user. Ask them to run the command themselves
|
|
76
|
-
(`! pnpm exec wervt env <name> --set
|
|
82
|
+
(`! pnpm exec wervt env <name> --set NUXT_API_KEY=…`) instead of pasting the value to you.
|
|
77
83
|
- `wervt deploy` requests `/` once the release started and fails (exit 1, with the first logged
|
|
78
84
|
error) on a 5xx. Add the pages you changed: `--check /,/todos,/api/todos`. The checks are
|
|
79
85
|
anonymous, so pages behind a sign-in only show they don't crash without a user.
|
|
80
86
|
- Then open the URL `wervt deploy` printed and check
|
|
81
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.
|
|
82
100
|
|
|
83
101
|
## 5. Deploy on push (when the user wants a repository)
|
|
84
102
|
|
|
@@ -101,7 +119,8 @@ release lists the commit.
|
|
|
101
119
|
|
|
102
120
|
```bash
|
|
103
121
|
pnpm exec wervt apps # every app, active release, URL
|
|
104
|
-
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
|
|
105
124
|
pnpm exec wervt logs <name> 'status:>=500' --since 1d --json
|
|
106
125
|
pnpm exec wervt tasks <name> # tasks, schedules, next runs
|
|
107
126
|
pnpm exec wervt tasks <name> --run <task> # run one now
|