@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
package/dist/smoke.js ADDED
@@ -0,0 +1,117 @@
1
+ // `shipvo smoke`: does the app load? Starts the simulator (startSim), opens the app, waits for the first screen to settle.
2
+ import { existsSync } from "node:fs";
3
+ import { args as parse, BOOLEAN_FLAGS, loadConfig } from "./config.js";
4
+ import { simDriver, startSim } from "./explore.js";
5
+ import { detectPackageManager, installCommand } from "./ensure-playwright.js";
6
+ import { emit, envelope } from "./json-out.js";
7
+ const MAX_ERRORS = 5;
8
+ /** Elements in an accessibility tree: lines that carry a ref. */
9
+ export const countElements = (tree) => (tree ?? "").split("\n").filter((l) => /\[ref=/.test(l)).length;
10
+ async function screenOf(base, session) {
11
+ const r = await fetch(base.replace(/\/$/, "") + "/act", { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ session, action: "screen" }), signal: AbortSignal.timeout(30000) });
12
+ if (!r.ok)
13
+ throw new Error(`simulator /act -> ${r.status}`);
14
+ return (await r.json()).screen ?? "";
15
+ }
16
+ const installNext = () => { const { cmd, args } = installCommand(detectPackageManager(process.cwd(), existsSync)); return { command: `${cmd} ${args.join(" ")} && npx playwright install chromium`, why: "Install the browser the simulator needs." }; };
17
+ /** Words that are not flags or flag values: the platform. */
18
+ const positionals = (argv) => { const out = []; for (let i = 0; i < argv.length; i++) {
19
+ const x = argv[i];
20
+ if (x.startsWith("--")) {
21
+ if (!BOOLEAN_FLAGS.has(x.slice(2)) && argv[i + 1] !== undefined && !argv[i + 1].startsWith("--"))
22
+ i++;
23
+ }
24
+ else
25
+ out.push(x);
26
+ } return out; };
27
+ /** Runs the check and returns the envelope and exit code. Never prints. */
28
+ export async function smokeCheck(argv, deps) {
29
+ const a = parse(argv);
30
+ const setup = (msg, next = [{ command: "shipvo doctor --json", why: "Check the setup." }]) => ({ doc: envelope("setup_error", msg, {}, next, 2), exit: 2 });
31
+ const pos = positionals(argv);
32
+ if (pos.length > 1 || (pos[0] !== undefined && pos[0] !== "android" && pos[0] !== "ios"))
33
+ return setup(`Unknown platform "${pos.join(" ")}". Use android or ios.`, [{ command: "shipvo smoke android", why: "Pick android or ios." }, { command: "shipvo smoke ios", why: "Or the iPhone style." }]);
34
+ const platform = pos[0] === "ios" ? "ios" : "android";
35
+ const t = a.timeout === undefined ? 30 : Number(a.timeout);
36
+ if (!(t > 0) || !Number.isFinite(t))
37
+ return setup("--timeout must be a number of seconds above 0", [{ command: "shipvo smoke --timeout 30", why: "Use a positive number." }]);
38
+ let cfg;
39
+ try {
40
+ cfg = loadConfig();
41
+ }
42
+ catch (e) {
43
+ return setup(e instanceof Error ? e.message : String(e), [{ command: "shipvo init .", why: "Set up this folder." }]);
44
+ }
45
+ const json = argv.includes("--json");
46
+ let sim;
47
+ try {
48
+ sim = await (deps?.start ?? (() => startSim(a, cfg, json)))();
49
+ }
50
+ catch (e) {
51
+ const msg = e instanceof Error ? e.message : String(e);
52
+ const browser = /playwright|chromium|browser|exited before it was ready/i.test(msg);
53
+ return setup(msg, browser ? [installNext(), { command: "shipvo doctor --json", why: "Check the setup." }] : undefined);
54
+ }
55
+ let timer;
56
+ try {
57
+ const session = `smoke-${Date.now().toString(36)}`;
58
+ const driver = (deps?.driver ?? simDriver)(sim.base, cfg.appId, session, platform);
59
+ const get = deps?.screen ?? screenOf;
60
+ const started = Date.now();
61
+ const timedOut = new Promise((ok) => { timer = setTimeout(() => ok("timeout"), t * 1000); });
62
+ const run = (async () => {
63
+ const opened = await driver.open();
64
+ if (!opened.ok)
65
+ throw new Error(opened.error ?? "the simulator could not open the app");
66
+ let tree = opened.screen ?? "";
67
+ // Settled: two snapshots in a row are the same.
68
+ for (let i = 0; i < 20; i++) {
69
+ const next = await get(sim.base, session);
70
+ const same = next === tree;
71
+ tree = next;
72
+ if (same)
73
+ break;
74
+ await new Promise((r) => setTimeout(r, 100));
75
+ }
76
+ return { ms: Date.now() - started, tree, errors: await driver.errors() };
77
+ })();
78
+ run.catch(() => { });
79
+ const r = await Promise.race([run, timedOut]);
80
+ if (r === "timeout") {
81
+ const data = { platform, time_to_first_screen_ms: null, elements: 0, errors: [], error_count: 0 };
82
+ return { doc: envelope("timeout", `The app did not show a first screen within ${t} s`, data, [{ command: `shipvo smoke ${platform} --timeout ${Math.ceil(t * 2)} --json`, why: "Try a longer timeout." }], 1), exit: 1 };
83
+ }
84
+ const elements = countElements(r.tree);
85
+ const data = { platform, time_to_first_screen_ms: r.ms, elements, errors: r.errors.slice(0, MAX_ERRORS), error_count: r.errors.length };
86
+ if (r.errors.length)
87
+ return { doc: envelope("errors", `The app threw ${r.errors.length} error${r.errors.length === 1 ? "" : "s"} while loading: ${r.errors[0]}`, data, [{ command: `shipvo smoke ${platform} --json`, why: "Run again after fixing." }], 1), exit: 1 };
88
+ if (!elements)
89
+ return { doc: envelope("empty_screen", "The first screen has no elements", data, [{ command: `shipvo smoke ${platform} --json`, why: "Run again after fixing." }], 1), exit: 1 };
90
+ return { doc: envelope("ok", `The app loads on ${platform}: first screen in ${r.ms} ms, ${elements} element${elements === 1 ? "" : "s"}, no errors`, data, [{ command: "shipvo test --json", why: "Run your step files." }], 0), exit: 0 };
91
+ }
92
+ catch (e) {
93
+ return setup(e instanceof Error ? e.message : String(e));
94
+ }
95
+ finally {
96
+ clearTimeout(timer);
97
+ sim.stop();
98
+ }
99
+ }
100
+ /** Returns the exit code: 0 loads, 1 failed, 2 setup error. */
101
+ export async function smoke(argv) {
102
+ const { doc, exit } = await smokeCheck(argv);
103
+ if (argv.includes("--json"))
104
+ emit(doc);
105
+ else {
106
+ console.log(doc.summary);
107
+ const d = doc.data;
108
+ for (const e of d.errors ?? [])
109
+ console.log(` error: ${e}`);
110
+ if ((d.error_count ?? 0) > (d.errors?.length ?? 0))
111
+ console.log(` …and ${d.error_count - d.errors.length} more`);
112
+ if (exit)
113
+ for (const n of doc.next)
114
+ console.error(`Next: ${n.command} (${n.why})`);
115
+ }
116
+ return exit;
117
+ }
@@ -0,0 +1,11 @@
1
+ export interface SpawnCall {
2
+ command: string;
3
+ args: string[];
4
+ shell?: boolean;
5
+ }
6
+ /**
7
+ * Build the arguments for `spawn`. On Windows, npx, npm and pnpm are `.cmd`
8
+ * shims, which need a shell, so the args are quoted for cmd.exe.
9
+ * On other platforms the call is returned unchanged.
10
+ */
11
+ export declare function spawnBin(command: string, args: string[], platform?: NodeJS.Platform): SpawnCall;
@@ -0,0 +1,21 @@
1
+ const WIN_CMD_SHIMS = new Set(["npx", "npm", "pnpm"]);
2
+ const NEEDS_QUOTES = /[\s&|<>^(),;=]/;
3
+ function quoteForCmd(arg) {
4
+ if (!NEEDS_QUOTES.test(arg))
5
+ return arg;
6
+ return `"${arg.replace(/"/g, '\\"')}"`;
7
+ }
8
+ /**
9
+ * Build the arguments for `spawn`. On Windows, npx, npm and pnpm are `.cmd`
10
+ * shims, which need a shell, so the args are quoted for cmd.exe.
11
+ * On other platforms the call is returned unchanged.
12
+ */
13
+ export function spawnBin(command, args, platform = process.platform) {
14
+ if (platform !== "win32")
15
+ return { command, args };
16
+ return {
17
+ command: WIN_CMD_SHIMS.has(command) ? `${command}.cmd` : command,
18
+ args: args.map(quoteForCmd),
19
+ shell: true,
20
+ };
21
+ }
package/dist/status.d.ts CHANGED
@@ -1,3 +1,5 @@
1
+ import { type Next } from "./json-out.js";
2
+ export type { Next };
1
3
  type Row = {
2
4
  release: number;
3
5
  events: Record<string, number>;
@@ -8,9 +10,77 @@ type AutoHalt = {
8
10
  threshold: number;
9
11
  min_reports: number;
10
12
  } | null;
13
+ type Halted = {
14
+ release: number;
15
+ reports: number;
16
+ failed: number;
17
+ threshold: number;
18
+ };
11
19
  /** Plain-text table: one row per release, one column per event name, then the two medians. */
12
20
  export declare function formatStatus(releases: Row[]): string;
21
+ /** What a band serves: a release number, null when it has none (404), or "?" when it cannot be read. */
22
+ export type BandValue = number | null | "?";
23
+ /** Release each band serves, from the public pointer. A halted release or a failed read gives "?", not "no release". */
24
+ export declare function fetchBands(base: string, appId: string, major: number, fetchFn: typeof fetch): Promise<Record<string, BandValue>>;
25
+ /** "bands: b0=4 b1=4 b2=4", plus a hint to promote when readable bands differ. "-" is no release, "?" is unreadable. */
26
+ export declare function describeBands(bands: Record<string, BandValue>): string[];
13
27
  /** "auto-halt: off" or "auto-halt: on at 20% after 20 reports". */
14
28
  export declare function describeAutoHalt(c: AutoHalt | undefined): string;
29
+ export type TestRelease = {
30
+ release: number;
31
+ passing: number;
32
+ failing: number;
33
+ failing_flows?: {
34
+ name: string;
35
+ first_failed_release: number;
36
+ }[];
37
+ };
38
+ /** Test results the app uploaded with shipvo test --upload. Empty when none exist or the control plane cannot say. */
39
+ export declare function fetchTestResults(base: string, appId: string, fetchFn: typeof fetch): Promise<TestRelease[]>;
40
+ /** "tests: release 3: 2 failing (checkout, since release 2)" lines, newest first. */
41
+ export declare function describeTests(tests: TestRelease[]): string[];
42
+ export type StatusJson = {
43
+ ok: boolean;
44
+ code: "ok" | "no_project" | "no_signing_key" | "release_halted" | "no_release" | "error";
45
+ summary: string;
46
+ data: {
47
+ project: {
48
+ kind: string;
49
+ app_id: string | null;
50
+ };
51
+ sim: {
52
+ running: boolean;
53
+ port: number;
54
+ };
55
+ run_server: {
56
+ running: boolean;
57
+ port: number | null;
58
+ };
59
+ signing_key: {
60
+ present: boolean;
61
+ };
62
+ pending_release: {
63
+ newest: number | null;
64
+ halted: Halted | null;
65
+ auto_halt: AutoHalt | null;
66
+ } | null;
67
+ store_step: {
68
+ id: string;
69
+ title: string;
70
+ state: string;
71
+ } | null;
72
+ skills: {
73
+ package: string;
74
+ name: string;
75
+ synced: boolean;
76
+ }[];
77
+ tests: TestRelease[];
78
+ };
79
+ next: Next[];
80
+ };
81
+ /** `shipvo status --json`: exit 0 ok, 1 action needed, 2 error. */
82
+ export declare function statusJson(appArg: string | undefined, fetchFn: typeof fetch, dir?: string): Promise<{
83
+ doc: StatusJson;
84
+ exit: number;
85
+ }>;
15
86
  export declare function status(argv: string[], fetchFn?: typeof fetch): Promise<number>;
16
- export {};
package/dist/status.js CHANGED
@@ -1,4 +1,10 @@
1
- import { args, loadConfig } from "./config.js";
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { routeStore } from "@shipvo/store";
4
+ import { args, loadConfig, publishToken } from "./config.js";
5
+ import { detectProject, skillStatus } from "./agents.js";
6
+ import { portFree } from "./sim.js";
7
+ import { emit } from "./json-out.js";
2
8
  const ms = (v) => (typeof v === "number" ? String(Math.round(v)) : "-");
3
9
  /** Plain-text table: one row per release, one column per event name, then the two medians. */
4
10
  export function formatStatus(releases) {
@@ -8,6 +14,41 @@ export function formatStatus(releases) {
8
14
  const w = rows[0].map((_, i) => Math.max(...rows.map((r) => r[i].length)));
9
15
  return rows.map((r) => r.map((c, i) => (i === 0 ? c.padEnd(w[i]) : c.padStart(w[i]))).join(" ")).join("\n");
10
16
  }
17
+ const BANDS = ["b0", "b1", "b2"];
18
+ /** Release each band serves, from the public pointer. A halted release or a failed read gives "?", not "no release". */
19
+ export async function fetchBands(base, appId, major, fetchFn) {
20
+ const out = {};
21
+ await Promise.all(BANDS.map(async (b) => {
22
+ out[b] = "?";
23
+ try {
24
+ const r = await fetchFn(`${base}/p/${encodeURIComponent(appId)}/${major}/${b}`);
25
+ if (r.status === 404) {
26
+ // Only "no_release" means none. release_halted and mismatch errors also answer 404.
27
+ const e = (await r.json().catch(() => null));
28
+ if (e?.error === "no_release")
29
+ out[b] = null;
30
+ return;
31
+ }
32
+ if (!r.ok)
33
+ return;
34
+ const env = (await r.json());
35
+ const rel = JSON.parse(String(env.body)).release;
36
+ if (typeof rel === "number")
37
+ out[b] = rel;
38
+ }
39
+ catch { /* unreadable: show "?" */ }
40
+ }));
41
+ return out;
42
+ }
43
+ /** "bands: b0=4 b1=4 b2=4", plus a hint to promote when readable bands differ. "-" is no release, "?" is unreadable. */
44
+ export function describeBands(bands) {
45
+ const line = `bands: ${BANDS.map((b) => `${b}=${bands[b] ?? "-"}`).join(" ")}`;
46
+ const readable = BANDS.map((b) => bands[b] ?? null).filter((v) => v !== "?");
47
+ if (new Set(readable).size < 2)
48
+ return [line];
49
+ const newest = Math.max(...readable.filter((v) => v !== null));
50
+ return [line, `Bands differ. Run shipvo promote to give every band the newest release (${newest}).`];
51
+ }
11
52
  /** "auto-halt: off" or "auto-halt: on at 20% after 20 reports". */
12
53
  export function describeAutoHalt(c) {
13
54
  if (!c || typeof c.threshold !== "number")
@@ -15,8 +56,122 @@ export function describeAutoHalt(c) {
15
56
  const n = c.min_reports ?? 20;
16
57
  return `auto-halt: on at ${Math.round(c.threshold * 1000) / 10}% after ${n} report${n === 1 ? "" : "s"}`;
17
58
  }
59
+ /** Test results the app uploaded with shipvo test --upload. Empty when none exist or the control plane cannot say. */
60
+ export async function fetchTestResults(base, appId, fetchFn) {
61
+ const token = publishToken();
62
+ if (!token)
63
+ return [];
64
+ try {
65
+ const r = await fetchFn(`${base}/r/${encodeURIComponent(appId)}/tests`, { headers: { authorization: `Bearer ${token}` } });
66
+ if (!r.ok)
67
+ return [];
68
+ const d = (await r.json());
69
+ return Array.isArray(d.releases) ? d.releases : [];
70
+ }
71
+ catch {
72
+ return [];
73
+ }
74
+ }
75
+ /** "tests: release 3: 2 failing (checkout, since release 2)" lines, newest first. */
76
+ export function describeTests(tests) {
77
+ return tests.map((t) => `tests: release ${t.release}: ${t.failing ? `${t.failing} failing (${(t.failing_flows ?? []).map((f) => `${f.name}${f.first_failed_release === t.release ? "" : `, since release ${f.first_failed_release}`}`).join("; ")})` : `all ${t.passing} passing`}`);
78
+ }
79
+ const alive = (pid) => { try {
80
+ process.kill(pid, 0);
81
+ return true;
82
+ }
83
+ catch {
84
+ return false;
85
+ } };
86
+ /** Local facts for `status --json`: no network. */
87
+ async function localFacts(dir, simPort = 4545) {
88
+ const hasKey = !!(process.env.SHIPVO_SIGNING_SEED || process.env.PURENATIVE_SIGNING_SEED || (process.env.SHIPVO_SIGNING_SEED_FILE && existsSync(process.env.SHIPVO_SIGNING_SEED_FILE)) || (process.env.PURENATIVE_SIGNING_SEED_FILE && existsSync(process.env.PURENATIVE_SIGNING_SEED_FILE)) || existsSync(join(dir, ".shipvo", "seed.txt")));
89
+ let run = { running: false, port: null };
90
+ try {
91
+ const r = JSON.parse(readFileSync(join(dir, ".shipvo", "run.json"), "utf8"));
92
+ if (typeof r.pid === "number" && alive(r.pid))
93
+ run = { running: true, port: typeof r.port === "number" ? r.port : null };
94
+ }
95
+ catch { /* not running */ }
96
+ let storeStep = null;
97
+ try {
98
+ const plan = JSON.parse(readFileSync(join(dir, "store-plan.json"), "utf8"));
99
+ const r = routeStore(plan.answers);
100
+ const step = [...r.play.steps, ...r.apple.steps].find((x) => (plan.marks?.[x.id] ?? "todo") !== "done");
101
+ if (step)
102
+ storeStep = { id: step.id, title: step.title, state: plan.marks?.[step.id] ?? "todo" };
103
+ }
104
+ catch { /* no plan */ }
105
+ return { hasKey, run, storeStep, sim: { running: !(await portFree(simPort)), port: simPort } };
106
+ }
107
+ /** `shipvo status --json`: exit 0 ok, 1 action needed, 2 error. */
108
+ export async function statusJson(appArg, fetchFn, dir = process.cwd()) {
109
+ const kind = detectProject(dir);
110
+ const f = await localFacts(dir);
111
+ const shared = { sim: f.sim, run_server: f.run, signing_key: { present: f.hasKey }, store_step: f.storeStep, skills: skillStatus(dir) };
112
+ let tests = [];
113
+ const mk = (code, summary, next, appId, pending, exit) => ({
114
+ doc: { ok: exit === 0, code, summary, data: { project: { kind, app_id: appId }, ...shared, pending_release: pending, tests }, next }, exit,
115
+ });
116
+ if (!existsSync(join(dir, "shipvo.json"))) {
117
+ return mk("no_project", "No shipvo.json here.", [{ command: kind === "unknown" ? "shipvo init myapp" : "shipvo init .", why: "This folder is not set up for Shipvo yet." }], null, null, 1);
118
+ }
119
+ let cfg;
120
+ try {
121
+ cfg = loadConfig(dir);
122
+ }
123
+ catch (e) {
124
+ return mk("error", e instanceof Error ? e.message : String(e), [{ command: "shipvo doctor", why: "shipvo.json is missing a field." }], null, null, 2);
125
+ }
126
+ const appId = appArg ?? cfg.appId;
127
+ let data;
128
+ const baseUrl = cfg.controlPlane.replace(/\/$/, "");
129
+ try {
130
+ const r = await fetchFn(`${baseUrl}/t/${encodeURIComponent(appId)}/summary`);
131
+ if (!r.ok)
132
+ throw new Error(`HTTP ${r.status}`);
133
+ data = (await r.json());
134
+ }
135
+ catch (e) {
136
+ return mk("error", `Could not reach the control plane at ${baseUrl} (${e instanceof Error ? e.message : String(e)}).`, [{ command: "shipvo doctor", why: "Check the control plane URL and network." }], appId, null, 2);
137
+ }
138
+ const releases = data.releases ?? [];
139
+ tests = await fetchTestResults(baseUrl, appId, fetchFn);
140
+ const pending = { newest: releases.length ? Math.max(...releases.map((r) => r.release)) : null, halted: data.halted ?? null, auto_halt: data.auto_halt ?? null };
141
+ const next = [];
142
+ let code = "ok", exit = 0;
143
+ let summary = pending.newest === null ? `No releases with telemetry yet for "${appId}".` : `Newest release ${pending.newest} is reporting for "${appId}".`;
144
+ if (!f.hasKey) {
145
+ code = "no_signing_key";
146
+ exit = 1;
147
+ summary = "No signing key found.";
148
+ next.push({ command: "shipvo keygen --out .shipvo/seed.txt", why: "You cannot publish without a signing key." });
149
+ }
150
+ if (pending.halted) {
151
+ code = "release_halted";
152
+ exit = 1;
153
+ summary = `Release ${pending.halted.release} is halted: ${pending.halted.failed} of ${pending.halted.reports} devices failed.`;
154
+ const prev = releases.map((r) => r.release).filter((n) => n < pending.halted.release);
155
+ const to = prev.length ? String(Math.max(...prev)) : "<release>";
156
+ next.unshift({ command: `shipvo rollback --to ${to}`, why: "Send devices back to a release that works, or publish a fixed one." });
157
+ }
158
+ else if (code === "ok" && pending.newest === null) {
159
+ code = "no_release";
160
+ next.push({ command: "shipvo publish --dry-run", why: "Check a first release before you publish it." });
161
+ }
162
+ if (!f.sim.running && !f.run.running)
163
+ next.push({ command: "shipvo sim", why: "Start the simulator to try the app." });
164
+ if (f.storeStep)
165
+ next.push({ command: f.storeStep.state === "waiting" ? "shipvo store status" : `shipvo store status --mark ${f.storeStep.id}`, why: `Store step "${f.storeStep.title}" is ${f.storeStep.state === "todo" ? "next" : f.storeStep.state}.` });
166
+ return mk(code, summary, next, appId, pending, exit);
167
+ }
18
168
  export async function status(argv, fetchFn = fetch) {
19
169
  const a = args(argv);
170
+ if (argv.includes("--json")) {
171
+ const { doc, exit } = await statusJson(a.app && a.app !== "true" ? a.app : undefined, fetchFn);
172
+ emit(doc);
173
+ return exit;
174
+ }
20
175
  const cfg = loadConfig();
21
176
  const appId = a.app && a.app !== "true" ? a.app : cfg.appId;
22
177
  const base = cfg.controlPlane.replace(/\/$/, "");
@@ -32,16 +187,15 @@ export async function status(argv, fetchFn = fetch) {
32
187
  console.error(`Could not reach the control plane at ${base} (${e instanceof Error ? e.message : String(e)}).`);
33
188
  return 1;
34
189
  }
35
- if (argv.includes("--json")) {
36
- console.log(JSON.stringify(data));
37
- return 0;
38
- }
39
190
  const releases = data.releases ?? [];
40
191
  // An older control plane has no auto_halt field: say nothing rather than a wrong "off".
41
192
  if ("auto_halt" in data)
42
193
  console.log(describeAutoHalt(data.auto_halt));
194
+ const printBands = async () => { for (const l of describeBands(await fetchBands(base, appId, cfg.shellMajor, fetchFn)))
195
+ console.log(l); };
43
196
  if (!releases.length) {
44
197
  console.log(`No telemetry yet for "${appId}".`);
198
+ await printBands();
45
199
  return 0;
46
200
  }
47
201
  const h = data.halted;
@@ -50,5 +204,8 @@ export async function status(argv, fetchFn = fetch) {
50
204
  console.log(`Release ${h.release} is halted: ${h.failed} of ${h.reports} devices failed (limit ${pct}%). Publish a new release or run shipvo rollback to clear.`);
51
205
  }
52
206
  console.log(formatStatus(releases));
207
+ for (const l of describeTests(await fetchTestResults(base, appId, fetchFn)))
208
+ console.log(l);
209
+ await printBands();
53
210
  return 0;
54
211
  }
package/dist/store.d.ts CHANGED
@@ -1,2 +1,38 @@
1
+ import type { ScreenshotSetId } from "@shipvo/store";
1
2
  /** `shipvo store details --dry-run [--out dir]`: print the answers, write two files. Nothing is uploaded; no credentials are read. */
2
3
  export declare function store(argv: string[]): Promise<void>;
4
+ /** Sets filled from app routes, with the one size we render each at and the pixel ratio. Feature graphic and icon are not app screens. */
5
+ export declare const RENDER_SETS: {
6
+ set: ScreenshotSetId;
7
+ width: number;
8
+ height: number;
9
+ scale: number;
10
+ }[];
11
+ /** Renders one screen at a size and returns JPEG bytes. `route` is undefined for the start screen. */
12
+ export type Renderer = (size: {
13
+ width: number;
14
+ height: number;
15
+ scale: number;
16
+ }, route: string | undefined) => Promise<Uint8Array>;
17
+ /** Fast mode: render each route at each set's size, write files, print the checker result per file. Returns the exit code. */
18
+ export declare function renderScreenshots(routes: string[] | undefined, render: Renderer, outDir: string): Promise<number>;
19
+ /** Add the group to a track's Google Groups through one Play edit, keeping groups already there. Returns the final list. */
20
+ export declare function addTesterGroup(opts: {
21
+ pkg: string;
22
+ token: string;
23
+ group: string;
24
+ track?: string;
25
+ fetch?: typeof fetch;
26
+ }): Promise<{
27
+ groups: string[];
28
+ added: boolean;
29
+ }>;
30
+ export declare function testersGuide(pkg: string, track?: string): {
31
+ package: string;
32
+ track: string;
33
+ group: string;
34
+ joinUrl: string;
35
+ optInUrl: string;
36
+ firstUploadByHand: string;
37
+ steps: string[];
38
+ };