@rayrun/cli 0.3.0 → 0.5.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 +198 -15
- package/index.js +4 -1
- package/package.json +9 -4
- package/src/deployments.js +233 -0
- package/src/main.js +26 -0
- package/src/management.js +1193 -4
- package/src/sanitizeTerminalError.js +16 -0
- package/src/skills.js +122 -0
package/README.md
CHANGED
|
@@ -1,17 +1,19 @@
|
|
|
1
1
|
# @rayrun/cli
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
public API.
|
|
3
|
+
Build and deploy complete MCP servers, connect supported MCP clients, execute approved tools through
|
|
4
|
+
OAuth, and manage Rayrun through its public API.
|
|
5
5
|
|
|
6
6
|
```sh
|
|
7
7
|
pnpm --package @rayrun/cli@latest dlx rayrun setup https://copy-the-endpoint-from-rayrun.example/mcp
|
|
8
8
|
```
|
|
9
9
|
|
|
10
|
-
The command detects Claude Code, Codex/ChatGPT desktop, Cursor, VS Code, Windsurf/Devin Local, and
|
|
10
|
+
The command detects Claude Code, Codex/ChatGPT desktop, Cursor, VS Code, Windsurf/Devin Local, and
|
|
11
|
+
OpenCode v2. It shows the targets before writing. Repeat `--client` to select targets, or use
|
|
12
|
+
`--all --yes` to configure all detected clients.
|
|
11
13
|
|
|
12
|
-
It writes only the public Rayrun endpoint. OAuth stays
|
|
13
|
-
|
|
14
|
-
|
|
14
|
+
It writes only the public Rayrun endpoint. OAuth stays in each client; no token or API key enters a
|
|
15
|
+
config file or shell history. Before writing, it creates private byte-for-byte backups under
|
|
16
|
+
`~/.rayrun/backups/setup`. Configuration writes are atomic and roll back together on failure.
|
|
15
17
|
|
|
16
18
|
```sh
|
|
17
19
|
rayrun setup <endpoint> --dry-run --all
|
|
@@ -20,9 +22,13 @@ rayrun setup <endpoint> --client codex --yes --login
|
|
|
20
22
|
rayrun setup rollback <backup-id>
|
|
21
23
|
```
|
|
22
24
|
|
|
23
|
-
Use `--project` for project-local configuration in Claude Code, Codex, Cursor, VS Code, Devin, and
|
|
25
|
+
Use `--project` for project-local configuration in Claude Code, Codex, Cursor, VS Code, Devin, and
|
|
26
|
+
OpenCode. Legacy Windsurf remains user-scoped. Claude Code asks you to approve a project MCP server
|
|
27
|
+
when the project opens. Codex loads `.codex/config.toml` after you trust the project; restart Codex
|
|
28
|
+
after doing so.
|
|
24
29
|
|
|
25
|
-
Interactive setup runs supported
|
|
30
|
+
Interactive setup runs supported OAuth login commands. `--yes` and non-interactive setup skip login
|
|
31
|
+
unless you pass `--login`; use `--no-login` in scripts.
|
|
26
32
|
|
|
27
33
|
## Execute tools
|
|
28
34
|
|
|
@@ -52,14 +58,86 @@ Approve in the browser, then run the printed command. `resume` replays only the
|
|
|
52
58
|
server-issued continuation state; its `accept` response cannot authorize the call without the
|
|
53
59
|
authenticated browser decision. Pending files can contain the exact tool arguments and are stored
|
|
54
60
|
under `~/.rayrun/execution/pending`. The resume window expires after 10 minutes; expired files are
|
|
55
|
-
pruned by the next execution command
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
61
|
+
pruned by the next execution command or `rayrun logout`. OAuth tokens use the same private,
|
|
62
|
+
per-host storage; keep `RAYRUN_CONFIG_HOME` on a local filesystem. POSIX systems enforce `0700`
|
|
63
|
+
directories and `0600` files; Windows uses the current profile's ACL. `logout` removes local tokens
|
|
64
|
+
and attempts revocation.
|
|
65
|
+
|
|
66
|
+
## Build and deploy an MCP server
|
|
67
|
+
|
|
68
|
+
`rayrun init` creates a complete TypeScript MCP server using Node.js 24,
|
|
69
|
+
[ViteMCP](https://github.com/vitemcp/server), Zod, and Vitest. Give the generated directory to Codex,
|
|
70
|
+
Claude Code, or another coding agent and ask it to implement the server—not just one tool. The
|
|
71
|
+
server may expose as many tools as the project needs, up to the release limit documented below.
|
|
72
|
+
|
|
73
|
+
```sh
|
|
74
|
+
rayrun init my-mcp
|
|
75
|
+
cd my-mcp
|
|
76
|
+
npm test
|
|
77
|
+
export RAYRUN_API_KEY='rak_...'
|
|
78
|
+
rayrun deploy --wait
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
On first deploy, Rayrun creates the service and saves its connection UID in
|
|
82
|
+
`.rayrun/project.json`. Later deploys update that same service. Use `--connection` to target a
|
|
83
|
+
different existing source deployment, and declare required secret names in `rayrun.json`:
|
|
84
|
+
|
|
85
|
+
```sh
|
|
86
|
+
rayrun deploy --secret GITHUB_TOKEN=<workspace-secret-uid> --wait
|
|
87
|
+
rayrun deployments list --connection <connection-uid>
|
|
88
|
+
rayrun deployments logs <build-uid>
|
|
89
|
+
rayrun deployments releases <connection-uid>
|
|
90
|
+
rayrun deployments rollback <connection-uid> <release-uid> --wait
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
The generated `rayrun.json` is the deployment contract. Visible configuration belongs in
|
|
94
|
+
`environment`; required secret environment names belong in `secrets` and are bound to
|
|
95
|
+
workspace-secret UIDs at deploy time:
|
|
96
|
+
|
|
97
|
+
```json
|
|
98
|
+
{
|
|
99
|
+
"entrypoint": "dist/server.js",
|
|
100
|
+
"environment": {
|
|
101
|
+
"INVOICE_API_URL": "https://api.example.com"
|
|
102
|
+
},
|
|
103
|
+
"name": "invoice-automation",
|
|
104
|
+
"secrets": ["INVOICE_API_TOKEN"],
|
|
105
|
+
"version": 1
|
|
106
|
+
}
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
The project root must also contain `package.json` with `"type": "module"` and a build script, plus
|
|
110
|
+
`package-lock.json`. The built server must listen on `PORT` and expose Streamable HTTP MCP at
|
|
111
|
+
`/mcp`. `NODE_ENV`, `PORT`, and `RAYRUN_DEPLOYMENT_UID` are reserved.
|
|
112
|
+
|
|
113
|
+
The recommended scaffold is a golden path, not a platform lock-in. Rayrun accepts a Node project
|
|
114
|
+
with a lockfile and build script whose configured entrypoint serves Streamable HTTP MCP at `/mcp`
|
|
115
|
+
on `PORT`. Source uploads reject symlinks, a Dockerfile, `.env` files, obvious private-key material,
|
|
116
|
+
more than 512 files, or more than 16 MiB. Source is encrypted at rest. Builds run remotely with a
|
|
117
|
+
fixed Node 24 Dockerfile whose runtime stage uses a non-root user, and activation happens only after
|
|
118
|
+
the candidate returns at least one tool from `tools/list`. A failed candidate leaves the active
|
|
119
|
+
release untouched.
|
|
120
|
+
|
|
121
|
+
Build logs are encrypted with the workspace data key and erased after 30 days. API keys need the
|
|
122
|
+
separate `deployments:logs:read` scope to read them.
|
|
123
|
+
The Dashboard's **Deploy services** key preset grants only the three deployment scopes required by
|
|
124
|
+
this CLI workflow.
|
|
125
|
+
|
|
126
|
+
Rotating a bound workspace secret marks the source service for redeploy. Running `rayrun deploy
|
|
127
|
+
--wait` again reuses the already-built image, verifies a candidate with the new secret version, and
|
|
128
|
+
activates it without uploading the secret value or rebuilding unchanged source.
|
|
129
|
+
|
|
130
|
+
`--wait` follows the exact release UID returned by Rayrun when a built image is reused. Source
|
|
131
|
+
images are limited to 512 MiB, workspace image retention is limited to 5 GiB, and inactive images
|
|
132
|
+
remain available for rollback for 30 days.
|
|
133
|
+
|
|
134
|
+
A workspace can hold 25 source-deployed services, run 10 source builds concurrently, and start 50
|
|
135
|
+
isolated builds in 24 hours. One service can build at a time. A release may advertise up to 1,000
|
|
136
|
+
tools; build logs are capped at 1 MiB and erased after 30 days.
|
|
60
137
|
|
|
61
|
-
|
|
62
|
-
|
|
138
|
+
Private package-manager credentials are not accepted: `.npmrc` and Yarn credential files are
|
|
139
|
+
excluded locally and rejected by the API. Build dependencies must be readable without placing a
|
|
140
|
+
registry token in the uploaded project.
|
|
63
141
|
|
|
64
142
|
## Manage a workspace
|
|
65
143
|
|
|
@@ -73,6 +151,21 @@ export RAYRUN_API_KEY='rak_...'
|
|
|
73
151
|
rayrun connect mcp https://mcp.example.com --name 'Internal tools'
|
|
74
152
|
rayrun connect openapi https://api.example.com/openapi.json --name 'Example API'
|
|
75
153
|
rayrun connections list
|
|
154
|
+
rayrun connections history <connection-uid>
|
|
155
|
+
rayrun connections diff <connection-uid> <version-uid>
|
|
156
|
+
rayrun connections restore <connection-uid> <version-uid> --reason 'Restore reviewed settings'
|
|
157
|
+
rayrun access-profiles list
|
|
158
|
+
rayrun access-profiles history <profile-uid>
|
|
159
|
+
rayrun access-profiles diff <profile-uid> <version-uid>
|
|
160
|
+
rayrun access-profiles restore <profile-uid> <version-uid> \
|
|
161
|
+
--reason 'Restore reviewed policy' --confirm-risk
|
|
162
|
+
rayrun webhooks list
|
|
163
|
+
rayrun webhooks update <webhook-uid> --config '{"enabled":false}' \
|
|
164
|
+
--reason 'Pause deliveries during maintenance'
|
|
165
|
+
rayrun webhooks history <webhook-uid>
|
|
166
|
+
rayrun webhooks diff <webhook-uid> <version-uid>
|
|
167
|
+
rayrun webhooks restore <webhook-uid> <version-uid> \
|
|
168
|
+
--reason 'Restore reviewed destination'
|
|
76
169
|
rayrun clients list
|
|
77
170
|
rayrun tools search create issue --connection <connection-uid>
|
|
78
171
|
rayrun policy inspect <client-uid> --query issue
|
|
@@ -84,3 +177,93 @@ List commands show a compact table. Add `--json` for stable machine-readable out
|
|
|
84
177
|
request up to 100 rows, and `--cursor` with the printed next cursor to continue. The CLI calls the
|
|
85
178
|
same hosted API and policy evaluator as the dashboard; it does not run a local gateway or store API
|
|
86
179
|
keys.
|
|
180
|
+
|
|
181
|
+
Connection history versions the service name, slug, description, timeout, enablement, and payload
|
|
182
|
+
capture together. Credentials, headers, OAuth state, authority policy, health, and index state never
|
|
183
|
+
enter the snapshot. `history` is cursor-paginated, `diff` compares with the immediate predecessor,
|
|
184
|
+
and `restore` creates a new attributed version using optimistic concurrency.
|
|
185
|
+
|
|
186
|
+
Access-profile history versions the name, description, default, and exact tool rules together.
|
|
187
|
+
Client assignments and archival remain separate authorization events. Restore refuses references to
|
|
188
|
+
tools that are no longer available; pass `--confirm-risk` only after reviewing every Destructive or
|
|
189
|
+
Unknown tool allowed by the restored default or an exact rule. Immutable access-profile history is
|
|
190
|
+
limited to 64 MiB per workspace.
|
|
191
|
+
|
|
192
|
+
Webhook history versions the destination URL, description, event selection, delivery mode, batch
|
|
193
|
+
size, enablement, and payload forwarding together. Signing secrets, deliveries, retry state, and
|
|
194
|
+
health never enter the snapshot. Restore revalidates the historical destination, creates a new
|
|
195
|
+
attributed version, and preserves the current signing secret. Immutable webhook history is limited
|
|
196
|
+
to 64 MiB per workspace. Destination paths and query strings can contain provider tokens, so
|
|
197
|
+
`diff` requires `webhooks:read`; add `webhooks:write` to reveal full destination URLs. Read-only
|
|
198
|
+
API keys receive `null` instead of current or historical URLs.
|
|
199
|
+
|
|
200
|
+
## Publish Workspace Skills
|
|
201
|
+
|
|
202
|
+
Workspace Skills are Agent Skills packages delivered by compatible clients through Rayrun's
|
|
203
|
+
SEP-2640 MCP extension. The package root must contain `SKILL.md`; local reads reject symlinks,
|
|
204
|
+
special files, unsafe paths, more than 512 files, or more than 16 MiB.
|
|
205
|
+
|
|
206
|
+
```sh
|
|
207
|
+
rayrun skills validate ./skills/incident-response
|
|
208
|
+
rayrun skills push ./skills/incident-response --publish \
|
|
209
|
+
--mode both --all-clients --reason 'Reviewed for production use'
|
|
210
|
+
rayrun skills list
|
|
211
|
+
rayrun skills show <skill-uid>
|
|
212
|
+
rayrun skills audience <skill-uid> --mode direct --profiles <profile-uid>
|
|
213
|
+
rayrun skills history <skill-uid>
|
|
214
|
+
rayrun skills diff <skill-uid> <version-uid>
|
|
215
|
+
rayrun skills pull <skill-uid> --output ./recovered-skill
|
|
216
|
+
rayrun skills restore <skill-uid> <version-uid> --reason 'Recover reviewed instructions'
|
|
217
|
+
rayrun skills disable <skill-uid>
|
|
218
|
+
rayrun skills archive <skill-uid>
|
|
219
|
+
rayrun skills delete <skill-uid> --confirm-name incident-response
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
`push` saves a draft unless `--publish` is explicit. A new draft never moves the existing published
|
|
223
|
+
pointer. Pass `--profiles` as comma-separated access-profile UIDs to narrow discovery, or explicitly
|
|
224
|
+
pass `--all-clients`. Later publish and audience commands preserve the current audience when both
|
|
225
|
+
flags are omitted. Script, binary-file, and possible-secret warnings require
|
|
226
|
+
`--confirm-risk` before publication. `allowed-tools` is guidance, not access policy.
|
|
227
|
+
|
|
228
|
+
`pull` refuses to overwrite an existing path and creates account-private directories and files.
|
|
229
|
+
Restore creates another immutable draft revision and does not publish it. Permanent deletion removes
|
|
230
|
+
all content and delivery summaries, cannot be undone, and keeps the Skill name reserved so an old
|
|
231
|
+
`skill://` URI cannot resolve to unrelated content.
|
|
232
|
+
|
|
233
|
+
## Author hosted tool hooks
|
|
234
|
+
|
|
235
|
+
Use a full-control API key. Hook source executes in Rayrun’s hosted sandbox, not in the CLI process.
|
|
236
|
+
|
|
237
|
+
```sh
|
|
238
|
+
rayrun hooks pull <connection-uid> <tool-uid> \
|
|
239
|
+
--output hook.ts --types-output rayrun-hooks.d.ts
|
|
240
|
+
|
|
241
|
+
rayrun hooks test <connection-uid> <tool-uid> \
|
|
242
|
+
--file hook.ts --arguments '{"query":"release"}' --mock-result '{"items":[]}'
|
|
243
|
+
|
|
244
|
+
rayrun hooks save <connection-uid> <tool-uid> \
|
|
245
|
+
--file hook.ts --reason 'Normalize release results'
|
|
246
|
+
rayrun hooks history <connection-uid> <tool-uid>
|
|
247
|
+
rayrun hooks diff <connection-uid> <tool-uid> <draft-version-uid>
|
|
248
|
+
rayrun hooks restore <connection-uid> <tool-uid> <draft-version-uid> \
|
|
249
|
+
--reason 'Restore the reviewed mapping'
|
|
250
|
+
rayrun hooks reset <connection-uid> <tool-uid> --confirm <hook-uid>@<version> \
|
|
251
|
+
--reason 'Remove obsolete experiment history'
|
|
252
|
+
rayrun hooks deploy <connection-uid> <tool-uid> --shadow
|
|
253
|
+
rayrun hooks logs <connection-uid> <tool-uid>
|
|
254
|
+
rayrun hooks rollback <connection-uid> <tool-uid> <revision-uid>
|
|
255
|
+
rayrun hooks deactivate <connection-uid> <tool-uid> --shadow
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
`pull` can write the draft and generated TypeScript declarations separately. `test` compiles and
|
|
259
|
+
runs before plus an optional after stage using your mock result; it never contacts the upstream.
|
|
260
|
+
Each material `save` creates an immutable, attributed draft version; identical saves do not create
|
|
261
|
+
noise. `history` is paginated, `diff` compares with the previous version, and `restore` creates a
|
|
262
|
+
new draft head. Draft history and deployment revisions remain separate. Source and config history
|
|
263
|
+
is limited to 64 MiB per workspace.
|
|
264
|
+
|
|
265
|
+
`reset` permanently removes the hook's history and deployment revisions using the current hook UID
|
|
266
|
+
and version. A connection cannot be deleted while it owns hook history. `deploy` creates an
|
|
267
|
+
immutable revision and defaults to active; rollback and deactivate also accept `--shadow`. Hook
|
|
268
|
+
payloads follow the service capture setting, while timing, outcome, revision, request ID, and shadow
|
|
269
|
+
comparison remain available. Captured details require a full-control key.
|
package/index.js
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
import { main } from './src/main.js';
|
|
4
|
+
import { sanitizeTerminalError } from './src/sanitizeTerminalError.js';
|
|
4
5
|
|
|
5
6
|
try {
|
|
6
7
|
await main(process.argv.slice(2));
|
|
7
8
|
} catch (error) {
|
|
8
|
-
process.stderr.write(
|
|
9
|
+
process.stderr.write(
|
|
10
|
+
`${sanitizeTerminalError(error instanceof Error ? error.message : String(error))}\n`,
|
|
11
|
+
);
|
|
9
12
|
process.exitCode = 1;
|
|
10
13
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rayrun/cli",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.5.0",
|
|
4
|
+
"description": "Build and deploy MCP servers, connect clients, execute tools, and manage Rayrun",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
7
7
|
"type": "git",
|
|
@@ -15,11 +15,14 @@
|
|
|
15
15
|
"index.js",
|
|
16
16
|
"src/clients.js",
|
|
17
17
|
"src/config.js",
|
|
18
|
+
"src/deployments.js",
|
|
18
19
|
"src/execution.js",
|
|
19
20
|
"src/main.js",
|
|
20
21
|
"src/management.js",
|
|
21
22
|
"src/oauth.js",
|
|
23
|
+
"src/sanitizeTerminalError.js",
|
|
22
24
|
"src/setup.js",
|
|
25
|
+
"src/skills.js",
|
|
23
26
|
"README.md",
|
|
24
27
|
"LICENSE"
|
|
25
28
|
],
|
|
@@ -29,12 +32,14 @@
|
|
|
29
32
|
},
|
|
30
33
|
"dependencies": {
|
|
31
34
|
"@modelcontextprotocol/client": "2.0.0",
|
|
32
|
-
"@rayrun/sdk": "^0.
|
|
35
|
+
"@rayrun/sdk": "^0.7.0",
|
|
36
|
+
"diff": "^9.0.0",
|
|
37
|
+
"ignore": "7.0.6",
|
|
33
38
|
"jsonc-parser": "^3.3.1",
|
|
34
39
|
"smol-toml": "^1.8.0"
|
|
35
40
|
},
|
|
36
41
|
"devDependencies": {
|
|
37
|
-
"vitest": "^4.1.
|
|
42
|
+
"vitest": "^4.1.11"
|
|
38
43
|
},
|
|
39
44
|
"engines": {
|
|
40
45
|
"node": ">=20"
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
import createIgnore from 'ignore';
|
|
2
|
+
import { spawn } from 'node:child_process';
|
|
3
|
+
import { randomUUID } from 'node:crypto';
|
|
4
|
+
import { lstat, mkdir, readFile, readdir, rename, unlink, writeFile } from 'node:fs/promises';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
|
|
7
|
+
const DEFAULT_IGNORES = [
|
|
8
|
+
'.git/',
|
|
9
|
+
'.rayrun/',
|
|
10
|
+
'.dockerignore',
|
|
11
|
+
'.env',
|
|
12
|
+
'.env.*',
|
|
13
|
+
'.npmrc',
|
|
14
|
+
'.yarnrc',
|
|
15
|
+
'.yarnrc.yml',
|
|
16
|
+
'!.env.example',
|
|
17
|
+
'!.env.sample',
|
|
18
|
+
'!.env.template',
|
|
19
|
+
'coverage/',
|
|
20
|
+
'dist/',
|
|
21
|
+
'node_modules/',
|
|
22
|
+
];
|
|
23
|
+
const MAX_SOURCE_BYTES = 16_777_216;
|
|
24
|
+
const MAX_SOURCE_FILES = 512;
|
|
25
|
+
|
|
26
|
+
const packageJsonFor = (name) => ({
|
|
27
|
+
dependencies: { '@vitemcp/server': '1.6.5', zod: '4.4.3' },
|
|
28
|
+
devDependencies: {
|
|
29
|
+
'@types/node': '26.1.1',
|
|
30
|
+
tsx: '4.23.12',
|
|
31
|
+
typescript: '6.0.2',
|
|
32
|
+
vitest: '4.1.10',
|
|
33
|
+
},
|
|
34
|
+
engines: { node: '>=24' },
|
|
35
|
+
name,
|
|
36
|
+
private: true,
|
|
37
|
+
scripts: {
|
|
38
|
+
build: 'tsc -p tsconfig.json',
|
|
39
|
+
dev: 'tsx watch src/server.ts',
|
|
40
|
+
start: 'node dist/server.js',
|
|
41
|
+
test: 'vitest run',
|
|
42
|
+
},
|
|
43
|
+
type: 'module',
|
|
44
|
+
version: '1.0.0',
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
const scaffoldFiles = (name) => ({
|
|
48
|
+
'.gitignore': 'node_modules/\ndist/\ncoverage/\n.env\n.env.*\n!.env.example\n.rayrun/\n',
|
|
49
|
+
'README.md': `# ${name}\n\nA stateless MCP server built with [ViteMCP](https://github.com/vitemcp/server) and ready for Rayrun.\n\n\`\`\`sh\nnpm test\nnpm run build\nnpx --yes rayrun@latest deploy --wait\n\`\`\`\n`,
|
|
50
|
+
'package.json': `${JSON.stringify(packageJsonFor(name), null, 2)}\n`,
|
|
51
|
+
'rayrun.json': `${JSON.stringify(
|
|
52
|
+
{ entrypoint: 'dist/server.js', environment: {}, name, secrets: [], version: 1 },
|
|
53
|
+
null,
|
|
54
|
+
2,
|
|
55
|
+
)}\n`,
|
|
56
|
+
'src/greet.ts': `export const greet = (name: string) => \`Hello, \${name}!\`;\n`,
|
|
57
|
+
'src/greet.test.ts': `import { greet } from './greet.js';\nimport { expect, test } from 'vitest';\n\ntest('greets the requested name', () => {\n expect(greet('Rayrun')).toBe('Hello, Rayrun!');\n});\n`,
|
|
58
|
+
'src/server.ts': `import { ViteMCP } from '@vitemcp/server';\nimport { z } from 'zod';\nimport { greet } from './greet.js';\n\nconst server = new ViteMCP({ name: '${name}', version: '1.0.0' });\n\nserver.addTool({\n annotations: { readOnlyHint: true },\n description: 'Return a friendly greeting.',\n execute: async ({ name }) => greet(name),\n name: 'greet',\n parameters: z.object({ name: z.string().min(1).describe('Name to greet') }),\n});\n\nawait server.start({\n httpStream: { endpoint: '/mcp', port: Number.parseInt(process.env.PORT ?? '8080', 10) },\n transportType: 'httpStream',\n});\n`,
|
|
59
|
+
'tsconfig.json': `${JSON.stringify(
|
|
60
|
+
{
|
|
61
|
+
compilerOptions: {
|
|
62
|
+
declaration: true,
|
|
63
|
+
esModuleInterop: true,
|
|
64
|
+
module: 'NodeNext',
|
|
65
|
+
moduleResolution: 'NodeNext',
|
|
66
|
+
noUncheckedIndexedAccess: true,
|
|
67
|
+
outDir: 'dist',
|
|
68
|
+
rootDir: 'src',
|
|
69
|
+
strict: true,
|
|
70
|
+
target: 'ES2023',
|
|
71
|
+
types: ['node'],
|
|
72
|
+
},
|
|
73
|
+
exclude: ['dist', 'node_modules', 'src/**/*.test.ts'],
|
|
74
|
+
include: ['src/**/*.ts'],
|
|
75
|
+
},
|
|
76
|
+
null,
|
|
77
|
+
2,
|
|
78
|
+
)}\n`,
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
const runNpmInstall = async (directory) => {
|
|
82
|
+
await new Promise((resolve, reject) => {
|
|
83
|
+
const child = spawn('npm', ['install', '--ignore-scripts'], {
|
|
84
|
+
cwd: directory,
|
|
85
|
+
stdio: 'inherit',
|
|
86
|
+
});
|
|
87
|
+
child.once('error', reject);
|
|
88
|
+
child.once('exit', (code) =>
|
|
89
|
+
code === 0 ? resolve() : reject(new Error(`npm install exited with code ${String(code)}.`)),
|
|
90
|
+
);
|
|
91
|
+
});
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
const validateDeploymentName = (value) => {
|
|
95
|
+
const name = value.trim().toLowerCase();
|
|
96
|
+
if (!/^[a-z0-9]+(?:-[a-z0-9]+)*$/u.test(name) || name.length > 64) {
|
|
97
|
+
throw new Error('--name must be a lowercase kebab-case name of at most 64 characters.');
|
|
98
|
+
}
|
|
99
|
+
return name;
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
export const scaffoldDeploymentProject = async ({ directory, install = true, name }) => {
|
|
103
|
+
const destination = path.resolve(directory);
|
|
104
|
+
const projectName = validateDeploymentName(name ?? path.basename(destination));
|
|
105
|
+
try {
|
|
106
|
+
await lstat(destination);
|
|
107
|
+
throw new Error(`Refusing to overwrite existing path: ${destination}`);
|
|
108
|
+
} catch (error) {
|
|
109
|
+
if (error?.code !== 'ENOENT') throw error;
|
|
110
|
+
}
|
|
111
|
+
await mkdir(destination, { recursive: true });
|
|
112
|
+
const files = scaffoldFiles(projectName);
|
|
113
|
+
for (const [relativePath, content] of Object.entries(files)) {
|
|
114
|
+
const filePath = path.join(destination, relativePath);
|
|
115
|
+
await mkdir(path.dirname(filePath), { recursive: true });
|
|
116
|
+
await writeFile(filePath, content, { flag: 'wx', mode: 0o600 });
|
|
117
|
+
}
|
|
118
|
+
if (install) await runNpmInstall(destination);
|
|
119
|
+
return {
|
|
120
|
+
directory: destination,
|
|
121
|
+
fileCount: Object.keys(files).length + (install ? 1 : 0),
|
|
122
|
+
name: projectName,
|
|
123
|
+
};
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
const optionalText = async (filePath) => {
|
|
127
|
+
try {
|
|
128
|
+
return await readFile(filePath, 'utf8');
|
|
129
|
+
} catch (error) {
|
|
130
|
+
if (error?.code === 'ENOENT') return '';
|
|
131
|
+
throw error;
|
|
132
|
+
}
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
export const readDeploymentDirectory = async (directory) => {
|
|
136
|
+
const root = path.resolve(directory);
|
|
137
|
+
const rootStat = await lstat(root);
|
|
138
|
+
if (!rootStat.isDirectory()) throw new Error(`${root} is not a directory.`);
|
|
139
|
+
const hardIgnored = createIgnore().add(DEFAULT_IGNORES);
|
|
140
|
+
const projectIgnored = createIgnore();
|
|
141
|
+
projectIgnored.add(await optionalText(path.join(root, '.gitignore')));
|
|
142
|
+
projectIgnored.add(await optionalText(path.join(root, '.dockerignore')));
|
|
143
|
+
const files = [];
|
|
144
|
+
let byteCount = 0;
|
|
145
|
+
|
|
146
|
+
const walk = async (relativeDirectory = '') => {
|
|
147
|
+
const entries = await readdir(path.join(root, relativeDirectory), { withFileTypes: true });
|
|
148
|
+
for (const entry of entries.toSorted((left, right) => left.name.localeCompare(right.name))) {
|
|
149
|
+
const relativePath = path.posix.join(relativeDirectory.split(path.sep).join('/'), entry.name);
|
|
150
|
+
const ignorePath = entry.isDirectory() ? `${relativePath}/` : relativePath;
|
|
151
|
+
if (hardIgnored.ignores(ignorePath) || projectIgnored.ignores(ignorePath)) continue;
|
|
152
|
+
if (entry.isSymbolicLink())
|
|
153
|
+
throw new Error(`Source symlinks are not allowed: ${relativePath}`);
|
|
154
|
+
if (entry.isDirectory()) {
|
|
155
|
+
await walk(relativePath);
|
|
156
|
+
continue;
|
|
157
|
+
}
|
|
158
|
+
if (!entry.isFile()) throw new Error(`Unsupported source file type: ${relativePath}`);
|
|
159
|
+
const bytes = await readFile(path.join(root, relativePath));
|
|
160
|
+
byteCount += bytes.byteLength;
|
|
161
|
+
if (files.length + 1 > MAX_SOURCE_FILES) {
|
|
162
|
+
throw new Error(`Source contains more than ${String(MAX_SOURCE_FILES)} files.`);
|
|
163
|
+
}
|
|
164
|
+
if (byteCount > MAX_SOURCE_BYTES) {
|
|
165
|
+
throw new Error('Source is larger than 16 MiB.');
|
|
166
|
+
}
|
|
167
|
+
files.push({ contentBase64: bytes.toString('base64'), path: relativePath });
|
|
168
|
+
}
|
|
169
|
+
};
|
|
170
|
+
await walk();
|
|
171
|
+
return files;
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
export const readDeploymentProjectState = async (directory) => {
|
|
175
|
+
try {
|
|
176
|
+
const stateDirectory = path.join(directory, '.rayrun');
|
|
177
|
+
const stateDirectoryStat = await lstat(stateDirectory);
|
|
178
|
+
if (!stateDirectoryStat.isDirectory() || stateDirectoryStat.isSymbolicLink()) {
|
|
179
|
+
throw new Error('.rayrun must be a real directory.');
|
|
180
|
+
}
|
|
181
|
+
const statePath = path.join(stateDirectory, 'project.json');
|
|
182
|
+
const stateStat = await lstat(statePath);
|
|
183
|
+
if (!stateStat.isFile() || stateStat.isSymbolicLink()) {
|
|
184
|
+
throw new Error('.rayrun/project.json must be a regular file.');
|
|
185
|
+
}
|
|
186
|
+
const parsed = JSON.parse(await readFile(statePath, 'utf8'));
|
|
187
|
+
return typeof parsed.connectionUid === 'string' ? parsed : null;
|
|
188
|
+
} catch (error) {
|
|
189
|
+
if (error?.code === 'ENOENT') return null;
|
|
190
|
+
throw new Error(
|
|
191
|
+
'.rayrun/project.json is invalid. Remove it or provide --connection explicitly.',
|
|
192
|
+
);
|
|
193
|
+
}
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
export const writeDeploymentProjectState = async (directory, connectionUid) => {
|
|
197
|
+
const stateDirectory = path.join(directory, '.rayrun');
|
|
198
|
+
try {
|
|
199
|
+
const state = await lstat(stateDirectory);
|
|
200
|
+
if (!state.isDirectory() || state.isSymbolicLink()) {
|
|
201
|
+
throw new Error('.rayrun must be a real directory.');
|
|
202
|
+
}
|
|
203
|
+
} catch (error) {
|
|
204
|
+
if (error?.code !== 'ENOENT') throw error;
|
|
205
|
+
await mkdir(stateDirectory, { mode: 0o700 });
|
|
206
|
+
}
|
|
207
|
+
const statePath = path.join(stateDirectory, 'project.json');
|
|
208
|
+
const temporaryPath = path.join(stateDirectory, `.project-${randomUUID()}.tmp`);
|
|
209
|
+
try {
|
|
210
|
+
await writeFile(temporaryPath, `${JSON.stringify({ connectionUid, version: 1 }, null, 2)}\n`, {
|
|
211
|
+
flag: 'wx',
|
|
212
|
+
mode: 0o600,
|
|
213
|
+
});
|
|
214
|
+
await rename(temporaryPath, statePath);
|
|
215
|
+
} catch (error) {
|
|
216
|
+
await unlink(temporaryPath).catch(() => undefined);
|
|
217
|
+
throw error;
|
|
218
|
+
}
|
|
219
|
+
};
|
|
220
|
+
|
|
221
|
+
export const parseSecretBindings = (values = []) => {
|
|
222
|
+
const bindings = {};
|
|
223
|
+
for (const value of values) {
|
|
224
|
+
const separator = value.indexOf('=');
|
|
225
|
+
if (separator <= 0 || separator === value.length - 1) {
|
|
226
|
+
throw new Error('--secret must use NAME=workspace-secret-uid.');
|
|
227
|
+
}
|
|
228
|
+
const name = value.slice(0, separator);
|
|
229
|
+
if (Object.hasOwn(bindings, name)) throw new Error(`${name} can only be bound once.`);
|
|
230
|
+
bindings[name] = value.slice(separator + 1);
|
|
231
|
+
}
|
|
232
|
+
return bindings;
|
|
233
|
+
};
|
package/src/main.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { clientDefinitions, detectClients } from './clients.js';
|
|
2
|
+
import { scaffoldDeploymentProject } from './deployments.js';
|
|
2
3
|
import { executionUsage, isExecutionCommand, runExecutionCommand } from './execution.js';
|
|
3
4
|
import { isManagementCommand, managementUsage, runManagementCommand } from './management.js';
|
|
4
5
|
import { applySetup, rollbackSetup } from './setup.js';
|
|
@@ -8,6 +9,7 @@ import { createInterface } from 'node:readline/promises';
|
|
|
8
9
|
const packageVersion = createRequire(import.meta.url)('../package.json').version;
|
|
9
10
|
|
|
10
11
|
const usage = `Usage:
|
|
12
|
+
rayrun init <directory> [--name <kebab-case-name>] [--no-install]
|
|
11
13
|
rayrun setup <endpoint> [--client <id> ... | --all] [--project] [--yes] [--dry-run] [--login | --no-login]
|
|
12
14
|
rayrun setup rollback <backup-id>
|
|
13
15
|
|
|
@@ -112,6 +114,30 @@ export const main = async (arguments_, dependencies) => {
|
|
|
112
114
|
process.stdout.write(`${String(packageVersion)}\n`);
|
|
113
115
|
return;
|
|
114
116
|
}
|
|
117
|
+
if (arguments_[0] === 'init') {
|
|
118
|
+
const directory = arguments_[1];
|
|
119
|
+
if (!directory || directory.startsWith('--')) {
|
|
120
|
+
throw new Error(usage);
|
|
121
|
+
}
|
|
122
|
+
let name;
|
|
123
|
+
let install = true;
|
|
124
|
+
for (let index = 2; index < arguments_.length; index += 1) {
|
|
125
|
+
const argument = arguments_[index];
|
|
126
|
+
if (argument === '--no-install') install = false;
|
|
127
|
+
else if (argument === '--name') {
|
|
128
|
+
if (name !== undefined) throw new Error('--name can only be provided once.');
|
|
129
|
+
name = arguments_[index + 1];
|
|
130
|
+
if (!name || name.startsWith('--')) throw new Error('--name requires a value.');
|
|
131
|
+
index += 1;
|
|
132
|
+
} else throw new Error(`Unknown option: ${String(argument)}`);
|
|
133
|
+
}
|
|
134
|
+
const created = await scaffoldDeploymentProject({ directory, install, name });
|
|
135
|
+
const output = dependencies?.output ?? process.stdout;
|
|
136
|
+
output.write(
|
|
137
|
+
`Created ${created.name} in ${created.directory}.\nNext: cd ${created.directory} && rayrun deploy --wait\n`,
|
|
138
|
+
);
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
115
141
|
if (isExecutionCommand(arguments_)) {
|
|
116
142
|
await runExecutionCommand(arguments_, { ...dependencies, version: packageVersion });
|
|
117
143
|
return;
|