runcloud 0.1.6 → 0.1.9
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 +135 -0
- package/dist/api.js +22 -11
- package/dist/commands/boxAccess.js +246 -0
- package/dist/commands/environment.js +1 -1
- package/dist/commands/login.js +149 -21
- package/dist/commands/run-cloud.js +85 -15
- package/dist/commands/sandbox.js +263 -4
- package/dist/config.js +6 -14
- package/dist/prompt.js +150 -0
- package/dist/run-cloud.js +2 -3
- package/dist/runCloudAuth.js +79 -0
- package/dist/{environmentShell.js → shellSession.js} +17 -3
- package/dist/version.js +1 -0
- package/examples/eight-device-mosaic/README.md +1 -1
- package/examples/eight-device-mosaic/demo.mjs +1 -1
- package/examples/eight-device-mosaic/verify.mjs +2 -1
- package/examples/live-camera-relay/App.tsx +2 -2
- package/examples/live-camera-relay/README.md +1 -1
- package/examples/live-camera-relay/demo.mjs +1 -1
- package/examples/live-camera-relay/verify.mjs +2 -0
- package/package.json +7 -2
- package/examples/parallel-simulators/README.md +0 -33
- package/examples/parallel-simulators/demo.mjs +0 -185
- package/examples/parallel-simulators/package.json +0 -13
- package/examples/parallel-simulators/verify.mjs +0 -78
package/dist/commands/login.js
CHANGED
|
@@ -1,8 +1,39 @@
|
|
|
1
1
|
import http from 'node:http';
|
|
2
2
|
import crypto from 'node:crypto';
|
|
3
|
+
import { hostname } from 'node:os';
|
|
3
4
|
import { spawn } from 'node:child_process';
|
|
4
|
-
import {
|
|
5
|
+
import { ApiClient, ApiError } from '../api.js';
|
|
6
|
+
import { clearCredentials, credentialsDisplayPath, normalizeApiUrl, saveCredentials } from '../config.js';
|
|
7
|
+
import { acquirePromptIO, ask, PromptAbortedError, select, } from '../prompt.js';
|
|
8
|
+
import { AuthError, createApiKey, fetchProviders, isValidEmail, isValidOtp, normalizeOtp, sendEmailCode, verifyEmailCode, } from '../runCloudAuth.js';
|
|
5
9
|
const LOGIN_TIMEOUT_MS = 5 * 60 * 1000;
|
|
10
|
+
export const DEFAULT_RUN_CLOUD_API_URL = 'https://api.run.cloud';
|
|
11
|
+
export function loginChoices(providers) {
|
|
12
|
+
const choices = [];
|
|
13
|
+
if (providers.emailOtp) {
|
|
14
|
+
choices.push({ value: 'email', label: 'Email', hint: 'a code is sent to your inbox' });
|
|
15
|
+
}
|
|
16
|
+
if (providers.google) {
|
|
17
|
+
choices.push({ value: 'google', label: 'Google', hint: 'opens your browser' });
|
|
18
|
+
}
|
|
19
|
+
if (providers.github) {
|
|
20
|
+
choices.push({ value: 'github', label: 'GitHub', hint: 'opens your browser' });
|
|
21
|
+
}
|
|
22
|
+
return choices;
|
|
23
|
+
}
|
|
24
|
+
export function methodFromFlags(flags) {
|
|
25
|
+
const chosen = [];
|
|
26
|
+
if (flags.email)
|
|
27
|
+
chosen.push('email');
|
|
28
|
+
if (flags.google)
|
|
29
|
+
chosen.push('google');
|
|
30
|
+
if (flags.github)
|
|
31
|
+
chosen.push('github');
|
|
32
|
+
if (chosen.length > 1) {
|
|
33
|
+
throw new Error(`Pick one sign-in method: ${chosen.map((m) => `--${m}`).join(' and ')} conflict.`);
|
|
34
|
+
}
|
|
35
|
+
return chosen[0] ?? null;
|
|
36
|
+
}
|
|
6
37
|
export async function startCallbackServer(state, appOrigin, timeoutMs) {
|
|
7
38
|
let resolvePayload;
|
|
8
39
|
let rejectPayload;
|
|
@@ -10,7 +41,10 @@ export async function startCallbackServer(state, appOrigin, timeoutMs) {
|
|
|
10
41
|
resolvePayload = resolve;
|
|
11
42
|
rejectPayload = reject;
|
|
12
43
|
});
|
|
13
|
-
const timer = setTimeout(() => rejectPayload(new Error(`Login timed out after ${timeoutMs / 1000}s`
|
|
44
|
+
const timer = setTimeout(() => rejectPayload(new Error(`Login timed out after ${timeoutMs / 1000}s.\n` +
|
|
45
|
+
"If the browser said the local handoff was blocked, Chrome gates HTTPS→localhost behind a permission: " +
|
|
46
|
+
'click the icon at the left of the address bar, allow "access to apps on this device", ' +
|
|
47
|
+
'then run login again (or use --manual).')), timeoutMs);
|
|
14
48
|
const corsHeaders = {
|
|
15
49
|
'Access-Control-Allow-Origin': appOrigin,
|
|
16
50
|
'Access-Control-Allow-Methods': 'POST, OPTIONS',
|
|
@@ -91,12 +125,54 @@ function openBrowser(url) {
|
|
|
91
125
|
});
|
|
92
126
|
child.unref();
|
|
93
127
|
}
|
|
94
|
-
export function cliAuthUrl(appUrl, port, state) {
|
|
128
|
+
export function cliAuthUrl(appUrl, port, state, provider) {
|
|
95
129
|
const url = new URL('/cli-auth', appUrl);
|
|
96
130
|
url.searchParams.set('port', String(port));
|
|
97
131
|
url.searchParams.set('state', state);
|
|
132
|
+
if (provider)
|
|
133
|
+
url.searchParams.set('provider', provider);
|
|
98
134
|
return url.toString();
|
|
99
135
|
}
|
|
136
|
+
export async function emailLogin(apiUrl, presetEmail, io) {
|
|
137
|
+
const email = presetEmail?.trim()
|
|
138
|
+
? presetEmail.trim()
|
|
139
|
+
: await ask(' Email: ', {
|
|
140
|
+
io,
|
|
141
|
+
validate: (value) => (isValidEmail(value) ? null : 'That does not look like an email address.'),
|
|
142
|
+
});
|
|
143
|
+
if (!isValidEmail(email))
|
|
144
|
+
throw new Error(`Not a valid email address: ${email}`);
|
|
145
|
+
await sendEmailCode(apiUrl, email);
|
|
146
|
+
console.log(` Code sent to ${email}\n`);
|
|
147
|
+
let session = null;
|
|
148
|
+
for (let attempt = 1; attempt <= 3 && !session; attempt += 1) {
|
|
149
|
+
const code = await ask(' Code: ', {
|
|
150
|
+
io,
|
|
151
|
+
validate: (value) => (isValidOtp(value) ? null : 'Codes are six digits.'),
|
|
152
|
+
});
|
|
153
|
+
try {
|
|
154
|
+
session = await verifyEmailCode(apiUrl, email, normalizeOtp(code));
|
|
155
|
+
}
|
|
156
|
+
catch (e) {
|
|
157
|
+
const wrongCode = e instanceof AuthError && (e.status === 400 || e.status === 401);
|
|
158
|
+
if (!wrongCode || attempt === 3)
|
|
159
|
+
throw e;
|
|
160
|
+
console.log(` ${e.message} — try again.\n`);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
if (!session)
|
|
164
|
+
throw new Error('Could not verify the code.');
|
|
165
|
+
const token = await createApiKey(apiUrl, session.sessionToken, `runcloud CLI (${hostLabel()})`);
|
|
166
|
+
return { email, token };
|
|
167
|
+
}
|
|
168
|
+
function hostLabel() {
|
|
169
|
+
try {
|
|
170
|
+
return hostname() || 'cli';
|
|
171
|
+
}
|
|
172
|
+
catch {
|
|
173
|
+
return 'cli';
|
|
174
|
+
}
|
|
175
|
+
}
|
|
100
176
|
export function parseManualCredential(blob) {
|
|
101
177
|
let parsed;
|
|
102
178
|
try {
|
|
@@ -112,6 +188,19 @@ export function parseManualCredential(blob) {
|
|
|
112
188
|
}
|
|
113
189
|
return { token: c.token, expiresIn: c.expiresIn, apiUrl: c.apiUrl };
|
|
114
190
|
}
|
|
191
|
+
export async function verifyRunCloudCredential(apiUrl, token) {
|
|
192
|
+
try {
|
|
193
|
+
await new ApiClient(normalizeApiUrl(apiUrl), token).get('/run-cloud/account');
|
|
194
|
+
}
|
|
195
|
+
catch (e) {
|
|
196
|
+
if (e instanceof ApiError && (e.status === 401 || e.status === 403)) {
|
|
197
|
+
throw new Error(`${apiUrl} rejected this credential (${e.status}). Make sure you signed in to the account ` +
|
|
198
|
+
'you want the CLI to use, then run login again.');
|
|
199
|
+
}
|
|
200
|
+
console.warn(`Warning: could not verify the credential against ${apiUrl} ` +
|
|
201
|
+
`(${e instanceof Error ? e.message : String(e)}). Saving it anyway.`);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
115
204
|
export function registerLogin(program) {
|
|
116
205
|
const isRunCloud = program.name() === 'runcloud';
|
|
117
206
|
const defaultAppUrl = isRunCloud
|
|
@@ -119,12 +208,18 @@ export function registerLogin(program) {
|
|
|
119
208
|
: process.env.NEWLY_APP_URL ?? 'https://newly.app';
|
|
120
209
|
program
|
|
121
210
|
.command('login')
|
|
122
|
-
.description(
|
|
211
|
+
.description(isRunCloud
|
|
212
|
+
? 'Log in with your run.cloud account: email, Google, or GitHub'
|
|
213
|
+
: 'Log in with your Newly account via the browser')
|
|
123
214
|
.option('--app-url <url>', 'product web app URL', defaultAppUrl)
|
|
124
215
|
.option('--manual', 'paste the credential from the browser instead of using the local callback', false)
|
|
125
216
|
.option('--api-url <url>', 'explicit API URL (skips browser; use with --token)')
|
|
126
217
|
.option('--token <token>', 'explicit credential token (skips browser; use with --api-url)')
|
|
127
218
|
.option('--expires-in <seconds>', 'session lifetime when using --token (default 7 days)', (v) => Number(v), 604800)
|
|
219
|
+
.option('--email [address]', 'sign in with an emailed code, without a browser')
|
|
220
|
+
.option('--google', 'sign in with Google', false)
|
|
221
|
+
.option('--github', 'sign in with GitHub', false)
|
|
222
|
+
.option('--browser', 'skip the picker and use the browser handoff', false)
|
|
128
223
|
.action(async (opts) => {
|
|
129
224
|
try {
|
|
130
225
|
const hasApiUrl = typeof opts.apiUrl === 'string' && opts.apiUrl.length > 0;
|
|
@@ -136,11 +231,8 @@ export function registerLogin(program) {
|
|
|
136
231
|
throw new Error('--api-url requires --token for headless login.');
|
|
137
232
|
}
|
|
138
233
|
if (hasToken) {
|
|
139
|
-
if (isRunCloud && !opts.token.startsWith('rc_live_')) {
|
|
140
|
-
throw new Error('runcloud login requires a Better Auth API key beginning with rc_live_.');
|
|
141
|
-
}
|
|
142
234
|
const apiUrl = isRunCloud
|
|
143
|
-
? opts.apiUrl ?? process.env.RUN_CLOUD_API_URL ?? 'https://api.
|
|
235
|
+
? opts.apiUrl ?? process.env.RUN_CLOUD_API_URL ?? 'https://api.run.cloud'
|
|
144
236
|
: opts.apiUrl;
|
|
145
237
|
try {
|
|
146
238
|
new URL(apiUrl);
|
|
@@ -170,39 +262,70 @@ export function registerLogin(program) {
|
|
|
170
262
|
const blob = await rl.question('Paste the credential from the browser: ');
|
|
171
263
|
rl.close();
|
|
172
264
|
const parsed = parseManualCredential(blob);
|
|
173
|
-
if (isRunCloud
|
|
174
|
-
|
|
175
|
-
}
|
|
265
|
+
if (isRunCloud)
|
|
266
|
+
await verifyRunCloudCredential(parsed.apiUrl, parsed.token);
|
|
176
267
|
saveCredentials({
|
|
177
268
|
token: parsed.token,
|
|
178
269
|
apiUrl: parsed.apiUrl,
|
|
179
270
|
appUrl: opts.appUrl,
|
|
180
|
-
expiresAt:
|
|
181
|
-
? Number.MAX_SAFE_INTEGER
|
|
182
|
-
: Math.floor(Date.now() / 1000) + parsed.expiresIn,
|
|
271
|
+
expiresAt: Math.floor(Date.now() / 1000) + parsed.expiresIn,
|
|
183
272
|
});
|
|
184
273
|
console.log('Logged in.');
|
|
185
274
|
return;
|
|
186
275
|
}
|
|
276
|
+
let socialProvider;
|
|
277
|
+
if (isRunCloud && !opts.browser) {
|
|
278
|
+
const authApiUrl = opts.apiUrl ?? process.env.RUN_CLOUD_API_URL ?? DEFAULT_RUN_CLOUD_API_URL;
|
|
279
|
+
let method = methodFromFlags(opts);
|
|
280
|
+
const terminal = method === 'email' || !method ? acquirePromptIO() : null;
|
|
281
|
+
try {
|
|
282
|
+
if (!method && terminal) {
|
|
283
|
+
const providers = await fetchProviders(authApiUrl);
|
|
284
|
+
const choices = loginChoices(providers);
|
|
285
|
+
if (choices.length === 0) {
|
|
286
|
+
throw new Error(`${authApiUrl} has no sign-in methods enabled.`);
|
|
287
|
+
}
|
|
288
|
+
method = await select(' How do you want to sign in?', choices, terminal.io);
|
|
289
|
+
}
|
|
290
|
+
if (method === 'email') {
|
|
291
|
+
const preset = typeof opts.email === 'string' ? opts.email : undefined;
|
|
292
|
+
const { email, token } = await emailLogin(authApiUrl, preset, terminal?.io);
|
|
293
|
+
saveCredentials({
|
|
294
|
+
token,
|
|
295
|
+
apiUrl: authApiUrl,
|
|
296
|
+
appUrl: opts.appUrl,
|
|
297
|
+
expiresAt: Number.MAX_SAFE_INTEGER,
|
|
298
|
+
});
|
|
299
|
+
console.log(`\n Signed in as ${email}`);
|
|
300
|
+
console.log(` Credential saved to ${credentialsDisplayPath()}\n`);
|
|
301
|
+
console.log(' Try:');
|
|
302
|
+
console.log(' runcloud ios create --open');
|
|
303
|
+
console.log(' runcloud sandbox run "echo hello"');
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
finally {
|
|
308
|
+
terminal?.close();
|
|
309
|
+
}
|
|
310
|
+
if (method === 'google' || method === 'github')
|
|
311
|
+
socialProvider = method;
|
|
312
|
+
}
|
|
187
313
|
const state = crypto.randomBytes(16).toString('hex');
|
|
188
314
|
const appOrigin = new URL(opts.appUrl).origin;
|
|
189
315
|
const { port, waitForToken, close } = await startCallbackServer(state, appOrigin, LOGIN_TIMEOUT_MS);
|
|
190
|
-
const loginUrl = cliAuthUrl(opts.appUrl, port, state);
|
|
316
|
+
const loginUrl = cliAuthUrl(opts.appUrl, port, state, socialProvider);
|
|
191
317
|
console.log(`Opening your browser to authorize the CLI…\n ${loginUrl}\n`);
|
|
192
318
|
console.log("If the browser doesn't open, paste the URL above into it manually.");
|
|
193
319
|
openBrowser(loginUrl);
|
|
194
320
|
try {
|
|
195
321
|
const payload = await waitForToken();
|
|
196
|
-
if (isRunCloud
|
|
197
|
-
|
|
198
|
-
}
|
|
322
|
+
if (isRunCloud)
|
|
323
|
+
await verifyRunCloudCredential(payload.apiUrl, payload.token);
|
|
199
324
|
saveCredentials({
|
|
200
325
|
token: payload.token,
|
|
201
326
|
apiUrl: payload.apiUrl,
|
|
202
327
|
appUrl: opts.appUrl,
|
|
203
|
-
expiresAt:
|
|
204
|
-
? Number.MAX_SAFE_INTEGER
|
|
205
|
-
: Math.floor(Date.now() / 1000) + payload.expiresIn,
|
|
328
|
+
expiresAt: Math.floor(Date.now() / 1000) + payload.expiresIn,
|
|
206
329
|
});
|
|
207
330
|
console.log(`Logged in. Credentials saved to ${credentialsDisplayPath()}`);
|
|
208
331
|
}
|
|
@@ -211,6 +334,11 @@ export function registerLogin(program) {
|
|
|
211
334
|
}
|
|
212
335
|
}
|
|
213
336
|
catch (e) {
|
|
337
|
+
if (e instanceof PromptAbortedError) {
|
|
338
|
+
console.log('Cancelled.');
|
|
339
|
+
process.exitCode = 130;
|
|
340
|
+
return;
|
|
341
|
+
}
|
|
214
342
|
console.error(e instanceof Error ? e.message : String(e));
|
|
215
343
|
process.exitCode = 1;
|
|
216
344
|
}
|
|
@@ -3,6 +3,7 @@ import { cpSync, existsSync, mkdirSync, readFileSync, rmSync, statSync, writeFil
|
|
|
3
3
|
import { basename, dirname, join, resolve } from 'node:path';
|
|
4
4
|
import { homedir } from 'node:os';
|
|
5
5
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
6
|
+
import { createHash } from 'node:crypto';
|
|
6
7
|
import { ApiClient, friendlyApiError } from '../api.js';
|
|
7
8
|
import { requireCredentials } from '../config.js';
|
|
8
9
|
export const RUN_CLOUD_SKILL = `---
|
|
@@ -17,10 +18,10 @@ Use this skill when a user asks an agent to create, inspect, smoke test, debug,
|
|
|
17
18
|
|
|
18
19
|
## Requirements
|
|
19
20
|
|
|
20
|
-
- 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.
|
|
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\`.
|
|
21
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.
|
|
22
23
|
- The TypeScript SDK requires Node.js 20 or newer.
|
|
23
|
-
- The account must have run.cloud access, available capacity, and
|
|
24
|
+
- The account must have run.cloud access, available capacity, and organization credit.
|
|
24
25
|
- App artifacts must match the target platform. iOS sessions need simulator-compatible builds; Android sessions need Android-compatible artifacts such as APKs.
|
|
25
26
|
|
|
26
27
|
## TypeScript SDK
|
|
@@ -75,7 +76,7 @@ Or authenticate non-interactively with both required environment variables:
|
|
|
75
76
|
|
|
76
77
|
\`\`\`bash
|
|
77
78
|
export RUN_CLOUD_API_KEY="rc_live_..."
|
|
78
|
-
export RUN_CLOUD_API_URL="https://api.
|
|
79
|
+
export RUN_CLOUD_API_URL="https://api.run.cloud"
|
|
79
80
|
\`\`\`
|
|
80
81
|
|
|
81
82
|
Then inspect the account:
|
|
@@ -95,6 +96,17 @@ runcloud ios delete "$SESSION_ID" --json
|
|
|
95
96
|
|
|
96
97
|
Use the corresponding \`runcloud android\` commands with an Android artifact for Android emulator sessions.
|
|
97
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
|
+
|
|
98
110
|
## Runnable SDK Example
|
|
99
111
|
|
|
100
112
|
The maintained example checks account state, creates iOS and Android sessions, opens a URL on each, and releases both sessions:
|
|
@@ -113,7 +125,6 @@ Use \`--platform ios\` or \`--platform android\` for one platform. Use \`--json\
|
|
|
113
125
|
These published demos exercise multi-simulator workflows:
|
|
114
126
|
|
|
115
127
|
\`\`\`bash
|
|
116
|
-
runcloud demo run parallel-simulators --open
|
|
117
128
|
runcloud demo run eight-device-mosaic --open
|
|
118
129
|
runcloud demo run live-camera-relay --open
|
|
119
130
|
\`\`\`
|
|
@@ -219,10 +230,58 @@ function installSkill(opts) {
|
|
|
219
230
|
return results;
|
|
220
231
|
}
|
|
221
232
|
const RUN_CLOUD_DEMOS = [
|
|
222
|
-
'parallel-simulators',
|
|
223
233
|
'eight-device-mosaic',
|
|
224
234
|
'live-camera-relay',
|
|
225
235
|
];
|
|
236
|
+
export const RUN_CLOUD_SAMPLES = {
|
|
237
|
+
ios: {
|
|
238
|
+
filename: 'run-cloud-sample-ios.app.tar.gz',
|
|
239
|
+
url: 'https://run.cloud/downloads/run-cloud-sample-ios.app.tar.gz',
|
|
240
|
+
sha256: '09c437b1f299d41f79a8453611d99b8b1e4a3f5198dd871f4dad183362f944b3',
|
|
241
|
+
byteSize: 19_516,
|
|
242
|
+
},
|
|
243
|
+
android: {
|
|
244
|
+
filename: 'run-cloud-sample-android.apk',
|
|
245
|
+
url: 'https://run.cloud/downloads/run-cloud-sample-android.apk',
|
|
246
|
+
sha256: '892d4441a24757fc88852d29abe87d42d14f04921cbe89802e369dad6e46edba',
|
|
247
|
+
byteSize: 6_521_225,
|
|
248
|
+
},
|
|
249
|
+
};
|
|
250
|
+
function samplePlatform(value) {
|
|
251
|
+
if (value === 'ios' || value === 'android')
|
|
252
|
+
return value;
|
|
253
|
+
throw new Error(`Unknown sample platform: ${value}. Choose ios or android.`);
|
|
254
|
+
}
|
|
255
|
+
function shellQuote(value) {
|
|
256
|
+
return `'${value.replaceAll("'", "'\\''")}'`;
|
|
257
|
+
}
|
|
258
|
+
export async function downloadRunCloudSample(rawPlatform, opts = {}, fetchSample = fetch, samples = RUN_CLOUD_SAMPLES) {
|
|
259
|
+
const platform = samplePlatform(rawPlatform);
|
|
260
|
+
const sample = samples[platform];
|
|
261
|
+
const target = resolve(opts.output ?? sample.filename);
|
|
262
|
+
if (existsSync(target) && !opts.force) {
|
|
263
|
+
throw new Error(`${target} already exists. Pass --force to overwrite it.`);
|
|
264
|
+
}
|
|
265
|
+
const response = await fetchSample(sample.url);
|
|
266
|
+
if (!response.ok) {
|
|
267
|
+
throw new Error(`Could not download the ${platform} sample: HTTP ${response.status}`);
|
|
268
|
+
}
|
|
269
|
+
const body = Buffer.from(await response.arrayBuffer());
|
|
270
|
+
const sha256 = createHash('sha256').update(body).digest('hex');
|
|
271
|
+
if (body.byteLength !== sample.byteSize || sha256 !== sample.sha256) {
|
|
272
|
+
throw new Error(`The ${platform} sample failed its checksum verification.`);
|
|
273
|
+
}
|
|
274
|
+
mkdirSync(dirname(target), { recursive: true });
|
|
275
|
+
writeFileSync(target, body);
|
|
276
|
+
return {
|
|
277
|
+
platform,
|
|
278
|
+
path: target,
|
|
279
|
+
url: sample.url,
|
|
280
|
+
sha256,
|
|
281
|
+
byteSize: body.byteLength,
|
|
282
|
+
next: `runcloud ${platform} create --install ${shellQuote(target)}`,
|
|
283
|
+
};
|
|
284
|
+
}
|
|
226
285
|
function experimentalWebRtcEnabled() {
|
|
227
286
|
return ['1', 'true', 'yes', 'on'].includes((process.env.RUN_CLOUD_EXPERIMENTAL_WEBRTC ?? '').trim().toLowerCase());
|
|
228
287
|
}
|
|
@@ -247,7 +306,7 @@ function demoDirectory(name) {
|
|
|
247
306
|
throw new Error(`The ${name} demo is missing from this runcloud package.`);
|
|
248
307
|
return source;
|
|
249
308
|
}
|
|
250
|
-
export function initRunCloudDemo(name = '
|
|
309
|
+
export function initRunCloudDemo(name = 'eight-device-mosaic', directory, opts = {}) {
|
|
251
310
|
const source = demoDirectory(name);
|
|
252
311
|
const target = resolve(directory ?? `run-cloud-${name}`);
|
|
253
312
|
if (existsSync(target)) {
|
|
@@ -258,13 +317,11 @@ export function initRunCloudDemo(name = 'parallel-simulators', directory, opts =
|
|
|
258
317
|
cpSync(source, target, { recursive: true });
|
|
259
318
|
return { example: name, path: target, status: 'created' };
|
|
260
319
|
}
|
|
261
|
-
export async function runCloudDemo(name = '
|
|
320
|
+
export async function runCloudDemo(name = 'eight-device-mosaic', opts = {}, loadDemo = async (url) => (await import(url))) {
|
|
262
321
|
if (name === 'eight-device-mosaic' && opts.duration) {
|
|
263
322
|
throw new Error('The eight-device mosaic uses a fixed 10-minute lease and does not accept --duration.');
|
|
264
323
|
}
|
|
265
324
|
const args = [];
|
|
266
|
-
if (opts.count)
|
|
267
|
-
args.push('--count', opts.count);
|
|
268
325
|
if (opts.duration)
|
|
269
326
|
args.push('--duration', opts.duration);
|
|
270
327
|
if (opts.open)
|
|
@@ -323,7 +380,7 @@ function registerSimulatorCommands(program, platform) {
|
|
|
323
380
|
.option('--codec <auto|mjpeg|webrtc>', 'force the simulator viewer stream codec', 'auto')
|
|
324
381
|
.option('--rm', 'release the session when this command exits', false)
|
|
325
382
|
.option('--json', 'output JSON', false)
|
|
326
|
-
.addOption(new Option('--dev-host <host>', 'dev-only: target a specific
|
|
383
|
+
.addOption(new Option('--dev-host <host>', 'dev-only: target a specific simulator fleet host').hideHelp())
|
|
327
384
|
.action((opts) => action(async () => {
|
|
328
385
|
print(await createSimulatorSession(platform, opts), opts);
|
|
329
386
|
}));
|
|
@@ -373,11 +430,25 @@ async function action(fn) {
|
|
|
373
430
|
}
|
|
374
431
|
}
|
|
375
432
|
export function registerRunCloud(program) {
|
|
433
|
+
const sample = program
|
|
434
|
+
.command('sample')
|
|
435
|
+
.alias('samples')
|
|
436
|
+
.description('Download installable onboarding sample apps');
|
|
437
|
+
sample
|
|
438
|
+
.command('download')
|
|
439
|
+
.description('Download a checksum-verified iOS or Android sample app')
|
|
440
|
+
.argument('<ios|android>', 'sample platform')
|
|
441
|
+
.option('-o, --output <path>', 'output file')
|
|
442
|
+
.option('--force', 'overwrite an existing output file', false)
|
|
443
|
+
.option('--json', 'output JSON', false)
|
|
444
|
+
.action((platform, opts) => action(async () => {
|
|
445
|
+
print(await downloadRunCloudSample(platform, opts), opts);
|
|
446
|
+
}));
|
|
376
447
|
const demo = program.command('demo').description('Create and run run.cloud demo projects');
|
|
377
448
|
demo
|
|
378
449
|
.command('init')
|
|
379
450
|
.description('Scaffold a standalone demo project')
|
|
380
|
-
.argument('[name]', 'demo to create', '
|
|
451
|
+
.argument('[name]', 'demo to create', 'eight-device-mosaic')
|
|
381
452
|
.argument('[directory]', 'output directory')
|
|
382
453
|
.option('--force', 'overwrite an existing output directory', false)
|
|
383
454
|
.option('--json', 'output JSON', false)
|
|
@@ -387,16 +458,15 @@ export function registerRunCloud(program) {
|
|
|
387
458
|
demo
|
|
388
459
|
.command('run')
|
|
389
460
|
.description('Run a bundled demo without scaffolding a project')
|
|
390
|
-
.argument('[name]', 'demo to run', '
|
|
391
|
-
.option('--
|
|
392
|
-
.option('--duration <seconds>', 'duration for parallel-simulators and live-camera-relay')
|
|
461
|
+
.argument('[name]', 'demo to run', 'eight-device-mosaic')
|
|
462
|
+
.option('--duration <seconds>', 'duration for live-camera-relay')
|
|
393
463
|
.option('--open', 'open the demo viewer or live sessions in the browser', false)
|
|
394
464
|
.action((name, opts) => action(async () => {
|
|
395
465
|
await runCloudDemo(name, opts);
|
|
396
466
|
}));
|
|
397
467
|
program
|
|
398
468
|
.command('account')
|
|
399
|
-
.description('Show run.cloud
|
|
469
|
+
.description('Show run.cloud account and organization access')
|
|
400
470
|
.option('--json', 'output JSON', false)
|
|
401
471
|
.action((opts) => action(async () => {
|
|
402
472
|
print(await client().get('/run-cloud/account'), opts);
|