@shipvo/cli 0.1.4 → 0.1.6

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.
Files changed (93) hide show
  1. package/README.md +3 -0
  2. package/dist/adopt.js +1 -2
  3. package/dist/agents.d.ts +29 -0
  4. package/dist/agents.js +183 -0
  5. package/dist/approvals.d.ts +9 -0
  6. package/dist/approvals.js +106 -0
  7. package/dist/audit.d.ts +13 -0
  8. package/dist/audit.js +92 -0
  9. package/dist/autohalt.d.ts +1 -1
  10. package/dist/autohalt.js +28 -16
  11. package/dist/bsdiff.d.ts +4 -0
  12. package/dist/bsdiff.js +465 -0
  13. package/dist/ci-comment.d.ts +38 -0
  14. package/dist/ci-comment.js +169 -0
  15. package/dist/ci-compare.d.ts +10 -0
  16. package/dist/ci-compare.js +25 -0
  17. package/dist/ci-shots.d.ts +9 -0
  18. package/dist/ci-shots.js +81 -0
  19. package/dist/ci.d.ts +11 -0
  20. package/dist/ci.js +100 -0
  21. package/dist/cli.js +51 -1
  22. package/dist/config.d.ts +15 -0
  23. package/dist/config.js +68 -13
  24. package/dist/doctor.d.ts +2 -0
  25. package/dist/doctor.js +88 -8
  26. package/dist/eas.d.ts +29 -0
  27. package/dist/eas.js +258 -0
  28. package/dist/ensure-playwright.d.ts +25 -0
  29. package/dist/ensure-playwright.js +69 -0
  30. package/dist/explore-issues.d.ts +12 -0
  31. package/dist/explore-issues.js +50 -0
  32. package/dist/explore.d.ts +160 -0
  33. package/dist/explore.js +496 -0
  34. package/dist/flows.d.ts +31 -0
  35. package/dist/flows.js +264 -0
  36. package/dist/help.d.ts +2 -2
  37. package/dist/help.js +37 -7
  38. package/dist/hermes.d.ts +10 -0
  39. package/dist/hermes.js +50 -0
  40. package/dist/init.d.ts +1 -0
  41. package/dist/init.js +71 -19
  42. package/dist/json-out.d.ts +53 -0
  43. package/dist/json-out.js +91 -0
  44. package/dist/keys-json.d.ts +1 -0
  45. package/dist/keys-json.js +71 -0
  46. package/dist/login.js +27 -11
  47. package/dist/lumi.d.ts +104 -0
  48. package/dist/lumi.js +364 -0
  49. package/dist/migrate.d.ts +27 -0
  50. package/dist/migrate.js +521 -0
  51. package/dist/policy.d.ts +2 -16
  52. package/dist/policy.js +2 -18
  53. package/dist/preview-recorder.d.ts +1 -0
  54. package/dist/preview-recorder.js +62 -0
  55. package/dist/preview.d.ts +4 -3
  56. package/dist/preview.js +114 -15
  57. package/dist/promote.d.ts +1 -1
  58. package/dist/promote.js +63 -14
  59. package/dist/publish.d.ts +14 -2
  60. package/dist/publish.js +131 -56
  61. package/dist/qr.d.ts +9 -0
  62. package/dist/qr.js +213 -0
  63. package/dist/report-md.d.ts +27 -0
  64. package/dist/report-md.js +58 -0
  65. package/dist/rotation.d.ts +57 -0
  66. package/dist/rotation.js +157 -0
  67. package/dist/run-bridge.d.ts +7 -0
  68. package/dist/run-bridge.js +728 -0
  69. package/dist/run-net.d.ts +39 -0
  70. package/dist/run-net.js +266 -0
  71. package/dist/run-protocol.d.ts +32 -0
  72. package/dist/run-protocol.js +22 -0
  73. package/dist/run-ws.d.ts +8 -0
  74. package/dist/run-ws.js +89 -0
  75. package/dist/run.d.ts +109 -0
  76. package/dist/run.js +385 -0
  77. package/dist/signing.js +13 -0
  78. package/dist/sim.d.ts +4 -0
  79. package/dist/sim.js +25 -7
  80. package/dist/smoke.d.ts +28 -0
  81. package/dist/smoke.js +117 -0
  82. package/dist/spawn-bin.d.ts +11 -0
  83. package/dist/spawn-bin.js +21 -0
  84. package/dist/status.d.ts +71 -1
  85. package/dist/status.js +162 -5
  86. package/dist/store.d.ts +36 -0
  87. package/dist/store.js +266 -3
  88. package/dist/templates/capacitor/github-actions/build.yml +122 -0
  89. package/dist/templates/github-action/shipvo-check.yml +18 -0
  90. package/dist/test.d.ts +46 -0
  91. package/dist/test.js +253 -0
  92. package/package.json +7 -4
  93. package/skills/shipvo-cli/SKILL.md +31 -0
@@ -0,0 +1,58 @@
1
+ const one = (s) => s.replace(/\s+/g, " ").trim();
2
+ /** Inline code that survives backticks in the text. */
3
+ function code(s) {
4
+ const t = one(s);
5
+ const fence = "`".repeat(Math.max(0, ...[...t.matchAll(/`+/g)].map((m) => m[0].length)) + 1);
6
+ return `${fence}${t.startsWith("`") || t.endsWith("`") ? ` ${t} ` : t}${fence}`;
7
+ }
8
+ export function stepText(s) {
9
+ const kind = s.kind ?? "step";
10
+ if (kind === "tap")
11
+ return `tap "${s.target ?? ""}"`;
12
+ if (kind === "type")
13
+ return `type "${s.text ?? ""}" into "${s.target ?? ""}"${s.enter ? " and press enter" : ""}`;
14
+ const arg = s.text ?? s.target;
15
+ return arg !== undefined ? `${kind} "${arg}"` : kind;
16
+ }
17
+ function block(s) {
18
+ const fence = "`".repeat(Math.max(2, ...[...s.matchAll(/`+/g)].map((m) => m[0].length)) + 1);
19
+ return `${fence}\n${s}\n${fence}`;
20
+ }
21
+ export function renderReportMarkdown(report) {
22
+ const flows = Array.isArray(report.flows) ? report.flows : [];
23
+ const failed = flows.filter((f) => f.status === "failed");
24
+ const out = [`# Shipvo test report: ${failed.length ? "failed" : "all flows passed"}`, ""];
25
+ const meta = [];
26
+ if (report.app)
27
+ meta.push(`- App: ${code(report.app)}`);
28
+ meta.push(`- Release: ${report.release ? code(report.release) : "not set"}`);
29
+ meta.push(`- Commit: ${report.commit ? code(report.commit) : "unknown"}`);
30
+ if (report.time)
31
+ meta.push(`- Time: ${report.time}`);
32
+ meta.push(`- Flows: ${flows.length - failed.length} passed, ${failed.length} failed`);
33
+ out.push(...meta, "");
34
+ if (!failed.length)
35
+ out.push(flows.length ? `All ${flows.length} flow${flows.length === 1 ? "" : "s"} passed.` : "No flows were run.", "");
36
+ for (const f of failed) {
37
+ out.push(`## Failing flow: ${code(f.name ?? "unnamed flow")}`, "");
38
+ const bad = f.failedStep;
39
+ if (bad)
40
+ out.push(`Failed step${bad.line ? ` (line ${bad.line})` : ""}: ${code(stepText(bad))}`, "");
41
+ if (f.message)
42
+ out.push("Error:", "", block(f.message), "");
43
+ const shots = [...(f.screenshots ?? []), ...(f.screenshot ? [f.screenshot] : [])];
44
+ if (shots.length)
45
+ out.push(`Screenshots: ${shots.map(code).join(", ")}`, "");
46
+ if (f.steps?.length) {
47
+ out.push("Steps:", "");
48
+ f.steps.forEach((s, i) => {
49
+ const mark = bad && s.line !== undefined && s.line === bad.line ? " ← failed" : "";
50
+ out.push(`${i + 1}. ${code(stepText(s))}${mark}`);
51
+ });
52
+ out.push("");
53
+ }
54
+ }
55
+ if (report.parityLimits?.length)
56
+ out.push("## Parity limits", "", ...report.parityLimits.map((l) => `- ${one(l)}`), "");
57
+ return out.join("\n");
58
+ }
@@ -0,0 +1,57 @@
1
+ export type RotationEnvelope = {
2
+ body: string;
3
+ signature: string;
4
+ };
5
+ export type RotationStatement = {
6
+ type: "key-rotation";
7
+ appId: string;
8
+ sequence: number;
9
+ newKey: string;
10
+ validFrom: string;
11
+ };
12
+ export type RotationFailure = "bad_envelope" | "bad_signature" | "bad_body" | "wrong_type" | "wrong_app" | "bad_sequence" | "replay" | "too_far" | "bad_key";
13
+ export type RotationResult = {
14
+ ok: true;
15
+ statement: RotationStatement;
16
+ } | {
17
+ ok: false;
18
+ reason: RotationFailure;
19
+ };
20
+ export declare const MAX_BODY_BYTES = 1024;
21
+ export declare const MAX_PENDING_DAYS = 90;
22
+ /** The body string. Key order is fixed; the signer keeps these exact bytes. */
23
+ export declare function buildStatementBody(s: {
24
+ appId: string;
25
+ sequence: number;
26
+ newKey: string;
27
+ validFrom: string;
28
+ }): string;
29
+ /** Sign the exact UTF-8 bytes of `body` with the root seed. */
30
+ export declare function signStatement(s: {
31
+ appId: string;
32
+ sequence: number;
33
+ newKey: string;
34
+ validFrom: string;
35
+ }, rootSeedB64: string): RotationEnvelope;
36
+ /**
37
+ * Check one envelope in the order of design section 8, then section 3.
38
+ * `effectiveSequence` is max(stored sequence, build minimum). `now` is the runtime clock.
39
+ */
40
+ export declare function verifyStatement(env: unknown, rootPublicKeyB64: string, ctx: {
41
+ appId: string;
42
+ effectiveSequence: number;
43
+ now: Date;
44
+ }): RotationResult;
45
+ export declare class RotateError extends Error {
46
+ }
47
+ type Cfg = {
48
+ appId: string;
49
+ controlPlane: string;
50
+ };
51
+ export declare const ROTATE_HELP = "Usage:\n shipvo keys rotate sign --root-seed-file <file> --new-key <public key> --sequence <n> [--valid-from <ISO time>] [--json]\n shipvo keys rotate upload <envelope.json> [--json]\n\nsign works offline. It reads the ROOT seed from a file (never the release seed) and prints the signed envelope.\n--valid-from defaults to now (an emergency rotation). It may be at most 90 days ahead.\nPick a sequence above every statement you signed before. To override a planned statement, use a higher one.\nupload needs a signed-in owner session (shipvo login). Tokens are refused by the control plane.\n";
52
+ /** Runs `shipvo keys rotate ...`. Throws RotateError for user mistakes; the caller prints it and exits 1. */
53
+ export declare function rotate(argv: string[], cfg: Cfg, deps?: {
54
+ fetchFn?: typeof fetch;
55
+ now?: () => Date;
56
+ }): Promise<void>;
57
+ export {};
@@ -0,0 +1,157 @@
1
+ // Root-signed key rotation statements (docs/design/key-rotation.md, sections 3, 6 and 8).
2
+ // `signStatement` and `verifyStatement` follow the wire format that the Android and iOS runtimes will check.
3
+ import * as ed from "@noble/ed25519";
4
+ import { readFileSync } from "node:fs";
5
+ import { b64, keyFromSeed, unb64 } from "./index.js";
6
+ import { args } from "./config.js";
7
+ import { savedCreds } from "./login.js";
8
+ export const MAX_BODY_BYTES = 1024;
9
+ export const MAX_PENDING_DAYS = 90;
10
+ const enc = new TextEncoder();
11
+ /** The body string. Key order is fixed; the signer keeps these exact bytes. */
12
+ export function buildStatementBody(s) {
13
+ if (!Number.isSafeInteger(s.sequence) || s.sequence < 1)
14
+ throw new Error("sequence must be an integer, at least 1 and at most 9007199254740991");
15
+ if (unb64(s.newKey).length !== 32)
16
+ throw new Error("the new release key must be a 32-byte Ed25519 public key (base64)");
17
+ const t = Date.parse(s.validFrom);
18
+ if (Number.isNaN(t))
19
+ throw new Error("validFrom must be an ISO 8601 UTC time");
20
+ return JSON.stringify({ type: "key-rotation", appId: s.appId, sequence: s.sequence, newKey: s.newKey, validFrom: new Date(t).toISOString() });
21
+ }
22
+ /** Sign the exact UTF-8 bytes of `body` with the root seed. */
23
+ export function signStatement(s, rootSeedB64) {
24
+ const body = buildStatementBody(s);
25
+ const { seed } = keyFromSeed(rootSeedB64);
26
+ return { body, signature: b64(ed.sign(enc.encode(body), seed)) };
27
+ }
28
+ /**
29
+ * Check one envelope in the order of design section 8, then section 3.
30
+ * `effectiveSequence` is max(stored sequence, build minimum). `now` is the runtime clock.
31
+ */
32
+ export function verifyStatement(env, rootPublicKeyB64, ctx) {
33
+ const e = env;
34
+ if (!e || typeof e.body !== "string" || typeof e.signature !== "string" || enc.encode(e.body).length > MAX_BODY_BYTES)
35
+ return { ok: false, reason: "bad_envelope" };
36
+ let good = false;
37
+ try {
38
+ good = ed.verify(unb64(e.signature), enc.encode(e.body), unb64(rootPublicKeyB64));
39
+ }
40
+ catch {
41
+ good = false;
42
+ }
43
+ if (!good)
44
+ return { ok: false, reason: "bad_signature" };
45
+ let s;
46
+ try {
47
+ s = JSON.parse(e.body);
48
+ }
49
+ catch {
50
+ return { ok: false, reason: "bad_body" };
51
+ }
52
+ if (!s || typeof s !== "object" || Array.isArray(s))
53
+ return { ok: false, reason: "bad_body" };
54
+ if (s.type !== "key-rotation")
55
+ return { ok: false, reason: "wrong_type" };
56
+ if (s.appId !== ctx.appId)
57
+ return { ok: false, reason: "wrong_app" };
58
+ if (!Number.isSafeInteger(s.sequence) || s.sequence < 1)
59
+ return { ok: false, reason: "bad_sequence" };
60
+ if (s.sequence <= ctx.effectiveSequence)
61
+ return { ok: false, reason: "replay" };
62
+ const from = typeof s.validFrom === "string" ? Date.parse(s.validFrom) : NaN;
63
+ if (Number.isNaN(from))
64
+ return { ok: false, reason: "bad_body" };
65
+ if (from > ctx.now.getTime() + MAX_PENDING_DAYS * 86_400_000)
66
+ return { ok: false, reason: "too_far" };
67
+ if (typeof s.newKey !== "string" || unb64(s.newKey).length !== 32)
68
+ return { ok: false, reason: "bad_key" };
69
+ return { ok: true, statement: s };
70
+ }
71
+ export class RotateError extends Error {
72
+ }
73
+ export const ROTATE_HELP = `Usage:
74
+ shipvo keys rotate sign --root-seed-file <file> --new-key <public key> --sequence <n> [--valid-from <ISO time>] [--json]
75
+ shipvo keys rotate upload <envelope.json> [--json]
76
+
77
+ sign works offline. It reads the ROOT seed from a file (never the release seed) and prints the signed envelope.
78
+ --valid-from defaults to now (an emergency rotation). It may be at most ${MAX_PENDING_DAYS} days ahead.
79
+ Pick a sequence above every statement you signed before. To override a planned statement, use a higher one.
80
+ upload needs a signed-in owner session (shipvo login). Tokens are refused by the control plane.
81
+ `;
82
+ /** Runs `shipvo keys rotate ...`. Throws RotateError for user mistakes; the caller prints it and exits 1. */
83
+ export async function rotate(argv, cfg, deps = {}) {
84
+ const [sub, ...rest] = argv, a = args(rest), json = a.json === "true";
85
+ const now = (deps.now ?? (() => new Date()))();
86
+ if (sub === "sign") {
87
+ const file = a["root-seed-file"], newKey = a["new-key"], seq = a.sequence;
88
+ if (!file || file === "true" || !newKey || newKey === "true" || !seq || seq === "true")
89
+ throw new RotateError(ROTATE_HELP);
90
+ let rootSeed;
91
+ try {
92
+ rootSeed = readFileSync(file, "utf8").trim();
93
+ }
94
+ catch {
95
+ throw new RotateError(`Cannot read the root seed file ${file}.`);
96
+ }
97
+ try {
98
+ keyFromSeed(rootSeed);
99
+ }
100
+ catch {
101
+ throw new RotateError("The root seed is not valid: it must be a 32-byte value in base64.");
102
+ }
103
+ if (!/^\d+$/.test(seq) || !Number.isSafeInteger(Number(seq)) || Number(seq) < 1)
104
+ throw new RotateError("--sequence must be a whole number from 1 to 9007199254740991.");
105
+ const validFrom = a["valid-from"] && a["valid-from"] !== "true" ? a["valid-from"] : now.toISOString();
106
+ const t = Date.parse(validFrom);
107
+ if (Number.isNaN(t))
108
+ throw new RotateError("--valid-from must be an ISO 8601 UTC time, such as 2026-11-06T00:00:00Z.");
109
+ if (t > now.getTime() + MAX_PENDING_DAYS * 86_400_000)
110
+ throw new RotateError(`--valid-from is more than ${MAX_PENDING_DAYS} days ahead. Phones would reject it.`);
111
+ let env;
112
+ try {
113
+ env = signStatement({ appId: cfg.appId, sequence: Number(seq), newKey, validFrom }, rootSeed);
114
+ }
115
+ catch (e) {
116
+ throw new RotateError(e instanceof Error ? e.message : String(e));
117
+ }
118
+ console.log(JSON.stringify(env, null, json ? 0 : 2));
119
+ if (!json)
120
+ console.error(`Signed rotation statement ${seq} for "${cfg.appId}". Confirm ${seq} is above every statement you signed before; this offline tool cannot check. Then run: shipvo keys rotate upload <file>`);
121
+ return;
122
+ }
123
+ if (sub === "upload") {
124
+ const file = rest.find((x) => !x.startsWith("--"));
125
+ if (!file)
126
+ throw new RotateError(ROTATE_HELP);
127
+ let env;
128
+ try {
129
+ env = JSON.parse(readFileSync(file === "-" ? 0 : file, "utf8"));
130
+ }
131
+ catch {
132
+ throw new RotateError(`${file} is not a readable JSON envelope.`);
133
+ }
134
+ if (typeof env?.body !== "string" || typeof env?.signature !== "string")
135
+ throw new RotateError("The envelope needs a string \"body\" and a string \"signature\".");
136
+ const creds = savedCreds();
137
+ if (!creds?.key)
138
+ throw new RotateError("Not signed in. Run `shipvo login` as the app owner. Tokens cannot upload rotation statements.");
139
+ const norm = (u) => u.replace(/\/$/, "");
140
+ if (norm(creds.control ?? "") !== norm(cfg.controlPlane))
141
+ throw new RotateError(`shipvo.json controlPlane is ${cfg.controlPlane}, but you signed in at ${creds.control || "another host"}. Not sending your session key there. Fix controlPlane or run \`shipvo login\` for that host.`);
142
+ const url = `${cfg.controlPlane.replace(/\/$/, "")}/rotation/${encodeURIComponent(cfg.appId)}`;
143
+ let r;
144
+ try {
145
+ r = await (deps.fetchFn ?? fetch)(url, { method: "POST", headers: { authorization: `Bearer ${creds.key}`, "content-type": "application/json" }, body: JSON.stringify({ body: env.body, signature: env.signature }) });
146
+ }
147
+ catch (e) {
148
+ throw new RotateError(`Could not reach the control plane at ${cfg.controlPlane} (${e instanceof Error ? e.message : String(e)}).`);
149
+ }
150
+ const out = await r.json().catch(() => ({}));
151
+ if (!r.ok)
152
+ throw new RotateError(`The control plane refused the statement (HTTP ${r.status}): ${out.message ?? out.reason ?? out.error ?? "no reason given"}`);
153
+ console.log(json ? JSON.stringify({ ok: true, ...out }) : `Uploaded. Phones switch when they next fetch it.`);
154
+ return;
155
+ }
156
+ throw new RotateError(ROTATE_HELP);
157
+ }
@@ -0,0 +1,7 @@
1
+ /**
2
+ * The shared screen rules (`@shipvo/sim`), as source that the page can run. Same copy the simulator uses.
3
+ * Only plain `export function` / `export const` lines are supported: `export default` and `export {…}` would not be stripped.
4
+ */
5
+ export declare function screenRulesSource(): string;
6
+ /** The script text served at `/__shipvo/bridge.js`. It reads the session token from its own `data-token` attribute and then removes the attribute. */
7
+ export declare function bridgeScript(rules?: string): string;