@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 +53 -2
- package/package.json +1 -1
- package/src/deploy-directory.mjs +23 -10
- package/src/index.mjs +6 -1
- package/src/status.mjs +136 -0
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.
|
|
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
package/src/deploy-directory.mjs
CHANGED
|
@@ -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
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
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
|
|
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
|
|
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
|
+
}
|