runcloud 0.1.17 → 0.1.19
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/commands/boxAccess.js +7 -1
- package/dist/commands/run-cloud.js +49 -19
- package/dist/version.js +1 -1
- package/package.json +1 -1
- package/skills/run-cloud/SKILL.md +22 -0
- package/skills/run-cloud/agents/openai.yaml +4 -0
- package/skills/run-cloud-sandboxes/SKILL.md +215 -0
- package/skills/run-cloud-sandboxes/agents/openai.yaml +4 -0
|
@@ -32,7 +32,7 @@ async function resolveSandboxId(client, reference) {
|
|
|
32
32
|
return sandbox.id;
|
|
33
33
|
return (await resolveBox(client, reference)).sandboxId ?? reference;
|
|
34
34
|
}
|
|
35
|
-
async function writeSandboxFile(client, sandboxId, path, content) {
|
|
35
|
+
export async function writeSandboxFile(client, sandboxId, path, content) {
|
|
36
36
|
const chunkSize = 512 * 1024;
|
|
37
37
|
const count = Math.max(1, Math.ceil(content.byteLength / chunkSize));
|
|
38
38
|
for (let index = 0; index < count; index += 1) {
|
|
@@ -47,6 +47,12 @@ async function writeSandboxFile(client, sandboxId, path, content) {
|
|
|
47
47
|
});
|
|
48
48
|
if (response?.error)
|
|
49
49
|
throw new Error(String(response.error));
|
|
50
|
+
const expectedNextOffset = offset + chunk.byteLength;
|
|
51
|
+
if (chunk.byteLength > 0
|
|
52
|
+
&& (!Number.isSafeInteger(response?.next_offset)
|
|
53
|
+
|| response.next_offset !== expectedNextOffset)) {
|
|
54
|
+
throw new Error('file write chunk was not acknowledged; the file may be incomplete');
|
|
55
|
+
}
|
|
50
56
|
}
|
|
51
57
|
}
|
|
52
58
|
async function readSandboxFile(client, sandboxId, path) {
|
|
@@ -6,20 +6,40 @@ import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
|
6
6
|
import { createHash } from 'node:crypto';
|
|
7
7
|
import { ApiClient, friendlyApiError } from '../api.js';
|
|
8
8
|
import { requireCredentials } from '../config.js';
|
|
9
|
-
const
|
|
10
|
-
|
|
9
|
+
export const RUN_CLOUD_SKILL_NAMES = [
|
|
10
|
+
'run-cloud',
|
|
11
|
+
'run-cloud-ios-simulator',
|
|
12
|
+
'run-cloud-sandboxes',
|
|
13
|
+
];
|
|
14
|
+
const RUN_CLOUD_SKILL_FILE_NAMES = ['SKILL.md', 'agents/openai.yaml'];
|
|
15
|
+
function loadRunCloudSkillFiles(skillName) {
|
|
16
|
+
const embedded = globalThis.__RUN_CLOUD_EMBEDDED_SKILLS__?.[skillName];
|
|
17
|
+
if (embedded)
|
|
18
|
+
return embedded;
|
|
11
19
|
const commandDirectory = dirname(fileURLToPath(import.meta.url));
|
|
12
20
|
const candidates = [
|
|
13
|
-
resolve(commandDirectory, '../../skills',
|
|
14
|
-
resolve(commandDirectory, '../../../.claude/skills',
|
|
21
|
+
resolve(commandDirectory, '../../skills', skillName),
|
|
22
|
+
resolve(commandDirectory, '../../../.claude/skills', skillName),
|
|
15
23
|
];
|
|
16
24
|
for (const candidate of candidates) {
|
|
17
|
-
if (existsSync(candidate))
|
|
18
|
-
return
|
|
25
|
+
if (RUN_CLOUD_SKILL_FILE_NAMES.every((filename) => existsSync(join(candidate, filename)))) {
|
|
26
|
+
return Object.fromEntries(RUN_CLOUD_SKILL_FILE_NAMES.map((filename) => [
|
|
27
|
+
filename,
|
|
28
|
+
readFileSync(join(candidate, filename), 'utf8'),
|
|
29
|
+
]));
|
|
30
|
+
}
|
|
19
31
|
}
|
|
20
|
-
throw new Error(
|
|
32
|
+
throw new Error(`run.cloud agent skill ${skillName} is missing from the package; reinstall runcloud or use npx skills add newly-app/run-cloud-examples`);
|
|
21
33
|
}
|
|
22
|
-
|
|
34
|
+
const RUN_CLOUD_SKILL_FILES = Object.fromEntries(RUN_CLOUD_SKILL_NAMES.map((skillName) => [
|
|
35
|
+
skillName,
|
|
36
|
+
loadRunCloudSkillFiles(skillName),
|
|
37
|
+
]));
|
|
38
|
+
export const RUN_CLOUD_SKILLS = Object.fromEntries(RUN_CLOUD_SKILL_NAMES.map((skillName) => [
|
|
39
|
+
skillName,
|
|
40
|
+
RUN_CLOUD_SKILL_FILES[skillName]['SKILL.md'],
|
|
41
|
+
]));
|
|
42
|
+
export const RUN_CLOUD_SKILL = RUN_CLOUD_SKILLS['run-cloud-ios-simulator'];
|
|
23
43
|
function client() {
|
|
24
44
|
const creds = requireCredentials();
|
|
25
45
|
return new ApiClient(creds.apiUrl, creds.token);
|
|
@@ -84,20 +104,30 @@ function skillBase(agent, scope) {
|
|
|
84
104
|
return join(homedir(), '.agents', 'skills');
|
|
85
105
|
throw new Error(`Unsupported agent: ${agent}`);
|
|
86
106
|
}
|
|
87
|
-
function
|
|
107
|
+
function installSkills(opts) {
|
|
88
108
|
const agents = opts.agents?.length ? opts.agents : ['claude', 'codex', 'cursor'];
|
|
89
109
|
const scope = opts.scope ?? 'project';
|
|
110
|
+
const targets = agents.flatMap((agent) => RUN_CLOUD_SKILL_NAMES.map((skill) => ({
|
|
111
|
+
agent,
|
|
112
|
+
skill,
|
|
113
|
+
path: join(skillBase(agent, scope), skill),
|
|
114
|
+
})));
|
|
115
|
+
if (!opts.force) {
|
|
116
|
+
const existing = targets.find((target) => existsSync(target.path));
|
|
117
|
+
if (existing) {
|
|
118
|
+
throw new Error(`${existing.path} already exists. Pass --force to overwrite.`);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
90
121
|
const results = [];
|
|
91
|
-
for (const agent of
|
|
92
|
-
|
|
93
|
-
if (existsSync(target)) {
|
|
94
|
-
if (!opts.force)
|
|
95
|
-
throw new Error(`${target} already exists. Pass --force to overwrite.`);
|
|
122
|
+
for (const { agent, skill, path: target } of targets) {
|
|
123
|
+
if (existsSync(target))
|
|
96
124
|
rmSync(target, { recursive: true, force: true });
|
|
125
|
+
for (const filename of RUN_CLOUD_SKILL_FILE_NAMES) {
|
|
126
|
+
const destination = join(target, filename);
|
|
127
|
+
mkdirSync(dirname(destination), { recursive: true });
|
|
128
|
+
writeFileSync(destination, RUN_CLOUD_SKILL_FILES[skill][filename]);
|
|
97
129
|
}
|
|
98
|
-
|
|
99
|
-
writeFileSync(join(target, 'SKILL.md'), RUN_CLOUD_SKILL);
|
|
100
|
-
results.push({ agent, path: target, status: 'installed' });
|
|
130
|
+
results.push({ agent, skill, path: target, status: 'installed' });
|
|
101
131
|
}
|
|
102
132
|
return results;
|
|
103
133
|
}
|
|
@@ -440,13 +470,13 @@ export function registerRunCloud(program) {
|
|
|
440
470
|
const skills = program.command('skills').description('Install run.cloud skills for AI coding agents');
|
|
441
471
|
skills
|
|
442
472
|
.command('install')
|
|
443
|
-
.description('Install the run.cloud
|
|
473
|
+
.description('Install the run.cloud router, mobile-session, and sandbox skills')
|
|
444
474
|
.option('--agents <agent>', 'agent to install for: claude, codex, cursor', (v, p) => [...p, v], [])
|
|
445
475
|
.option('--scope <project|global>', 'install scope', 'project')
|
|
446
476
|
.option('--force', 'overwrite existing skill', false)
|
|
447
477
|
.option('--json', 'output JSON', false)
|
|
448
478
|
.action((opts) => action(async () => {
|
|
449
479
|
const scope = opts.scope === 'global' ? 'global' : 'project';
|
|
450
|
-
print(
|
|
480
|
+
print(installSkills({ ...opts, scope }), opts);
|
|
451
481
|
}));
|
|
452
482
|
}
|
package/dist/version.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const CLI_VERSION = '0.1.
|
|
1
|
+
export const CLI_VERSION = '0.1.19';
|
package/package.json
CHANGED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: run-cloud
|
|
3
|
+
description: Route run.cloud work to the focused mobile-session or sandbox skill. Use when a request mentions run.cloud generally, spans both products, or does not yet distinguish remote iOS and Android sessions from microVM sandboxes.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Route run.cloud Work
|
|
7
|
+
|
|
8
|
+
Keep this skill as a router. Do not duplicate operational instructions here.
|
|
9
|
+
|
|
10
|
+
- Use `$run-cloud-ios-simulator` for remote iOS simulators, Android emulators,
|
|
11
|
+
app installation, mobile smoke tests, simulator screenshots, media injection,
|
|
12
|
+
iframe embeds, Metro tunnels, mobile assets, and mobile session cleanup.
|
|
13
|
+
- Use `$run-cloud-sandboxes` for microVM sandboxes, command execution, files,
|
|
14
|
+
snapshots, images, SSH, secrets, public ports, desktop automation, resource
|
|
15
|
+
sizing, and sandbox cleanup.
|
|
16
|
+
- Use both focused skills when a workflow genuinely combines mobile sessions
|
|
17
|
+
and sandboxes. Follow each skill's authentication, security, metering, and
|
|
18
|
+
cleanup guardrails.
|
|
19
|
+
|
|
20
|
+
If the request is ambiguous, infer the product from the resource noun and
|
|
21
|
+
desired outcome. Ask only when choosing the wrong product would materially
|
|
22
|
+
change the work.
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: run-cloud-sandboxes
|
|
3
|
+
description: Operate run.cloud microVM sandboxes with the CLI or TypeScript SDK. Use for creating isolated compute, running commands, moving files, managing snapshots or images, exposing ports, using SSH or desktop automation, attaching secrets, sizing resources, or cleaning up sandboxes.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Operate run.cloud Sandboxes
|
|
7
|
+
|
|
8
|
+
Use the `runcloud` CLI for terminal and interactive workflows. Use
|
|
9
|
+
`@run-cloud/sdk` for TypeScript applications, CI, and agent code.
|
|
10
|
+
|
|
11
|
+
## Authenticate
|
|
12
|
+
|
|
13
|
+
- Install the CLI with `npm install -g runcloud`.
|
|
14
|
+
- Use `runcloud login` for an interactive browser handoff. Use
|
|
15
|
+
`runcloud login --manual` when a local callback cannot open.
|
|
16
|
+
- In CI, set `RUN_CLOUD_API_KEY`. `RUN_CLOUD_API_TOKEN` is an equivalent alias.
|
|
17
|
+
- Set `RUN_CLOUD_API_URL` only to override the production default
|
|
18
|
+
`https://api.run.cloud`.
|
|
19
|
+
- Never print, commit, or place credentials in a skill file.
|
|
20
|
+
- Treat signed desktop and tunnel URLs as bearer secrets.
|
|
21
|
+
- Require Node.js 20 or newer for the CLI and TypeScript SDK.
|
|
22
|
+
|
|
23
|
+
Inspect account and organization usage before starting metered work:
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
runcloud account --json
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Choose the Interface
|
|
30
|
+
|
|
31
|
+
- Prefer CLI commands with `--json` for shell automation.
|
|
32
|
+
- Prefer the TypeScript SDK when code needs streaming command output, binary
|
|
33
|
+
file transfer, retry-safe creation, or short-lived public tunnels.
|
|
34
|
+
- Inspect `runcloud sandbox --help`, a subcommand's `--help`, or installed SDK
|
|
35
|
+
types before using a method not documented here.
|
|
36
|
+
- Use the `sandbox` noun. The older `box` commands are deprecated aliases.
|
|
37
|
+
|
|
38
|
+
## Run a CLI Lifecycle
|
|
39
|
+
|
|
40
|
+
Create a sandbox, capture its ID, run a command, and destroy it:
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
SANDBOX_ID=$(runcloud sandbox create \
|
|
44
|
+
--image runcloud/agent-base \
|
|
45
|
+
--timeout 900 \
|
|
46
|
+
--json | jq -r '.id')
|
|
47
|
+
|
|
48
|
+
trap 'runcloud sandbox rm "$SANDBOX_ID" >/dev/null 2>&1 || true' EXIT
|
|
49
|
+
|
|
50
|
+
runcloud sandbox get "$SANDBOX_ID" --json
|
|
51
|
+
runcloud sandbox exec "$SANDBOX_ID" "npm install && npm test"
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
The main lifecycle commands are:
|
|
55
|
+
|
|
56
|
+
- `runcloud sandbox create`
|
|
57
|
+
- `runcloud sandbox list [--state <state>]`
|
|
58
|
+
- `runcloud sandbox get <id>`
|
|
59
|
+
- `runcloud sandbox exec <id> <cmd...>`
|
|
60
|
+
- `runcloud sandbox shell <id>`
|
|
61
|
+
- `runcloud sandbox pause|resume <id>`
|
|
62
|
+
- `runcloud sandbox logs <id> [--lines <n>]`
|
|
63
|
+
- `runcloud sandbox metrics <id> [--range <range>] [--watch]`
|
|
64
|
+
- `runcloud sandbox rm <id>`
|
|
65
|
+
|
|
66
|
+
Create accepts `--image`, `--region`, `--name`, `--org`, `--cpu`, `--memory`,
|
|
67
|
+
`--disk`, `--idle-pause`, `--timeout`, `--persistent`, `--expose`, secret
|
|
68
|
+
selectors, `--no-wait`, and `--json`.
|
|
69
|
+
|
|
70
|
+
The default reservation is 0.125 vCPU and 128 MiB when CPU and memory are
|
|
71
|
+
omitted. Set finite timeouts for unattended work. Use `--timeout 0` only when a
|
|
72
|
+
persistent workload is intentional.
|
|
73
|
+
|
|
74
|
+
CLI `exec` runs through `/bin/sh -c` and returns the guest command's exit code.
|
|
75
|
+
A paused sandbox must be resumed before `exec`; `shell` resumes it
|
|
76
|
+
automatically.
|
|
77
|
+
|
|
78
|
+
## Use the TypeScript SDK
|
|
79
|
+
|
|
80
|
+
Install the SDK:
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
npm install @run-cloud/sdk
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Use an idempotency key when a job runner may retry creation, check non-zero
|
|
87
|
+
exit codes explicitly, and always destroy metered resources:
|
|
88
|
+
|
|
89
|
+
```ts
|
|
90
|
+
import { Client } from "@run-cloud/sdk";
|
|
91
|
+
|
|
92
|
+
const cloud = new Client();
|
|
93
|
+
const sandbox = await cloud.sandboxes.create({
|
|
94
|
+
image: "runcloud/agent-base",
|
|
95
|
+
cpu: 1,
|
|
96
|
+
memory: 1024,
|
|
97
|
+
timeoutSeconds: 900,
|
|
98
|
+
idempotencyKey: process.env.CI_JOB_ID,
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
try {
|
|
102
|
+
const result = await cloud.sandboxes.exec(
|
|
103
|
+
sandbox.id,
|
|
104
|
+
["npm", "test"],
|
|
105
|
+
{
|
|
106
|
+
onStdout: (chunk) => process.stdout.write(chunk),
|
|
107
|
+
onStderr: (chunk) => process.stderr.write(chunk),
|
|
108
|
+
},
|
|
109
|
+
);
|
|
110
|
+
if (result.exitCode !== 0) {
|
|
111
|
+
throw new Error(`tests failed with exit code ${result.exitCode}`);
|
|
112
|
+
}
|
|
113
|
+
} finally {
|
|
114
|
+
await cloud.sandboxes.destroy(sandbox.id);
|
|
115
|
+
}
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
The TypeScript sandbox surface is:
|
|
119
|
+
|
|
120
|
+
- `cloud.sandboxes`: `create`, `list`, `get`, `exec`, `readFile`,
|
|
121
|
+
`writeFile`, `openTunnel`, `closeTunnel`, `setTimeout`, `snapshot`,
|
|
122
|
+
`destroy`, and the `delete` alias
|
|
123
|
+
- `cloud.snapshots`: `list`, `restore`, `delete`
|
|
124
|
+
- `cloud.account()` and `cloud.usage({ orgId? })`
|
|
125
|
+
|
|
126
|
+
A string passed to SDK `exec` runs through `/bin/sh -c`; an argv array executes
|
|
127
|
+
directly. A non-zero guest exit code is returned rather than thrown. Use
|
|
128
|
+
`cwd`, `env`, `timeoutSeconds`, `onStdout`, `onStderr`, and `signal` as needed.
|
|
129
|
+
|
|
130
|
+
Use `readFile` and `writeFile` for binary-safe SDK file transfer. For
|
|
131
|
+
interactive terminal workflows, configure SSH with `runcloud sandbox setup-ssh`
|
|
132
|
+
and inspect `runcloud sandbox ssh`, `cp`, and `code` help before use.
|
|
133
|
+
|
|
134
|
+
## Snapshot and Restore
|
|
135
|
+
|
|
136
|
+
Snapshot a prepared filesystem and restore it into fresh sandboxes:
|
|
137
|
+
|
|
138
|
+
```bash
|
|
139
|
+
runcloud sandbox snapshot create "$SANDBOX_ID" --label deps-installed --json
|
|
140
|
+
runcloud sandbox snapshot list --json
|
|
141
|
+
runcloud sandbox restore <snapshot-id> --json
|
|
142
|
+
runcloud sandbox snapshot rm <snapshot-id>
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
The SDK equivalents are `cloud.sandboxes.snapshot`,
|
|
146
|
+
`cloud.snapshots.restore`, `cloud.snapshots.list`, and
|
|
147
|
+
`cloud.snapshots.delete`.
|
|
148
|
+
|
|
149
|
+
A restore creates a new billed sandbox with a new ID. One snapshot can fan out
|
|
150
|
+
to parallel workers, but every restored sandbox counts toward concurrency and
|
|
151
|
+
must be destroyed. A restored sandbox inherits no secrets; attach only the
|
|
152
|
+
secrets it needs.
|
|
153
|
+
|
|
154
|
+
## Use Images
|
|
155
|
+
|
|
156
|
+
Use reusable images when every sandbox needs the same base tools:
|
|
157
|
+
|
|
158
|
+
```bash
|
|
159
|
+
runcloud image create my-agent --dockerfile ./Dockerfile
|
|
160
|
+
runcloud image list --json
|
|
161
|
+
runcloud sandbox create --image my-agent --json
|
|
162
|
+
runcloud image refresh my-agent
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
Inspect `runcloud image --help` for current build-source options. Do not invent
|
|
166
|
+
image methods on the TypeScript SDK.
|
|
167
|
+
|
|
168
|
+
## Expose a Service
|
|
169
|
+
|
|
170
|
+
Choose one exposure model deliberately:
|
|
171
|
+
|
|
172
|
+
- For a stable hostname, create with
|
|
173
|
+
`runcloud sandbox create --name <name> --expose <port> --persistent`, or use
|
|
174
|
+
`runcloud sandbox expose <id> --port <port>`.
|
|
175
|
+
- For a short-lived random URL in TypeScript, call
|
|
176
|
+
`cloud.sandboxes.openTunnel(id, port, { ttlSeconds })`, then
|
|
177
|
+
`closeTunnel(id, tunnel.id)`.
|
|
178
|
+
|
|
179
|
+
An SDK tunnel does not make the sandbox persistent or disable idle pause. Do
|
|
180
|
+
not log its URL. Revocation can take effect shortly after `closeTunnel`
|
|
181
|
+
returns; stop the guest service or destroy the sandbox when access must end
|
|
182
|
+
immediately.
|
|
183
|
+
|
|
184
|
+
## Handle Secrets Safely
|
|
185
|
+
|
|
186
|
+
- Create secret groups from `--from-dotenv`, `--from-json`, `--stdin`, a file,
|
|
187
|
+
or a hidden prompt. Never pass a secret value as a command argument.
|
|
188
|
+
- Attach only the required groups or names with repeatable `--secret-group` and
|
|
189
|
+
`--secret`. Later selectors win on name collisions; `--env` is applied last.
|
|
190
|
+
- Use `--no-secrets` to state explicitly that a new sandbox needs none.
|
|
191
|
+
- Treat `runcloud sandbox secrets <id> ...` as full replacement, not a merge.
|
|
192
|
+
- Remember that values cannot be read back and snapshots do not contain
|
|
193
|
+
secrets.
|
|
194
|
+
|
|
195
|
+
Inspect `runcloud secret-group --help` and `runcloud secrets --help` for the
|
|
196
|
+
current non-plaintext input forms.
|
|
197
|
+
|
|
198
|
+
## Operate Desktop Sandboxes
|
|
199
|
+
|
|
200
|
+
For a compatible desktop image, use `runcloud sandbox desktop <id>` to open its
|
|
201
|
+
signed browser desktop. The CLI also provides `screenshot`, `click`, `type`,
|
|
202
|
+
and `key` subcommands for explicit pixel-coordinate automation. Keep signed
|
|
203
|
+
desktop URLs private and inspect each subcommand's help before automation.
|
|
204
|
+
|
|
205
|
+
## Guardrails
|
|
206
|
+
|
|
207
|
+
- Destroy every sandbox created during a task unless the user explicitly asks
|
|
208
|
+
to keep it. Also remove unused snapshots, tunnels, and public hostnames.
|
|
209
|
+
- Use `try/finally` or a shell trap around every metered lifecycle.
|
|
210
|
+
- Check `exitCode`; do not treat a completed SDK `exec` call as success by
|
|
211
|
+
itself.
|
|
212
|
+
- Do not expose API credentials, secret values, signed desktop URLs, or tunnel
|
|
213
|
+
URLs in logs, screenshots, PR comments, or chat output.
|
|
214
|
+
- Do not claim that CLI-only lifecycle, image, secret-group, desktop, or stable
|
|
215
|
+
hostname commands are TypeScript SDK methods.
|