@anyship/cli 0.3.0 → 0.3.2

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
@@ -21,23 +21,20 @@ never hand-pick files.
21
21
 
22
22
  ### `anyship login`
23
23
 
24
- Run **`anyship login`** with no arguments and it opens your browser to sign in
25
- (OAuth) the same login you use for the anyship connector, no key required. It
26
- stores the resulting token in `~/.anyship/config.json` (mode `0600`), refreshing
27
- it automatically. This is the common path now that most people connect with
28
- browser login.
29
-
30
- Prefer an API key (CI, scripts)? Pass one and it's validated + stored instead:
24
+ An **`anyship_sk_…` API key is the only credential there is no browser/OAuth
25
+ login.** `anyship login` takes your key, validates it against the control plane,
26
+ and stores it in `~/.anyship/config.json` (mode `0600`), so `deploy` and `mcp`
27
+ authenticate automatically afterward. Grab the key from the anyship dashboard.
31
28
 
32
29
  ```bash
33
- anyship login # browser login (OAuth) no key
34
- anyship login --key anyship_sk_… # or an API key: echo $KEY | anyship login --with-token
30
+ anyship login --key anyship_sk_… # validate + store the key
31
+ echo $KEY | anyship login --with-token # or pipe it (CI / scripts no key in shell history)
35
32
  anyship logout # remove stored credentials
36
33
  ```
37
34
 
38
- `deploy` and `mcp` then authenticate automatically. If neither a key nor a stored
39
- token is present, `deploy` triggers the browser login on first use; the `mcp`
40
- server uses only stored auth (run `anyship login` once first).
35
+ If no key is present, `login` errors telling you to supply one it never opens a
36
+ browser. (OAuth exists only for **dashboard** sign-in Google/GitHub never for
37
+ the CLI, MCP, or deploy.)
41
38
 
42
39
  ### `anyship deploy [path]`
43
40
 
@@ -47,36 +44,88 @@ stack, builds if needed, and returns a live URL.
47
44
  ```bash
48
45
  anyship deploy --project my-app .
49
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
50
48
  ```
51
49
 
52
50
  Options: `--project <name>`, `--api <url>`, `--key <token>`, `--env <json>`,
53
51
  `--dry-run`, `--json`.
54
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
+
55
66
  ### `anyship mcp`
56
67
 
57
68
  Run a local [MCP](https://modelcontextprotocol.io) server over stdio. Its
58
69
  `deploy` tool takes a **directory path** and gathers the files for you; every
59
70
  other tool (`list_projects`, `deployment_status`, `add_domain`, `logs`, …) is
60
- proxied to the control plane. Add it to Claude Code:
71
+ proxied to the control plane.
72
+
73
+ **Claude Code**
61
74
 
62
75
  ```bash
63
- anyship login
64
- claude mcp add anyship -- npx -y @anyship/cli mcp
76
+ anyship login --key anyship_sk_…
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_…"
65
108
  ```
66
109
 
67
110
  Then in any conversation: **"deploy this to anyship."** The agent never selects
68
111
  files and never handles your API key.
69
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
+
70
118
  ## Configuration
71
119
 
72
- Auth resolves in this order an **API key** if you have one, otherwise **OAuth**:
120
+ The `anyship_sk_…` API key is the only credential. It resolves in this order (first
121
+ hit wins); if none is found, the command fails loudly rather than falling back to
122
+ any other account:
73
123
 
74
124
  1. `--key` flag
75
125
  2. `ANYSHIP_API_KEY` env
76
- 3. a stored API key (`~/.anyship/config.json`)
77
- 4. an `mcp_servers.anyship` entry in `~/.codex/config.toml`
78
- 5. a stored **OAuth token** (`~/.anyship/config.json`, auto-refreshed)
79
- 6. otherwise → browser login (interactive commands only)
126
+ 3. a stored key (`~/.anyship/config.json`, written by `anyship login`)
127
+ 4. the anyship MCP-connector key from an existing client config — `~/.codex/config.toml`
128
+ or `~/.claude.json` (so configuring the connector is enough for the CLI too)
80
129
 
81
- The control-plane URL comes from `--api`, `ANYSHIP_API_URL`, the stored config, or
82
- the default. Requires Node.js 18+.
130
+ There is no OAuth token and no browser-login fallback. The control-plane URL comes
131
+ from `--api`, `ANYSHIP_API_URL`, the stored config, or the default. Requires Node.js 18+.
package/package.json CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "name": "@anyship/cli",
3
- "version": "0.3.0",
3
+ "version": "0.3.2",
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": {
7
- "anyship": "./bin/anyship.mjs"
7
+ "anyship": "bin/anyship.mjs"
8
8
  },
9
9
  "exports": {
10
10
  ".": "./src/index.mjs"
@@ -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
  }
@@ -16,8 +16,16 @@ import { resolve } from "node:path";
16
16
  const PROTOCOL_VERSION = "2025-06-18";
17
17
  const SERVER_INFO = { name: "anyship", version: "0.1.0" };
18
18
 
19
+ // The AUTH paragraph is load-bearing — keep it this explicit. This server runs
20
+ // from the CLI, which already holds the credential, so an agent should never
21
+ // think about auth; past agents mistook a generic "requires authentication"
22
+ // prompt for an OAuth step and stalled. Shut that down here, permanently.
19
23
  const SERVER_INSTRUCTIONS = [
20
- "anyship deploys a web app to a live URL. To deploy a project on disk, call `deploy` with the project name and the directory path — anyship gathers every file that isn't gitignored (via git), uploads it, detects the stack, and builds. You never select, list, or exclude files, and you never handle an API key: this server is already authenticated.",
24
+ "anyship deploys a web app to a live URL. To deploy a project on disk, call `deploy` with the project name and the directory path — anyship gathers every file that isn't gitignored (via git), uploads it, detects the stack, and builds. You never select, list, or exclude files.",
25
+ "",
26
+ "DEPLOY is deterministic; you are not — run the one call and trust it, and do NOT reason about, verify, or optimize any part of it. This tool does 100% of the work. Do NOT: choose which subdirectory to send, run a build yourself, generate or read config files, hunt for a 'web root'/output dir, dry-run to inspect files, or judge size. Point it at the project's top-level directory and let anyship handle detection, build, and what to serve. If a deploy fails, read its error — don't predict failures beforehand.",
27
+ "",
28
+ "AUTH — you do not deal with this, at all. This server runs from the anyship CLI, which already holds the credential, so it is authenticated for you. Treat auth as an invisible, solved detail: there is NO OAuth and NO sign-in flow, and no token for you to obtain, paste, or ask anyone for (the only credential is an `anyship_sk_` API key the CLI supplies automatically). Never tell the user to \"authorize\" / \"connect\" / \"sign in\", never ask for auth codes, tokens, or callback URLs, and never report auth (or a client's generic \"requires authentication\" prompt) as a reason you can't deploy. Just call the tools. The ONE exception: if a tool actually returns `not authenticated`, the whole fix is one shell command the user runs once — `anyship login --key anyship_sk_…` — then retry.",
21
29
  "",
22
30
  "Point `deploy` at the project's web root — the folder holding index.html / package.json (defaults to the current directory).",
23
31
  "",
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
+ }