runcloud 0.1.15 → 0.1.17
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/api.js +16 -1
- package/dist/commands/boxAccess.js +61 -2
- package/dist/commands/image.js +66 -21
- package/dist/commands/run-cloud.js +36 -146
- package/dist/commands/sandbox.js +329 -26
- package/dist/commands/sandboxOutput.js +70 -0
- package/dist/commands/secrets.js +203 -0
- package/dist/prompt.js +44 -0
- package/dist/run-cloud.js +2 -0
- package/dist/sandboxExec.js +91 -0
- package/dist/shellSession.js +4 -2
- package/dist/terminal.js +158 -0
- package/dist/version.js +1 -1
- package/package.json +1 -1
- package/skills/run-cloud-ios-simulator/SKILL.md +196 -0
- package/skills/run-cloud-ios-simulator/agents/openai.yaml +4 -0
package/dist/api.js
CHANGED
|
@@ -21,6 +21,9 @@ function parsedErrorDetail(body) {
|
|
|
21
21
|
}
|
|
22
22
|
catch {
|
|
23
23
|
}
|
|
24
|
+
if (/<(?:!doctype|html)\b/i.test(body)) {
|
|
25
|
+
return 'the API edge returned an HTML error page; retry the request or use the streaming/direct-transfer command';
|
|
26
|
+
}
|
|
24
27
|
return body;
|
|
25
28
|
}
|
|
26
29
|
export class ApiClient {
|
|
@@ -69,6 +72,9 @@ export class ApiClient {
|
|
|
69
72
|
const contentType = res.headers.get('content-type') ?? '';
|
|
70
73
|
return contentType.includes('application/json') ? res.json() : res;
|
|
71
74
|
}
|
|
75
|
+
put(path, body) {
|
|
76
|
+
return this.request('PUT', path, body);
|
|
77
|
+
}
|
|
72
78
|
patch(path, body) {
|
|
73
79
|
return this.request('PATCH', path, body);
|
|
74
80
|
}
|
|
@@ -172,6 +178,14 @@ function headline(err) {
|
|
|
172
178
|
if (err.status === 403)
|
|
173
179
|
return err.detail ? `Forbidden: ${err.detail}` : 'Forbidden.';
|
|
174
180
|
if (err.status === 404) {
|
|
181
|
+
const snapshotRestoreMatch = err.path?.match(/^\/run-cloud\/snapshots\/([^/]+)\/restore$/);
|
|
182
|
+
if (snapshotRestoreMatch) {
|
|
183
|
+
const id = decodeURIComponent(snapshotRestoreMatch[1]);
|
|
184
|
+
if (/^sb(?:x)?_/.test(id)) {
|
|
185
|
+
return `Snapshot ID not found: ${id}. \`sandbox restore\` expects a snapshot ID; to restart this sandbox, run \`runcloud sandbox resume ${id}\`.`;
|
|
186
|
+
}
|
|
187
|
+
return `Snapshot ID not found: ${id}.`;
|
|
188
|
+
}
|
|
175
189
|
const sandboxMatch = err.path?.match(/^\/run-cloud\/sandboxes\/([^/]+)/);
|
|
176
190
|
if (sandboxMatch) {
|
|
177
191
|
return `Sandbox ID not found: ${decodeURIComponent(sandboxMatch[1])}.`;
|
|
@@ -196,5 +210,6 @@ function headline(err) {
|
|
|
196
210
|
}
|
|
197
211
|
export function friendlyApiError(err) {
|
|
198
212
|
const message = headline(err);
|
|
199
|
-
|
|
213
|
+
const debug = process.env.RUNCLOUD_DEBUG === '1' || process.env.NEWLY_DEBUG === '1';
|
|
214
|
+
return !debug || err instanceof ApiError ? message : `${message}${detailSuffix(err)}`;
|
|
200
215
|
}
|
|
@@ -20,10 +20,58 @@ async function resolveBox(client, reference) {
|
|
|
20
20
|
item.name === reference ||
|
|
21
21
|
item.hostname === reference);
|
|
22
22
|
if (!box) {
|
|
23
|
-
throw new Error(`No exposed sandbox matches ${reference}
|
|
23
|
+
throw new Error(`No exposed sandbox matches ${reference}. Publish it first with \`runcloud sandbox expose ${reference} --name <name>\`, or use \`runcloud sandbox shell ${reference}\` without SSH.`);
|
|
24
24
|
}
|
|
25
25
|
return box;
|
|
26
26
|
}
|
|
27
|
+
async function resolveSandboxId(client, reference) {
|
|
28
|
+
const response = await client.get('/run-cloud/sandboxes');
|
|
29
|
+
const sandboxes = (response?.items ?? []);
|
|
30
|
+
const sandbox = sandboxes.find((item) => item.id === reference || item.name === reference);
|
|
31
|
+
if (sandbox?.id)
|
|
32
|
+
return sandbox.id;
|
|
33
|
+
return (await resolveBox(client, reference)).sandboxId ?? reference;
|
|
34
|
+
}
|
|
35
|
+
async function writeSandboxFile(client, sandboxId, path, content) {
|
|
36
|
+
const chunkSize = 512 * 1024;
|
|
37
|
+
const count = Math.max(1, Math.ceil(content.byteLength / chunkSize));
|
|
38
|
+
for (let index = 0; index < count; index += 1) {
|
|
39
|
+
const offset = index * chunkSize;
|
|
40
|
+
const chunk = content.subarray(offset, Math.min(content.byteLength, offset + chunkSize));
|
|
41
|
+
const response = await client.post(`/run-cloud/sandboxes/${encodeURIComponent(sandboxId)}/fs`, {
|
|
42
|
+
op: 'write',
|
|
43
|
+
path,
|
|
44
|
+
offset,
|
|
45
|
+
content: chunk.toString('base64'),
|
|
46
|
+
truncate: index === 0,
|
|
47
|
+
});
|
|
48
|
+
if (response?.error)
|
|
49
|
+
throw new Error(String(response.error));
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
async function readSandboxFile(client, sandboxId, path) {
|
|
53
|
+
const chunks = [];
|
|
54
|
+
let offset = 0;
|
|
55
|
+
for (;;) {
|
|
56
|
+
const response = await client.post(`/run-cloud/sandboxes/${encodeURIComponent(sandboxId)}/fs`, {
|
|
57
|
+
op: 'read',
|
|
58
|
+
path,
|
|
59
|
+
offset,
|
|
60
|
+
length: 512 * 1024,
|
|
61
|
+
});
|
|
62
|
+
if (response?.error)
|
|
63
|
+
throw new Error(String(response.error));
|
|
64
|
+
const chunk = Buffer.from(String(response?.content ?? ''), 'base64');
|
|
65
|
+
chunks.push(chunk);
|
|
66
|
+
const next = Number(response?.next_offset ?? offset + chunk.byteLength);
|
|
67
|
+
if (response?.eof === true || chunk.byteLength === 0)
|
|
68
|
+
break;
|
|
69
|
+
if (!Number.isSafeInteger(next) || next <= offset)
|
|
70
|
+
throw new Error('file read did not advance');
|
|
71
|
+
offset = next;
|
|
72
|
+
}
|
|
73
|
+
return Buffer.concat(chunks);
|
|
74
|
+
}
|
|
27
75
|
function sshHome() {
|
|
28
76
|
const base = process.env.RUN_CLOUD_HOME || join(homedir(), '.run-cloud');
|
|
29
77
|
return join(base, 'ssh');
|
|
@@ -174,7 +222,7 @@ export function registerAccessCommands(parent, opts = {}) {
|
|
|
174
222
|
});
|
|
175
223
|
parent
|
|
176
224
|
.command('cp')
|
|
177
|
-
.description('Copy
|
|
225
|
+
.description('Copy a file directly; prefix the remote path with a colon')
|
|
178
226
|
.argument('<sandbox>', REFERENCE_HELP)
|
|
179
227
|
.argument('<source>', 'local path or :/remote/path')
|
|
180
228
|
.argument('<destination>', 'local path or :/remote/path')
|
|
@@ -185,6 +233,17 @@ export function registerAccessCommands(parent, opts = {}) {
|
|
|
185
233
|
if (source.startsWith(':') === destination.startsWith(':')) {
|
|
186
234
|
throw new Error('Exactly one path must be remote (prefix it with :)');
|
|
187
235
|
}
|
|
236
|
+
if (!cmdOpts.recursive) {
|
|
237
|
+
const client = api();
|
|
238
|
+
const sandboxId = await resolveSandboxId(client, reference);
|
|
239
|
+
if (source.startsWith(':')) {
|
|
240
|
+
writeFileSync(destination, await readSandboxFile(client, sandboxId, source.slice(1)));
|
|
241
|
+
}
|
|
242
|
+
else {
|
|
243
|
+
await writeSandboxFile(client, sandboxId, destination.slice(1), readFileSync(source));
|
|
244
|
+
}
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
188
247
|
const configured = await setupSsh(reference);
|
|
189
248
|
const remote = (path) => path.startsWith(':') ? `${configured.alias}:${path.slice(1)}` : path;
|
|
190
249
|
const args = [
|
package/dist/commands/image.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import { InvalidArgumentError } from 'commander';
|
|
1
|
+
import { InvalidArgumentError, Option } from 'commander';
|
|
2
2
|
import { ApiClient, friendlyApiError } from '../api.js';
|
|
3
3
|
import { requireCredentials } from '../config.js';
|
|
4
|
+
import { formatAge, formatState, printJson, renderDetails, renderTable, shortDigest, StatusLine, style, } from '../terminal.js';
|
|
4
5
|
const POLL_INTERVAL_MS = 3_000;
|
|
5
6
|
const DEFAULT_WAIT_TIMEOUT_SECONDS = 1_200;
|
|
6
7
|
function runCloudApi() {
|
|
@@ -9,23 +10,44 @@ function runCloudApi() {
|
|
|
9
10
|
}
|
|
10
11
|
function print(value, opts) {
|
|
11
12
|
if (opts.json) {
|
|
12
|
-
|
|
13
|
+
printJson(value);
|
|
13
14
|
return;
|
|
14
15
|
}
|
|
15
|
-
|
|
16
|
-
for (const item of value)
|
|
17
|
-
console.log(formatRecord(item));
|
|
18
|
-
return;
|
|
19
|
-
}
|
|
20
|
-
console.log(formatRecord(value));
|
|
16
|
+
console.log(renderImage(value));
|
|
21
17
|
}
|
|
22
|
-
function
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
18
|
+
function renderImage(row, title) {
|
|
19
|
+
const heading = title ??
|
|
20
|
+
(row.state === 'ready'
|
|
21
|
+
? '✓ Image ready'
|
|
22
|
+
: row.state === 'failed'
|
|
23
|
+
? '✗ Image build failed'
|
|
24
|
+
: '◐ Image build started');
|
|
25
|
+
const footer = row.state === 'building'
|
|
26
|
+
? `${style.dim('Follow:')} runcloud image list`
|
|
27
|
+
: undefined;
|
|
28
|
+
return renderDetails(heading, [
|
|
29
|
+
{ label: 'Reference', value: row.ref ?? row.name },
|
|
30
|
+
{ label: 'State', value: formatState(row.state) },
|
|
31
|
+
{ label: 'Digest', value: shortDigest(row.digest) },
|
|
32
|
+
{ label: 'Created', value: formatAge(row.createdAt) },
|
|
33
|
+
{ label: 'Error', value: row.error },
|
|
34
|
+
], footer);
|
|
35
|
+
}
|
|
36
|
+
function renderImageList(rows) {
|
|
37
|
+
if (rows.length === 0) {
|
|
38
|
+
return `${style.bold('Images')}\n\nNo images registered.\n\n${style.dim('Register one:')} runcloud image create --ref python:3.12-slim`;
|
|
39
|
+
}
|
|
40
|
+
return renderTable(`Images (${rows.length})`, [
|
|
41
|
+
{ key: 'ref', label: 'REFERENCE', maxWidth: 44 },
|
|
42
|
+
{ key: 'state', label: 'STATE', maxWidth: 12 },
|
|
43
|
+
{ key: 'digest', label: 'DIGEST' },
|
|
44
|
+
{ key: 'age', label: 'AGE' },
|
|
45
|
+
], rows.map((row) => ({
|
|
46
|
+
ref: row.ref ?? row.name,
|
|
47
|
+
state: formatState(row.state),
|
|
48
|
+
digest: shortDigest(row.digest) ?? '—',
|
|
49
|
+
age: formatAge(row.createdAt) ?? '—',
|
|
50
|
+
})));
|
|
29
51
|
}
|
|
30
52
|
async function run(fn) {
|
|
31
53
|
try {
|
|
@@ -55,6 +77,8 @@ export async function pollImageRegistration(api, ref, opts = {}) {
|
|
|
55
77
|
const data = await api.get('/run-cloud/images');
|
|
56
78
|
const items = (data?.items ?? []);
|
|
57
79
|
const row = items.find((item) => item.ref === ref || item.name === ref);
|
|
80
|
+
if (row)
|
|
81
|
+
opts.onState?.(row.state);
|
|
58
82
|
if (row && TERMINAL_STATES.has(row.state))
|
|
59
83
|
return row;
|
|
60
84
|
if (Date.now() > deadline) {
|
|
@@ -71,30 +95,51 @@ export function registerImage(program) {
|
|
|
71
95
|
.command('create')
|
|
72
96
|
.description('Register (build-once) an OCI image ref for run.cloud sandboxes')
|
|
73
97
|
.requiredOption('--ref <ref>', 'OCI image ref to register, e.g. python:3.12-slim')
|
|
74
|
-
.option('--wait', '
|
|
75
|
-
.option('--timeout <seconds>',
|
|
98
|
+
.option('--no-wait', 'return after scheduling instead of waiting for the image build')
|
|
99
|
+
.option('--timeout <seconds>', 'max time to wait in seconds', parseTimeoutSeconds, DEFAULT_WAIT_TIMEOUT_SECONDS)
|
|
100
|
+
.addOption(new Option('--wait').hideHelp())).action((opts) => run(async () => {
|
|
76
101
|
const api = runCloudApi();
|
|
77
102
|
const created = await api.post('/run-cloud/images', { ref: opts.ref });
|
|
78
|
-
if (!opts.wait) {
|
|
103
|
+
if (!opts.wait || created?.state === 'ready') {
|
|
79
104
|
print(created, opts);
|
|
80
105
|
return;
|
|
81
106
|
}
|
|
82
|
-
const
|
|
107
|
+
const status = opts.json ? undefined : new StatusLine();
|
|
108
|
+
status?.update(`Building ${opts.ref}`);
|
|
109
|
+
const finalRow = await pollImageRegistration(api, opts.ref, {
|
|
110
|
+
timeoutMs: opts.timeout * 1000,
|
|
111
|
+
onState: (state) => status?.update(state === 'building' ? `Building ${opts.ref}` : `${opts.ref} is ${state}`),
|
|
112
|
+
});
|
|
83
113
|
if (finalRow.state === 'failed') {
|
|
114
|
+
status?.fail(`Image build failed`);
|
|
84
115
|
throw new Error(finalRow.error
|
|
85
116
|
? `image ${opts.ref} failed to build: ${finalRow.error}`
|
|
86
117
|
: `image ${opts.ref} failed to build`);
|
|
87
118
|
}
|
|
119
|
+
status?.clear();
|
|
88
120
|
print(finalRow, opts);
|
|
89
121
|
}));
|
|
90
122
|
withOutput(image.command('list').description('List images your organization has registered')).action((opts) => run(async () => {
|
|
91
123
|
const data = await runCloudApi().get('/run-cloud/images');
|
|
92
|
-
|
|
124
|
+
const rows = (data?.items ?? []);
|
|
125
|
+
if (opts.json)
|
|
126
|
+
printJson(rows);
|
|
127
|
+
else
|
|
128
|
+
console.log(renderImageList(rows));
|
|
93
129
|
}));
|
|
94
130
|
withOutput(image
|
|
95
131
|
.command('refresh')
|
|
96
132
|
.description("Move a tag's pin to its current upstream digest")
|
|
97
133
|
.argument('<ref>', 'OCI image ref to refresh')).action((ref, opts) => run(async () => {
|
|
98
|
-
|
|
134
|
+
const refreshed = await runCloudApi().post('/run-cloud/images/refresh', { ref });
|
|
135
|
+
if (opts.json)
|
|
136
|
+
printJson(refreshed);
|
|
137
|
+
else {
|
|
138
|
+
console.log(renderDetails('✓ Image refreshed', [
|
|
139
|
+
{ label: 'Reference', value: refreshed?.ref ?? ref },
|
|
140
|
+
{ label: 'Previous', value: shortDigest(refreshed?.old_digest) },
|
|
141
|
+
{ label: 'Digest', value: shortDigest(refreshed?.new_digest ?? refreshed?.digest) },
|
|
142
|
+
]));
|
|
143
|
+
}
|
|
99
144
|
}));
|
|
100
145
|
}
|
|
@@ -6,148 +6,20 @@ 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
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
- Read SDK credentials from \`RUN_CLOUD_API_KEY\`. Never print it, commit it, or write it into a skill file. The SDK uses \`RUN_CLOUD_API_URL\` when set and otherwise defaults to \`https://api.run.cloud\`.
|
|
22
|
-
- Authenticate the CLI with either a saved \`runcloud login\` credential or \`RUN_CLOUD_API_KEY\` together with \`RUN_CLOUD_API_URL\`. Do not require both a saved login and an API key.
|
|
23
|
-
- The TypeScript SDK requires Node.js 20 or newer.
|
|
24
|
-
- The account must have run.cloud access, available capacity, and organization credit.
|
|
25
|
-
- App artifacts must match the target platform. iOS sessions need simulator-compatible builds; Android sessions need Android-compatible artifacts such as APKs.
|
|
26
|
-
|
|
27
|
-
## TypeScript SDK
|
|
28
|
-
|
|
29
|
-
Prefer \`@run-cloud/sdk\` for applications, CI, and agent code:
|
|
30
|
-
|
|
31
|
-
\`\`\`bash
|
|
32
|
-
npm install @run-cloud/sdk
|
|
33
|
-
\`\`\`
|
|
34
|
-
|
|
35
|
-
Use the platform client when the platform is known, and always release metered sessions in \`finally\`:
|
|
36
|
-
|
|
37
|
-
\`\`\`ts
|
|
38
|
-
import { Client } from "@run-cloud/sdk";
|
|
39
|
-
|
|
40
|
-
const cloud = new Client();
|
|
41
|
-
const session = await cloud.ios.create({
|
|
42
|
-
displayName: "Agent smoke",
|
|
43
|
-
labels: { owner: "agent" },
|
|
44
|
-
inactivityTimeout: "60s",
|
|
45
|
-
});
|
|
46
|
-
|
|
47
|
-
try {
|
|
48
|
-
await cloud.ios.openUrl(session.id, "https://run.cloud");
|
|
49
|
-
console.log(session.url);
|
|
50
|
-
} finally {
|
|
51
|
-
await cloud.ios.delete(session.id);
|
|
9
|
+
const RUN_CLOUD_SKILL_FILENAME = 'run-cloud-ios-simulator/SKILL.md';
|
|
10
|
+
function loadRunCloudSkill() {
|
|
11
|
+
const commandDirectory = dirname(fileURLToPath(import.meta.url));
|
|
12
|
+
const candidates = [
|
|
13
|
+
resolve(commandDirectory, '../../skills', RUN_CLOUD_SKILL_FILENAME),
|
|
14
|
+
resolve(commandDirectory, '../../../.claude/skills', RUN_CLOUD_SKILL_FILENAME),
|
|
15
|
+
];
|
|
16
|
+
for (const candidate of candidates) {
|
|
17
|
+
if (existsSync(candidate))
|
|
18
|
+
return readFileSync(candidate, 'utf8');
|
|
19
|
+
}
|
|
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');
|
|
52
21
|
}
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
Use \`cloud.android\` for Android. When the platform is selected at runtime, use \`cloud.simulators\` and pass \`session.platform\` to \`get\`, \`openUrl\`, or \`delete\`.
|
|
56
|
-
|
|
57
|
-
The implemented SDK surface is:
|
|
58
|
-
|
|
59
|
-
- \`cloud.account()\`;
|
|
60
|
-
- \`cloud.ios\` and \`cloud.android\`: \`create\`, \`list\`, \`get\`, \`openUrl\`, \`delete\`;
|
|
61
|
-
- \`cloud.simulators\`: the same lifecycle with a runtime \`platform\` option;
|
|
62
|
-
- \`cloud.assets\`: \`upload\`, \`list\`, \`delete\`.
|
|
63
|
-
|
|
64
|
-
Do not invent screenshot, tap, typing, recording, app lifecycle, sandbox, build, or compatibility-adapter methods. Check the installed package types and https://docs.run.cloud/cli/typescript-sdk before using a method not listed here.
|
|
65
|
-
|
|
66
|
-
## CLI Workflow
|
|
67
|
-
|
|
68
|
-
Use the CLI for interactive terminal work. Authenticate with a saved login:
|
|
69
|
-
|
|
70
|
-
\`\`\`bash
|
|
71
|
-
npm install -g runcloud
|
|
72
|
-
runcloud login
|
|
73
|
-
\`\`\`
|
|
74
|
-
|
|
75
|
-
Or authenticate non-interactively with both required environment variables:
|
|
76
|
-
|
|
77
|
-
\`\`\`bash
|
|
78
|
-
export RUN_CLOUD_API_KEY="rc_live_..."
|
|
79
|
-
export RUN_CLOUD_API_URL="https://api.run.cloud"
|
|
80
|
-
\`\`\`
|
|
81
|
-
|
|
82
|
-
Then inspect the account:
|
|
83
|
-
|
|
84
|
-
\`\`\`bash
|
|
85
|
-
runcloud account --json
|
|
86
|
-
\`\`\`
|
|
87
|
-
|
|
88
|
-
Create, inspect, open a URL, and release an iOS session:
|
|
89
|
-
|
|
90
|
-
\`\`\`bash
|
|
91
|
-
runcloud ios create --install ./build/MyApp.tar.gz --json
|
|
92
|
-
runcloud ios get "$SESSION_ID" --json
|
|
93
|
-
runcloud ios open-url myapp://settings --id "$SESSION_ID"
|
|
94
|
-
runcloud ios delete "$SESSION_ID" --json
|
|
95
|
-
\`\`\`
|
|
96
|
-
|
|
97
|
-
Use the corresponding \`runcloud android\` commands with an Android artifact for Android emulator sessions.
|
|
98
|
-
|
|
99
|
-
Download checksum-verified onboarding artifacts when no local build is
|
|
100
|
-
available:
|
|
101
|
-
|
|
102
|
-
\`\`\`bash
|
|
103
|
-
runcloud sample download ios
|
|
104
|
-
runcloud ios create --install ./run-cloud-sample-ios.app.tar.gz
|
|
105
|
-
|
|
106
|
-
runcloud sample download android
|
|
107
|
-
runcloud android create --install ./run-cloud-sample-android.apk
|
|
108
|
-
\`\`\`
|
|
109
|
-
|
|
110
|
-
## Runnable SDK Example
|
|
111
|
-
|
|
112
|
-
The maintained example checks account state, creates iOS and Android sessions, opens a URL on each, and releases both sessions:
|
|
113
|
-
|
|
114
|
-
\`\`\`bash
|
|
115
|
-
git clone --depth 1 https://github.com/newly-app/run-cloud-examples.git
|
|
116
|
-
cd run-cloud-examples/sdk-ios-android
|
|
117
|
-
npm install
|
|
118
|
-
npm run demo -- --platform both --open
|
|
119
|
-
\`\`\`
|
|
120
|
-
|
|
121
|
-
Use \`--platform ios\` or \`--platform android\` for one platform. Use \`--json\` for machine-readable output. The example releases sessions on completion, failure, SIGINT, and SIGTERM unless the user explicitly passes \`--keep\`.
|
|
122
|
-
|
|
123
|
-
## Bundled CLI Demos
|
|
124
|
-
|
|
125
|
-
These published demos exercise multi-simulator workflows:
|
|
126
|
-
|
|
127
|
-
\`\`\`bash
|
|
128
|
-
runcloud demo run eight-device-mosaic --open
|
|
129
|
-
runcloud demo run live-camera-relay --open
|
|
130
|
-
\`\`\`
|
|
131
|
-
|
|
132
|
-
They use the same CLI authentication choices described above and release every session automatically.
|
|
133
|
-
|
|
134
|
-
## Embedded Iframes
|
|
135
|
-
|
|
136
|
-
- Use \`inactivityTimeout: "60s"\` in the SDK, or \`--inactivity-timeout 60s\` in the CLI, when an embed should auto-close after user inactivity.
|
|
137
|
-
- Omit the option or pass \`null\`/\`none\` when the user needs a metered session without idle auto-close.
|
|
138
|
-
- Treat the returned signed session URL as a secret. Do not publish it in logs.
|
|
139
|
-
- Iframes post \`ios-simulator:status\`, \`ios-simulator:auth-error\`, \`ios-simulator:session-ended\`, and \`ios-simulator:session-restart-requested\` messages to the parent window.
|
|
140
|
-
- Verify \`event.source\` before acting on iframe messages.
|
|
141
|
-
- When \`ios-simulator:session-restart-requested\` arrives, create a fresh session; do not reuse the ended iframe URL.
|
|
142
|
-
|
|
143
|
-
## Rules
|
|
144
|
-
|
|
145
|
-
- Prefer the SDK for code and \`--json\` CLI output for shell automation.
|
|
146
|
-
- Always release sessions you create unless the user asks to keep them open.
|
|
147
|
-
- If installation fails, verify that the artifact matches the target platform before attempting code changes.
|
|
148
|
-
- Do not assume a local tunnel is installed on the user's machine.
|
|
149
|
-
- Do not expose API keys, CLI tokens, signed simulator URLs, or simulator tokens in logs or screenshots.
|
|
150
|
-
`;
|
|
22
|
+
export const RUN_CLOUD_SKILL = loadRunCloudSkill();
|
|
151
23
|
function client() {
|
|
152
24
|
const creds = requireCredentials();
|
|
153
25
|
return new ApiClient(creds.apiUrl, creds.token);
|
|
@@ -332,11 +204,29 @@ export async function runCloudDemo(name = 'eight-device-mosaic', opts = {}, load
|
|
|
332
204
|
}
|
|
333
205
|
async function pushAsset(path, opts = {}) {
|
|
334
206
|
const { blob, name } = fileBlob(path);
|
|
335
|
-
const
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
207
|
+
const api = client();
|
|
208
|
+
const prepared = await api.post('/run-cloud/assets/uploads', {
|
|
209
|
+
filename: name,
|
|
210
|
+
...(opts.name ? { name: opts.name } : {}),
|
|
211
|
+
contentType: blob.type || 'application/octet-stream',
|
|
212
|
+
byteSize: blob.size,
|
|
213
|
+
});
|
|
214
|
+
try {
|
|
215
|
+
const uploaded = await fetch(prepared.upload.url, {
|
|
216
|
+
method: 'PUT',
|
|
217
|
+
headers: prepared.upload.headers,
|
|
218
|
+
body: blob,
|
|
219
|
+
});
|
|
220
|
+
if (!uploaded.ok)
|
|
221
|
+
throw new Error(`asset storage upload failed with HTTP ${uploaded.status}`);
|
|
222
|
+
}
|
|
223
|
+
catch (error) {
|
|
224
|
+
if (prepared.asset.id) {
|
|
225
|
+
await api.delete(`/run-cloud/assets/${encodeURIComponent(prepared.asset.id)}`).catch(() => undefined);
|
|
226
|
+
}
|
|
227
|
+
throw error;
|
|
228
|
+
}
|
|
229
|
+
return prepared.asset;
|
|
340
230
|
}
|
|
341
231
|
async function createSimulatorSession(platform, opts) {
|
|
342
232
|
assertDevHostAllowed(opts.devHost);
|