@shipvo/cli 0.1.0 → 0.1.1

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/app.d.ts ADDED
@@ -0,0 +1 @@
1
+ export declare function appCmd(argv: string[]): Promise<void>;
package/dist/app.js ADDED
@@ -0,0 +1,62 @@
1
+ // `shipvo app create`: register this app with the hosted control plane and get a scoped publish token.
2
+ // `shipvo app list`: the apps your account has registered.
3
+ import { mkdirSync, writeFileSync } from "node:fs";
4
+ import { join } from "node:path";
5
+ import { loadConfig } from "./config.js";
6
+ import { savedCreds } from "./login.js";
7
+ export async function appCmd(argv) {
8
+ const [sub] = argv;
9
+ const c = savedCreds();
10
+ if (!c) {
11
+ console.error("Not signed in. Run `shipvo login` first.");
12
+ process.exit(1);
13
+ }
14
+ if (sub === "list") {
15
+ const r = await fetch(`${c.control}/apps`, { headers: { authorization: `Bearer ${c.key}` } });
16
+ const d = await r.json();
17
+ if (!r.ok) {
18
+ console.error(d.hint ?? d.error);
19
+ process.exit(1);
20
+ }
21
+ console.log(d.apps.length ? d.apps.map((a) => `${a.app_id} ${a.name} r${a.latest_release ?? "-"} ${(a.bytes_used / 1048576).toFixed(1)} MB ${a.listed ? (a.approved ? "public" : "listing under review") : "private"}`).join("\n") : "No apps yet. Run `shipvo app create` inside an app made with `shipvo init`.");
22
+ return;
23
+ }
24
+ if (sub === "delete") {
25
+ const id = argv[1];
26
+ if (!id || !argv.includes("--yes")) {
27
+ console.error("Usage: shipvo app delete <app-id> --yes (removes the app's published files, tokens and registration; installed shells keep the code they have)");
28
+ process.exit(2);
29
+ }
30
+ const r = await fetch(`${c.control}/apps/${id}`, { method: "DELETE", headers: { authorization: `Bearer ${c.key}` } });
31
+ const d = await r.json();
32
+ if (!r.ok) {
33
+ console.error(r.status === 404 ? `No app "${id}" on your account.` : d.hint ?? d.error);
34
+ process.exit(1);
35
+ }
36
+ console.log(`Deleted "${id}" (${d.files_deleted} files).`);
37
+ return;
38
+ }
39
+ if (sub !== "create") {
40
+ console.error("Usage: shipvo app create | list | delete <id> --yes");
41
+ process.exit(2);
42
+ }
43
+ const cfg = loadConfig();
44
+ const r = await fetch(`${c.control}/apps`, { method: "POST", headers: { authorization: `Bearer ${c.key}`, "content-type": "application/json" }, body: JSON.stringify({ app_id: cfg.appId, name: cfg.name, public_key: cfg.publicKey, description: cfg.listing?.description ?? "", category: cfg.listing?.category ?? "other", listed: !!cfg.listing?.public }) });
45
+ const d = await r.json();
46
+ if (!r.ok) {
47
+ console.error(`Could not register "${cfg.appId}": ${d.message ?? d.hint ?? d.error}`);
48
+ process.exit(1);
49
+ }
50
+ mkdirSync(join(process.cwd(), ".shipvo"), { recursive: true });
51
+ writeFileSync(join(process.cwd(), ".shipvo", "publish-token"), d.publish_token + "\n", { mode: 0o600 });
52
+ console.log(`${d.created ? "Registered" : "Updated"} "${d.app_id}" (publish token expires ${String(d.expires_at).slice(0, 10)}).`);
53
+ if (cfg.listing?.public)
54
+ console.log(`Listing requested: once reviewed, "${cfg.appId}" appears on shipvo.dev/apps and any signed-in agent can open it. Set listing.public to false in shipvo.json and run this again to withdraw.`);
55
+ else
56
+ console.log(`Private: only your key opens it. To list it publicly, add { "listing": { "public": true, "description": "...", "category": "shop" } } to shipvo.json and run this again.`);
57
+ console.log(`Token saved to .shipvo/publish-token (git-ignored), so \`shipvo publish\` works on this machine now.`);
58
+ console.log(`For CI, add two repository secrets: SHIPVO_PUBLISH_TOKEN (the token) and SHIPVO_SIGNING_SEED (from .shipvo/seed.txt).`);
59
+ if (cfg.controlPlane.replace(/\/$/, "") !== String(d.control_plane).replace(/\/$/, ""))
60
+ console.log(`Note: shipvo.json controlPlane is ${cfg.controlPlane}, but you registered on ${d.control_plane}. Installed shells only trust the one built into them.`);
61
+ console.log(`Agents can open it once published: open_app("${d.app_id}@latest") with your key from \`shipvo mcp\`.`);
62
+ }
package/dist/cli.js CHANGED
@@ -7,6 +7,7 @@ import { init } from "./init.js";
7
7
  import { publish, rollback } from "./publish.js";
8
8
  import { sim } from "./sim.js";
9
9
  import { keys, login, mcp } from "./login.js";
10
+ import { appCmd } from "./app.js";
10
11
  const [cmd, ...rest] = process.argv.slice(2);
11
12
  const help = `shipvo <command>
12
13
 
@@ -14,6 +15,7 @@ const help = `shipvo <command>
14
15
  sim [--port 4545] Build the app for the simulator and serve it (agents: open_app("<appId>"))
15
16
  publish [--bands b0,b1,b2] Build, sign and publish an over-the-air release (+ the agent-facing web artifact)
16
17
  rollback --to <release> Republish an earlier release's code as a new release
18
+ app create | list | delete Register this app with the hosted control plane (publish token), list yours, or remove one
17
19
  login Sign in with GitHub and get a key for the hosted simulator
18
20
  keys Show the stored key's status and quota
19
21
  mcp Print the MCP config agents need (hosted and local)
@@ -41,6 +43,9 @@ switch (cmd) {
41
43
  case "login":
42
44
  await login(rest);
43
45
  break;
46
+ case "app":
47
+ await appCmd(rest);
48
+ break;
44
49
  case "keys":
45
50
  await keys();
46
51
  break;
package/dist/config.d.ts CHANGED
@@ -11,12 +11,20 @@ export type ShipvoConfig = {
11
11
  kv?: {
12
12
  namespaceId: string;
13
13
  };
14
+ /** Marketplace listing (hosted control plane): shown to agents and on shipvo.dev once reviewed. */
15
+ listing?: {
16
+ public?: boolean;
17
+ description?: string;
18
+ category?: string;
19
+ };
14
20
  };
15
21
  export declare function loadConfig(dir?: string): ShipvoConfig;
16
22
  export declare const args: (argv: string[]) => Record<string, string | undefined>;
17
23
  /** Signing seed: SHIPVO_SIGNING_SEED (PURENATIVE_SIGNING_SEED still accepted) or a *_FILE path. */
18
24
  export declare function seed(): string;
19
- /** Cloudflare KV access for publish/rollback. */
25
+ /** The publish token `shipvo app create` issued: SHIPVO_PUBLISH_TOKEN (CI) or .shipvo/publish-token. */
26
+ export declare function publishToken(): string | null;
27
+ /** Where publish/rollback write: your own Cloudflare KV (self-hosted control plane) or the hosted control plane with a publish token. */
20
28
  export declare function kv(cfg: ShipvoConfig): {
21
29
  get(k: string): Promise<string | null>;
22
30
  getBytes(k: string): Promise<Uint8Array<ArrayBuffer> | null>;
package/dist/config.js CHANGED
@@ -17,18 +17,64 @@ export function seed() {
17
17
  const e = process.env;
18
18
  const s = e.SHIPVO_SIGNING_SEED || e.PURENATIVE_SIGNING_SEED
19
19
  || ((e.SHIPVO_SIGNING_SEED_FILE || e.PURENATIVE_SIGNING_SEED_FILE) && readFileSync((e.SHIPVO_SIGNING_SEED_FILE || e.PURENATIVE_SIGNING_SEED_FILE), "utf8").trim());
20
+ const local = (() => { try {
21
+ return readFileSync(join(process.cwd(), ".shipvo", "seed.txt"), "utf8").trim();
22
+ }
23
+ catch {
24
+ return "";
25
+ } })(); // what `shipvo init` wrote
26
+ if (!s && local)
27
+ return local;
20
28
  if (!s) {
21
- console.error("Set SHIPVO_SIGNING_SEED or SHIPVO_SIGNING_SEED_FILE (the seed `shipvo keygen` printed).");
29
+ console.error("Set SHIPVO_SIGNING_SEED or SHIPVO_SIGNING_SEED_FILE (the seed `shipvo keygen` printed), or keep it at .shipvo/seed.txt.");
22
30
  process.exit(2);
23
31
  }
24
32
  return s;
25
33
  }
26
- /** Cloudflare KV access for publish/rollback. */
34
+ /** The publish token `shipvo app create` issued: SHIPVO_PUBLISH_TOKEN (CI) or .shipvo/publish-token. */
35
+ export function publishToken() {
36
+ if (process.env.SHIPVO_PUBLISH_TOKEN)
37
+ return process.env.SHIPVO_PUBLISH_TOKEN.trim();
38
+ try {
39
+ return readFileSync(join(process.cwd(), ".shipvo", "publish-token"), "utf8").trim() || null;
40
+ }
41
+ catch {
42
+ return null;
43
+ }
44
+ }
45
+ /** Publishing through the hosted control plane: the same get/put/keys, scoped to this app and signed-pointer checked server side. */
46
+ function hostedKv(cfg, token) {
47
+ const base = `${cfg.controlPlane.replace(/\/$/, "")}/apps/${cfg.appId}/kv`;
48
+ const auth = { Authorization: `Bearer ${token}` };
49
+ const path = (k) => `${base}/${k.split("/").map(encodeURIComponent).join("/")}`;
50
+ return {
51
+ async get(k) { const r = await fetch(path(k), { headers: auth }); if (r.status === 401)
52
+ throw new Error("publish token rejected: run `shipvo app create` for a new one"); return r.ok ? r.text() : null; },
53
+ async getBytes(k) { const r = await fetch(path(k), { headers: auth }); return r.ok ? new Uint8Array(await r.arrayBuffer()) : null; },
54
+ async keys(prefix) { const r = await fetch(`${base}?prefix=${encodeURIComponent(prefix)}`, { headers: auth }); return r.ok ? (await r.json()).keys : []; },
55
+ async put(k, body, type) {
56
+ const r = await fetch(path(k), { method: "PUT", headers: { ...auth, "content-type": type }, body });
57
+ if (!r.ok) {
58
+ let m = "";
59
+ try {
60
+ const j = await r.json();
61
+ m = j.message ?? j.error;
62
+ }
63
+ catch { }
64
+ throw new Error(`publish ${k}: ${r.status} ${m}`);
65
+ }
66
+ },
67
+ };
68
+ }
69
+ /** Where publish/rollback write: your own Cloudflare KV (self-hosted control plane) or the hosted control plane with a publish token. */
27
70
  export function kv(cfg) {
28
71
  const ACC = process.env.CLOUDFLARE_ACCOUNT_ID, TOKEN = process.env.CLOUDFLARE_API_TOKEN;
29
72
  const NS = process.env.SHIPVO_KV_ID || process.env.PURENATIVE_KV_ID || cfg.kv?.namespaceId;
30
- if (!ACC || !TOKEN || !NS) {
31
- console.error("Need CLOUDFLARE_ACCOUNT_ID, CLOUDFLARE_API_TOKEN and the control plane's KV namespace (shipvo.json kv.namespaceId or SHIPVO_KV_ID).");
73
+ if (!(ACC && TOKEN && NS)) {
74
+ const pt = publishToken();
75
+ if (pt)
76
+ return hostedKv(cfg, pt);
77
+ 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.");
32
78
  process.exit(2);
33
79
  }
34
80
  const url = (k) => `https://api.cloudflare.com/client/v4/accounts/${ACC}/storage/kv/namespaces/${NS}/values/${encodeURIComponent(k)}`;
package/dist/init.js CHANGED
@@ -5,7 +5,7 @@ import { randomBytes } from "node:crypto";
5
5
  import { join, resolve } from "node:path";
6
6
  import { b64, keyFromSeed } from "./index.js";
7
7
  import { args as parse } from "./config.js";
8
- const HOSTED = "https://purenative-control.purenative.workers.dev";
8
+ const HOSTED = "https://api.shipvo.dev";
9
9
  const SDK = { expo: "~58.0.21", camera: "~58.0.14", clipboard: "~58.0.4", localAuth: "~58.0.2", location: "~58.0.20", notifications: "~58.1.7", statusBar: "~58.0.4", constants: "~58.0.0", metroRuntime: "~58.0.15", react: "19.3.0", rn: "0.88.1", rnw: "^0.21.4" };
10
10
  const title = (s) => s.replace(/[-_]+/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
11
11
  export async function init(argv) {
@@ -34,7 +34,7 @@ export async function init(argv) {
34
34
  seedNote = `A release signing key was created at .shipvo/seed.txt (git-ignored). Back it up offline and put it in the SHIPVO_SIGNING_SEED secret of your CI. Lose it and you cannot publish to installed shells.`;
35
35
  }
36
36
  const w = (p, body) => { mkdirSync(join(dir, p, ".."), { recursive: true }); writeFileSync(join(dir, p), body); };
37
- w("shipvo.json", JSON.stringify({ appId, name, shellMajor: 1, controlPlane, publicKey }, null, 2) + "\n");
37
+ w("shipvo.json", JSON.stringify({ appId, name, shellMajor: 1, controlPlane, publicKey, listing: { public: false, description: "", category: "other" } }, null, 2) + "\n");
38
38
  w("package.json", JSON.stringify({
39
39
  name: appId, version: "1.0.0", main: "index.ts", private: true,
40
40
  scripts: { start: "expo start", android: "expo run:android", ios: "expo run:ios", web: "expo start --web", sim: "shipvo sim", publish: "shipvo publish", rollback: "shipvo rollback", typecheck: "tsc --noEmit" },
@@ -83,8 +83,8 @@ Created ${name} in ${dirArg}
83
83
  npx -y @shipvo/mcp # point Claude / Cursor / ChatGPT at it and open_app("${appId}")
84
84
 
85
85
  Phones: npx expo run:android (needs Android Studio once; the OTA runtime is Android-first)
86
- Publish: set CLOUDFLARE_API_TOKEN, CLOUDFLARE_ACCOUNT_ID, SHIPVO_KV_ID and SHIPVO_SIGNING_SEED, then npm run publish
87
- (self-host the control plane from services/control-plane, or ask for a hosted app id during the beta)
86
+ Publish: shipvo login, then shipvo app create (hosted), or set CLOUDFLARE_API_TOKEN, CLOUDFLARE_ACCOUNT_ID and SHIPVO_KV_ID (self-hosted); then npm run publish
87
+ (self-hosting instead? see docs/self-hosting.md)
88
88
 
89
89
  ${seedNote}
90
90
  Public key pinned in the shell: ${publicKey}
@@ -178,9 +178,7 @@ jobs:
178
178
  - run: pip install --break-system-packages bsdiff4
179
179
  - name: Build, sign and publish over the air
180
180
  env:
181
- CLOUDFLARE_API_TOKEN: \${{ secrets.CLOUDFLARE_API_TOKEN }}
182
- CLOUDFLARE_ACCOUNT_ID: \${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
183
- SHIPVO_KV_ID: \${{ secrets.SHIPVO_KV_ID }}
181
+ SHIPVO_PUBLISH_TOKEN: \${{ secrets.SHIPVO_PUBLISH_TOKEN }} # from \`shipvo app create\`; self-hosting instead: CLOUDFLARE_API_TOKEN, CLOUDFLARE_ACCOUNT_ID, SHIPVO_KV_ID
184
182
  SHIPVO_SIGNING_SEED: \${{ secrets.SHIPVO_SIGNING_SEED }}
185
183
  run: npx shipvo publish | tee -a "$GITHUB_STEP_SUMMARY"
186
184
  `;
package/dist/login.js CHANGED
@@ -4,7 +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
- const HOSTED = "https://purenative-control.purenative.workers.dev";
7
+ const HOSTED = "https://api.shipvo.dev";
8
8
  const credFile = () => join(process.env.SHIPVO_HOME || join(homedir(), ".shipvo"), "credentials.json");
9
9
  export function savedCreds() {
10
10
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shipvo/cli",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "type": "module",
5
5
  "license": "Apache-2.0",
6
6
  "bin": {