@shipvo/cli 0.1.5 → 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 (77) hide show
  1. package/README.md +3 -0
  2. package/dist/adopt.js +1 -2
  3. package/dist/agents.d.ts +15 -0
  4. package/dist/agents.js +68 -9
  5. package/dist/approvals.d.ts +9 -0
  6. package/dist/approvals.js +106 -0
  7. package/dist/audit.d.ts +1 -1
  8. package/dist/audit.js +28 -11
  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 +8 -0
  20. package/dist/ci.js +64 -6
  21. package/dist/cli.js +23 -1
  22. package/dist/config.d.ts +13 -0
  23. package/dist/config.js +43 -3
  24. package/dist/doctor.js +63 -7
  25. package/dist/ensure-playwright.d.ts +4 -0
  26. package/dist/ensure-playwright.js +7 -2
  27. package/dist/explore-issues.d.ts +12 -0
  28. package/dist/explore-issues.js +50 -0
  29. package/dist/explore.d.ts +83 -7
  30. package/dist/explore.js +326 -77
  31. package/dist/flows.d.ts +31 -0
  32. package/dist/flows.js +264 -0
  33. package/dist/help.d.ts +2 -2
  34. package/dist/help.js +19 -10
  35. package/dist/hermes.d.ts +10 -0
  36. package/dist/hermes.js +50 -0
  37. package/dist/init.d.ts +1 -0
  38. package/dist/init.js +41 -6
  39. package/dist/json-out.d.ts +53 -0
  40. package/dist/json-out.js +91 -0
  41. package/dist/keys-json.d.ts +1 -0
  42. package/dist/keys-json.js +71 -0
  43. package/dist/login.js +27 -11
  44. package/dist/lumi.d.ts +104 -0
  45. package/dist/lumi.js +364 -0
  46. package/dist/migrate.d.ts +27 -0
  47. package/dist/migrate.js +521 -0
  48. package/dist/preview.d.ts +1 -0
  49. package/dist/preview.js +4 -2
  50. package/dist/promote.d.ts +1 -1
  51. package/dist/promote.js +38 -16
  52. package/dist/publish.d.ts +14 -2
  53. package/dist/publish.js +126 -62
  54. package/dist/qr.d.ts +9 -0
  55. package/dist/qr.js +213 -0
  56. package/dist/report-md.d.ts +1 -0
  57. package/dist/report-md.js +1 -1
  58. package/dist/rotation.d.ts +57 -0
  59. package/dist/rotation.js +157 -0
  60. package/dist/run-bridge.js +356 -2
  61. package/dist/run-net.d.ts +39 -0
  62. package/dist/run-net.js +266 -0
  63. package/dist/run.d.ts +23 -0
  64. package/dist/run.js +101 -17
  65. package/dist/signing.js +13 -0
  66. package/dist/sim.js +7 -3
  67. package/dist/smoke.d.ts +28 -0
  68. package/dist/smoke.js +117 -0
  69. package/dist/spawn-bin.d.ts +11 -0
  70. package/dist/spawn-bin.js +21 -0
  71. package/dist/status.d.ts +26 -4
  72. package/dist/status.js +69 -5
  73. package/dist/templates/github-action/shipvo-check.yml +18 -0
  74. package/dist/test.d.ts +1 -1
  75. package/dist/test.js +81 -18
  76. package/package.json +4 -3
  77. package/skills/shipvo-cli/SKILL.md +31 -0
@@ -0,0 +1,91 @@
1
+ /** Exit codes shared by the `--json` commands. */
2
+ export const EXIT = { ok: 0, failed: 1, usage: 2 };
3
+ /** Print the envelope as one line on stdout. */
4
+ export function emit(doc) {
5
+ console.log(JSON.stringify(doc));
6
+ }
7
+ /** Build an envelope. `ok` is true exactly when the exit code is 0. */
8
+ export function envelope(code, summary, data, next, exit) {
9
+ return { ok: exit === 0, code, summary, data, next: next.slice(0, 3) };
10
+ }
11
+ /** A failure with a stable code, an exit code and a suggested follow-up. Without `--json` it is a plain error. */
12
+ export class CliFailure extends Error {
13
+ code;
14
+ exit;
15
+ next;
16
+ constructor(message, code = "failed", exit = EXIT.failed, next = []) {
17
+ super(message);
18
+ this.code = code;
19
+ this.exit = exit;
20
+ this.next = next;
21
+ }
22
+ }
23
+ /** A bad flag or value. Exit code 2. */
24
+ export class UsageError extends CliFailure {
25
+ constructor(message) { super(message, "usage", EXIT.usage, [{ command: "shipvo help", why: "See the commands and flags." }]); }
26
+ }
27
+ /**
28
+ * Run `fn` so that stdout holds only the envelope: console.log goes to stderr while it runs.
29
+ * A thrown error becomes a failure envelope (exit 2 for a UsageError, 1 otherwise). Sets process.exitCode.
30
+ */
31
+ export async function runJson(fn, fallbackNext) {
32
+ const log = console.log;
33
+ console.log = console.error;
34
+ let doc, exit;
35
+ try {
36
+ const d = await fn();
37
+ exit = 0;
38
+ doc = envelope(d.code, d.summary, d.data, d.next, 0);
39
+ }
40
+ catch (e) {
41
+ const f = e instanceof CliFailure ? e : null;
42
+ exit = f?.exit ?? EXIT.failed;
43
+ doc = envelope(f?.code ?? "failed", e instanceof Error ? e.message : String(e), {}, f && f.next.length ? f.next : fallbackNext, exit);
44
+ }
45
+ finally {
46
+ console.log = log;
47
+ }
48
+ emit(doc);
49
+ process.exitCode = exit;
50
+ }
51
+ class ExitSignal extends Error {
52
+ status;
53
+ constructor(status) {
54
+ super("exit");
55
+ this.status = status;
56
+ }
57
+ }
58
+ /**
59
+ * Run a text-mode command that ends with `process.exit` after printing to stderr. An exit becomes a thrown
60
+ * CliFailure built by `classify(message, status)`; the stderr text is still written. Status 0 returns normally.
61
+ */
62
+ export async function trapExit(fn, classify) {
63
+ const exit = process.exit, err = console.error, msgs = [];
64
+ process.exit = ((c) => { throw new ExitSignal(c ?? 0); });
65
+ console.error = (...a) => { msgs.push(a.map(String).join(" ")); err(...a); };
66
+ try {
67
+ await fn();
68
+ }
69
+ catch (e) {
70
+ if (!(e instanceof ExitSignal))
71
+ throw e;
72
+ if (e.status !== 0)
73
+ throw classify(msgs.join("\n").trim(), e.status);
74
+ }
75
+ finally {
76
+ process.exit = exit;
77
+ console.error = err;
78
+ }
79
+ }
80
+ /** Collect what `fn` prints with console.log instead of printing it. Used so output that holds a secret never reaches a stream. */
81
+ export async function captureLog(fn) {
82
+ const log = console.log, lines = [];
83
+ console.log = (...a) => { lines.push(a.map(String).join(" ")); };
84
+ try {
85
+ await fn();
86
+ }
87
+ finally {
88
+ console.log = log;
89
+ }
90
+ return lines;
91
+ }
@@ -0,0 +1 @@
1
+ export declare function keysJson(argv: string[]): Promise<void>;
@@ -0,0 +1,71 @@
1
+ // `shipvo keys ... --json`: status, export, import and rotate as one envelope. Wraps the text commands, so their output paths are untouched.
2
+ // The seed is never in an envelope or on a stream: export needs --out, and import reports only the public key.
3
+ import { args, loadConfig } from "./config.js";
4
+ import { keys } from "./login.js";
5
+ import { signingKey } from "./signing.js";
6
+ import { CliFailure, UsageError, captureLog, runJson, trapExit } from "./json-out.js";
7
+ const LOGIN = { command: "shipvo login", why: "Sign in again." };
8
+ const FALLBACK = [{ command: "shipvo doctor --json", why: "Check your setup." }];
9
+ function project() {
10
+ try {
11
+ return loadConfig();
12
+ }
13
+ catch (e) {
14
+ throw new CliFailure(e instanceof Error ? e.message : String(e), "no_project", 1, [{ command: "shipvo init . --yes --json", why: "Add Shipvo to this app." }, { command: "shipvo init myapp --json", why: "Or start a new app." }]);
15
+ }
16
+ }
17
+ const failIn = (table, fallbackCode = "failed") => (m, st) => {
18
+ const hit = table.find(([re]) => re.test(m));
19
+ return new CliFailure(m, st === 2 ? "usage" : hit?.[1] ?? fallbackCode, st, hit?.[2] ?? FALLBACK);
20
+ };
21
+ export async function keysJson(argv) {
22
+ const [sub, ...rest] = argv.filter((x) => x !== "--json");
23
+ const rawRest = rest;
24
+ return runJson(async () => {
25
+ if (sub === undefined) {
26
+ const lines = await captureLog(() => trapExit(() => keys(), failIn([
27
+ [/Not signed in/, "not_signed_in", [LOGIN]], [/expired/, "key_expired", [LOGIN]], [/not valid any more/, "key_invalid", [LOGIN]],
28
+ ])));
29
+ const d = JSON.parse(lines.join("\n"));
30
+ return { code: "ok", summary: `Signed in as ${d.login}. Key ${d.prefix}… has used ${d.used_minutes} of ${d.quota_minutes} sandbox minutes.`, data: d, next: [{ command: "shipvo app create", why: "Register the app and save a publish token." }, { command: "shipvo doctor --json", why: "Check the setup." }] };
31
+ }
32
+ if (sub !== "export" && sub !== "import" && sub !== "rotate")
33
+ throw new UsageError(`Unknown keys command "${sub}". Use keys, keys export, keys import or keys rotate.`);
34
+ const cfg = project();
35
+ if (sub === "export") {
36
+ const out = args(rawRest).out;
37
+ if (!out || out === "true")
38
+ throw new UsageError("keys export --json needs --out <file>. The seed is never printed in an envelope.");
39
+ await trapExit(() => signingKey(["export", ...rawRest]), failIn([
40
+ [/Set SHIPVO_SIGNING_SEED/, "no_seed", [{ command: "shipvo keys import", why: "Restore the seed on this machine." }]],
41
+ [/does not match the public key/, "seed_mismatch", [{ command: "shipvo keys import", why: "Restore the seed that matches shipvo.json." }]],
42
+ ]));
43
+ return { code: "exported", summary: `Seed for "${cfg.appId}" written to ${out}. Move it to a password manager, then delete the file.`, data: { app_id: cfg.appId, public_key: cfg.publicKey, out }, next: [{ command: `shipvo keys import --file ${out}`, why: "Restore it on another machine." }] };
44
+ }
45
+ if (sub === "import") {
46
+ const lines = await captureLog(() => trapExit(() => signingKey(["import", ...rawRest]), failIn([
47
+ [/Usage: shipvo keys import/, "usage", [{ command: "shipvo keys import --file seed.txt", why: "Read the seed from a file." }]],
48
+ [/does not look like/, "invalid_seed", [{ command: "shipvo keys import --file seed.txt", why: "Use the seed that `shipvo keys export` saved." }]],
49
+ [/belongs to a different key/, "seed_mismatch", [{ command: "shipvo keys import --file seed.txt", why: "Use the seed for this app's public key." }]],
50
+ ])));
51
+ const text = lines.join("\n");
52
+ return {
53
+ code: /Already in place/.test(text) ? "unchanged" : "imported",
54
+ summary: /Already in place/.test(text) ? "The seed was already in place. Nothing to do." : `Signing key for "${cfg.appId}" restored to .shipvo/seed.txt (matches shipvo.json).`,
55
+ data: { app_id: cfg.appId, public_key: cfg.publicKey, seed_file: ".shipvo/seed.txt", gitignore_warning: /\.gitignore does not ignore/.test(text) },
56
+ next: [{ command: "shipvo login", why: "Sign in on this machine." }, { command: "shipvo app create", why: "Get a publish token on this machine." }],
57
+ };
58
+ }
59
+ // rotate sign | upload. The signed envelope is public, so it is safe in data.
60
+ const lines = await captureLog(() => trapExit(() => signingKey(["rotate", ...rawRest, "--json"]), failIn([
61
+ [/^Usage:/, "usage", [{ command: "shipvo keys rotate sign --root-seed-file <file> --new-key <public key> --sequence <n>", why: "Sign a rotation statement offline." }]],
62
+ [/Not signed in/, "not_signed_in", [LOGIN]],
63
+ [/refused the statement/, "refused", FALLBACK],
64
+ [/Could not reach/, "remote_failed", FALLBACK],
65
+ ], "rotate_failed")));
66
+ const out = JSON.parse(lines.join("\n"));
67
+ if (rest[0] === "sign")
68
+ return { code: "rotation_signed", summary: `Signed rotation statement ${args(rest).sequence} for "${cfg.appId}". Save data.envelope to a file, then upload it.`, data: { app_id: cfg.appId, envelope: out }, next: [{ command: "shipvo keys rotate upload <file>", why: "Upload the saved envelope. Needs shipvo login as the app owner." }] };
69
+ return { code: "rotation_uploaded", summary: "Uploaded. Phones switch when they next fetch it.", data: { app_id: cfg.appId, response: out }, next: [{ command: "shipvo status --json", why: "Check the app." }] };
70
+ }, FALLBACK);
71
+ }
package/dist/login.js CHANGED
@@ -4,6 +4,7 @@ import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
4
4
  import { homedir } from "node:os";
5
5
  import { join } from "node:path";
6
6
  import { args as parse } from "./config.js";
7
+ import { CliFailure, runJson } from "./json-out.js";
7
8
  const HOSTED = "https://api.shipvo.dev";
8
9
  const credFile = () => join(process.env.SHIPVO_HOME || join(homedir(), ".shipvo"), "credentials.json");
9
10
  export function savedCreds() {
@@ -15,14 +16,25 @@ export function savedCreds() {
15
16
  }
16
17
  }
17
18
  export async function login(argv) {
19
+ if (argv.includes("--json"))
20
+ return runJson(() => loginRun(argv, true), [{ command: "shipvo login --json", why: "Try signing in again." }, { command: "shipvo doctor --json", why: "Check your setup." }]);
21
+ await loginRun(argv, false);
22
+ }
23
+ /** Device sign-in. With `json`, failures throw (the envelope reports them), the device URL and code go in `data`, and the key is never printed. */
24
+ async function loginRun(argv, json) {
25
+ const again = [{ command: "shipvo login --json", why: "Start a new sign-in." }];
26
+ const fail = (msg, code) => {
27
+ if (json)
28
+ throw new CliFailure(msg, code, 1, again);
29
+ console.error(msg);
30
+ process.exit(1);
31
+ };
18
32
  const a = parse(argv);
19
33
  const control = (a["control-plane"] || process.env.SHIPVO_CONTROL || HOSTED).replace(/\/$/, "");
20
34
  const start = await fetch(`${control}/auth/device/start`, { method: "POST" });
21
35
  const s = await start.json();
22
- if (!start.ok) {
23
- console.error(`Login is not available on ${control}: ${s.message ?? s.error}`);
24
- process.exit(1);
25
- }
36
+ if (!start.ok)
37
+ fail(`Login is not available on ${control}: ${s.message ?? s.error}`, "login_unavailable");
26
38
  console.log(`\nOpen ${s.verification_uri} and enter the code:\n\n ${s.user_code}\n\nWaiting for GitHub…`);
27
39
  const deadline = Date.now() + (s.expires_in ?? 900) * 1000;
28
40
  let interval = (s.interval ?? 5) * 1000;
@@ -35,18 +47,22 @@ export async function login(argv) {
35
47
  interval += 5000;
36
48
  continue;
37
49
  }
38
- if (!r.ok) {
39
- console.error(`Login failed: ${d.status ?? d.error}`);
40
- process.exit(1);
41
- }
50
+ if (!r.ok)
51
+ fail(`Login failed: ${d.status ?? d.error}`, d.status === "expired_token" || d.status === "expired" ? "login_expired" : "login_failed");
42
52
  mkdirSync(join(credFile(), ".."), { recursive: true });
43
53
  writeFileSync(credFile(), JSON.stringify({ key: d.key, control, mcp_url: d.mcp.url, login: d.login, apps: d.apps, saved_at: new Date().toISOString() }, null, 2), { mode: 0o600 });
54
+ if (json) {
55
+ return {
56
+ code: "signed_in", summary: `Signed in as ${d.login}. The key is saved to ${credFile()}.`,
57
+ data: { login: d.login, key_prefix: d.prefix, saved_to: credFile(), quota_minutes: d.quota_minutes, expires_at: d.expires_at, apps: d.apps, mcp_url: d.mcp.url, device: { verification_uri: s.verification_uri, user_code: s.user_code } },
58
+ next: [{ command: "shipvo app create", why: "Register the app and save a publish token." }, { command: "shipvo doctor --json", why: "Check the setup." }],
59
+ };
60
+ }
44
61
  console.log(`\nSigned in as ${d.login}. Key ${d.prefix}… saved to ${credFile()} (quota: ${d.quota_minutes} sandbox minutes; expires ${String(d.expires_at).slice(0, 10)}; apps: ${d.apps.join(", ")}).\n`);
45
62
  printMcp(d.mcp.url, d.key);
46
- return;
63
+ return { code: "signed_in", summary: "", data: {}, next: [] };
47
64
  }
48
- console.error("Timed out waiting for GitHub.");
49
- process.exit(1);
65
+ return fail("Timed out waiting for GitHub.", "login_timeout");
50
66
  }
51
67
  export async function keys() {
52
68
  const c = savedCreds();
package/dist/lumi.d.ts ADDED
@@ -0,0 +1,104 @@
1
+ import { type Server } from "node:http";
2
+ import { type Driver, type Skin } from "./explore.js";
3
+ export type Mood = "idle" | "working" | "happy" | "worried";
4
+ export type Command = {
5
+ kind: "test-all";
6
+ } | {
7
+ kind: "test-flow";
8
+ flow: string;
9
+ } | {
10
+ kind: "bad-input";
11
+ flow: string;
12
+ } | {
13
+ kind: "stop";
14
+ } | {
15
+ kind: "replay";
16
+ n: number;
17
+ } | {
18
+ kind: "help";
19
+ } | {
20
+ kind: "unknown";
21
+ };
22
+ /** Reads a line of text as a command. No AI: a few patterns, case and spacing ignored. */
23
+ export declare function parseCommand(text: string): Command;
24
+ export declare const COMMAND_LIST: string[];
25
+ export type FeedItem = {
26
+ at: string;
27
+ text: string;
28
+ kind: "step" | "info" | "bug";
29
+ };
30
+ export type State = {
31
+ mood: Mood;
32
+ running: boolean;
33
+ label: string;
34
+ feed: FeedItem[];
35
+ findings: {
36
+ n: number;
37
+ kind: string;
38
+ message: string;
39
+ steps: string[];
40
+ }[];
41
+ coverage: {
42
+ screens: number;
43
+ controlsSeen: number;
44
+ controlsTried: number;
45
+ };
46
+ };
47
+ export type LumiOptions = {
48
+ routes?: string[];
49
+ maxDepth?: number;
50
+ maxSteps?: number;
51
+ seed?: number;
52
+ skin?: Skin;
53
+ app?: string;
54
+ settleMs?: number;
55
+ walkDelayMs?: number;
56
+ };
57
+ export declare class Lumi {
58
+ private driver;
59
+ private o;
60
+ private feed;
61
+ private known;
62
+ private screens;
63
+ private seen;
64
+ private tried;
65
+ private mood;
66
+ private running;
67
+ private label;
68
+ private stopFlag;
69
+ private listeners;
70
+ private job;
71
+ constructor(driver: Driver, o?: LumiOptions);
72
+ subscribe(fn: (s: State) => void): () => void;
73
+ state(): State;
74
+ /** Resolves when the current run or replay has ended. */
75
+ idle(): Promise<void>;
76
+ private emit;
77
+ private say;
78
+ /** Names you can use after `test` and `try`: configured routes, then the controls on the start screen. */
79
+ flows(): Promise<string[]>;
80
+ private resolve;
81
+ /** Handles one line from the command box. Returns the short reply at once; a run goes on in the background (see `idle`). */
82
+ command(text: string): Promise<string>;
83
+ private start;
84
+ private run;
85
+ /** Does the steps of a bug again, one by one, and says whether the screen is the same as when it was found. */
86
+ private replay;
87
+ }
88
+ /** "tide://cart" -> "cart", "/orders/open" -> "open". */
89
+ export declare function routeName(url: string): string;
90
+ /** The whole page: the phone frame with the live app picture in it, and the sidebar. */
91
+ export declare function lumiPage(frame: {
92
+ html: string;
93
+ css: string;
94
+ }, title: string): string;
95
+ export type LumiServerOptions = {
96
+ lumi: Lumi;
97
+ page: string;
98
+ shot: () => Promise<Buffer | null>;
99
+ port: number;
100
+ };
101
+ /** Local server for the page. It only answers requests whose Host is this machine's address, and only commands from this page. */
102
+ export declare function startLumiServer(o: LumiServerOptions): Promise<Server>;
103
+ /** Builds the app, opens it on Shipvo Phone in the browser with the Lumi sidebar. Runs until Ctrl-C. */
104
+ export declare function lumi(argv: string[]): Promise<number>;