@shipvo/cli 0.1.4 → 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/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/ci.d.ts +3 -0
- package/dist/ci.js +42 -0
- package/dist/cli.js +28 -0
- package/dist/config.d.ts +2 -0
- package/dist/config.js +25 -10
- package/dist/doctor.d.ts +2 -0
- package/dist/doctor.js +25 -1
- 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 +2 -2
- package/dist/help.js +24 -3
- package/dist/init.js +31 -14
- package/dist/policy.d.ts +2 -16
- package/dist/policy.js +2 -18
- package/dist/preview-recorder.d.ts +1 -0
- package/dist/preview-recorder.js +62 -0
- package/dist/preview.d.ts +3 -3
- package/dist/preview.js +110 -13
- package/dist/promote.d.ts +1 -1
- package/dist/promote.js +29 -2
- package/dist/publish.d.ts +1 -1
- package/dist/publish.js +12 -1
- 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 +4 -0
- package/dist/sim.js +19 -5
- package/dist/status.d.ts +48 -0
- package/dist/status.js +97 -4
- package/dist/store.d.ts +36 -0
- package/dist/store.js +266 -3
- 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 +6 -4
package/dist/agents.d.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export type ProjectKind = "shipvo" | "expo" | "react-native" | "capacitor" | "unknown";
|
|
2
|
+
export declare const BEGIN = "<!-- shipvo:begin -->";
|
|
3
|
+
export declare const END = "<!-- shipvo:end -->";
|
|
4
|
+
export declare const SKILL_LINE = "npx skills add shipvo/shipvo --skill shipvo -g";
|
|
5
|
+
/** What kind of app lives in `dir`. A shipvo.json wins; then Capacitor, Expo, bare React Native. */
|
|
6
|
+
export declare function detectProject(dir: string): ProjectKind;
|
|
7
|
+
/** The text between (and including) our markers. */
|
|
8
|
+
export declare function agentsSection(kind: ProjectKind): string;
|
|
9
|
+
/** 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). */
|
|
10
|
+
export declare function upsertSection(text: string, section: string): string | null;
|
|
11
|
+
/** Add the `shipvo` server and keep every other server and key. Returns null if the file is not valid JSON. */
|
|
12
|
+
export declare function mergeMcp(text: string | null): string | null;
|
|
13
|
+
/** `shipvo agents setup [--dry-run] [--json]`. Exit 0 done, 2 error. */
|
|
14
|
+
export declare function agentsCmd(argv: string[], dir?: string): Promise<number>;
|
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
|
+
}
|
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
|
@@ -7,15 +7,22 @@ import { init } from "./init.js";
|
|
|
7
7
|
import { publish, rollback } from "./publish.js";
|
|
8
8
|
import { promote } from "./promote.js";
|
|
9
9
|
import { autoHalt } from "./autohalt.js";
|
|
10
|
+
import { explore } from "./explore.js";
|
|
10
11
|
import { sim } from "./sim.js";
|
|
11
12
|
import { preview } from "./preview.js";
|
|
13
|
+
import { run } from "./run.js";
|
|
12
14
|
import { keys, login, mcp } from "./login.js";
|
|
13
15
|
import { verifyPointer } from "./verify.js";
|
|
14
16
|
import { signingKey } from "./signing.js";
|
|
15
17
|
import { appCmd } from "./app.js";
|
|
16
18
|
import { status } from "./status.js";
|
|
19
|
+
import { agentsCmd } from "./agents.js";
|
|
17
20
|
import { doctor } from "./doctor.js";
|
|
21
|
+
import { audit } from "./audit.js";
|
|
18
22
|
import { store } from "./store.js";
|
|
23
|
+
import { ci } from "./ci.js";
|
|
24
|
+
import { easCmd } from "./eas.js";
|
|
25
|
+
import { testCmd } from "./test.js";
|
|
19
26
|
import { failure, help, unknownCommand } from "./help.js";
|
|
20
27
|
const [cmd, ...rest] = process.argv.slice(2);
|
|
21
28
|
try {
|
|
@@ -26,6 +33,12 @@ try {
|
|
|
26
33
|
case "sim":
|
|
27
34
|
await sim(rest);
|
|
28
35
|
break;
|
|
36
|
+
case "explore":
|
|
37
|
+
process.exitCode = await explore(rest);
|
|
38
|
+
break;
|
|
39
|
+
case "run":
|
|
40
|
+
process.exitCode = await run(rest);
|
|
41
|
+
break;
|
|
29
42
|
case "preview":
|
|
30
43
|
await preview(rest);
|
|
31
44
|
break;
|
|
@@ -50,9 +63,24 @@ try {
|
|
|
50
63
|
case "status":
|
|
51
64
|
process.exitCode = await status(rest);
|
|
52
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;
|
|
53
75
|
case "store":
|
|
54
76
|
await store(rest);
|
|
55
77
|
break;
|
|
78
|
+
case "ci":
|
|
79
|
+
process.exitCode = await ci(rest);
|
|
80
|
+
break;
|
|
81
|
+
case "eas":
|
|
82
|
+
process.exitCode = await easCmd(rest);
|
|
83
|
+
break;
|
|
56
84
|
case "app":
|
|
57
85
|
await appCmd(rest);
|
|
58
86
|
break;
|
package/dist/config.d.ts
CHANGED
|
@@ -21,6 +21,8 @@ export type ShipvoConfig = {
|
|
|
21
21
|
preview?: "here.now" | "control-plane";
|
|
22
22
|
/** Release rules checked by `shipvo publish --dry-run`. Validated there, not at load, so a bad section never breaks other commands. */
|
|
23
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[];
|
|
24
26
|
/** Marketplace listing (hosted control plane): shown to agents and on shipvo.dev once reviewed. */
|
|
25
27
|
listing?: {
|
|
26
28
|
public?: boolean;
|
package/dist/config.js
CHANGED
|
@@ -42,31 +42,46 @@ export function publishToken() {
|
|
|
42
42
|
return null;
|
|
43
43
|
}
|
|
44
44
|
}
|
|
45
|
+
const RETRIES = 2;
|
|
45
46
|
/** Publishing through the hosted control plane: the same get/put/keys, scoped to this app and signed-pointer checked server side. */
|
|
46
47
|
function hostedKv(cfg, token) {
|
|
47
48
|
const base = `${cfg.controlPlane.replace(/\/$/, "")}/apps/${cfg.appId}/kv`;
|
|
48
|
-
const auth = { Authorization: `Bearer ${token}
|
|
49
|
+
const auth = { Authorization: `Bearer ${token}`, ...(process.env.SHIPVO_ACTOR === "agent" ? { "x-shipvo-actor": "agent" } : {}) };
|
|
49
50
|
const path = (k) => `${base}/${k.split("/").map(encodeURIComponent).join("/")}`;
|
|
50
51
|
const host = cfg.controlPlane.replace(/\/$/, "");
|
|
51
52
|
/** 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
|
+
const call = async (url, init, what) => {
|
|
53
54
|
let r;
|
|
54
|
-
|
|
55
|
-
|
|
55
|
+
for (let attempt = 0;; attempt++) {
|
|
56
|
+
try {
|
|
57
|
+
r = await fetch(url, init);
|
|
58
|
+
break;
|
|
59
|
+
}
|
|
60
|
+
catch (err) {
|
|
61
|
+
// Network errors only, never HTTP statuses. Every call here is a GET or a PUT of the same bytes to the same key, so repeating it is safe.
|
|
62
|
+
if (attempt < RETRIES) {
|
|
63
|
+
await new Promise((ok) => setTimeout(ok, 150 * 2 ** attempt));
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
const cause = err?.cause?.code ?? err?.code ?? err?.cause?.message ?? err?.message ?? "unknown error";
|
|
67
|
+
throw new Error(`Cannot reach the control plane at ${host} (${cause}) while ${init.method ?? "GET"} ${what}. Check "controlPlane" in shipvo.json, then run \`shipvo doctor\`.`);
|
|
68
|
+
}
|
|
56
69
|
}
|
|
57
|
-
|
|
58
|
-
|
|
70
|
+
if (r.status === 403) { // a typed policy refusal is not a bad token
|
|
71
|
+
const j = await r.clone().json().catch(() => null);
|
|
72
|
+
if (j?.code === "policy_refused")
|
|
73
|
+
throw new Error(`Refused by policy rule ${j.rule}: ${j.reason}`);
|
|
59
74
|
}
|
|
60
75
|
if (r.status === 401 || r.status === 403)
|
|
61
76
|
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
77
|
return r;
|
|
63
78
|
};
|
|
64
79
|
return {
|
|
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 : []; },
|
|
80
|
+
async get(k) { const r = await call(path(k), { headers: auth }, k); return r.ok ? r.text() : null; },
|
|
81
|
+
async getBytes(k) { const r = await call(path(k), { headers: auth }, k); return r.ok ? new Uint8Array(await r.arrayBuffer()) : null; },
|
|
82
|
+
async keys(prefix) { const r = await call(`${base}?prefix=${encodeURIComponent(prefix)}`, { headers: auth }, `keys ${prefix}`); return r.ok ? (await r.json()).keys : []; },
|
|
68
83
|
async put(k, body, type) {
|
|
69
|
-
const r = await call(path(k), { method: "PUT", headers: { ...auth, "content-type": type }, body });
|
|
84
|
+
const r = await call(path(k), { method: "PUT", headers: { ...auth, "content-type": type }, body }, k);
|
|
70
85
|
if (!r.ok) {
|
|
71
86
|
let m = "";
|
|
72
87
|
try {
|
package/dist/doctor.d.ts
CHANGED
|
@@ -18,6 +18,8 @@ type DoctorDeps = {
|
|
|
18
18
|
port: number;
|
|
19
19
|
/** Path of the Playwright Chromium binary, or null if Playwright is not installed. */
|
|
20
20
|
chromiumPath: (cwd: string) => string | null;
|
|
21
|
+
/** Version of the playwright that chromiumPath resolved, or null if unknown. */
|
|
22
|
+
playwrightVersion: (cwd: string) => string | null;
|
|
21
23
|
portFree: (port: number) => Promise<boolean>;
|
|
22
24
|
/** Skip the checks that need the network (control plane reachable and /health). */
|
|
23
25
|
offline: boolean;
|
package/dist/doctor.js
CHANGED
|
@@ -22,6 +22,12 @@ const defaultDeps = () => ({
|
|
|
22
22
|
} },
|
|
23
23
|
port: 4545,
|
|
24
24
|
chromiumPath: playwrightChromiumPath,
|
|
25
|
+
playwrightVersion: (cwd) => { try {
|
|
26
|
+
return createRequire(join(cwd, "package.json"))("playwright/package.json").version;
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
return null;
|
|
30
|
+
} },
|
|
25
31
|
portFree,
|
|
26
32
|
offline: false,
|
|
27
33
|
skipSim: false,
|
|
@@ -42,6 +48,19 @@ function playwrightChromiumPath(cwd) {
|
|
|
42
48
|
}
|
|
43
49
|
return null;
|
|
44
50
|
}
|
|
51
|
+
/** Where system Chrome or Edge lives. Same places as the simulator's browser resolver (packages/sim/browser-resolver.mjs). */
|
|
52
|
+
function systemBrowserPaths(platform, env) {
|
|
53
|
+
if (platform === "win32") {
|
|
54
|
+
const roots = [env.PROGRAMFILES, env["PROGRAMFILES(X86)"], env.LOCALAPPDATA].filter(Boolean);
|
|
55
|
+
return roots.flatMap((r) => [["Chrome", r + "\\Google\\Chrome\\Application\\chrome.exe"], ["Edge", r + "\\Microsoft\\Edge\\Application\\msedge.exe"]]);
|
|
56
|
+
}
|
|
57
|
+
if (platform === "darwin")
|
|
58
|
+
return [["Chrome", "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome"], ["Edge", "/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge"]];
|
|
59
|
+
return [
|
|
60
|
+
["Chrome", "/usr/bin/google-chrome"], ["Chrome", "/usr/bin/google-chrome-stable"], ["Chrome", "/opt/google/chrome/chrome"],
|
|
61
|
+
["Edge", "/usr/bin/microsoft-edge"], ["Edge", "/usr/bin/microsoft-edge-stable"], ["Edge", "/opt/microsoft/msedge/msedge"],
|
|
62
|
+
];
|
|
63
|
+
}
|
|
45
64
|
function hasSimScript(deps) {
|
|
46
65
|
try {
|
|
47
66
|
const pkg = JSON.parse(deps.readText(join(deps.cwd, "package.json")));
|
|
@@ -186,8 +205,13 @@ export async function runDoctorChecks(overrides = {}) {
|
|
|
186
205
|
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
206
|
// Simulator checks only apply to projects that have a `sim` script.
|
|
188
207
|
if (!deps.skipSim && hasSimScript(deps)) {
|
|
208
|
+
const system = systemBrowserPaths(deps.platform, deps.env).find(([, p]) => deps.fileExists(p));
|
|
189
209
|
const exe = deps.chromiumPath(deps.cwd);
|
|
190
|
-
|
|
210
|
+
const cached = !!exe && deps.fileExists(exe);
|
|
211
|
+
const noAuto = deps.env.SHIPVO_NO_AUTO_INSTALL === "1";
|
|
212
|
+
const ok = !!system || cached || !noAuto;
|
|
213
|
+
const note = system ? `system ${system[0]}` : cached ? "Playwright Chromium" : "none found yet; the first run downloads Chromium once";
|
|
214
|
+
results.push({ name: "Browser for the simulator", ok, fix: `SHIPVO_NO_AUTO_INSTALL=1 is set and no Chrome, Edge or Playwright Chromium was found; install Chrome or Edge, or run \`npx playwright install chromium\` in the app folder${deps.playwrightVersion(deps.cwd) ? ` (playwright ${deps.playwrightVersion(deps.cwd)})` : ""}. On Linux, if it then fails to start, run \`npx playwright install-deps chromium\``, note });
|
|
191
215
|
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
216
|
}
|
|
193
217
|
return results;
|
package/dist/eas.d.ts
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
export declare const ANDROID_IMAGE = "latest";
|
|
2
|
+
export declare const IOS_IMAGE = "macos-tahoe-26.5-xcode-26.6";
|
|
3
|
+
export declare const NODE_VERSION = "22";
|
|
4
|
+
export declare const JDK_VERSION = "21";
|
|
5
|
+
export interface CapacitorProject {
|
|
6
|
+
configFile: string;
|
|
7
|
+
android: boolean;
|
|
8
|
+
ios: boolean;
|
|
9
|
+
}
|
|
10
|
+
/** Look for a Capacitor project at `root`. Returns undefined when it is not one. */
|
|
11
|
+
export declare function detectCapacitor(root: string): CapacitorProject | undefined;
|
|
12
|
+
export interface PlannedFile {
|
|
13
|
+
path: string;
|
|
14
|
+
content: string;
|
|
15
|
+
action: "create" | "update";
|
|
16
|
+
}
|
|
17
|
+
/** Work out which files need writing. Existing build YAMLs and schemes are never overwritten. */
|
|
18
|
+
export declare function planEasSetup(root: string, p: CapacitorProject): PlannedFile[];
|
|
19
|
+
/** Create the `ios/App.xcodeproj -> App/App.xcodeproj` link the EAS CLI scheme check needs. Returns true if it made one. */
|
|
20
|
+
export declare function ensureSchemeShim(root: string): boolean;
|
|
21
|
+
export declare const ONE_TIME_STEPS: readonly [readonly ["eas-cli", "init"], readonly ["eas-cli", "build:version:set", "-p", "ios"]];
|
|
22
|
+
export declare const IOS_UNPROVEN = "Note: the iOS signing steps in .eas/build/ios.yml are unproven. The step names were not checked against Expo's custom build functions (https://docs.expo.dev/custom-builds/schema/), so your first iOS build is the check.";
|
|
23
|
+
export type Runner = (cmd: string, args: string[], cwd: string) => number;
|
|
24
|
+
export interface EasOptions {
|
|
25
|
+
cwd?: string;
|
|
26
|
+
log?: (s: string) => void;
|
|
27
|
+
runner?: Runner;
|
|
28
|
+
}
|
|
29
|
+
export declare function easCmd(args: string[], o?: EasOptions): Promise<number>;
|