@anyship/cli 0.3.1 → 0.4.0

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/README.md CHANGED
@@ -44,26 +44,77 @@ stack, builds if needed, and returns a live URL.
44
44
  ```bash
45
45
  anyship deploy --project my-app .
46
46
  anyship deploy --project my-app . --dry-run # list what would be uploaded
47
+ anyship status --project my-app --watch # poll until deployed or failed
47
48
  ```
48
49
 
49
50
  Options: `--project <name>`, `--api <url>`, `--key <token>`, `--env <json>`,
50
51
  `--dry-run`, `--json`.
51
52
 
53
+ ### `anyship status --project <name>`
54
+
55
+ Show deployment status for a project — same information as the MCP
56
+ `deployment_status` tool, via the REST API (not `/mcp`).
57
+
58
+ ```bash
59
+ anyship status --project my-app
60
+ anyship status --project my-app --watch --interval 15
61
+ ```
62
+
63
+ Options: `--project <name>`, `--api <url>`, `--key <token>`, `--json`, `--watch`,
64
+ `--interval <sec>`.
65
+
52
66
  ### `anyship mcp`
53
67
 
54
68
  Run a local [MCP](https://modelcontextprotocol.io) server over stdio. Its
55
69
  `deploy` tool takes a **directory path** and gathers the files for you; every
56
70
  other tool (`list_projects`, `deployment_status`, `add_domain`, `logs`, …) is
57
- proxied to the control plane. Add it to Claude Code:
71
+ proxied to the control plane.
72
+
73
+ **Claude Code**
58
74
 
59
75
  ```bash
60
76
  anyship login --key anyship_sk_…
61
- claude mcp add anyship -- npx -y @anyship/cli mcp
77
+ claude mcp add --scope user anyship -e ANYSHIP_API_KEY=anyship_sk_… -- npx -y @anyship/cli@latest mcp
78
+ ```
79
+
80
+ **Cursor** (`~/.cursor/mcp.json`), **Claude Desktop** (`claude_desktop_config.json`), and **Codex** (`~/.codex/config.toml`):
81
+
82
+ Electron-based clients spawn MCP with a minimal environment — bare `npx` can fail
83
+ before the server starts. Wrap the launch in your login shell so `npx` resolves
84
+ from your normal PATH (nvm, Homebrew, fnm, …). Use `zsh` on macOS, `bash` on
85
+ Linux:
86
+
87
+ ```json
88
+ {
89
+ "mcpServers": {
90
+ "anyship": {
91
+ "command": "zsh",
92
+ "args": ["-lc", "npx -y @anyship/cli@latest mcp"],
93
+ "env": { "ANYSHIP_API_KEY": "anyship_sk_…" }
94
+ }
95
+ }
96
+ }
97
+ ```
98
+
99
+ Codex (`~/.codex/config.toml`) — same idea:
100
+
101
+ ```toml
102
+ [mcp_servers.anyship]
103
+ command = "zsh"
104
+ args = ["-lc", "npx -y @anyship/cli@latest mcp"]
105
+
106
+ [mcp_servers.anyship.env]
107
+ ANYSHIP_API_KEY = "anyship_sk_…"
62
108
  ```
63
109
 
64
110
  Then in any conversation: **"deploy this to anyship."** The agent never selects
65
111
  files and never handles your API key.
66
112
 
113
+ > The `@latest` tag is deliberate: the stdio server is long-lived and `npx`
114
+ > caches by spec, so an unpinned `@anyship/cli` keeps launching whatever version
115
+ > is already cached — a republish would never reach you. `@latest` re-resolves
116
+ > against the registry each time the client (re)starts the server. Keep it pinned.
117
+
67
118
  ## Configuration
68
119
 
69
120
  The `anyship_sk_…` API key is the only credential. It resolves in this order (first
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@anyship/cli",
3
- "version": "0.3.1",
3
+ "version": "0.4.0",
4
4
  "description": "anyship CLI — deploy a local project directory, run a local MCP server for AI agents, and log in.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -96,16 +96,26 @@ export function codexAnyshipConfig(home = homedir()) {
96
96
  const configPath = join(home, ".codex", "config.toml");
97
97
  if (!existsSync(configPath)) return {};
98
98
  const lines = readFileSync(configPath, "utf8").split(/\n/);
99
- const start = lines.findIndex((line) => /^\[mcp_servers\.("?anyship"?)\]\s*$/.test(line.trim()));
100
- if (start < 0) return {};
101
- const section = [];
102
- for (let i = start; i < lines.length; i += 1) {
103
- if (i !== start && lines[i].trim().startsWith("[")) break;
104
- section.push(lines[i]);
105
- }
106
- const text = section.join("\n");
99
+ const sectionText = (header) => {
100
+ const start = lines.findIndex((line) => line.trim() === header);
101
+ if (start < 0) return "";
102
+ const section = [];
103
+ for (let i = start; i < lines.length; i += 1) {
104
+ if (i !== start && lines[i].trim().startsWith("[")) break;
105
+ section.push(lines[i]);
106
+ }
107
+ return section.join("\n");
108
+ };
109
+ const main = sectionText("[mcp_servers.anyship]");
110
+ const env = sectionText("[mcp_servers.anyship.env]");
111
+ if (!main && !env) return {};
112
+ const text = `${main}\n${env}`;
107
113
  const url = text.match(/url\s*=\s*"([^"]+)"/)?.[1]?.replace(/\/mcp\/?$/, "");
108
- const token = text.match(/Bearer ([^"]+)/)?.[1]?.trim();
114
+ const bearer = text.match(/Bearer ([^"]+)/)?.[1]?.trim();
115
+ const envKey =
116
+ text.match(/ANYSHIP_API_KEY\s*=\s*"([^"]+)"/)?.[1]?.trim() ||
117
+ text.match(/ANYSHIP_API_KEY\s*=\s*'([^']+)'/)?.[1]?.trim();
118
+ const token = bearer || envKey;
109
119
  return { url, token };
110
120
  }
111
121
 
@@ -127,7 +137,10 @@ export function claudeCodeAnyshipConfig(home = homedir()) {
127
137
  if (!entry || typeof entry !== "object") return null;
128
138
  const url = typeof entry.url === "string" ? entry.url.replace(/\/mcp\/?$/, "") : undefined;
129
139
  const auth = (entry.headers && (entry.headers.Authorization || entry.headers.authorization)) || "";
130
- const token = /Bearer\s+(anyship_sk_[A-Za-z0-9]+)/.exec(auth)?.[1];
140
+ const bearer = /Bearer\s+(anyship_sk_[A-Za-z0-9]+)/.exec(auth)?.[1];
141
+ const envKey =
142
+ typeof entry.env?.ANYSHIP_API_KEY === "string" ? entry.env.ANYSHIP_API_KEY.trim() : undefined;
143
+ const token = bearer || envKey;
131
144
  return url || token ? { url, token } : null;
132
145
  };
133
146
  const found = [];
package/src/index.mjs CHANGED
@@ -1,6 +1,7 @@
1
1
  import { runDeploy } from "./deploy-directory.mjs";
2
2
  import { runMcpServer } from "./mcp-server.mjs";
3
3
  import { runLogin, runLogout } from "./login.mjs";
4
+ import { runStatus } from "./status.mjs";
4
5
 
5
6
  export function usage(commandName = "anyship") {
6
7
  return `Usage: ${commandName} <command>
@@ -9,6 +10,7 @@ Commands:
9
10
  login Save your anyship API key so deploy/mcp authenticate automatically
10
11
  logout Remove stored credentials
11
12
  deploy Upload a complete safe source archive and deploy it to anyship
13
+ status Show deployment status for a project (poll with --watch after deploy)
12
14
  mcp Run a local MCP server (stdio) so an AI agent can deploy directories directly
13
15
 
14
16
  Run "${commandName} <command> --help" for command options.`;
@@ -29,9 +31,12 @@ export async function runCli(argv, io = defaultIo()) {
29
31
  if (command === "deploy") {
30
32
  return runDeploy(rest, io, { commandName: "anyship deploy" });
31
33
  }
34
+ if (command === "status") {
35
+ return runStatus(rest, io, { commandName: "anyship status" });
36
+ }
32
37
  if (command === "mcp") {
33
38
  // Long-lived: reads JSON-RPC on stdin, writes on stdout. Configured in a
34
- // client via `claude mcp add anyship -- npx -y @anyship/cli mcp` (after
39
+ // client via `claude mcp add anyship -- npx -y @anyship/cli@latest mcp` (after
35
40
  // `anyship login`, no --env needed).
36
41
  return runMcpServer();
37
42
  }
package/src/status.mjs ADDED
@@ -0,0 +1,136 @@
1
+ import {
2
+ apiErrorMessage,
3
+ apiUrl,
4
+ mcpConnectorConfig,
5
+ resolveBearer,
6
+ } from "./deploy-directory.mjs";
7
+
8
+ function parseJson(text) {
9
+ if (!text) return null;
10
+ try {
11
+ return JSON.parse(text);
12
+ } catch {
13
+ return null;
14
+ }
15
+ }
16
+
17
+ export function statusUsage(commandName = "anyship status") {
18
+ return `Usage: ${commandName} --project <name>
19
+
20
+ Options:
21
+ --project <name> anyship project name
22
+ --api <url> control-plane URL (defaults to ANYSHIP_API_URL, ~/.anyship, or MCP connector config)
23
+ --key <token> anyship API key (defaults to ANYSHIP_API_KEY, ~/.anyship, or MCP connector config)
24
+ --json Print the full JSON response
25
+ --watch Poll until the latest deployment is deployed or failed
26
+ --interval <sec> Seconds between polls when --watch is set (default 10)`;
27
+ }
28
+
29
+ export function parseStatusArgs(argv) {
30
+ const args = { interval: 10 };
31
+ for (let i = 0; i < argv.length; i += 1) {
32
+ const arg = argv[i];
33
+ if (arg === "--help" || arg === "-h") {
34
+ args.help = true;
35
+ continue;
36
+ }
37
+ if (arg === "--json") {
38
+ args.json = true;
39
+ continue;
40
+ }
41
+ if (arg === "--watch") {
42
+ args.watch = true;
43
+ continue;
44
+ }
45
+ if (arg === "--project") {
46
+ args.project = requiredValue(argv, i, arg);
47
+ i += 1;
48
+ continue;
49
+ }
50
+ if (arg === "--api") {
51
+ args.api = requiredValue(argv, i, arg);
52
+ i += 1;
53
+ continue;
54
+ }
55
+ if (arg === "--key") {
56
+ args.key = requiredValue(argv, i, arg);
57
+ i += 1;
58
+ continue;
59
+ }
60
+ if (arg === "--interval") {
61
+ const raw = Number(requiredValue(argv, i, arg));
62
+ if (!Number.isFinite(raw) || raw < 1) throw new Error("--interval must be a positive number");
63
+ args.interval = raw;
64
+ i += 1;
65
+ continue;
66
+ }
67
+ throw new Error(`unknown argument: ${arg}`);
68
+ }
69
+ if (!args.help && !args.project) throw new Error("--project is required");
70
+ return args;
71
+ }
72
+
73
+ function requiredValue(argv, index, flag) {
74
+ const value = argv[index + 1];
75
+ if (!value || value.startsWith("--")) throw new Error(`${flag} requires a value`);
76
+ return value;
77
+ }
78
+
79
+ export async function fetchProjectStatus(args, hooks = {}) {
80
+ const config = hooks.config ?? mcpConnectorConfig();
81
+ const api = hooks.apiUrl ? hooks.apiUrl(args, config) : apiUrl(args, config);
82
+ const io = hooks.io ?? { stdout: () => {}, stderr: (t) => process.stderr.write(`${t}\n`) };
83
+ const token = await resolveBearer(args, io, { interactive: hooks.interactive ?? false });
84
+ const fetchImpl = hooks.fetch ?? fetch;
85
+ const res = await fetchImpl(
86
+ `${api}/api/deploy-from-archive/status?project=${encodeURIComponent(args.project)}`,
87
+ { headers: { Authorization: `Bearer ${token}` } },
88
+ );
89
+ const text = await res.text();
90
+ const data = parseJson(text);
91
+ if (!res.ok) {
92
+ throw new Error(apiErrorMessage(res.status, data, text));
93
+ }
94
+ return data ?? {};
95
+ }
96
+
97
+ function terminalStatus(status) {
98
+ return status === "deployed" || status === "failed";
99
+ }
100
+
101
+ function sleep(ms) {
102
+ return new Promise((resolve) => setTimeout(resolve, ms));
103
+ }
104
+
105
+ export async function runStatus(argv, io = defaultIo(), options = {}) {
106
+ const commandName = options.commandName ?? "anyship status";
107
+ const args = parseStatusArgs(argv);
108
+ if (args.help) {
109
+ io.stdout(statusUsage(commandName));
110
+ return 0;
111
+ }
112
+
113
+ do {
114
+ const status = await fetchProjectStatus(args, {
115
+ io,
116
+ interactive: false,
117
+ fetch: options.fetch,
118
+ });
119
+ if (args.json) {
120
+ io.stdout(JSON.stringify(status, null, 2));
121
+ } else {
122
+ io.stdout(String(status.message ?? "No status available."));
123
+ }
124
+ if (!args.watch || terminalStatus(status.latest?.status)) break;
125
+ await sleep(args.interval * 1000);
126
+ } while (true);
127
+
128
+ return 0;
129
+ }
130
+
131
+ function defaultIo() {
132
+ return {
133
+ stdout: (text) => console.log(text),
134
+ stderr: (text) => console.error(text),
135
+ };
136
+ }