@shipvo/cli 0.1.3 → 0.1.5
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/agents.d.ts +14 -0
- package/dist/agents.js +124 -0
- package/dist/audit.d.ts +13 -0
- package/dist/audit.js +75 -0
- package/dist/autohalt.d.ts +5 -0
- package/dist/autohalt.js +84 -0
- package/dist/ci.d.ts +3 -0
- package/dist/ci.js +42 -0
- package/dist/cli.js +142 -88
- package/dist/config.d.ts +10 -0
- package/dist/config.js +38 -10
- package/dist/doctor.d.ts +14 -0
- package/dist/doctor.js +132 -7
- package/dist/eas.d.ts +29 -0
- package/dist/eas.js +258 -0
- package/dist/ensure-playwright.d.ts +21 -0
- package/dist/ensure-playwright.js +64 -0
- package/dist/explore.d.ts +84 -0
- package/dist/explore.js +247 -0
- package/dist/help.d.ts +4 -2
- package/dist/help.js +39 -3
- package/dist/init.js +501 -17
- package/dist/native.d.ts +3 -0
- package/dist/native.js +39 -0
- package/dist/policy.d.ts +4 -0
- package/dist/policy.js +17 -0
- package/dist/preview-recorder.d.ts +1 -0
- package/dist/preview-recorder.js +62 -0
- package/dist/preview.d.ts +35 -0
- package/dist/preview.js +346 -0
- package/dist/promote.d.ts +9 -0
- package/dist/promote.js +153 -0
- package/dist/publish.d.ts +1 -1
- package/dist/publish.js +73 -29
- package/dist/report-md.d.ts +26 -0
- package/dist/report-md.js +58 -0
- package/dist/run-bridge.d.ts +7 -0
- package/dist/run-bridge.js +374 -0
- package/dist/run-protocol.d.ts +32 -0
- package/dist/run-protocol.js +22 -0
- package/dist/run-ws.d.ts +8 -0
- package/dist/run-ws.js +89 -0
- package/dist/run.d.ts +86 -0
- package/dist/run.js +301 -0
- package/dist/sim.d.ts +31 -0
- package/dist/sim.js +124 -5
- package/dist/status.d.ts +64 -0
- package/dist/status.js +147 -0
- package/dist/store.d.ts +38 -0
- package/dist/store.js +297 -0
- package/dist/templates/capacitor/github-actions/build.yml +122 -0
- package/dist/test.d.ts +46 -0
- package/dist/test.js +190 -0
- package/package.json +10 -5
package/dist/agents.js
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { dirname, join } from "node:path";
|
|
3
|
+
import { args } from "./config.js";
|
|
4
|
+
export const BEGIN = "<!-- shipvo:begin -->";
|
|
5
|
+
export const END = "<!-- shipvo:end -->";
|
|
6
|
+
export const SKILL_LINE = "npx skills add shipvo/shipvo --skill shipvo -g";
|
|
7
|
+
/** What kind of app lives in `dir`. A shipvo.json wins; then Capacitor, Expo, bare React Native. */
|
|
8
|
+
export function detectProject(dir) {
|
|
9
|
+
if (existsSync(join(dir, "shipvo.json")))
|
|
10
|
+
return "shipvo";
|
|
11
|
+
let deps = {};
|
|
12
|
+
try {
|
|
13
|
+
const p = JSON.parse(readFileSync(join(dir, "package.json"), "utf8"));
|
|
14
|
+
deps = { ...p.devDependencies, ...p.dependencies };
|
|
15
|
+
}
|
|
16
|
+
catch { /* no package.json */ }
|
|
17
|
+
if (existsSync(join(dir, "capacitor.config.ts")) || existsSync(join(dir, "capacitor.config.json")) || "@capacitor/core" in deps)
|
|
18
|
+
return "capacitor";
|
|
19
|
+
if ("expo" in deps)
|
|
20
|
+
return "expo";
|
|
21
|
+
if ("react-native" in deps)
|
|
22
|
+
return "react-native";
|
|
23
|
+
return "unknown";
|
|
24
|
+
}
|
|
25
|
+
const LABEL = { shipvo: "a Shipvo app", expo: "an Expo app", "react-native": "a bare React Native app", capacitor: "a Capacitor app", unknown: "an app" };
|
|
26
|
+
/** The text between (and including) our markers. */
|
|
27
|
+
export function agentsSection(kind) {
|
|
28
|
+
return [
|
|
29
|
+
BEGIN,
|
|
30
|
+
"## Shipvo",
|
|
31
|
+
"",
|
|
32
|
+
`This project is ${LABEL[kind]} that uses Shipvo for over-the-air updates and the app simulator.`,
|
|
33
|
+
"",
|
|
34
|
+
"- Use `shipvo` commands instead of editing release or signing files by hand.",
|
|
35
|
+
"- When you are not sure what to do next, run `shipvo status --json`. It prints `next`: the commands to run and why.",
|
|
36
|
+
"- Exit codes of `shipvo status --json`: 0 ok, 1 action needed, 2 error.",
|
|
37
|
+
"- To drive the app, use the `shipvo` MCP server from `.mcp.json` (`@shipvo/mcp`).",
|
|
38
|
+
END,
|
|
39
|
+
].join("\n");
|
|
40
|
+
}
|
|
41
|
+
/** Put `section` between the markers. Text outside the markers is never changed. Returns null when the markers are broken (one missing, or END before BEGIN). */
|
|
42
|
+
export function upsertSection(text, section) {
|
|
43
|
+
const b = text.indexOf(BEGIN);
|
|
44
|
+
const e = text.indexOf(END);
|
|
45
|
+
if ((b === -1) !== (e === -1) || (b !== -1 && e < b))
|
|
46
|
+
return null;
|
|
47
|
+
if (b !== -1 && e !== -1)
|
|
48
|
+
return text.slice(0, b) + section + text.slice(e + END.length);
|
|
49
|
+
if (!text.trim())
|
|
50
|
+
return section + "\n";
|
|
51
|
+
return text.replace(/\n*$/, "\n\n") + section + "\n";
|
|
52
|
+
}
|
|
53
|
+
const SERVER = { command: "npx", args: ["-y", "@shipvo/mcp"] };
|
|
54
|
+
/** Add the `shipvo` server and keep every other server and key. Returns null if the file is not valid JSON. */
|
|
55
|
+
export function mergeMcp(text) {
|
|
56
|
+
let doc = {};
|
|
57
|
+
if (text !== null && text.trim()) {
|
|
58
|
+
try {
|
|
59
|
+
doc = JSON.parse(text);
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
if (!doc || typeof doc !== "object" || Array.isArray(doc))
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
const servers = (doc.mcpServers && typeof doc.mcpServers === "object" ? doc.mcpServers : {});
|
|
68
|
+
servers.shipvo = SERVER;
|
|
69
|
+
return JSON.stringify({ ...doc, mcpServers: servers }, null, 2) + "\n";
|
|
70
|
+
}
|
|
71
|
+
/** `shipvo agents setup [--dry-run] [--json]`. Exit 0 done, 2 error. */
|
|
72
|
+
export async function agentsCmd(argv, dir = process.cwd()) {
|
|
73
|
+
const [sub, ...rest] = argv;
|
|
74
|
+
if (sub !== "setup") {
|
|
75
|
+
console.error("Usage: shipvo agents setup [--dry-run] [--json]");
|
|
76
|
+
return 2;
|
|
77
|
+
}
|
|
78
|
+
const a = args(rest);
|
|
79
|
+
const dry = a["dry-run"] !== undefined, json = a.json !== undefined;
|
|
80
|
+
const kind = detectProject(dir);
|
|
81
|
+
const changes = [];
|
|
82
|
+
const writes = [];
|
|
83
|
+
const plan = (file, next, note) => {
|
|
84
|
+
const path = join(dir, file);
|
|
85
|
+
const old = existsSync(path) ? readFileSync(path, "utf8") : null;
|
|
86
|
+
if (next === null) {
|
|
87
|
+
changes.push({ file, action: "skip", note });
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
if (old === next) {
|
|
91
|
+
changes.push({ file, action: "unchanged" });
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
changes.push({ file, action: old === null ? "create" : "update" });
|
|
95
|
+
writes.push({ path, text: next });
|
|
96
|
+
};
|
|
97
|
+
const read = (f) => existsSync(join(dir, f)) ? readFileSync(join(dir, f), "utf8") : null;
|
|
98
|
+
plan("AGENTS.md", upsertSection(read("AGENTS.md") ?? "", agentsSection(kind)), "AGENTS.md has a broken shipvo marker; fix it and run again.");
|
|
99
|
+
const targets = [".mcp.json", ...(existsSync(join(dir, ".cursor")) ? [".cursor/mcp.json"] : [])];
|
|
100
|
+
for (const f of targets) {
|
|
101
|
+
const merged = mergeMcp(read(f));
|
|
102
|
+
plan(f, merged, `${f} is not valid JSON; I left it alone. Fix it and run again.`);
|
|
103
|
+
}
|
|
104
|
+
const bad = changes.some((c) => c.action === "skip");
|
|
105
|
+
if (!dry && !bad)
|
|
106
|
+
for (const w of writes) {
|
|
107
|
+
mkdirSync(dirname(w.path), { recursive: true });
|
|
108
|
+
writeFileSync(w.path, w.text);
|
|
109
|
+
}
|
|
110
|
+
const wrote = !dry && !bad;
|
|
111
|
+
if (json)
|
|
112
|
+
console.log(JSON.stringify({ ok: !bad, project: kind, dry_run: dry, changes, skill_install: SKILL_LINE }));
|
|
113
|
+
else {
|
|
114
|
+
console.log(`Project: ${LABEL[kind]}.`);
|
|
115
|
+
for (const c of changes)
|
|
116
|
+
console.log(` ${c.file}: ${c.action === "skip" ? c.note : c.action === "unchanged" ? "already up to date" : `${dry ? "would " : ""}${c.action}${dry ? "" : "d"}`}`);
|
|
117
|
+
if (dry)
|
|
118
|
+
console.log("Dry run: nothing was written.");
|
|
119
|
+
else if (wrote && !changes.some((c) => c.action !== "unchanged"))
|
|
120
|
+
console.log("Nothing to change.");
|
|
121
|
+
console.log(`Install the skill: ${SKILL_LINE}`);
|
|
122
|
+
}
|
|
123
|
+
return bad ? 2 : 0;
|
|
124
|
+
}
|
package/dist/audit.d.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export declare const AUDIT_FLAGS: string[];
|
|
2
|
+
export declare const AUDIT_COLUMNS: readonly ["id", "app_id", "at", "actor", "actor_kind", "action", "release", "release_id", "before", "after"];
|
|
3
|
+
export declare const AUDIT_HELP = "Usage: shipvo audit <app> [--format json|csv] [--limit n]\n\nPrint the app's audit log, newest first, to stdout. Pages through the control plane.\n\nFlags (write a value after a space, not =):\n --format json|csv Output format (default json)\n --limit n Stop after n entries (default: all)\n --help Show this help\n\nNeeds SHIPVO_OWNER_TOKEN, or the publish token from shipvo app create (SHIPVO_PUBLISH_TOKEN or .shipvo/publish-token).\n";
|
|
4
|
+
type Row = Record<string, unknown> & {
|
|
5
|
+
id: number;
|
|
6
|
+
};
|
|
7
|
+
/** One CSV field: quoted, with "" for quotes. Values that start with = + - @ get a leading ' so spreadsheets do not run them. */
|
|
8
|
+
export declare function csvField(v: unknown): string;
|
|
9
|
+
export declare function toCsv(rows: Row[]): string;
|
|
10
|
+
export declare function audit(argv: string[], deps?: {
|
|
11
|
+
fetchFn?: typeof fetch;
|
|
12
|
+
}): Promise<void>;
|
|
13
|
+
export {};
|
package/dist/audit.js
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
// `shipvo audit <app>`: export an app's audit log as JSON or CSV on stdout.
|
|
2
|
+
import { args as parse, loadConfig, publishToken } from "./config.js";
|
|
3
|
+
export const AUDIT_FLAGS = ["format", "limit"];
|
|
4
|
+
export const AUDIT_COLUMNS = ["id", "app_id", "at", "actor", "actor_kind", "action", "release", "release_id", "before", "after"];
|
|
5
|
+
const PAGE = 200;
|
|
6
|
+
export const AUDIT_HELP = `Usage: shipvo audit <app> [--format json|csv] [--limit n]
|
|
7
|
+
|
|
8
|
+
Print the app's audit log, newest first, to stdout. Pages through the control plane.
|
|
9
|
+
|
|
10
|
+
Flags (write a value after a space, not =):
|
|
11
|
+
--format json|csv Output format (default json)
|
|
12
|
+
--limit n Stop after n entries (default: all)
|
|
13
|
+
--help Show this help
|
|
14
|
+
|
|
15
|
+
Needs SHIPVO_OWNER_TOKEN, or the publish token from shipvo app create (SHIPVO_PUBLISH_TOKEN or .shipvo/publish-token).
|
|
16
|
+
`;
|
|
17
|
+
/** One CSV field: quoted, with "" for quotes. Values that start with = + - @ get a leading ' so spreadsheets do not run them. */
|
|
18
|
+
export function csvField(v) {
|
|
19
|
+
let s = v === null || v === undefined ? "" : typeof v === "object" ? JSON.stringify(v) : String(v);
|
|
20
|
+
if (/^[=+\-@]/.test(s))
|
|
21
|
+
s = "'" + s;
|
|
22
|
+
return `"${s.replace(/"/g, '""')}"`;
|
|
23
|
+
}
|
|
24
|
+
export function toCsv(rows) {
|
|
25
|
+
return [AUDIT_COLUMNS.map(csvField).join(","), ...rows.map((r) => AUDIT_COLUMNS.map((c) => csvField(r[c])).join(","))].join("\n");
|
|
26
|
+
}
|
|
27
|
+
export async function audit(argv, deps = {}) {
|
|
28
|
+
if (argv.includes("--help") || argv.includes("-h")) {
|
|
29
|
+
console.log(AUDIT_HELP);
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
const bad = argv.filter((x) => x.startsWith("--") && !AUDIT_FLAGS.includes(x.slice(2)));
|
|
33
|
+
if (bad.length)
|
|
34
|
+
throw new Error(`Unknown flag ${bad.join(", ")}. Known flags: ${AUDIT_FLAGS.map((k) => "--" + k).join(" ")}`);
|
|
35
|
+
const a = parse(argv);
|
|
36
|
+
// Positional app id: the first argument that is not a flag or a flag's value.
|
|
37
|
+
const appId = argv.find((x, i) => !x.startsWith("--") && !(i > 0 && AUDIT_FLAGS.includes(argv[i - 1].slice(2)) && argv[i - 1].startsWith("--")));
|
|
38
|
+
if (!appId)
|
|
39
|
+
throw new Error("Usage: shipvo audit <app> [--format json|csv] [--limit n]");
|
|
40
|
+
const format = a.format ?? "json";
|
|
41
|
+
if (format !== "json" && format !== "csv")
|
|
42
|
+
throw new Error(`--format must be json or csv (got "${a.format}").`);
|
|
43
|
+
let max = Infinity;
|
|
44
|
+
if (argv.includes("--limit")) {
|
|
45
|
+
max = Number(a.limit);
|
|
46
|
+
if (!Number.isInteger(max) || max < 1)
|
|
47
|
+
throw new Error(`--limit must be a positive whole number (got "${a.limit}").`);
|
|
48
|
+
}
|
|
49
|
+
const token = process.env.SHIPVO_OWNER_TOKEN?.trim() || publishToken();
|
|
50
|
+
if (!token)
|
|
51
|
+
throw new Error("No credentials. Set SHIPVO_OWNER_TOKEN, or run `shipvo app create` to get a publish token (SHIPVO_PUBLISH_TOKEN or .shipvo/publish-token).");
|
|
52
|
+
const base = loadConfig().controlPlane.replace(/\/$/, "");
|
|
53
|
+
const f = deps.fetchFn ?? fetch;
|
|
54
|
+
const rows = [];
|
|
55
|
+
let before;
|
|
56
|
+
while (rows.length < max) {
|
|
57
|
+
const want = Math.min(PAGE, max - rows.length);
|
|
58
|
+
const url = `${base}/audit/${encodeURIComponent(appId)}?limit=${want}${before !== undefined ? `&before=${before}` : ""}`;
|
|
59
|
+
let r;
|
|
60
|
+
try {
|
|
61
|
+
r = await f(url, { headers: { authorization: `Bearer ${token}` } });
|
|
62
|
+
}
|
|
63
|
+
catch (e) {
|
|
64
|
+
throw new Error(`Could not reach the control plane at ${base} (${e instanceof Error ? e.message : String(e)}).`);
|
|
65
|
+
}
|
|
66
|
+
if (!r.ok)
|
|
67
|
+
throw new Error(`Audit request failed: HTTP ${r.status}${r.status === 401 ? " (the token was not accepted for this app)" : ""}.`);
|
|
68
|
+
const page = (await r.json()).rows ?? [];
|
|
69
|
+
rows.push(...page);
|
|
70
|
+
if (page.length < want)
|
|
71
|
+
break;
|
|
72
|
+
before = page[page.length - 1].id;
|
|
73
|
+
}
|
|
74
|
+
console.log(format === "csv" ? toCsv(rows) : JSON.stringify(rows, null, 2));
|
|
75
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export declare const AUTOHALT_FLAGS: string[];
|
|
2
|
+
export declare const AUTOHALT_HELP = "Usage: shipvo auto-halt [flags]\n\nStop serving a release on its own when too many devices fail to apply it.\nWith no flags, print the current setting.\n\nFlags (write a value after a space, not =):\n --threshold 0.2 Halt when more than this share of reports failed (above 0, at most 1)\n --min-reports 20 Wait for this many reports first (1 to 10000, default 20)\n --off Turn auto-halt off\n --dry-run Print what would change, write nothing\n --help Show this help\n\nNeeds SHIPVO_OWNER_TOKEN, or the publish token from shipvo app create (SHIPVO_PUBLISH_TOKEN or .shipvo/publish-token).\n";
|
|
3
|
+
export declare function autoHalt(argv: string[], deps?: {
|
|
4
|
+
fetchFn?: typeof fetch;
|
|
5
|
+
}): Promise<void>;
|
package/dist/autohalt.js
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
// `shipvo auto-halt`: show, turn on or turn off auto-halt for this app, through the control plane.
|
|
2
|
+
import { args as parse, loadConfig, publishToken } from "./config.js";
|
|
3
|
+
import { describeAutoHalt } from "./status.js";
|
|
4
|
+
export const AUTOHALT_FLAGS = ["threshold", "min-reports", "off", "dry-run"];
|
|
5
|
+
export const AUTOHALT_HELP = `Usage: shipvo auto-halt [flags]
|
|
6
|
+
|
|
7
|
+
Stop serving a release on its own when too many devices fail to apply it.
|
|
8
|
+
With no flags, print the current setting.
|
|
9
|
+
|
|
10
|
+
Flags (write a value after a space, not =):
|
|
11
|
+
--threshold 0.2 Halt when more than this share of reports failed (above 0, at most 1)
|
|
12
|
+
--min-reports 20 Wait for this many reports first (1 to 10000, default 20)
|
|
13
|
+
--off Turn auto-halt off
|
|
14
|
+
--dry-run Print what would change, write nothing
|
|
15
|
+
--help Show this help
|
|
16
|
+
|
|
17
|
+
Needs SHIPVO_OWNER_TOKEN, or the publish token from shipvo app create (SHIPVO_PUBLISH_TOKEN or .shipvo/publish-token).
|
|
18
|
+
`;
|
|
19
|
+
const describe = (s) => describeAutoHalt(s.enabled ? { threshold: s.threshold, min_reports: s.min_reports } : null);
|
|
20
|
+
export async function autoHalt(argv, deps = {}) {
|
|
21
|
+
if (argv.includes("--help") || argv.includes("-h")) {
|
|
22
|
+
console.log(AUTOHALT_HELP);
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
const bad = argv.filter((x) => x.startsWith("--") && !AUTOHALT_FLAGS.includes(x.slice(2)));
|
|
26
|
+
if (bad.length) {
|
|
27
|
+
const eq = bad.find((x) => x.includes("="));
|
|
28
|
+
throw new Error(`Unknown flag ${bad.join(", ")}.${eq ? ` Write "${eq.replace("=", " ")}" (a space, not =).` : ""} Known flags: ${AUTOHALT_FLAGS.map((k) => "--" + k).join(" ")}`);
|
|
29
|
+
}
|
|
30
|
+
const a = parse(argv), cfg = loadConfig();
|
|
31
|
+
const dry = argv.includes("--dry-run"), off = argv.includes("--off");
|
|
32
|
+
const hasT = argv.includes("--threshold"), hasM = argv.includes("--min-reports");
|
|
33
|
+
if (off && (hasT || hasM))
|
|
34
|
+
throw new Error("--off cannot be combined with --threshold or --min-reports.");
|
|
35
|
+
let threshold, minReports = 20;
|
|
36
|
+
if (hasT || hasM) {
|
|
37
|
+
if (!hasT)
|
|
38
|
+
throw new Error("--min-reports needs --threshold too, for example --threshold 0.2 --min-reports 20.");
|
|
39
|
+
threshold = a.threshold === "true" || a.threshold === undefined ? NaN : Number(a.threshold);
|
|
40
|
+
if (!(threshold > 0 && threshold <= 1))
|
|
41
|
+
throw new Error(`--threshold must be a number above 0 and at most 1, for example 0.2 (got "${a.threshold}").`);
|
|
42
|
+
if (hasM) {
|
|
43
|
+
minReports = a["min-reports"] === "true" || a["min-reports"] === undefined ? NaN : Number(a["min-reports"]);
|
|
44
|
+
if (!Number.isInteger(minReports) || minReports < 1 || minReports > 10000)
|
|
45
|
+
throw new Error(`--min-reports must be a whole number from 1 to 10000 (got "${a["min-reports"]}").`);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
const token = process.env.SHIPVO_OWNER_TOKEN?.trim() || publishToken();
|
|
49
|
+
if (!token)
|
|
50
|
+
throw new Error("No credentials. Set SHIPVO_OWNER_TOKEN, or run `shipvo app create` to get a publish token (SHIPVO_PUBLISH_TOKEN or .shipvo/publish-token).");
|
|
51
|
+
const base = cfg.controlPlane.replace(/\/$/, "");
|
|
52
|
+
const url = `${base}/app/${encodeURIComponent(cfg.appId)}/api/halt-config`;
|
|
53
|
+
const call = async (method, body) => {
|
|
54
|
+
let r;
|
|
55
|
+
try {
|
|
56
|
+
r = await (deps.fetchFn ?? fetch)(url, { method, headers: { authorization: `Bearer ${token}`, ...(body ? { "content-type": "application/json" } : {}) }, body: body ? JSON.stringify(body) : undefined });
|
|
57
|
+
}
|
|
58
|
+
catch (e) {
|
|
59
|
+
throw new Error(`Could not reach the control plane at ${base} (${e instanceof Error ? e.message : String(e)}).`);
|
|
60
|
+
}
|
|
61
|
+
if (r.status === 401 || r.status === 403)
|
|
62
|
+
throw new Error(`The control plane at ${base} rejected your token (${r.status}). Check SHIPVO_OWNER_TOKEN or your publish token.`);
|
|
63
|
+
if (!r.ok) {
|
|
64
|
+
let m = "";
|
|
65
|
+
try {
|
|
66
|
+
m = (await r.json()).message ?? "";
|
|
67
|
+
}
|
|
68
|
+
catch { /* no body */ }
|
|
69
|
+
throw new Error(`Auto-halt request failed: HTTP ${r.status}${m ? ` ${m}` : ""}`);
|
|
70
|
+
}
|
|
71
|
+
return (await r.json());
|
|
72
|
+
};
|
|
73
|
+
if (!off && threshold === undefined) {
|
|
74
|
+
console.log(describe(await call("GET")));
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
const want = off ? { enabled: false } : { enabled: true, threshold, min_reports: minReports };
|
|
78
|
+
if (dry) {
|
|
79
|
+
console.log(`Would set ${describe(want)}`);
|
|
80
|
+
console.log("Dry run: nothing was written.");
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
console.log(describe(off ? await call("DELETE") : await call("PUT", { threshold, min_reports: minReports })));
|
|
84
|
+
}
|
package/dist/ci.d.ts
ADDED
package/dist/ci.js
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { dirname, join, resolve } from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
import { args } from "./config.js";
|
|
5
|
+
export const WORKFLOW_PATH = join(".github", "workflows", "shipvo-build.yml");
|
|
6
|
+
/** Find the template: next to the built CLI (npm), or in the repo's `templates/` (development). */
|
|
7
|
+
function templatePath(framework) {
|
|
8
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
9
|
+
const rel = join("templates", framework, "github-actions", "build.yml");
|
|
10
|
+
for (const c of [join(here, rel), join(here, "..", "..", "..", rel)])
|
|
11
|
+
if (existsSync(c))
|
|
12
|
+
return c;
|
|
13
|
+
throw new Error(`template for ${framework} not found`);
|
|
14
|
+
}
|
|
15
|
+
/** `shipvo ci init --framework capacitor [--dry-run] [--force]`. Returns the exit code. */
|
|
16
|
+
export async function ci(argv, cwd = process.cwd()) {
|
|
17
|
+
const [sub, ...rest] = argv;
|
|
18
|
+
if (sub !== "init") {
|
|
19
|
+
console.error("Usage: shipvo ci init --framework capacitor [--dry-run] [--force]");
|
|
20
|
+
return 2;
|
|
21
|
+
}
|
|
22
|
+
const a = args(rest);
|
|
23
|
+
if (a.framework !== "capacitor") {
|
|
24
|
+
console.error("shipvo ci init: --framework capacitor is the only framework with a template so far");
|
|
25
|
+
return 2;
|
|
26
|
+
}
|
|
27
|
+
const body = readFileSync(templatePath("capacitor"), "utf8");
|
|
28
|
+
const target = resolve(cwd, WORKFLOW_PATH);
|
|
29
|
+
if (a["dry-run"] !== undefined && a["dry-run"] !== "false") {
|
|
30
|
+
console.log(`Would write ${WORKFLOW_PATH}${existsSync(target) ? " (file exists; --force needed)" : ""}. Nothing was written.\n`);
|
|
31
|
+
console.log(body);
|
|
32
|
+
return 0;
|
|
33
|
+
}
|
|
34
|
+
if (existsSync(target) && (a.force === undefined || a.force === "false")) {
|
|
35
|
+
console.error(`${WORKFLOW_PATH} already exists. Use --force to overwrite it.`);
|
|
36
|
+
return 1;
|
|
37
|
+
}
|
|
38
|
+
mkdirSync(dirname(target), { recursive: true });
|
|
39
|
+
writeFileSync(target, body);
|
|
40
|
+
console.log(`Wrote ${WORKFLOW_PATH}. Add the secrets listed in docs/capacitor.md, then run the "Shipvo build" workflow.`);
|
|
41
|
+
return 0;
|
|
42
|
+
}
|
package/dist/cli.js
CHANGED
|
@@ -5,104 +5,158 @@ 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";
|
|
10
|
+
import { explore } from "./explore.js";
|
|
8
11
|
import { sim } from "./sim.js";
|
|
12
|
+
import { preview } from "./preview.js";
|
|
13
|
+
import { run } from "./run.js";
|
|
9
14
|
import { keys, login, mcp } from "./login.js";
|
|
10
15
|
import { verifyPointer } from "./verify.js";
|
|
11
16
|
import { signingKey } from "./signing.js";
|
|
12
17
|
import { appCmd } from "./app.js";
|
|
18
|
+
import { status } from "./status.js";
|
|
19
|
+
import { agentsCmd } from "./agents.js";
|
|
13
20
|
import { doctor } from "./doctor.js";
|
|
14
|
-
import {
|
|
21
|
+
import { audit } from "./audit.js";
|
|
22
|
+
import { store } from "./store.js";
|
|
23
|
+
import { ci } from "./ci.js";
|
|
24
|
+
import { easCmd } from "./eas.js";
|
|
25
|
+
import { testCmd } from "./test.js";
|
|
26
|
+
import { failure, help, unknownCommand } from "./help.js";
|
|
15
27
|
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
|
-
|
|
28
|
+
try {
|
|
29
|
+
switch (cmd) {
|
|
30
|
+
case "init":
|
|
31
|
+
await init(rest);
|
|
32
|
+
break;
|
|
33
|
+
case "sim":
|
|
34
|
+
await sim(rest);
|
|
35
|
+
break;
|
|
36
|
+
case "explore":
|
|
37
|
+
process.exitCode = await explore(rest);
|
|
38
|
+
break;
|
|
39
|
+
case "run":
|
|
40
|
+
process.exitCode = await run(rest);
|
|
41
|
+
break;
|
|
42
|
+
case "preview":
|
|
43
|
+
await preview(rest);
|
|
44
|
+
break;
|
|
45
|
+
case "publish":
|
|
46
|
+
await publish(rest);
|
|
47
|
+
break;
|
|
48
|
+
case "promote":
|
|
49
|
+
await promote(rest);
|
|
50
|
+
break;
|
|
51
|
+
case "auto-halt":
|
|
52
|
+
await autoHalt(rest);
|
|
53
|
+
break;
|
|
54
|
+
case "rollback":
|
|
55
|
+
await rollback(rest);
|
|
56
|
+
break;
|
|
57
|
+
case "login":
|
|
58
|
+
await login(rest);
|
|
59
|
+
break;
|
|
60
|
+
case "doctor":
|
|
61
|
+
await doctor(rest);
|
|
62
|
+
break;
|
|
63
|
+
case "status":
|
|
64
|
+
process.exitCode = await status(rest);
|
|
65
|
+
break;
|
|
66
|
+
case "agents":
|
|
67
|
+
process.exitCode = await agentsCmd(rest);
|
|
68
|
+
break;
|
|
69
|
+
case "audit":
|
|
70
|
+
await audit(rest);
|
|
71
|
+
break;
|
|
72
|
+
case "test":
|
|
73
|
+
process.exitCode = await testCmd(rest);
|
|
74
|
+
break;
|
|
75
|
+
case "store":
|
|
76
|
+
await store(rest);
|
|
77
|
+
break;
|
|
78
|
+
case "ci":
|
|
79
|
+
process.exitCode = await ci(rest);
|
|
80
|
+
break;
|
|
81
|
+
case "eas":
|
|
82
|
+
process.exitCode = await easCmd(rest);
|
|
83
|
+
break;
|
|
84
|
+
case "app":
|
|
85
|
+
await appCmd(rest);
|
|
86
|
+
break;
|
|
87
|
+
case "keys":
|
|
88
|
+
if (rest[0] === "export" || rest[0] === "import")
|
|
89
|
+
await signingKey(rest);
|
|
90
|
+
else
|
|
91
|
+
await keys();
|
|
92
|
+
break;
|
|
93
|
+
case "mcp":
|
|
94
|
+
mcp();
|
|
95
|
+
break;
|
|
96
|
+
case "keygen": {
|
|
97
|
+
const s = b64(randomBytes(32));
|
|
98
|
+
const out = rest[rest.indexOf("--out") + 1];
|
|
99
|
+
if (rest.includes("--out") && out) {
|
|
100
|
+
writeFileSync(out, s, { mode: 0o600 });
|
|
101
|
+
console.error(`Seed written to ${out}. Back it up offline; never commit it.`);
|
|
102
|
+
}
|
|
103
|
+
else
|
|
104
|
+
console.log(`seed (secret): ${s}`);
|
|
105
|
+
console.log(`public key: ${b64(keyFromSeed(s).publicKey)}`);
|
|
106
|
+
break;
|
|
53
107
|
}
|
|
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);
|
|
108
|
+
case "pubkey":
|
|
109
|
+
console.log(b64(keyFromSeed(seed()).publicKey));
|
|
110
|
+
break;
|
|
111
|
+
case "hash": {
|
|
112
|
+
const b = readFileSync(rest[0]);
|
|
113
|
+
console.log(JSON.stringify({ sha256: sha256Hex(b), size: b.length }));
|
|
114
|
+
break;
|
|
115
|
+
}
|
|
116
|
+
case "sign-file":
|
|
117
|
+
console.log(signBytes(new Uint8Array(readFileSync(rest[0])), seed()));
|
|
118
|
+
break;
|
|
119
|
+
case "sign-json": {
|
|
120
|
+
const d = JSON.parse(readFileSync(rest[0], "utf8"));
|
|
121
|
+
writeFileSync(rest[0], JSON.stringify(signDoc(d, seed()), null, 2));
|
|
122
|
+
console.log("signed");
|
|
123
|
+
break;
|
|
85
124
|
}
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
125
|
+
case "verify-json": {
|
|
126
|
+
const ok = verifyDoc(JSON.parse(readFileSync(rest[0], "utf8")), rest[1]);
|
|
127
|
+
console.log(ok ? "valid" : "INVALID");
|
|
128
|
+
process.exit(ok ? 0 : 1);
|
|
90
129
|
}
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
130
|
+
case "verify": {
|
|
131
|
+
if (!rest[0]) {
|
|
132
|
+
console.error("Usage: shipvo verify <pointer file> [public-key-b64]");
|
|
133
|
+
process.exit(2);
|
|
134
|
+
}
|
|
135
|
+
const key = rest[1] ?? b64(keyFromSeed(seed()).publicKey);
|
|
136
|
+
let text;
|
|
137
|
+
try {
|
|
138
|
+
text = readFileSync(rest[0], "utf8");
|
|
139
|
+
}
|
|
140
|
+
catch {
|
|
141
|
+
console.error(`Cannot read ${rest[0]}`);
|
|
142
|
+
process.exit(2);
|
|
143
|
+
}
|
|
144
|
+
const ok = verifyPointer(text, key);
|
|
145
|
+
console.log(ok ? "valid" : "INVALID");
|
|
146
|
+
process.exit(ok ? 0 : 1);
|
|
94
147
|
}
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
148
|
+
case undefined:
|
|
149
|
+
case "--help":
|
|
150
|
+
case "-h":
|
|
151
|
+
case "help":
|
|
152
|
+
console.log(help);
|
|
153
|
+
break;
|
|
154
|
+
default:
|
|
155
|
+
console.error(unknownCommand(cmd));
|
|
156
|
+
process.exit(1);
|
|
98
157
|
}
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
console.log(help);
|
|
104
|
-
break;
|
|
105
|
-
default:
|
|
106
|
-
console.error(unknownCommand(cmd));
|
|
107
|
-
process.exit(1);
|
|
158
|
+
}
|
|
159
|
+
catch (e) {
|
|
160
|
+
console.error(failure(e));
|
|
161
|
+
process.exit(1);
|
|
108
162
|
}
|
package/dist/config.d.ts
CHANGED
|
@@ -9,10 +9,20 @@ 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;
|
|
24
|
+
/** Deep links `shipvo explore` opens after the start screen (for example "tide://cart"). Needs an onUrl listener in the app. */
|
|
25
|
+
routes?: string[];
|
|
16
26
|
/** Marketplace listing (hosted control plane): shown to agents and on shipvo.dev once reviewed. */
|
|
17
27
|
listing?: {
|
|
18
28
|
public?: boolean;
|