@shipvo/cli 0.1.3 → 0.1.4
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/dist/adopt-bare.d.ts +39 -0
- package/dist/adopt-bare.js +236 -0
- package/dist/adopt.d.ts +26 -0
- package/dist/adopt.js +257 -0
- package/dist/autohalt.d.ts +5 -0
- package/dist/autohalt.js +84 -0
- package/dist/cli.js +114 -88
- package/dist/config.d.ts +8 -0
- package/dist/config.js +22 -9
- package/dist/doctor.d.ts +12 -0
- package/dist/doctor.js +108 -7
- package/dist/help.d.ts +4 -2
- package/dist/help.js +17 -2
- package/dist/init.js +483 -16
- package/dist/native.d.ts +3 -0
- package/dist/native.js +39 -0
- package/dist/policy.d.ts +18 -0
- package/dist/policy.js +33 -0
- package/dist/preview.d.ts +35 -0
- package/dist/preview.js +249 -0
- package/dist/promote.d.ts +9 -0
- package/dist/promote.js +126 -0
- package/dist/publish.d.ts +1 -1
- package/dist/publish.js +61 -28
- package/dist/sim.d.ts +27 -0
- package/dist/sim.js +108 -3
- package/dist/status.d.ts +16 -0
- package/dist/status.js +54 -0
- package/dist/store.d.ts +2 -0
- package/dist/store.js +34 -0
- package/package.json +7 -4
package/dist/cli.js
CHANGED
|
@@ -5,104 +5,130 @@ import { b64, keyFromSeed, sha256Hex, signBytes, signDoc, verifyDoc } from "./in
|
|
|
5
5
|
import { seed } from "./config.js";
|
|
6
6
|
import { init } from "./init.js";
|
|
7
7
|
import { publish, rollback } from "./publish.js";
|
|
8
|
+
import { promote } from "./promote.js";
|
|
9
|
+
import { autoHalt } from "./autohalt.js";
|
|
8
10
|
import { sim } from "./sim.js";
|
|
11
|
+
import { preview } from "./preview.js";
|
|
9
12
|
import { keys, login, mcp } from "./login.js";
|
|
10
13
|
import { verifyPointer } from "./verify.js";
|
|
11
14
|
import { signingKey } from "./signing.js";
|
|
12
15
|
import { appCmd } from "./app.js";
|
|
16
|
+
import { status } from "./status.js";
|
|
13
17
|
import { doctor } from "./doctor.js";
|
|
14
|
-
import {
|
|
18
|
+
import { store } from "./store.js";
|
|
19
|
+
import { failure, help, unknownCommand } from "./help.js";
|
|
15
20
|
const [cmd, ...rest] = process.argv.slice(2);
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
await
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
21
|
+
try {
|
|
22
|
+
switch (cmd) {
|
|
23
|
+
case "init":
|
|
24
|
+
await init(rest);
|
|
25
|
+
break;
|
|
26
|
+
case "sim":
|
|
27
|
+
await sim(rest);
|
|
28
|
+
break;
|
|
29
|
+
case "preview":
|
|
30
|
+
await preview(rest);
|
|
31
|
+
break;
|
|
32
|
+
case "publish":
|
|
33
|
+
await publish(rest);
|
|
34
|
+
break;
|
|
35
|
+
case "promote":
|
|
36
|
+
await promote(rest);
|
|
37
|
+
break;
|
|
38
|
+
case "auto-halt":
|
|
39
|
+
await autoHalt(rest);
|
|
40
|
+
break;
|
|
41
|
+
case "rollback":
|
|
42
|
+
await rollback(rest);
|
|
43
|
+
break;
|
|
44
|
+
case "login":
|
|
45
|
+
await login(rest);
|
|
46
|
+
break;
|
|
47
|
+
case "doctor":
|
|
48
|
+
await doctor(rest);
|
|
49
|
+
break;
|
|
50
|
+
case "status":
|
|
51
|
+
process.exitCode = await status(rest);
|
|
52
|
+
break;
|
|
53
|
+
case "store":
|
|
54
|
+
await store(rest);
|
|
55
|
+
break;
|
|
56
|
+
case "app":
|
|
57
|
+
await appCmd(rest);
|
|
58
|
+
break;
|
|
59
|
+
case "keys":
|
|
60
|
+
if (rest[0] === "export" || rest[0] === "import")
|
|
61
|
+
await signingKey(rest);
|
|
62
|
+
else
|
|
63
|
+
await keys();
|
|
64
|
+
break;
|
|
65
|
+
case "mcp":
|
|
66
|
+
mcp();
|
|
67
|
+
break;
|
|
68
|
+
case "keygen": {
|
|
69
|
+
const s = b64(randomBytes(32));
|
|
70
|
+
const out = rest[rest.indexOf("--out") + 1];
|
|
71
|
+
if (rest.includes("--out") && out) {
|
|
72
|
+
writeFileSync(out, s, { mode: 0o600 });
|
|
73
|
+
console.error(`Seed written to ${out}. Back it up offline; never commit it.`);
|
|
74
|
+
}
|
|
75
|
+
else
|
|
76
|
+
console.log(`seed (secret): ${s}`);
|
|
77
|
+
console.log(`public key: ${b64(keyFromSeed(s).publicKey)}`);
|
|
78
|
+
break;
|
|
53
79
|
}
|
|
54
|
-
|
|
55
|
-
console.log(
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
case "sign-json": {
|
|
71
|
-
const d = JSON.parse(readFileSync(rest[0], "utf8"));
|
|
72
|
-
writeFileSync(rest[0], JSON.stringify(signDoc(d, seed()), null, 2));
|
|
73
|
-
console.log("signed");
|
|
74
|
-
break;
|
|
75
|
-
}
|
|
76
|
-
case "verify-json": {
|
|
77
|
-
const ok = verifyDoc(JSON.parse(readFileSync(rest[0], "utf8")), rest[1]);
|
|
78
|
-
console.log(ok ? "valid" : "INVALID");
|
|
79
|
-
process.exit(ok ? 0 : 1);
|
|
80
|
-
}
|
|
81
|
-
case "verify": {
|
|
82
|
-
if (!rest[0]) {
|
|
83
|
-
console.error("Usage: shipvo verify <pointer file> [public-key-b64]");
|
|
84
|
-
process.exit(2);
|
|
80
|
+
case "pubkey":
|
|
81
|
+
console.log(b64(keyFromSeed(seed()).publicKey));
|
|
82
|
+
break;
|
|
83
|
+
case "hash": {
|
|
84
|
+
const b = readFileSync(rest[0]);
|
|
85
|
+
console.log(JSON.stringify({ sha256: sha256Hex(b), size: b.length }));
|
|
86
|
+
break;
|
|
87
|
+
}
|
|
88
|
+
case "sign-file":
|
|
89
|
+
console.log(signBytes(new Uint8Array(readFileSync(rest[0])), seed()));
|
|
90
|
+
break;
|
|
91
|
+
case "sign-json": {
|
|
92
|
+
const d = JSON.parse(readFileSync(rest[0], "utf8"));
|
|
93
|
+
writeFileSync(rest[0], JSON.stringify(signDoc(d, seed()), null, 2));
|
|
94
|
+
console.log("signed");
|
|
95
|
+
break;
|
|
85
96
|
}
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
97
|
+
case "verify-json": {
|
|
98
|
+
const ok = verifyDoc(JSON.parse(readFileSync(rest[0], "utf8")), rest[1]);
|
|
99
|
+
console.log(ok ? "valid" : "INVALID");
|
|
100
|
+
process.exit(ok ? 0 : 1);
|
|
90
101
|
}
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
102
|
+
case "verify": {
|
|
103
|
+
if (!rest[0]) {
|
|
104
|
+
console.error("Usage: shipvo verify <pointer file> [public-key-b64]");
|
|
105
|
+
process.exit(2);
|
|
106
|
+
}
|
|
107
|
+
const key = rest[1] ?? b64(keyFromSeed(seed()).publicKey);
|
|
108
|
+
let text;
|
|
109
|
+
try {
|
|
110
|
+
text = readFileSync(rest[0], "utf8");
|
|
111
|
+
}
|
|
112
|
+
catch {
|
|
113
|
+
console.error(`Cannot read ${rest[0]}`);
|
|
114
|
+
process.exit(2);
|
|
115
|
+
}
|
|
116
|
+
const ok = verifyPointer(text, key);
|
|
117
|
+
console.log(ok ? "valid" : "INVALID");
|
|
118
|
+
process.exit(ok ? 0 : 1);
|
|
94
119
|
}
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
120
|
+
case undefined:
|
|
121
|
+
case "--help":
|
|
122
|
+
case "-h":
|
|
123
|
+
case "help":
|
|
124
|
+
console.log(help);
|
|
125
|
+
break;
|
|
126
|
+
default:
|
|
127
|
+
console.error(unknownCommand(cmd));
|
|
128
|
+
process.exit(1);
|
|
98
129
|
}
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
console.log(help);
|
|
104
|
-
break;
|
|
105
|
-
default:
|
|
106
|
-
console.error(unknownCommand(cmd));
|
|
107
|
-
process.exit(1);
|
|
130
|
+
}
|
|
131
|
+
catch (e) {
|
|
132
|
+
console.error(failure(e));
|
|
133
|
+
process.exit(1);
|
|
108
134
|
}
|
package/dist/config.d.ts
CHANGED
|
@@ -9,10 +9,18 @@ export type ShipvoConfig = {
|
|
|
9
9
|
publicKey: string;
|
|
10
10
|
/** Hermes bytecode version(s) the app's shells run. If set, `shipvo publish` refuses bundles that differ. */
|
|
11
11
|
hermesBytecodeVersion?: number | number[];
|
|
12
|
+
/** What AI agents may do through `@shipvo/mcp` owner commands. "ask" (default): irreversible commands need confirm: true. "deny": they are refused. "allow": no confirm step (the owner must set it). */
|
|
13
|
+
agentPolicy?: "ask" | "deny" | "allow";
|
|
14
|
+
/** Owner commands agents may never run. */
|
|
15
|
+
agentDeny?: string[];
|
|
12
16
|
/** Where `shipvo publish` writes: the control plane's Cloudflare KV namespace. */
|
|
13
17
|
kv?: {
|
|
14
18
|
namespaceId: string;
|
|
15
19
|
};
|
|
20
|
+
/** Where `shipvo preview` publishes: "here.now" (default, no account) or "control-plane" (not built yet). */
|
|
21
|
+
preview?: "here.now" | "control-plane";
|
|
22
|
+
/** Release rules checked by `shipvo publish --dry-run`. Validated there, not at load, so a bad section never breaks other commands. */
|
|
23
|
+
policy?: unknown;
|
|
16
24
|
/** Marketplace listing (hosted control plane): shown to agents and on shipvo.dev once reviewed. */
|
|
17
25
|
listing?: {
|
|
18
26
|
public?: boolean;
|
package/dist/config.js
CHANGED
|
@@ -3,7 +3,7 @@ import { join } from "node:path";
|
|
|
3
3
|
export function loadConfig(dir = process.cwd()) {
|
|
4
4
|
const p = join(dir, "shipvo.json");
|
|
5
5
|
if (!existsSync(p))
|
|
6
|
-
throw new Error("No shipvo.json
|
|
6
|
+
throw new Error("No shipvo.json in this folder. Run this inside your app's folder. To start a new app, run `shipvo init myshop`.");
|
|
7
7
|
const c = JSON.parse(readFileSync(p, "utf8"));
|
|
8
8
|
for (const k of ["appId", "controlPlane", "publicKey"])
|
|
9
9
|
if (!c[k])
|
|
@@ -26,8 +26,8 @@ export function seed() {
|
|
|
26
26
|
if (!s && local)
|
|
27
27
|
return local;
|
|
28
28
|
if (!s) {
|
|
29
|
-
console.error("Set SHIPVO_SIGNING_SEED or SHIPVO_SIGNING_SEED_FILE (the seed `shipvo keygen` printed), or keep it at .shipvo/seed.txt.");
|
|
30
|
-
process.exit(
|
|
29
|
+
console.error("Set SHIPVO_SIGNING_SEED or SHIPVO_SIGNING_SEED_FILE (the seed `shipvo keygen` printed), or keep it at .shipvo/seed.txt. No key yet? Run `shipvo keygen`.");
|
|
30
|
+
process.exit(1);
|
|
31
31
|
}
|
|
32
32
|
return s;
|
|
33
33
|
}
|
|
@@ -47,13 +47,26 @@ function hostedKv(cfg, token) {
|
|
|
47
47
|
const base = `${cfg.controlPlane.replace(/\/$/, "")}/apps/${cfg.appId}/kv`;
|
|
48
48
|
const auth = { Authorization: `Bearer ${token}` };
|
|
49
49
|
const path = (k) => `${base}/${k.split("/").map(encodeURIComponent).join("/")}`;
|
|
50
|
+
const host = cfg.controlPlane.replace(/\/$/, "");
|
|
51
|
+
/** One place for the two first-run failures: the server is not there, or it does not accept the token. */
|
|
52
|
+
const call = async (url, init) => {
|
|
53
|
+
let r;
|
|
54
|
+
try {
|
|
55
|
+
r = await fetch(url, init);
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
throw new Error(`Cannot reach the control plane at ${host}. Check "controlPlane" in shipvo.json, then run \`shipvo doctor\`.`);
|
|
59
|
+
}
|
|
60
|
+
if (r.status === 401 || r.status === 403)
|
|
61
|
+
throw new Error(`The control plane at ${host} rejected your publish token (${r.status}). Run \`shipvo login\`, then \`shipvo app create\` for a new token.`);
|
|
62
|
+
return r;
|
|
63
|
+
};
|
|
50
64
|
return {
|
|
51
|
-
async get(k) { const r = await
|
|
52
|
-
|
|
53
|
-
async
|
|
54
|
-
async keys(prefix) { const r = await fetch(`${base}?prefix=${encodeURIComponent(prefix)}`, { headers: auth }); return r.ok ? (await r.json()).keys : []; },
|
|
65
|
+
async get(k) { const r = await call(path(k), { headers: auth }); return r.ok ? r.text() : null; },
|
|
66
|
+
async getBytes(k) { const r = await call(path(k), { headers: auth }); return r.ok ? new Uint8Array(await r.arrayBuffer()) : null; },
|
|
67
|
+
async keys(prefix) { const r = await call(`${base}?prefix=${encodeURIComponent(prefix)}`, { headers: auth }); return r.ok ? (await r.json()).keys : []; },
|
|
55
68
|
async put(k, body, type) {
|
|
56
|
-
const r = await
|
|
69
|
+
const r = await call(path(k), { method: "PUT", headers: { ...auth, "content-type": type }, body });
|
|
57
70
|
if (!r.ok) {
|
|
58
71
|
let m = "";
|
|
59
72
|
try {
|
|
@@ -75,7 +88,7 @@ export function kv(cfg) {
|
|
|
75
88
|
if (pt)
|
|
76
89
|
return hostedKv(cfg, pt);
|
|
77
90
|
console.error("Nothing to publish with. Hosted: run `shipvo login`, then `shipvo app create` (or set SHIPVO_PUBLISH_TOKEN). Self-hosted: set CLOUDFLARE_ACCOUNT_ID, CLOUDFLARE_API_TOKEN and SHIPVO_KV_ID.");
|
|
78
|
-
process.exit(
|
|
91
|
+
process.exit(1);
|
|
79
92
|
}
|
|
80
93
|
const url = (k) => `https://api.cloudflare.com/client/v4/accounts/${ACC}/storage/kv/namespaces/${NS}/values/${encodeURIComponent(k)}`;
|
|
81
94
|
const auth = { Authorization: `Bearer ${TOKEN}` };
|
package/dist/doctor.d.ts
CHANGED
|
@@ -3,6 +3,7 @@ type CheckResult = {
|
|
|
3
3
|
name: string;
|
|
4
4
|
ok: boolean;
|
|
5
5
|
fix: string;
|
|
6
|
+
note?: string;
|
|
6
7
|
};
|
|
7
8
|
type DoctorDeps = {
|
|
8
9
|
cwd: string;
|
|
@@ -11,6 +12,17 @@ type DoctorDeps = {
|
|
|
11
12
|
fetchFn: typeof fetch;
|
|
12
13
|
fileExists: typeof existsSync;
|
|
13
14
|
readText: (path: string) => string;
|
|
15
|
+
platform: NodeJS.Platform;
|
|
16
|
+
fileMode: (path: string) => number | null;
|
|
17
|
+
/** Port the simulator will use (default 4545). */
|
|
18
|
+
port: number;
|
|
19
|
+
/** Path of the Playwright Chromium binary, or null if Playwright is not installed. */
|
|
20
|
+
chromiumPath: (cwd: string) => string | null;
|
|
21
|
+
portFree: (port: number) => Promise<boolean>;
|
|
22
|
+
/** Skip the checks that need the network (control plane reachable and /health). */
|
|
23
|
+
offline: boolean;
|
|
24
|
+
/** Skip the simulator checks (Chromium, port) even when package.json has a `sim` script. */
|
|
25
|
+
skipSim: boolean;
|
|
14
26
|
};
|
|
15
27
|
export declare function runDoctorChecks(overrides?: Partial<DoctorDeps>): Promise<CheckResult[]>;
|
|
16
28
|
export declare function doctor(argv: string[]): Promise<void>;
|
package/dist/doctor.js
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
|
-
import { existsSync, readFileSync } from "node:fs";
|
|
1
|
+
import { existsSync, readFileSync, statSync } from "node:fs";
|
|
2
2
|
import { join } from "node:path";
|
|
3
|
+
import { createRequire } from "node:module";
|
|
4
|
+
import { execSync } from "node:child_process";
|
|
5
|
+
import { portFree } from "./sim.js";
|
|
6
|
+
import { args as parseArgs } from "./config.js";
|
|
3
7
|
import { b64, keyFromSeed } from "./index.js";
|
|
4
8
|
import { parseDeclared } from "./hermes.js";
|
|
5
9
|
const defaultDeps = () => ({
|
|
@@ -9,9 +13,51 @@ const defaultDeps = () => ({
|
|
|
9
13
|
fetchFn: fetch,
|
|
10
14
|
fileExists: existsSync,
|
|
11
15
|
readText: (path) => readFileSync(path, "utf8"),
|
|
16
|
+
platform: process.platform,
|
|
17
|
+
fileMode: (path) => { try {
|
|
18
|
+
return statSync(path).mode & 0o777;
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
return null;
|
|
22
|
+
} },
|
|
23
|
+
port: 4545,
|
|
24
|
+
chromiumPath: playwrightChromiumPath,
|
|
25
|
+
portFree,
|
|
26
|
+
offline: false,
|
|
27
|
+
skipSim: false,
|
|
12
28
|
});
|
|
29
|
+
/** Finds Playwright the way packages/sim/server.mjs does (the app's install, then this package's and @shipvo/sim's, then the global npm root) and asks it where Chromium should be. */
|
|
30
|
+
function playwrightChromiumPath(cwd) {
|
|
31
|
+
const tries = [
|
|
32
|
+
() => createRequire(join(cwd, "package.json"))("playwright"),
|
|
33
|
+
() => createRequire(import.meta.url)("playwright"),
|
|
34
|
+
() => createRequire(createRequire(import.meta.url).resolve("@shipvo/sim/package.json"))("playwright"),
|
|
35
|
+
() => createRequire(import.meta.url)(join(execSync("npm root -g", { stdio: ["ignore", "pipe", "ignore"] }).toString().trim(), "playwright")),
|
|
36
|
+
];
|
|
37
|
+
for (const get of tries) {
|
|
38
|
+
try {
|
|
39
|
+
return get().chromium.executablePath();
|
|
40
|
+
}
|
|
41
|
+
catch { }
|
|
42
|
+
}
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
function hasSimScript(deps) {
|
|
46
|
+
try {
|
|
47
|
+
const pkg = JSON.parse(deps.readText(join(deps.cwd, "package.json")));
|
|
48
|
+
return typeof pkg?.scripts?.sim === "string";
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
13
54
|
const parseMajor = (v) => Number(v.split(".")[0] || 0);
|
|
14
55
|
const pub = (s) => b64(keyFromSeed(s).publicKey);
|
|
56
|
+
function seedFilePath(env, cwd) {
|
|
57
|
+
if (env.SHIPVO_SIGNING_SEED || env.PURENATIVE_SIGNING_SEED)
|
|
58
|
+
return null;
|
|
59
|
+
return env.SHIPVO_SIGNING_SEED_FILE || env.PURENATIVE_SIGNING_SEED_FILE || join(cwd, ".shipvo", "seed.txt");
|
|
60
|
+
}
|
|
15
61
|
function resolveSeed(env, cwd, readText) {
|
|
16
62
|
const direct = env.SHIPVO_SIGNING_SEED || env.PURENATIVE_SIGNING_SEED;
|
|
17
63
|
if (direct)
|
|
@@ -71,9 +117,10 @@ export async function runDoctorChecks(overrides = {}) {
|
|
|
71
117
|
})();
|
|
72
118
|
const needCfg = "fix shipvo.json first";
|
|
73
119
|
results.push({ name: "signing key matches shipvo.json publicKey", ok: signingOk, fix: !cfg ? needCfg : "restore the right seed with `shipvo keys import` (or set SHIPVO_SIGNING_SEED)" });
|
|
120
|
+
const skipped = "skipped, --offline";
|
|
74
121
|
let controlPlaneOk = false;
|
|
75
122
|
let cpFix = "check shipvo.json controlPlane and your network access (5 second timeout)";
|
|
76
|
-
if (cfg?.controlPlane && typeof cfg.controlPlane === "string") {
|
|
123
|
+
if (!deps.offline && cfg?.controlPlane && typeof cfg.controlPlane === "string") {
|
|
77
124
|
try {
|
|
78
125
|
const res = await deps.fetchFn(cfg.controlPlane.replace(/\/$/, ""), { signal: AbortSignal.timeout(5000) });
|
|
79
126
|
if (res.status >= 500)
|
|
@@ -83,7 +130,50 @@ export async function runDoctorChecks(overrides = {}) {
|
|
|
83
130
|
}
|
|
84
131
|
catch { }
|
|
85
132
|
}
|
|
86
|
-
|
|
133
|
+
if (deps.offline)
|
|
134
|
+
results.push({ name: "control plane reachable", ok: true, fix: "none", note: skipped });
|
|
135
|
+
else
|
|
136
|
+
results.push({ name: "control plane reachable", ok: controlPlaneOk, fix: !cfg ? needCfg : cpFix });
|
|
137
|
+
const keyName = "signing key file is private";
|
|
138
|
+
const keyPath = seedFilePath(deps.env, deps.cwd);
|
|
139
|
+
if (deps.platform === "win32") {
|
|
140
|
+
results.push({ name: keyName, ok: true, fix: "none", note: "skipped on Windows, file modes do not apply" });
|
|
141
|
+
}
|
|
142
|
+
else if (!keyPath) {
|
|
143
|
+
results.push({ name: keyName, ok: true, fix: "none", note: "skipped, the seed comes from an environment variable" });
|
|
144
|
+
}
|
|
145
|
+
else {
|
|
146
|
+
const mode = deps.fileMode(keyPath);
|
|
147
|
+
if (mode === null)
|
|
148
|
+
results.push({ name: keyName, ok: true, fix: "none", note: "skipped, no key file found" });
|
|
149
|
+
else
|
|
150
|
+
results.push({ name: keyName, ok: (mode & 0o077) === 0, fix: `run \`chmod 600 ${keyPath}\` (it is mode ${mode.toString(8).padStart(3, "0")})` });
|
|
151
|
+
}
|
|
152
|
+
let healthOk = false;
|
|
153
|
+
let healthFix = "check shipvo.json controlPlane and your network access (5 second timeout)";
|
|
154
|
+
if (!deps.offline && cfg?.controlPlane && typeof cfg.controlPlane === "string") {
|
|
155
|
+
try {
|
|
156
|
+
const res = await deps.fetchFn(cfg.controlPlane.replace(/\/$/, "") + "/health", { signal: AbortSignal.timeout(5000) });
|
|
157
|
+
if (res.status !== 200)
|
|
158
|
+
healthFix = `/health answered HTTP ${res.status}, expected 200; check the control plane URL and deploy`;
|
|
159
|
+
else {
|
|
160
|
+
try {
|
|
161
|
+
await res.json();
|
|
162
|
+
healthOk = true;
|
|
163
|
+
}
|
|
164
|
+
catch {
|
|
165
|
+
healthFix = "/health answered 200 but not JSON; controlPlane may point at the wrong service";
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
catch (e) {
|
|
170
|
+
healthFix = e instanceof Error && e.name === "TimeoutError" ? "/health did not answer within 5 seconds; check the URL and your network" : healthFix;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
if (deps.offline)
|
|
174
|
+
results.push({ name: "control plane /health returns JSON", ok: true, fix: "none", note: skipped });
|
|
175
|
+
else
|
|
176
|
+
results.push({ name: "control plane /health returns JSON", ok: healthOk, fix: !cfg ? needCfg : healthFix });
|
|
87
177
|
let hermesOk = true;
|
|
88
178
|
if (cfg && Object.prototype.hasOwnProperty.call(cfg, "hermesBytecodeVersion")) {
|
|
89
179
|
try {
|
|
@@ -94,16 +184,27 @@ export async function runDoctorChecks(overrides = {}) {
|
|
|
94
184
|
}
|
|
95
185
|
}
|
|
96
186
|
results.push({ name: "hermesBytecodeVersion (if present) is set", ok: hermesOk, fix: "set hermesBytecodeVersion to a positive integer or list (for example 96 or [96, 98])" });
|
|
187
|
+
// Simulator checks only apply to projects that have a `sim` script.
|
|
188
|
+
if (!deps.skipSim && hasSimScript(deps)) {
|
|
189
|
+
const exe = deps.chromiumPath(deps.cwd);
|
|
190
|
+
results.push({ name: "Playwright Chromium installed", ok: !!exe && deps.fileExists(exe), fix: "npx playwright install chromium" });
|
|
191
|
+
results.push({ name: `sim port ${deps.port} is free`, ok: await deps.portFree(deps.port), fix: "Stop the other process or pass --port <n>" });
|
|
192
|
+
}
|
|
97
193
|
return results;
|
|
98
194
|
}
|
|
99
195
|
export async function doctor(argv) {
|
|
100
|
-
|
|
101
|
-
|
|
196
|
+
const a = parseArgs(argv);
|
|
197
|
+
const port = a.port === undefined ? 4545 : Number(a.port);
|
|
198
|
+
const offline = argv.includes("--offline");
|
|
199
|
+
const skipSim = argv.includes("--skip-sim");
|
|
200
|
+
const stray = argv.some((x, i) => !x.startsWith("--") && argv[i - 1] !== "--port");
|
|
201
|
+
if (stray || !Number.isInteger(port) || port < 1 || port > 65535) {
|
|
202
|
+
console.error("Usage: shipvo doctor [--port <n>] [--offline] [--skip-sim]");
|
|
102
203
|
process.exit(2);
|
|
103
204
|
}
|
|
104
|
-
const results = await runDoctorChecks();
|
|
205
|
+
const results = await runDoctorChecks({ port, offline, skipSim });
|
|
105
206
|
for (const r of results)
|
|
106
|
-
console.log(`${r.ok ? `PASS ${r.name}` : `FAIL ${r.name} — fix: ${r.fix}`}`);
|
|
207
|
+
console.log(`${r.ok ? `PASS ${r.name}${r.note ? ` (${r.note})` : ""}` : `FAIL ${r.name} — fix: ${r.fix}`}`);
|
|
107
208
|
if (results.some((r) => !r.ok))
|
|
108
209
|
process.exit(1);
|
|
109
210
|
}
|
package/dist/help.d.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
|
-
export declare const COMMANDS: readonly ["init", "sim", "publish", "rollback", "app", "login", "doctor", "keys", "mcp", "keygen", "pubkey", "hash", "sign-file", "sign-json", "verify-json", "verify"];
|
|
2
|
-
export declare const help = "shipvo <command>\n\nStart\n init <dir> Create a new app wired to Shipvo\n sim [--port 4545] Build the app and run it in the simulator\n\nShip\n publish [--bands b0,b1,b2] Build, sign and publish an over-the-air release\n publish --dry-run [--out d] Check a release offline: build and sign, write nothing remote\n rollback --to <release> Republish an earlier release as a new release\n app create | list | delete Register, list or remove apps on the hosted control plane\n login Sign in with GitHub and get a key for the hosted simulator\n doctor
|
|
1
|
+
export declare const COMMANDS: readonly ["init", "sim", "preview", "publish", "promote", "auto-halt", "rollback", "app", "login", "doctor", "store", "status", "keys", "mcp", "keygen", "pubkey", "hash", "sign-file", "sign-json", "verify-json", "verify"];
|
|
2
|
+
export declare const help = "shipvo <command>\n\nStart\n init <dir> Create a new app wired to Shipvo\n sim [--port 4545] Build the app and run it in the simulator\n sim --watch [--verbose] Rebuild on file changes and keep serving; --verbose shows full build errors\n preview [--json] Publish a public 24-hour preview link (here.now, no account)\n\nShip\n publish [--bands b0,b1,b2] Build, sign and publish an over-the-air release\n publish --dry-run [--out d] Check a release offline: build and sign, write nothing remote\n promote [--to b1,b2] Widen a staged release to more bands (same release, new signature)\n promote --dry-run Preview a promote, write nothing\n auto-halt [--threshold 0.2 --min-reports 20 | --off] Show, turn on or turn off auto-halt\n rollback --to <release> Republish an earlier release as a new release\n rollback --to <n> --dry-run Preview a rollback, write nothing\n app create | list | delete Register, list or remove apps on the hosted control plane\n login Sign in with GitHub and get a key for the hosted simulator\n store details --dry-run Draft Play Data safety answers and a privacy policy from shipvo.json\n status [--app id] [--json] Show how the newest releases are doing on phones\n doctor [--port <n>] Check setup before first publish\n keys Show the stored key's status and quota\n keys export [--out f] Print or save this app's signing seed, to back it up\n keys import [seed|--file f] Restore the signing seed on a new machine\n mcp Print the MCP config that agents need\n\nAdvanced\n keygen [--out seed.txt] Create an Ed25519 signing key and print the public key\n pubkey Print the public key for SHIPVO_SIGNING_SEED\n hash <file> Print the SHA-256 and size of a file\n sign-file <file> Print the Ed25519 signature (base64) of a file\n sign-json <file.json> Sign a JSON document and write the signature into it\n verify <pointer> [public-key-b64]\n Check a release pointer (such as one from publish --dry-run)\n verify-json <file.json> <public-key-b64>\n Check the signature of a JSON document\n";
|
|
3
3
|
export declare function distance(a: string, b: string): number;
|
|
4
4
|
/** Closest command within edit distance 2, or undefined. */
|
|
5
5
|
export declare function suggest(input: string): string | undefined;
|
|
6
6
|
export declare function unknownCommand(input: string): string;
|
|
7
|
+
/** What to print when a command throws: the message, not a stack trace. Set SHIPVO_DEBUG=1 for the stack. */
|
|
8
|
+
export declare function failure(e: unknown, debug?: boolean): string;
|
package/dist/help.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export const COMMANDS = [
|
|
2
|
-
"init", "sim", "publish", "rollback", "app", "login", "doctor", "keys", "mcp",
|
|
2
|
+
"init", "sim", "preview", "publish", "promote", "auto-halt", "rollback", "app", "login", "doctor", "store", "status", "keys", "mcp",
|
|
3
3
|
"keygen", "pubkey", "hash", "sign-file", "sign-json", "verify-json", "verify",
|
|
4
4
|
];
|
|
5
5
|
export const help = `shipvo <command>
|
|
@@ -7,14 +7,22 @@ export const help = `shipvo <command>
|
|
|
7
7
|
Start
|
|
8
8
|
init <dir> Create a new app wired to Shipvo
|
|
9
9
|
sim [--port 4545] Build the app and run it in the simulator
|
|
10
|
+
sim --watch [--verbose] Rebuild on file changes and keep serving; --verbose shows full build errors
|
|
11
|
+
preview [--json] Publish a public 24-hour preview link (here.now, no account)
|
|
10
12
|
|
|
11
13
|
Ship
|
|
12
14
|
publish [--bands b0,b1,b2] Build, sign and publish an over-the-air release
|
|
13
15
|
publish --dry-run [--out d] Check a release offline: build and sign, write nothing remote
|
|
16
|
+
promote [--to b1,b2] Widen a staged release to more bands (same release, new signature)
|
|
17
|
+
promote --dry-run Preview a promote, write nothing
|
|
18
|
+
auto-halt [--threshold 0.2 --min-reports 20 | --off] Show, turn on or turn off auto-halt
|
|
14
19
|
rollback --to <release> Republish an earlier release as a new release
|
|
20
|
+
rollback --to <n> --dry-run Preview a rollback, write nothing
|
|
15
21
|
app create | list | delete Register, list or remove apps on the hosted control plane
|
|
16
22
|
login Sign in with GitHub and get a key for the hosted simulator
|
|
17
|
-
|
|
23
|
+
store details --dry-run Draft Play Data safety answers and a privacy policy from shipvo.json
|
|
24
|
+
status [--app id] [--json] Show how the newest releases are doing on phones
|
|
25
|
+
doctor [--port <n>] Check setup before first publish
|
|
18
26
|
keys Show the stored key's status and quota
|
|
19
27
|
keys export [--out f] Print or save this app's signing seed, to back it up
|
|
20
28
|
keys import [seed|--file f] Restore the signing seed on a new machine
|
|
@@ -59,3 +67,10 @@ export function unknownCommand(input) {
|
|
|
59
67
|
const s = suggest(input);
|
|
60
68
|
return `shipvo: '${input}' is not a command.${s ? ` Did you mean '${s}'?` : ""}\nRun 'shipvo --help' to see all commands.\n`;
|
|
61
69
|
}
|
|
70
|
+
/** What to print when a command throws: the message, not a stack trace. Set SHIPVO_DEBUG=1 for the stack. */
|
|
71
|
+
export function failure(e, debug = !!process.env.SHIPVO_DEBUG) {
|
|
72
|
+
if (debug && e instanceof Error && e.stack)
|
|
73
|
+
return e.stack;
|
|
74
|
+
const m = e instanceof Error ? e.message : String(e);
|
|
75
|
+
return `shipvo: ${m}\nRun 'shipvo doctor' to check your setup, or set SHIPVO_DEBUG=1 to see the full error.\n`;
|
|
76
|
+
}
|