runcloud 0.1.8 → 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/dist/api.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { loginCommandName } from './config.js';
1
2
  export class ApiError extends Error {
2
3
  status;
3
4
  detail;
@@ -165,7 +166,7 @@ function detailSuffix(err) {
165
166
  function headline(err) {
166
167
  if (err instanceof ApiError) {
167
168
  if (err.status === 401) {
168
- const command = (process.argv[1] ?? '').includes('run-cloud') ? 'runcloud login' : 'newly login';
169
+ const command = loginCommandName();
169
170
  return `Session expired or invalid — run \`${command}\`.`;
170
171
  }
171
172
  if (err.status === 403)
@@ -1,6 +1,6 @@
1
1
  import { createInterface } from 'node:readline/promises';
2
2
  import { ApiClient, friendlyApiError } from '../api.js';
3
- import { runEnvironmentDatabaseShell } from '../environmentShell.js';
3
+ import { runEnvironmentDatabaseShell } from '../shellSession.js';
4
4
  import { clearEnvironmentSelection, readEnvironmentSelection, requireCredentials, requireEnvironmentSelection, resolveProject, writeEnvironmentSelection, } from '../config.js';
5
5
  const POLL_INTERVAL_MS = 2_000;
6
6
  const POLL_TIMEOUT_MS = 10 * 60 * 1_000;
@@ -1,9 +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
5
  import { ApiClient, ApiError } from '../api.js';
5
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';
6
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
+ }
7
37
  export async function startCallbackServer(state, appOrigin, timeoutMs) {
8
38
  let resolvePayload;
9
39
  let rejectPayload;
@@ -95,12 +125,54 @@ function openBrowser(url) {
95
125
  });
96
126
  child.unref();
97
127
  }
98
- export function cliAuthUrl(appUrl, port, state) {
128
+ export function cliAuthUrl(appUrl, port, state, provider) {
99
129
  const url = new URL('/cli-auth', appUrl);
100
130
  url.searchParams.set('port', String(port));
101
131
  url.searchParams.set('state', state);
132
+ if (provider)
133
+ url.searchParams.set('provider', provider);
102
134
  return url.toString();
103
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
+ }
104
176
  export function parseManualCredential(blob) {
105
177
  let parsed;
106
178
  try {
@@ -136,12 +208,18 @@ export function registerLogin(program) {
136
208
  : process.env.NEWLY_APP_URL ?? 'https://newly.app';
137
209
  program
138
210
  .command('login')
139
- .description(`Log in with your ${isRunCloud ? 'run.cloud' : 'Newly'} account via the browser`)
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')
140
214
  .option('--app-url <url>', 'product web app URL', defaultAppUrl)
141
215
  .option('--manual', 'paste the credential from the browser instead of using the local callback', false)
142
216
  .option('--api-url <url>', 'explicit API URL (skips browser; use with --token)')
143
217
  .option('--token <token>', 'explicit credential token (skips browser; use with --api-url)')
144
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)
145
223
  .action(async (opts) => {
146
224
  try {
147
225
  const hasApiUrl = typeof opts.apiUrl === 'string' && opts.apiUrl.length > 0;
@@ -154,7 +232,7 @@ export function registerLogin(program) {
154
232
  }
155
233
  if (hasToken) {
156
234
  const apiUrl = isRunCloud
157
- ? opts.apiUrl ?? process.env.RUN_CLOUD_API_URL ?? 'https://api.newly.app'
235
+ ? opts.apiUrl ?? process.env.RUN_CLOUD_API_URL ?? 'https://api.run.cloud'
158
236
  : opts.apiUrl;
159
237
  try {
160
238
  new URL(apiUrl);
@@ -195,10 +273,47 @@ export function registerLogin(program) {
195
273
  console.log('Logged in.');
196
274
  return;
197
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
+ }
198
313
  const state = crypto.randomBytes(16).toString('hex');
199
314
  const appOrigin = new URL(opts.appUrl).origin;
200
315
  const { port, waitForToken, close } = await startCallbackServer(state, appOrigin, LOGIN_TIMEOUT_MS);
201
- const loginUrl = cliAuthUrl(opts.appUrl, port, state);
316
+ const loginUrl = cliAuthUrl(opts.appUrl, port, state, socialProvider);
202
317
  console.log(`Opening your browser to authorize the CLI…\n ${loginUrl}\n`);
203
318
  console.log("If the browser doesn't open, paste the URL above into it manually.");
204
319
  openBrowser(loginUrl);
@@ -219,6 +334,11 @@ export function registerLogin(program) {
219
334
  }
220
335
  }
221
336
  catch (e) {
337
+ if (e instanceof PromptAbortedError) {
338
+ console.log('Cancelled.');
339
+ process.exitCode = 130;
340
+ return;
341
+ }
222
342
  console.error(e instanceof Error ? e.message : String(e));
223
343
  process.exitCode = 1;
224
344
  }
@@ -18,7 +18,7 @@ Use this skill when a user asks an agent to create, inspect, smoke test, debug,
18
18
 
19
19
  ## Requirements
20
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.newly.app\`.
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
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
23
  - The TypeScript SDK requires Node.js 20 or newer.
24
24
  - The account must have run.cloud access, available capacity, and organization credit.
@@ -76,7 +76,7 @@ Or authenticate non-interactively with both required environment variables:
76
76
 
77
77
  \`\`\`bash
78
78
  export RUN_CLOUD_API_KEY="rc_live_..."
79
- export RUN_CLOUD_API_URL="https://api.newly.app"
79
+ export RUN_CLOUD_API_URL="https://api.run.cloud"
80
80
  \`\`\`
81
81
 
82
82
  Then inspect the account:
@@ -4,6 +4,7 @@ import { writeFileSync } from 'node:fs';
4
4
  import { resolve } from 'node:path';
5
5
  import { ApiClient, friendlyApiError } from '../api.js';
6
6
  import { requireCredentials } from '../config.js';
7
+ import { runSandboxShell } from '../shellSession.js';
7
8
  import { registerBoxAccessCommands } from './boxAccess.js';
8
9
  const MAX_SANDBOX_DISK_GIB = 2048;
9
10
  const DEFAULT_BOX_CPU_CORES = 2;
@@ -151,6 +152,18 @@ export function registerSandbox(program) {
151
152
  }
152
153
  process.exitCode = r.exit_code ?? 0;
153
154
  }));
155
+ sandbox
156
+ .command('shell')
157
+ .description('Open an interactive shell in a running sandbox')
158
+ .argument('<id>', 'sandbox id')
159
+ .action((id) => run(async () => {
160
+ const credentials = requireCredentials();
161
+ process.exitCode = await runSandboxShell({
162
+ apiUrl: credentials.apiUrl,
163
+ token: credentials.token,
164
+ sandboxId: id,
165
+ });
166
+ }));
154
167
  withOutput(sandbox
155
168
  .command('logs')
156
169
  .description('Show recent sandbox logs (one-shot journalctl tail; streaming is a follow-up)')
package/dist/config.js CHANGED
@@ -1,17 +1,13 @@
1
1
  import { chmodSync, existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
2
2
  import { homedir } from 'node:os';
3
3
  import { basename, join } from 'node:path';
4
- function isRunCloud() {
5
- const invoked = basename(process.argv[1] ?? '');
6
- return invoked === 'runcloud' || invoked === 'run-cloud.js';
4
+ export function isRunCloud() {
5
+ const names = [process.argv[1], process.execPath].map((path) => basename(path ?? ''));
6
+ return names.some((name) => name === 'runcloud' || name === 'run-cloud' || name === 'run-cloud.js');
7
7
  }
8
8
  function credentialsHome() {
9
- if (process.env.RUN_CLOUD_HOME)
10
- return process.env.RUN_CLOUD_HOME;
11
- if (process.env.NEWLY_HOME)
12
- return process.env.NEWLY_HOME;
13
9
  if (isRunCloud())
14
- return join(homedir(), '.run-cloud');
10
+ return process.env.RUN_CLOUD_HOME ?? join(homedir(), '.run-cloud');
15
11
  return process.env.NEWLY_HOME ?? join(homedir(), '.newly');
16
12
  }
17
13
  function credentialsPath() {
@@ -22,7 +18,7 @@ export function credentialsDisplayPath() {
22
18
  const home = homedir();
23
19
  return path.startsWith(home) ? `~${path.slice(home.length)}` : path;
24
20
  }
25
- function loginCommandName() {
21
+ export function loginCommandName() {
26
22
  return isRunCloud() ? 'runcloud login' : 'newly login';
27
23
  }
28
24
  export function normalizeApiUrl(url) {
@@ -44,7 +40,7 @@ export function loadCredentials() {
44
40
  if (envToken?.trim() && (isRunCloud() || envApiUrl?.trim())) {
45
41
  return {
46
42
  token: envToken.trim(),
47
- apiUrl: normalizeApiUrl(envApiUrl?.trim() || 'https://api.newly.app'),
43
+ apiUrl: normalizeApiUrl(envApiUrl?.trim() || 'https://api.run.cloud'),
48
44
  appUrl: isRunCloud()
49
45
  ? process.env.RUN_CLOUD_APP_URL ?? 'https://run.cloud'
50
46
  : process.env.NEWLY_APP_URL ?? 'https://newly.app',
package/dist/prompt.js ADDED
@@ -0,0 +1,150 @@
1
+ import { openSync, closeSync } from 'node:fs';
2
+ import { emitKeypressEvents } from 'node:readline';
3
+ import { ReadStream } from 'node:tty';
4
+ function defaultIO() {
5
+ return { input: process.stdin, output: process.stdout };
6
+ }
7
+ export function openControllingTerminal() {
8
+ let fd;
9
+ try {
10
+ fd = openSync('/dev/tty', 'r+');
11
+ }
12
+ catch {
13
+ return null;
14
+ }
15
+ try {
16
+ const input = new ReadStream(fd);
17
+ if (!input.isTTY)
18
+ throw new Error('/dev/tty is not a terminal');
19
+ return {
20
+ io: { input, output: process.stdout },
21
+ close: () => {
22
+ try {
23
+ input.destroy();
24
+ closeSync(fd);
25
+ }
26
+ catch {
27
+ }
28
+ },
29
+ };
30
+ }
31
+ catch {
32
+ try {
33
+ closeSync(fd);
34
+ }
35
+ catch {
36
+ }
37
+ return null;
38
+ }
39
+ }
40
+ export function acquirePromptIO() {
41
+ const stdinIO = defaultIO();
42
+ if (isInteractive(stdinIO))
43
+ return { io: stdinIO, close: () => { } };
44
+ return openControllingTerminal();
45
+ }
46
+ export function isInteractive(io = defaultIO()) {
47
+ return Boolean(io.input.isTTY && typeof io.input.setRawMode === 'function');
48
+ }
49
+ export function canPrompt() {
50
+ if (isInteractive())
51
+ return true;
52
+ const terminal = openControllingTerminal();
53
+ if (!terminal)
54
+ return false;
55
+ terminal.close();
56
+ return true;
57
+ }
58
+ export class PromptAbortedError extends Error {
59
+ constructor() {
60
+ super('Cancelled.');
61
+ this.name = 'PromptAbortedError';
62
+ }
63
+ }
64
+ function render(io, question, choices, selected, first) {
65
+ if (!first)
66
+ io.output.write(`\x1b[${choices.length + 1}A\x1b[J`);
67
+ else
68
+ io.output.write(`\n${question}\n\n`);
69
+ if (!first)
70
+ io.output.write('\n');
71
+ const width = Math.max(...choices.map((choice) => choice.label.length));
72
+ for (const [index, choice] of choices.entries()) {
73
+ const active = index === selected;
74
+ const marker = active ? '❯' : ' ';
75
+ const label = active ? `\x1b[36m${choice.label}\x1b[0m` : choice.label;
76
+ const padding = ' '.repeat(width - choice.label.length + 4);
77
+ const hint = choice.hint ? `\x1b[90m${padding}${choice.hint}\x1b[0m` : '';
78
+ io.output.write(` ${marker} ${label}${hint}\n`);
79
+ }
80
+ }
81
+ export async function select(question, choices, io = defaultIO()) {
82
+ if (choices.length === 0)
83
+ throw new Error('select() needs at least one choice');
84
+ if (choices.length === 1)
85
+ return choices[0].value;
86
+ return new Promise((resolve, reject) => {
87
+ let selected = 0;
88
+ let first = true;
89
+ emitKeypressEvents(io.input);
90
+ io.input.setRawMode?.(true);
91
+ io.input.resume();
92
+ render(io, question, choices, selected, first);
93
+ first = false;
94
+ const finish = (action) => {
95
+ io.input.setRawMode?.(false);
96
+ io.input.pause();
97
+ io.input.removeListener('keypress', onKeypress);
98
+ io.output.write('\n');
99
+ action();
100
+ };
101
+ function onKeypress(chunk, key) {
102
+ const name = key?.name ?? '';
103
+ if ((key?.ctrl && name === 'c') || name === 'escape') {
104
+ finish(() => reject(new PromptAbortedError()));
105
+ return;
106
+ }
107
+ if (name === 'return' || name === 'enter') {
108
+ finish(() => resolve(choices[selected].value));
109
+ return;
110
+ }
111
+ if (name === 'up' || name === 'k') {
112
+ selected = (selected - 1 + choices.length) % choices.length;
113
+ }
114
+ else if (name === 'down' || name === 'j' || name === 'tab') {
115
+ selected = (selected + 1) % choices.length;
116
+ }
117
+ else if (/^[1-9]$/.test(chunk ?? '')) {
118
+ const index = Number(chunk) - 1;
119
+ if (index < choices.length)
120
+ selected = index;
121
+ }
122
+ else {
123
+ return;
124
+ }
125
+ render(io, question, choices, selected, first);
126
+ }
127
+ io.input.on('keypress', onKeypress);
128
+ });
129
+ }
130
+ export async function ask(question, options = {}) {
131
+ const { validate, attempts = 3, io = defaultIO() } = options;
132
+ const { createInterface } = await import('node:readline/promises');
133
+ const rl = createInterface({ input: io.input, output: io.output });
134
+ try {
135
+ for (let attempt = 1; attempt <= attempts; attempt += 1) {
136
+ const answer = (await rl.question(question)).trim();
137
+ const problem = validate?.(answer) ?? null;
138
+ if (!problem)
139
+ return answer;
140
+ io.output.write(` ${problem}\n`);
141
+ if (attempt === attempts) {
142
+ throw new Error(`${problem} Giving up after ${attempts} attempts.`);
143
+ }
144
+ }
145
+ throw new Error('unreachable');
146
+ }
147
+ finally {
148
+ rl.close();
149
+ }
150
+ }
package/dist/run-cloud.js CHANGED
@@ -1,13 +1,12 @@
1
1
  #!/usr/bin/env node
2
- import { readFileSync } from 'node:fs';
3
2
  import { Command } from 'commander';
4
3
  import { registerLogin } from './commands/login.js';
5
4
  import { registerRunCloud } from './commands/run-cloud.js';
6
5
  import { registerSandbox } from './commands/sandbox.js';
7
- const { version } = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8'));
6
+ import { CLI_VERSION } from './version.js';
8
7
  const program = new Command('runcloud')
9
8
  .description('Create and control remote mobile simulators and microVM sandboxes')
10
- .version(version);
9
+ .version(CLI_VERSION);
11
10
  registerLogin(program);
12
11
  registerRunCloud(program);
13
12
  registerSandbox(program);
@@ -0,0 +1,79 @@
1
+ const SESSION_TOKEN_HEADER = 'set-auth-token';
2
+ export class AuthError extends Error {
3
+ status;
4
+ constructor(message, status) {
5
+ super(message);
6
+ this.status = status;
7
+ this.name = 'AuthError';
8
+ }
9
+ }
10
+ function apiBase(apiUrl) {
11
+ return apiUrl.replace(/\/+$/, '');
12
+ }
13
+ async function readError(response) {
14
+ const body = await response.text();
15
+ try {
16
+ const parsed = JSON.parse(body);
17
+ return parsed.error ?? parsed.message ?? body.slice(0, 200);
18
+ }
19
+ catch {
20
+ return body.slice(0, 200) || response.statusText;
21
+ }
22
+ }
23
+ async function post(apiUrl, path, body, token) {
24
+ const response = await fetch(`${apiBase(apiUrl)}${path}`, {
25
+ method: 'POST',
26
+ headers: {
27
+ 'content-type': 'application/json',
28
+ ...(token ? { authorization: `Bearer ${token}` } : {}),
29
+ },
30
+ body: JSON.stringify(body),
31
+ });
32
+ if (!response.ok)
33
+ throw new AuthError(await readError(response), response.status);
34
+ const sessionToken = response.headers.get(SESSION_TOKEN_HEADER);
35
+ const text = await response.text();
36
+ const data = (text ? JSON.parse(text) : {});
37
+ return { data, sessionToken: sessionToken?.trim() || null };
38
+ }
39
+ export async function fetchProviders(apiUrl) {
40
+ const response = await fetch(`${apiBase(apiUrl)}/auth/web/providers`);
41
+ if (!response.ok)
42
+ throw new AuthError(await readError(response), response.status);
43
+ const payload = (await response.json());
44
+ return {
45
+ emailOtp: payload.emailOtp === true,
46
+ google: payload.google === true,
47
+ github: payload.github === true,
48
+ apple: payload.apple === true,
49
+ };
50
+ }
51
+ export async function sendEmailCode(apiUrl, email) {
52
+ await post(apiUrl, '/api/auth/email-otp/send-verification-otp', { email, type: 'sign-in' });
53
+ }
54
+ export async function verifyEmailCode(apiUrl, email, otp) {
55
+ const { data, sessionToken } = await post(apiUrl, '/api/auth/sign-in/email-otp', { email, otp, name: email.split('@')[0] || 'Run Cloud user' });
56
+ if (!sessionToken) {
57
+ throw new AuthError(`${apiUrl} accepted the code but did not return a session token.`, 502);
58
+ }
59
+ return {
60
+ user: { id: data.user?.id ?? '', email: data.user?.email ?? email },
61
+ sessionToken,
62
+ };
63
+ }
64
+ export async function createApiKey(apiUrl, sessionToken, name) {
65
+ const { data } = await post(apiUrl, '/run-cloud/api-keys', { name }, sessionToken);
66
+ const key = data.apiKey?.key;
67
+ if (!key)
68
+ throw new AuthError(`${apiUrl} did not return an API key.`, 502);
69
+ return key;
70
+ }
71
+ export function isValidEmail(value) {
72
+ return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value.trim());
73
+ }
74
+ export function normalizeOtp(value) {
75
+ return value.replace(/[\s-]/g, '');
76
+ }
77
+ export function isValidOtp(value) {
78
+ return /^\d{6}$/.test(normalizeOtp(value));
79
+ }
@@ -1,9 +1,9 @@
1
1
  import WebSocket from 'ws';
2
- export async function runEnvironmentDatabaseShell(input) {
2
+ export async function runShellSession(input) {
3
3
  const stdin = input.stdin ?? process.stdin;
4
4
  const stdout = input.stdout ?? process.stdout;
5
5
  const stderr = input.stderr ?? process.stderr;
6
- const url = new URL(`/environment-databases/${encodeURIComponent(input.databaseId)}/shell`, input.apiUrl);
6
+ const url = new URL(input.path, input.apiUrl);
7
7
  url.protocol = url.protocol === 'https:' ? 'wss:' : 'ws:';
8
8
  const createWebSocket = input.createWebSocket ?? ((address, options) => new WebSocket(address, options));
9
9
  const socket = createWebSocket(url.toString(), {
@@ -60,7 +60,7 @@ export async function runEnvironmentDatabaseShell(input) {
60
60
  socket.once('close', (code, reason) => {
61
61
  restore();
62
62
  if (code >= 4400) {
63
- reject(new Error(reason.toString() || `Database shell rejected (${code})`));
63
+ reject(new Error(reason.toString() || `${input.label} rejected (${code})`));
64
64
  }
65
65
  else {
66
66
  resolve(exitCode);
@@ -68,3 +68,17 @@ export async function runEnvironmentDatabaseShell(input) {
68
68
  });
69
69
  });
70
70
  }
71
+ export function runEnvironmentDatabaseShell(input) {
72
+ return runShellSession({
73
+ ...input,
74
+ path: `/environment-databases/${encodeURIComponent(input.databaseId)}/shell`,
75
+ label: 'Database shell',
76
+ });
77
+ }
78
+ export function runSandboxShell(input) {
79
+ return runShellSession({
80
+ ...input,
81
+ path: `/run-cloud/sandboxes/${encodeURIComponent(input.sandboxId)}/exec/stream`,
82
+ label: 'Sandbox shell',
83
+ });
84
+ }
@@ -0,0 +1 @@
1
+ export const CLI_VERSION = '0.1.9';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "runcloud",
3
- "version": "0.1.8",
3
+ "version": "0.1.9",
4
4
  "description": "Create and control run.cloud remote mobile simulators and cloud sandboxes",
5
5
  "license": "UNLICENSED",
6
6
  "type": "module",