runcloud 0.1.8 → 0.1.10

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 CHANGED
@@ -16,12 +16,15 @@ Installs the `runcloud` binary. Requires **Node ≥ 20**.
16
16
  ## Log in
17
17
 
18
18
  ```bash
19
- runcloud login # authenticate in the browser; stores a token in ~/.run-cloud/credentials
20
- runcloud login --manual # paste the credential instead of using the browser callback
21
- runcloud logout # delete the stored credential
19
+ runcloud login # sign in with email, Google, or GitHub; stores a token in ~/.run-cloud/credentials
20
+ runcloud logout # delete the stored credential
22
21
  runcloud account # show your entitlement and metered balance
23
22
  ```
24
23
 
24
+ Email signs you in without leaving the terminal. Google and GitHub open a browser and
25
+ show a short code to confirm, so login works over SSH and on headless machines too —
26
+ pick with `--email`, `--google`, or `--github` to skip the prompt.
27
+
25
28
  The token expires — re-run `runcloud login` when a command reports you're logged out.
26
29
 
27
30
  ## Remote simulators
@@ -83,6 +86,31 @@ runcloud sandbox key "$SANDBOX_ID" "ctrl+l"
83
86
  Treat a signed desktop URL as a secret. It grants temporary access to that
84
87
  desktop without asking the browser to send your CLI credential.
85
88
 
89
+ ### Persistence and a public hostname
90
+
91
+ A sandbox pauses when idle by default. Turn persistence on with `--persistent`,
92
+ and publish a guest port at a stable `<name>-box.run.cloud` hostname with
93
+ `--expose`:
94
+
95
+ ```bash
96
+ runcloud sandbox create --name my-project --persistent # never pauses
97
+ runcloud sandbox create --name my-project --expose 3000 # persistent + https://my-project-box.run.cloud
98
+ runcloud sandbox expose <id> --port 8080 # expose later, or move the published port
99
+ runcloud sandbox get <id> # inspect, including the hostname
100
+ ```
101
+
102
+ Exposed sandboxes support SSH, file transfer, VS Code, and custom domains:
103
+
104
+ ```bash
105
+ runcloud sandbox ssh my-project # shell over authenticated SSH (no public port 22)
106
+ runcloud sandbox cp my-project ./f :/workspace/f # scp; prefix the remote path with :
107
+ runcloud sandbox code my-project # VS Code Remote SSH
108
+ runcloud sandbox domain add my-project app.example.com
109
+ ```
110
+
111
+ `runcloud box …` from earlier releases keeps working as a deprecated alias for
112
+ these commands.
113
+
86
114
  ### Snapshot warm-start
87
115
 
88
116
  Freeze a "repo + deps ready" sandbox, then fork a new one from it in a fraction of a second instead of re-cloning and re-installing:
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)
@@ -5,6 +5,9 @@ import { join } from 'node:path';
5
5
  import WebSocket, { createWebSocketStream } from 'ws';
6
6
  import { ApiClient, friendlyApiError } from '../api.js';
7
7
  import { requireCredentials } from '../config.js';
8
+ export function noteDeprecated(oldCommand, replacement) {
9
+ process.stderr.write(`runcloud: '${oldCommand}' is deprecated — use '${replacement}'\n`);
10
+ }
8
11
  function api() {
9
12
  const credentials = requireCredentials();
10
13
  return new ApiClient(credentials.apiUrl, credentials.token);
@@ -13,10 +16,12 @@ async function resolveBox(client, reference) {
13
16
  const response = await client.get('/run-cloud/boxes');
14
17
  const boxes = (response?.items ?? []);
15
18
  const box = boxes.find((item) => item.id === reference ||
19
+ item.sandboxId === reference ||
16
20
  item.name === reference ||
17
21
  item.hostname === reference);
18
- if (!box)
19
- throw new Error(`Box not found: ${reference}`);
22
+ if (!box) {
23
+ throw new Error(`No exposed sandbox matches ${reference} — publish a hostname first: runcloud sandbox expose <id>`);
24
+ }
20
25
  return box;
21
26
  }
22
27
  function sshHome() {
@@ -75,7 +80,7 @@ function writeSshConfig(box, privateKey) {
75
80
  ' IdentitiesOnly yes',
76
81
  ' StrictHostKeyChecking accept-new',
77
82
  ` UserKnownHostsFile "${join(sshHome(), 'known_hosts')}"`,
78
- ` ProxyCommand runcloud box proxy ${box.id}`,
83
+ ` ProxyCommand runcloud sandbox proxy ${box.id}`,
79
84
  end,
80
85
  ].join('\n');
81
86
  const pattern = new RegExp(`${start.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}[\\s\\S]*?${end.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\n?`, 'g');
@@ -114,19 +119,27 @@ function openUrl(url) {
114
119
  return exec('cmd', ['/c', 'start', '', url]);
115
120
  return exec('xdg-open', [url]);
116
121
  }
117
- export function registerBoxAccessCommands(box) {
118
- box
122
+ const REFERENCE_HELP = 'sandbox id, box id, name, or hostname';
123
+ export function registerAccessCommands(parent, opts = {}) {
124
+ const note = (verb) => {
125
+ if (opts.deprecated)
126
+ noteDeprecated(`box ${verb}`, `sandbox ${verb}`);
127
+ };
128
+ parent
119
129
  .command('setup-ssh')
120
- .description('Install a dedicated key and write this box to ~/.ssh/config')
121
- .argument('<box>', 'box id, name, or hostname')
122
- .action((reference) => action(async () => {
123
- const configured = await setupSsh(reference);
124
- console.log(`SSH configured: ssh ${configured.alias}`);
125
- }));
126
- box
130
+ .description('Install a dedicated key and write this host to ~/.ssh/config')
131
+ .argument('<sandbox>', REFERENCE_HELP)
132
+ .action((reference) => {
133
+ note('setup-ssh');
134
+ return action(async () => {
135
+ const configured = await setupSsh(reference);
136
+ console.log(`SSH configured: ssh ${configured.alias}`);
137
+ });
138
+ });
139
+ parent
127
140
  .command('proxy', { hidden: true })
128
141
  .description('Authenticated WebSocket transport used by SSH ProxyCommand')
129
- .argument('<box>', 'box id, name, or hostname')
142
+ .argument('<sandbox>', REFERENCE_HELP)
130
143
  .action((reference) => action(async () => {
131
144
  const client = api();
132
145
  const resolved = await resolveBox(client, reference);
@@ -145,102 +158,127 @@ export function registerBoxAccessCommands(box) {
145
158
  socket.once('error', reject);
146
159
  });
147
160
  }));
148
- box
161
+ parent
149
162
  .command('ssh')
150
- .description('Open a shell in a box')
151
- .argument('<box>', 'box id, name, or hostname')
163
+ .description('Open a shell in an exposed sandbox')
164
+ .argument('<sandbox>', REFERENCE_HELP)
152
165
  .argument('[command...]', 'optional remote command')
153
- .action((reference, command) => action(async () => {
154
- const configured = await setupSsh(reference);
155
- const code = await exec('ssh', [configured.alias, ...command]);
156
- if (code !== 0)
157
- process.exitCode = code;
158
- }));
159
- box
166
+ .action((reference, command) => {
167
+ note('ssh');
168
+ return action(async () => {
169
+ const configured = await setupSsh(reference);
170
+ const code = await exec('ssh', [configured.alias, ...command]);
171
+ if (code !== 0)
172
+ process.exitCode = code;
173
+ });
174
+ });
175
+ parent
160
176
  .command('cp')
161
177
  .description('Copy files with scp; prefix the remote path with a colon')
162
- .argument('<box>', 'box id, name, or hostname')
178
+ .argument('<sandbox>', REFERENCE_HELP)
163
179
  .argument('<source>', 'local path or :/remote/path')
164
180
  .argument('<destination>', 'local path or :/remote/path')
165
181
  .option('-r, --recursive', 'copy directories recursively', false)
166
- .action((reference, source, destination, opts) => action(async () => {
167
- if (source.startsWith(':') === destination.startsWith(':')) {
168
- throw new Error('Exactly one path must be remote (prefix it with :)');
169
- }
170
- const configured = await setupSsh(reference);
171
- const remote = (path) => path.startsWith(':') ? `${configured.alias}:${path.slice(1)}` : path;
172
- const args = [
173
- ...(opts.recursive ? ['-r'] : []),
174
- '--',
175
- remote(source),
176
- remote(destination),
177
- ];
178
- const code = await exec('scp', args);
179
- if (code !== 0)
180
- process.exitCode = code;
181
- }));
182
- box
182
+ .action((reference, source, destination, cmdOpts) => {
183
+ note('cp');
184
+ return action(async () => {
185
+ if (source.startsWith(':') === destination.startsWith(':')) {
186
+ throw new Error('Exactly one path must be remote (prefix it with :)');
187
+ }
188
+ const configured = await setupSsh(reference);
189
+ const remote = (path) => path.startsWith(':') ? `${configured.alias}:${path.slice(1)}` : path;
190
+ const args = [
191
+ ...(cmdOpts.recursive ? ['-r'] : []),
192
+ '--',
193
+ remote(source),
194
+ remote(destination),
195
+ ];
196
+ const code = await exec('scp', args);
197
+ if (code !== 0)
198
+ process.exitCode = code;
199
+ });
200
+ });
201
+ parent
183
202
  .command('code')
184
- .description('Open a box in VS Code Remote SSH')
185
- .argument('<box>', 'box id, name, or hostname')
203
+ .description('Open an exposed sandbox in VS Code Remote SSH')
204
+ .argument('<sandbox>', REFERENCE_HELP)
186
205
  .argument('[path]', 'remote folder', '/workspace')
187
206
  .option('--no-open', 'print the VS Code URL without opening it')
188
- .action((reference, path, opts) => action(async () => {
189
- const configured = await setupSsh(reference);
190
- const remotePath = path.startsWith('/') ? path : `/${path}`;
191
- const url = `vscode://vscode-remote/ssh-remote+${encodeURIComponent(configured.alias)}` +
192
- remotePath.split('/').map(encodeURIComponent).join('/');
193
- if (opts.open === false)
194
- console.log(url);
195
- else
196
- await openUrl(url);
197
- }));
198
- const domain = box.command('domain').description('Manage custom domains');
207
+ .action((reference, path, cmdOpts) => {
208
+ note('code');
209
+ return action(async () => {
210
+ const configured = await setupSsh(reference);
211
+ const remotePath = path.startsWith('/') ? path : `/${path}`;
212
+ const url = `vscode://vscode-remote/ssh-remote+${encodeURIComponent(configured.alias)}` +
213
+ remotePath.split('/').map(encodeURIComponent).join('/');
214
+ if (cmdOpts.open === false)
215
+ console.log(url);
216
+ else
217
+ await openUrl(url);
218
+ });
219
+ });
220
+ const domain = parent.command('domain').description('Manage custom domains');
221
+ const domainNote = (verb) => {
222
+ if (opts.deprecated)
223
+ noteDeprecated(`box domain ${verb}`, `sandbox domain ${verb}`);
224
+ };
199
225
  domain
200
226
  .command('add')
201
- .argument('<box>', 'box id, name, or hostname')
227
+ .argument('<sandbox>', REFERENCE_HELP)
202
228
  .argument('<hostname>', 'custom hostname')
203
229
  .option('--json', 'output JSON', false)
204
- .action((reference, hostname, opts) => action(async () => {
205
- const client = api();
206
- const resolved = await resolveBox(client, reference);
207
- output(await client.post(`/run-cloud/boxes/${encodeURIComponent(resolved.id)}/domains`, {
208
- hostname,
209
- }), opts);
210
- }));
230
+ .action((reference, hostname, cmdOpts) => {
231
+ domainNote('add');
232
+ return action(async () => {
233
+ const client = api();
234
+ const resolved = await resolveBox(client, reference);
235
+ output(await client.post(`/run-cloud/boxes/${encodeURIComponent(resolved.id)}/domains`, {
236
+ hostname,
237
+ }), cmdOpts);
238
+ });
239
+ });
211
240
  domain
212
241
  .command('list')
213
- .argument('<box>', 'box id, name, or hostname')
242
+ .argument('<sandbox>', REFERENCE_HELP)
214
243
  .option('--json', 'output JSON', false)
215
- .action((reference, opts) => action(async () => {
216
- const client = api();
217
- const resolved = await resolveBox(client, reference);
218
- const response = await client.get(`/run-cloud/boxes/${encodeURIComponent(resolved.id)}/domains`);
219
- const items = response?.items ?? [];
220
- if (opts.json)
221
- console.log(JSON.stringify(items, null, 2));
222
- else
223
- for (const item of items)
224
- output(item, opts);
225
- }));
244
+ .action((reference, cmdOpts) => {
245
+ domainNote('list');
246
+ return action(async () => {
247
+ const client = api();
248
+ const resolved = await resolveBox(client, reference);
249
+ const response = await client.get(`/run-cloud/boxes/${encodeURIComponent(resolved.id)}/domains`);
250
+ const items = response?.items ?? [];
251
+ if (cmdOpts.json)
252
+ console.log(JSON.stringify(items, null, 2));
253
+ else
254
+ for (const item of items)
255
+ output(item, cmdOpts);
256
+ });
257
+ });
226
258
  domain
227
259
  .command('status')
228
- .argument('<box>', 'box id, name, or hostname')
229
- .argument('<domain-id>', 'domain id from `box domain list`')
260
+ .argument('<sandbox>', REFERENCE_HELP)
261
+ .argument('<domain-id>', 'domain id from `domain list`')
230
262
  .option('--json', 'output JSON', false)
231
- .action((reference, domainId, opts) => action(async () => {
232
- const client = api();
233
- const resolved = await resolveBox(client, reference);
234
- output(await client.post(`/run-cloud/boxes/${encodeURIComponent(resolved.id)}/domains/${encodeURIComponent(domainId)}/refresh`), opts);
235
- }));
263
+ .action((reference, domainId, cmdOpts) => {
264
+ domainNote('status');
265
+ return action(async () => {
266
+ const client = api();
267
+ const resolved = await resolveBox(client, reference);
268
+ output(await client.post(`/run-cloud/boxes/${encodeURIComponent(resolved.id)}/domains/${encodeURIComponent(domainId)}/refresh`), cmdOpts);
269
+ });
270
+ });
236
271
  domain
237
272
  .command('rm')
238
- .argument('<box>', 'box id, name, or hostname')
239
- .argument('<domain-id>', 'domain id from `box domain list`')
240
- .action((reference, domainId) => action(async () => {
241
- const client = api();
242
- const resolved = await resolveBox(client, reference);
243
- await client.delete(`/run-cloud/boxes/${encodeURIComponent(resolved.id)}/domains/${encodeURIComponent(domainId)}`);
244
- console.log(`removed custom domain ${domainId}`);
245
- }));
273
+ .argument('<sandbox>', REFERENCE_HELP)
274
+ .argument('<domain-id>', 'domain id from `domain list`')
275
+ .action((reference, domainId) => {
276
+ domainNote('rm');
277
+ return action(async () => {
278
+ const client = api();
279
+ const resolved = await resolveBox(client, reference);
280
+ await client.delete(`/run-cloud/boxes/${encodeURIComponent(resolved.id)}/domains/${encodeURIComponent(domainId)}`);
281
+ console.log(`removed custom domain ${domainId}`);
282
+ });
283
+ });
246
284
  }
@@ -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;
@@ -84,7 +114,7 @@ export async function startCallbackServer(state, appOrigin, timeoutMs) {
84
114
  },
85
115
  };
86
116
  }
87
- function openBrowser(url) {
117
+ export function openBrowser(url) {
88
118
  const [cmd, args] = process.platform === 'darwin'
89
119
  ? ['open', [url]]
90
120
  : process.platform === 'win32'
@@ -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
- .option('--manual', 'paste the credential from the browser instead of using the local callback', false)
215
+ .option('--manual', 'newly skin only: 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);
@@ -173,19 +251,12 @@ export function registerLogin(program) {
173
251
  console.log(`Logged in headlessly. Credentials written for API ${apiUrl}.`);
174
252
  return;
175
253
  }
176
- if (opts.manual) {
177
- if (isRunCloud) {
178
- const manualUrl = `${opts.appUrl.replace(/\/$/, '')}/cli-auth?manual=1`;
179
- console.log(`Opening your browser to authorize the CLI…\n ${manualUrl}\n`);
180
- openBrowser(manualUrl);
181
- }
254
+ if (opts.manual && !isRunCloud) {
182
255
  const { createInterface } = await import('node:readline/promises');
183
256
  const rl = createInterface({ input: process.stdin, output: process.stdout });
184
257
  const blob = await rl.question('Paste the credential from the browser: ');
185
258
  rl.close();
186
259
  const parsed = parseManualCredential(blob);
187
- if (isRunCloud)
188
- await verifyRunCloudCredential(parsed.apiUrl, parsed.token);
189
260
  saveCredentials({
190
261
  token: parsed.token,
191
262
  apiUrl: parsed.apiUrl,
@@ -195,10 +266,69 @@ export function registerLogin(program) {
195
266
  console.log('Logged in.');
196
267
  return;
197
268
  }
269
+ let socialProvider;
270
+ if (isRunCloud && !opts.browser) {
271
+ const authApiUrl = opts.apiUrl ?? process.env.RUN_CLOUD_API_URL ?? DEFAULT_RUN_CLOUD_API_URL;
272
+ let method = methodFromFlags(opts);
273
+ const terminal = method === 'email' || !method ? acquirePromptIO() : null;
274
+ try {
275
+ if (!method && terminal) {
276
+ const providers = await fetchProviders(authApiUrl);
277
+ const choices = loginChoices(providers);
278
+ if (choices.length === 0) {
279
+ throw new Error(`${authApiUrl} has no sign-in methods enabled.`);
280
+ }
281
+ method = await select(' How do you want to sign in?', choices, terminal.io);
282
+ }
283
+ if (method === 'email') {
284
+ const preset = typeof opts.email === 'string' ? opts.email : undefined;
285
+ const { email, token } = await emailLogin(authApiUrl, preset, terminal?.io);
286
+ saveCredentials({
287
+ token,
288
+ apiUrl: authApiUrl,
289
+ appUrl: opts.appUrl,
290
+ expiresAt: Number.MAX_SAFE_INTEGER,
291
+ });
292
+ console.log(`\n Signed in as ${email}`);
293
+ console.log(` Credential saved to ${credentialsDisplayPath()}\n`);
294
+ console.log(' Try:');
295
+ console.log(' runcloud ios create --open');
296
+ console.log(' runcloud sandbox run "echo hello"');
297
+ return;
298
+ }
299
+ }
300
+ finally {
301
+ terminal?.close();
302
+ }
303
+ if (method === 'google' || method === 'github')
304
+ socialProvider = method;
305
+ }
306
+ if (isRunCloud) {
307
+ if (opts.manual) {
308
+ console.log('Note: --manual is no longer needed for run.cloud — `runcloud login` works over SSH and headless via device flow.');
309
+ }
310
+ const apiUrl = opts.apiUrl ?? process.env.RUN_CLOUD_API_URL ?? DEFAULT_RUN_CLOUD_API_URL;
311
+ const { runDeviceLogin } = await import('../deviceLogin.js');
312
+ const payload = await runDeviceLogin({
313
+ apiUrl,
314
+ appUrl: opts.appUrl,
315
+ provider: socialProvider,
316
+ openBrowser,
317
+ });
318
+ await verifyRunCloudCredential(payload.apiUrl, payload.token);
319
+ saveCredentials({
320
+ token: payload.token,
321
+ apiUrl: payload.apiUrl,
322
+ appUrl: opts.appUrl,
323
+ expiresAt: Math.floor(Date.now() / 1000) + payload.expiresIn,
324
+ });
325
+ console.log(`Logged in. Credentials saved to ${credentialsDisplayPath()}`);
326
+ return;
327
+ }
198
328
  const state = crypto.randomBytes(16).toString('hex');
199
329
  const appOrigin = new URL(opts.appUrl).origin;
200
330
  const { port, waitForToken, close } = await startCallbackServer(state, appOrigin, LOGIN_TIMEOUT_MS);
201
- const loginUrl = cliAuthUrl(opts.appUrl, port, state);
331
+ const loginUrl = cliAuthUrl(opts.appUrl, port, state, socialProvider);
202
332
  console.log(`Opening your browser to authorize the CLI…\n ${loginUrl}\n`);
203
333
  console.log("If the browser doesn't open, paste the URL above into it manually.");
204
334
  openBrowser(loginUrl);
@@ -219,6 +349,11 @@ export function registerLogin(program) {
219
349
  }
220
350
  }
221
351
  catch (e) {
352
+ if (e instanceof PromptAbortedError) {
353
+ console.log('Cancelled.');
354
+ process.exitCode = 130;
355
+ return;
356
+ }
222
357
  console.error(e instanceof Error ? e.message : String(e));
223
358
  process.exitCode = 1;
224
359
  }
@@ -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: