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
|
@@ -35,7 +35,8 @@ test('renders eight signed embeds without printing them into the viewer URL', as
|
|
|
35
35
|
}));
|
|
36
36
|
const html = mosaicViewerHtml(sessions, 601_000);
|
|
37
37
|
assert.equal((html.match(/<iframe /g) || []).length, 8);
|
|
38
|
-
assert.match(html, /
|
|
38
|
+
assert.match(html, /embed=1/);
|
|
39
|
+
assert.doesNotMatch(html, /loadingGuard=1/);
|
|
39
40
|
assert.match(html, /const deadline = 601000/);
|
|
40
41
|
|
|
41
42
|
const viewer = await startMosaicViewer(sessions, 601_000);
|
|
@@ -239,13 +239,13 @@ export default function App() {
|
|
|
239
239
|
let stream: MediaStream;
|
|
240
240
|
try {
|
|
241
241
|
stream = await navigator.mediaDevices.getUserMedia({
|
|
242
|
-
video:
|
|
242
|
+
video: true,
|
|
243
243
|
audio: true,
|
|
244
244
|
});
|
|
245
245
|
} catch (withAudioError) {
|
|
246
246
|
try {
|
|
247
247
|
stream = await navigator.mediaDevices.getUserMedia({
|
|
248
|
-
video:
|
|
248
|
+
video: true,
|
|
249
249
|
audio: false,
|
|
250
250
|
});
|
|
251
251
|
} catch (videoError) {
|
|
@@ -11,7 +11,7 @@ and sends the real WebRTC video stream to both receiver simulators.
|
|
|
11
11
|
- `runcloud` 0.1.3 or newer: `npm install -g runcloud`
|
|
12
12
|
- A simulator account authenticated either with `runcloud login` or with both
|
|
13
13
|
`RUN_CLOUD_API_KEY` and `RUN_CLOUD_API_URL`
|
|
14
|
-
- run.cloud simulator access, capacity, and enough
|
|
14
|
+
- run.cloud simulator access, capacity, and enough organization credit for three sessions
|
|
15
15
|
- A browser with webcam permission
|
|
16
16
|
|
|
17
17
|
The local viewer binds only to `127.0.0.1`. Signed simulator URLs and camera
|
|
@@ -67,6 +67,8 @@ test('renders three signed embeds while keeping credentials out of the local URL
|
|
|
67
67
|
const demoSessions = sessions();
|
|
68
68
|
const html = cameraViewerHtml(demoSessions, 60, 'room-12345678', 1_000);
|
|
69
69
|
assert.equal((html.match(/<iframe /g) || []).length, 3);
|
|
70
|
+
assert.match(html, /embed=1/);
|
|
71
|
+
assert.doesNotMatch(html, /loadingGuard=1/);
|
|
70
72
|
assert.match(html, /Connect webcam/);
|
|
71
73
|
assert.match(html, /com\.apple\.mobilesafari/);
|
|
72
74
|
assert.match(html, /audio: false/);
|
package/package.json
CHANGED
|
@@ -1,9 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "runcloud",
|
|
3
|
-
"version": "0.1.
|
|
4
|
-
"description": "Create and control run.cloud remote mobile
|
|
3
|
+
"version": "0.1.9",
|
|
4
|
+
"description": "Create and control run.cloud remote mobile simulators and cloud sandboxes",
|
|
5
5
|
"license": "UNLICENSED",
|
|
6
6
|
"type": "module",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/newly-app/newly.git",
|
|
10
|
+
"directory": "newly-cli"
|
|
11
|
+
},
|
|
7
12
|
"bin": {
|
|
8
13
|
"runcloud": "dist/run-cloud.js"
|
|
9
14
|
},
|
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
# Parallel iOS simulators
|
|
2
|
-
|
|
3
|
-
Start two or three run.cloud iOS simulator sessions at the same time, open a
|
|
4
|
-
different site in each simulator, and view the live sessions side by side.
|
|
5
|
-
The demo releases every session automatically after two minutes. A ten-minute
|
|
6
|
-
server-side timeout is also set in case the local process is interrupted.
|
|
7
|
-
|
|
8
|
-
## Requirements
|
|
9
|
-
|
|
10
|
-
- Node.js 20 or newer
|
|
11
|
-
- `runcloud` 0.1.3 or newer: `npm install -g runcloud`
|
|
12
|
-
- A simulator account authenticated either with `runcloud login` or with both
|
|
13
|
-
`RUN_CLOUD_API_KEY` and `RUN_CLOUD_API_URL`
|
|
14
|
-
- run.cloud simulator access and enough balance for two or three sessions
|
|
15
|
-
|
|
16
|
-
The demo uses the CLI credential resolution path. It does not require both a
|
|
17
|
-
saved login and an API key.
|
|
18
|
-
|
|
19
|
-
## Run
|
|
20
|
-
|
|
21
|
-
```bash
|
|
22
|
-
npm run demo -- --open
|
|
23
|
-
```
|
|
24
|
-
|
|
25
|
-
The default starts three sessions, opens the session URLs in browser tabs, and
|
|
26
|
-
releases the sessions after 120 seconds. To run a smaller or shorter demo:
|
|
27
|
-
|
|
28
|
-
```bash
|
|
29
|
-
npm run demo -- --count 2 --duration 60 --open
|
|
30
|
-
```
|
|
31
|
-
|
|
32
|
-
Press Ctrl+C to release the sessions early. Omit `--open` when you only want
|
|
33
|
-
the live session URLs printed to the terminal.
|
|
@@ -1,185 +0,0 @@
|
|
|
1
|
-
import { spawn } from 'node:child_process';
|
|
2
|
-
|
|
3
|
-
const DEFAULT_TARGETS = [
|
|
4
|
-
'https://run.cloud',
|
|
5
|
-
'https://newly.app',
|
|
6
|
-
'https://reactnative.dev',
|
|
7
|
-
];
|
|
8
|
-
|
|
9
|
-
export function parseOptions(args) {
|
|
10
|
-
const options = { count: 3, duration: 120, open: false };
|
|
11
|
-
for (let index = 0; index < args.length; index += 1) {
|
|
12
|
-
const argument = args[index];
|
|
13
|
-
if (argument === '--open') {
|
|
14
|
-
options.open = true;
|
|
15
|
-
continue;
|
|
16
|
-
}
|
|
17
|
-
if (argument === '--count' || argument === '--duration') {
|
|
18
|
-
const raw = args[index + 1];
|
|
19
|
-
if (!raw) throw new Error(`${argument} requires a value.`);
|
|
20
|
-
options[argument.slice(2)] = Number(raw);
|
|
21
|
-
index += 1;
|
|
22
|
-
continue;
|
|
23
|
-
}
|
|
24
|
-
if (argument === '--help' || argument === '-h') {
|
|
25
|
-
options.help = true;
|
|
26
|
-
continue;
|
|
27
|
-
}
|
|
28
|
-
throw new Error(`Unknown option: ${argument}`);
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
if (!Number.isInteger(options.count) || options.count < 2 || options.count > 3) {
|
|
32
|
-
throw new Error('--count must be 2 or 3.');
|
|
33
|
-
}
|
|
34
|
-
if (!Number.isInteger(options.duration) || options.duration < 1 || options.duration > 600) {
|
|
35
|
-
throw new Error('--duration must be between 1 and 600 seconds.');
|
|
36
|
-
}
|
|
37
|
-
return options;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
function runCloud(args) {
|
|
41
|
-
return new Promise((resolve, reject) => {
|
|
42
|
-
const child = spawn(process.env.RUNCLOUD_BIN || 'runcloud', args, {
|
|
43
|
-
stdio: ['ignore', 'pipe', 'pipe'],
|
|
44
|
-
});
|
|
45
|
-
let stdout = '';
|
|
46
|
-
let stderr = '';
|
|
47
|
-
child.stdout.on('data', (chunk) => { stdout += chunk; });
|
|
48
|
-
child.stderr.on('data', (chunk) => { stderr += chunk; });
|
|
49
|
-
child.on('error', reject);
|
|
50
|
-
child.on('close', (code) => {
|
|
51
|
-
if (code !== 0) {
|
|
52
|
-
reject(new Error(stderr.trim() || stdout.trim() || `runcloud exited with code ${code}`));
|
|
53
|
-
return;
|
|
54
|
-
}
|
|
55
|
-
try {
|
|
56
|
-
resolve(JSON.parse(stdout));
|
|
57
|
-
} catch {
|
|
58
|
-
reject(new Error(`runcloud returned invalid JSON: ${stdout.trim()}`));
|
|
59
|
-
}
|
|
60
|
-
});
|
|
61
|
-
});
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
export function browserCommand(url, platform = process.platform) {
|
|
65
|
-
return platform === 'darwin'
|
|
66
|
-
? ['open', [url]]
|
|
67
|
-
: platform === 'win32'
|
|
68
|
-
? ['cmd', ['/c', 'start', '', url]]
|
|
69
|
-
: ['xdg-open', [url]];
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
function openBrowser(url) {
|
|
73
|
-
const command = browserCommand(url);
|
|
74
|
-
const child = spawn(command[0], command[1], { detached: true, stdio: 'ignore' });
|
|
75
|
-
child.on('error', () => undefined);
|
|
76
|
-
child.unref();
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
function waitForDemo(seconds) {
|
|
80
|
-
return new Promise((resolve) => {
|
|
81
|
-
const finish = (reason) => {
|
|
82
|
-
clearTimeout(timer);
|
|
83
|
-
process.removeListener('SIGINT', interrupt);
|
|
84
|
-
process.removeListener('SIGTERM', interrupt);
|
|
85
|
-
resolve(reason);
|
|
86
|
-
};
|
|
87
|
-
const timer = setTimeout(() => finish('timer'), seconds * 1000);
|
|
88
|
-
const interrupt = () => finish('interrupt');
|
|
89
|
-
process.once('SIGINT', interrupt);
|
|
90
|
-
process.once('SIGTERM', interrupt);
|
|
91
|
-
});
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
async function releaseSessions(sessions) {
|
|
95
|
-
if (sessions.length === 0) return;
|
|
96
|
-
console.log('\nReleasing simulator sessions...');
|
|
97
|
-
const results = await Promise.allSettled(
|
|
98
|
-
sessions.map((session) => runCloud(['ios', 'delete', session.id, '--json'])),
|
|
99
|
-
);
|
|
100
|
-
results.forEach((result, index) => {
|
|
101
|
-
const session = sessions[index];
|
|
102
|
-
if (result.status === 'fulfilled') console.log(` released ${session.id}`);
|
|
103
|
-
else console.error(` failed to release ${session.id}: ${result.reason.message}`);
|
|
104
|
-
});
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
export async function runDemo(args = process.argv.slice(2)) {
|
|
108
|
-
const options = parseOptions(args);
|
|
109
|
-
if (options.help) {
|
|
110
|
-
console.log('Usage: npm run demo -- [--count 2|3] [--duration seconds] [--open]');
|
|
111
|
-
return;
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
console.log(`Starting ${options.count} iOS simulator sessions in parallel...`);
|
|
115
|
-
const starts = await Promise.allSettled(
|
|
116
|
-
Array.from({ length: options.count }, async (_, index) => {
|
|
117
|
-
const session = await runCloud([
|
|
118
|
-
'ios',
|
|
119
|
-
'create',
|
|
120
|
-
'--display-name',
|
|
121
|
-
`parallel-demo-${index + 1}`,
|
|
122
|
-
'--label',
|
|
123
|
-
'demo=parallel-simulators',
|
|
124
|
-
'--hard-timeout',
|
|
125
|
-
'10m',
|
|
126
|
-
'--json',
|
|
127
|
-
]);
|
|
128
|
-
try {
|
|
129
|
-
await runCloud([
|
|
130
|
-
'ios',
|
|
131
|
-
'open-url',
|
|
132
|
-
DEFAULT_TARGETS[index],
|
|
133
|
-
'--id',
|
|
134
|
-
session.id,
|
|
135
|
-
'--json',
|
|
136
|
-
]);
|
|
137
|
-
return { session, warning: null };
|
|
138
|
-
} catch (error) {
|
|
139
|
-
return {
|
|
140
|
-
session,
|
|
141
|
-
warning: error instanceof Error ? error.message : String(error),
|
|
142
|
-
};
|
|
143
|
-
}
|
|
144
|
-
}),
|
|
145
|
-
);
|
|
146
|
-
|
|
147
|
-
const sessions = starts
|
|
148
|
-
.filter((result) => result.status === 'fulfilled')
|
|
149
|
-
.map((result) => result.value.session);
|
|
150
|
-
const failures = starts
|
|
151
|
-
.map((result, index) => ({ result, index }))
|
|
152
|
-
.filter(({ result }) => result.status === 'rejected');
|
|
153
|
-
|
|
154
|
-
starts.forEach((result, index) => {
|
|
155
|
-
if (result.status === 'fulfilled' && result.value.warning) {
|
|
156
|
-
console.error(` simulator ${index + 1} started, but its page did not open: ${result.value.warning}`);
|
|
157
|
-
}
|
|
158
|
-
});
|
|
159
|
-
failures.forEach(({ result, index }) => {
|
|
160
|
-
console.error(` simulator ${index + 1} failed: ${result.reason.message}`);
|
|
161
|
-
});
|
|
162
|
-
if (sessions.length === 0) throw new Error('No simulator sessions could be started.');
|
|
163
|
-
|
|
164
|
-
console.log('\nLive sessions:');
|
|
165
|
-
sessions.forEach((session, index) => {
|
|
166
|
-
console.log(` ${index + 1}. ${session.url || session.baseUrl || session.id}`);
|
|
167
|
-
if (options.open && session.url) openBrowser(session.url);
|
|
168
|
-
});
|
|
169
|
-
|
|
170
|
-
try {
|
|
171
|
-
console.log(`\nKeeping the demo live for ${options.duration} seconds. Press Ctrl+C to finish early.`);
|
|
172
|
-
await waitForDemo(options.duration);
|
|
173
|
-
} finally {
|
|
174
|
-
await releaseSessions(sessions);
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
if (failures.length > 0) process.exitCode = 1;
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
if (import.meta.url === new URL(process.argv[1], 'file:').href) {
|
|
181
|
-
runDemo().catch((error) => {
|
|
182
|
-
console.error(error instanceof Error ? error.message : String(error));
|
|
183
|
-
process.exitCode = 1;
|
|
184
|
-
});
|
|
185
|
-
}
|
|
@@ -1,78 +0,0 @@
|
|
|
1
|
-
import assert from 'node:assert/strict';
|
|
2
|
-
import { chmodSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
|
3
|
-
import test from 'node:test';
|
|
4
|
-
import { tmpdir } from 'node:os';
|
|
5
|
-
import { dirname, join } from 'node:path';
|
|
6
|
-
import { spawnSync } from 'node:child_process';
|
|
7
|
-
import { fileURLToPath } from 'node:url';
|
|
8
|
-
import { browserCommand, parseOptions } from './demo.mjs';
|
|
9
|
-
|
|
10
|
-
test('defaults to three two-minute sessions', () => {
|
|
11
|
-
assert.deepEqual(parseOptions([]), { count: 3, duration: 120, open: false });
|
|
12
|
-
});
|
|
13
|
-
|
|
14
|
-
test('accepts a two-session short demo', () => {
|
|
15
|
-
assert.deepEqual(parseOptions(['--count', '2', '--duration', '30', '--open']), {
|
|
16
|
-
count: 2,
|
|
17
|
-
duration: 30,
|
|
18
|
-
open: true,
|
|
19
|
-
});
|
|
20
|
-
});
|
|
21
|
-
|
|
22
|
-
test('keeps the demo within the supported parallel range', () => {
|
|
23
|
-
assert.throws(() => parseOptions(['--count', '20']), /must be 2 or 3/);
|
|
24
|
-
});
|
|
25
|
-
|
|
26
|
-
test('opens session URLs on Windows', () => {
|
|
27
|
-
assert.deepEqual(browserCommand('https://sim.example/session', 'win32'), [
|
|
28
|
-
'cmd',
|
|
29
|
-
['/c', 'start', '', 'https://sim.example/session'],
|
|
30
|
-
]);
|
|
31
|
-
});
|
|
32
|
-
|
|
33
|
-
test('starts, opens, and releases every simulator', () => {
|
|
34
|
-
const root = mkdtempSync(join(tmpdir(), 'run-cloud-parallel-demo-'));
|
|
35
|
-
const fakeCli = join(root, 'runcloud');
|
|
36
|
-
const operationLog = join(root, 'operations.jsonl');
|
|
37
|
-
writeFileSync(
|
|
38
|
-
fakeCli,
|
|
39
|
-
`#!/usr/bin/env node
|
|
40
|
-
import { appendFileSync } from 'node:fs';
|
|
41
|
-
const args = process.argv.slice(2);
|
|
42
|
-
appendFileSync(process.env.RUN_CLOUD_DEMO_LOG, JSON.stringify(args) + '\\n');
|
|
43
|
-
if (args[0] === 'ios' && args[1] === 'create') {
|
|
44
|
-
const name = args[args.indexOf('--display-name') + 1];
|
|
45
|
-
process.stdout.write(JSON.stringify({ id: name, url: 'https://sim.example/' + name }));
|
|
46
|
-
} else {
|
|
47
|
-
process.stdout.write(JSON.stringify({ ok: true }));
|
|
48
|
-
}
|
|
49
|
-
`,
|
|
50
|
-
);
|
|
51
|
-
chmodSync(fakeCli, 0o755);
|
|
52
|
-
|
|
53
|
-
try {
|
|
54
|
-
const result = spawnSync(
|
|
55
|
-
process.execPath,
|
|
56
|
-
['demo.mjs', '--count', '2', '--duration', '1'],
|
|
57
|
-
{
|
|
58
|
-
cwd: dirname(fileURLToPath(import.meta.url)),
|
|
59
|
-
encoding: 'utf8',
|
|
60
|
-
env: {
|
|
61
|
-
...process.env,
|
|
62
|
-
RUNCLOUD_BIN: fakeCli,
|
|
63
|
-
RUN_CLOUD_DEMO_LOG: operationLog,
|
|
64
|
-
},
|
|
65
|
-
},
|
|
66
|
-
);
|
|
67
|
-
assert.equal(result.status, 0, result.stderr);
|
|
68
|
-
const operations = readFileSync(operationLog, 'utf8')
|
|
69
|
-
.trim()
|
|
70
|
-
.split('\n')
|
|
71
|
-
.map((line) => JSON.parse(line));
|
|
72
|
-
assert.equal(operations.filter((args) => args[1] === 'create').length, 2);
|
|
73
|
-
assert.equal(operations.filter((args) => args[1] === 'open-url').length, 2);
|
|
74
|
-
assert.equal(operations.filter((args) => args[1] === 'delete').length, 2);
|
|
75
|
-
} finally {
|
|
76
|
-
rmSync(root, { recursive: true, force: true });
|
|
77
|
-
}
|
|
78
|
-
});
|