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.
@@ -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 RUN_CLOUD_SKILL_FILENAME = 'run-cloud-ios-simulator/SKILL.md';
10
- function loadRunCloudSkill() {
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', RUN_CLOUD_SKILL_FILENAME),
14
- resolve(commandDirectory, '../../../.claude/skills', RUN_CLOUD_SKILL_FILENAME),
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 readFileSync(candidate, 'utf8');
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('run.cloud agent skill is missing from the package; reinstall runcloud or use npx skills add newly-app/run-cloud-examples --skill run-cloud-ios-simulator');
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
- export const RUN_CLOUD_SKILL = loadRunCloudSkill();
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 installSkill(opts) {
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 agents) {
92
- const target = join(skillBase(agent, scope), 'run-cloud-ios-simulator');
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
- mkdirSync(target, { recursive: true });
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 agent runtime skill')
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(installSkill({ ...opts, scope }), opts);
480
+ print(installSkills({ ...opts, scope }), opts);
451
481
  }));
452
482
  }
package/dist/version.js CHANGED
@@ -1 +1 @@
1
- export const CLI_VERSION = '0.1.17';
1
+ export const CLI_VERSION = '0.1.19';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "runcloud",
3
- "version": "0.1.17",
3
+ "version": "0.1.19",
4
4
  "description": "Create and control run.cloud remote mobile simulators and cloud sandboxes",
5
5
  "license": "Apache-2.0",
6
6
  "keywords": [
@@ -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,4 @@
1
+ interface:
2
+ display_name: "Run Cloud"
3
+ short_description: "Route run.cloud work to focused agent skills"
4
+ default_prompt: "Use $run-cloud to select the focused run.cloud skill for this task."
@@ -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.
@@ -0,0 +1,4 @@
1
+ interface:
2
+ display_name: "Run Cloud Sandboxes"
3
+ short_description: "Operate secure remote microVM sandboxes"
4
+ default_prompt: "Use $run-cloud-sandboxes to create, operate, and clean up run.cloud sandboxes."