runcloud 0.1.5 → 0.1.6
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 +6 -3
- package/dist/commands/login.js +62 -12
- package/dist/commands/sandbox.js +22 -51
- package/dist/config.js +6 -2
- package/package.json +1 -1
package/dist/api.js
CHANGED
|
@@ -9,7 +9,8 @@ export class ApiError extends Error {
|
|
|
9
9
|
}
|
|
10
10
|
function parsedErrorDetail(body) {
|
|
11
11
|
try {
|
|
12
|
-
const
|
|
12
|
+
const parsed = JSON.parse(body);
|
|
13
|
+
const detail = parsed.detail ?? parsed.error ?? parsed.message;
|
|
13
14
|
if (typeof detail === 'string')
|
|
14
15
|
return detail;
|
|
15
16
|
if (detail !== undefined)
|
|
@@ -161,8 +162,10 @@ function detailSuffix(err) {
|
|
|
161
162
|
}
|
|
162
163
|
function headline(err) {
|
|
163
164
|
if (err instanceof ApiError) {
|
|
164
|
-
if (err.status === 401)
|
|
165
|
-
|
|
165
|
+
if (err.status === 401) {
|
|
166
|
+
const command = (process.argv[1] ?? '').includes('run-cloud') ? 'runcloud login' : 'newly login';
|
|
167
|
+
return `Session expired or invalid — run \`${command}\`.`;
|
|
168
|
+
}
|
|
166
169
|
if (err.status === 403)
|
|
167
170
|
return err.detail ? `Forbidden: ${err.detail}` : 'Forbidden.';
|
|
168
171
|
if (err.status === 404)
|
package/dist/commands/login.js
CHANGED
|
@@ -15,6 +15,7 @@ export async function startCallbackServer(state, appOrigin, timeoutMs) {
|
|
|
15
15
|
'Access-Control-Allow-Origin': appOrigin,
|
|
16
16
|
'Access-Control-Allow-Methods': 'POST, OPTIONS',
|
|
17
17
|
'Access-Control-Allow-Headers': 'content-type',
|
|
18
|
+
'Access-Control-Allow-Private-Network': 'true',
|
|
18
19
|
};
|
|
19
20
|
const server = http.createServer((req, res) => {
|
|
20
21
|
if (req.method === 'OPTIONS') {
|
|
@@ -37,6 +38,22 @@ export async function startCallbackServer(state, appOrigin, timeoutMs) {
|
|
|
37
38
|
res.end('state mismatch');
|
|
38
39
|
return;
|
|
39
40
|
}
|
|
41
|
+
if (typeof parsed.token !== 'string' ||
|
|
42
|
+
parsed.token.length === 0 ||
|
|
43
|
+
typeof parsed.expiresIn !== 'number' ||
|
|
44
|
+
!Number.isFinite(parsed.expiresIn) ||
|
|
45
|
+
parsed.expiresIn <= 0 ||
|
|
46
|
+
typeof parsed.apiUrl !== 'string') {
|
|
47
|
+
res.writeHead(400, corsHeaders);
|
|
48
|
+
res.end('invalid credential payload');
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
const apiUrl = new URL(parsed.apiUrl);
|
|
52
|
+
if (!['http:', 'https:'].includes(apiUrl.protocol)) {
|
|
53
|
+
res.writeHead(400, corsHeaders);
|
|
54
|
+
res.end('invalid API URL');
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
40
57
|
res.writeHead(200, { ...corsHeaders, 'Content-Type': 'application/json' });
|
|
41
58
|
res.end(JSON.stringify({ ok: true }));
|
|
42
59
|
clearTimeout(timer);
|
|
@@ -74,6 +91,12 @@ function openBrowser(url) {
|
|
|
74
91
|
});
|
|
75
92
|
child.unref();
|
|
76
93
|
}
|
|
94
|
+
export function cliAuthUrl(appUrl, port, state) {
|
|
95
|
+
const url = new URL('/cli-auth', appUrl);
|
|
96
|
+
url.searchParams.set('port', String(port));
|
|
97
|
+
url.searchParams.set('state', state);
|
|
98
|
+
return url.toString();
|
|
99
|
+
}
|
|
77
100
|
export function parseManualCredential(blob) {
|
|
78
101
|
let parsed;
|
|
79
102
|
try {
|
|
@@ -90,12 +113,13 @@ export function parseManualCredential(blob) {
|
|
|
90
113
|
return { token: c.token, expiresIn: c.expiresIn, apiUrl: c.apiUrl };
|
|
91
114
|
}
|
|
92
115
|
export function registerLogin(program) {
|
|
93
|
-
const
|
|
116
|
+
const isRunCloud = program.name() === 'runcloud';
|
|
117
|
+
const defaultAppUrl = isRunCloud
|
|
94
118
|
? process.env.RUN_CLOUD_APP_URL ?? 'https://run.cloud'
|
|
95
119
|
: process.env.NEWLY_APP_URL ?? 'https://newly.app';
|
|
96
120
|
program
|
|
97
121
|
.command('login')
|
|
98
|
-
.description(
|
|
122
|
+
.description(`Log in with your ${isRunCloud ? 'run.cloud' : 'Newly'} account via the browser`)
|
|
99
123
|
.option('--app-url <url>', 'product web app URL', defaultAppUrl)
|
|
100
124
|
.option('--manual', 'paste the credential from the browser instead of using the local callback', false)
|
|
101
125
|
.option('--api-url <url>', 'explicit API URL (skips browser; use with --token)')
|
|
@@ -105,36 +129,57 @@ export function registerLogin(program) {
|
|
|
105
129
|
try {
|
|
106
130
|
const hasApiUrl = typeof opts.apiUrl === 'string' && opts.apiUrl.length > 0;
|
|
107
131
|
const hasToken = typeof opts.token === 'string' && opts.token.length > 0;
|
|
108
|
-
if (hasApiUrl !== hasToken) {
|
|
132
|
+
if (!isRunCloud && hasApiUrl !== hasToken) {
|
|
109
133
|
throw new Error('--api-url and --token must be provided together for headless login.');
|
|
110
134
|
}
|
|
111
|
-
if (hasApiUrl && hasToken) {
|
|
135
|
+
if (isRunCloud && hasApiUrl && !hasToken) {
|
|
136
|
+
throw new Error('--api-url requires --token for headless login.');
|
|
137
|
+
}
|
|
138
|
+
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
|
+
const apiUrl = isRunCloud
|
|
143
|
+
? opts.apiUrl ?? process.env.RUN_CLOUD_API_URL ?? 'https://api.newly.app'
|
|
144
|
+
: opts.apiUrl;
|
|
112
145
|
try {
|
|
113
|
-
new URL(
|
|
146
|
+
new URL(apiUrl);
|
|
114
147
|
}
|
|
115
148
|
catch {
|
|
116
|
-
throw new Error(`--api-url is not a valid URL: ${
|
|
149
|
+
throw new Error(`--api-url is not a valid URL: ${apiUrl}`);
|
|
117
150
|
}
|
|
118
151
|
saveCredentials({
|
|
119
152
|
token: opts.token,
|
|
120
|
-
apiUrl
|
|
153
|
+
apiUrl,
|
|
121
154
|
appUrl: opts.appUrl,
|
|
122
|
-
expiresAt:
|
|
155
|
+
expiresAt: isRunCloud
|
|
156
|
+
? Number.MAX_SAFE_INTEGER
|
|
157
|
+
: Math.floor(Date.now() / 1000) + opts.expiresIn,
|
|
123
158
|
});
|
|
124
|
-
console.log(`Logged in headlessly. Credentials written for API ${
|
|
159
|
+
console.log(`Logged in headlessly. Credentials written for API ${apiUrl}.`);
|
|
125
160
|
return;
|
|
126
161
|
}
|
|
127
162
|
if (opts.manual) {
|
|
163
|
+
if (isRunCloud) {
|
|
164
|
+
const manualUrl = `${opts.appUrl.replace(/\/$/, '')}/cli-auth?manual=1`;
|
|
165
|
+
console.log(`Opening your browser to authorize the CLI…\n ${manualUrl}\n`);
|
|
166
|
+
openBrowser(manualUrl);
|
|
167
|
+
}
|
|
128
168
|
const { createInterface } = await import('node:readline/promises');
|
|
129
169
|
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
130
170
|
const blob = await rl.question('Paste the credential from the browser: ');
|
|
131
171
|
rl.close();
|
|
132
172
|
const parsed = parseManualCredential(blob);
|
|
173
|
+
if (isRunCloud && !parsed.token.startsWith('rc_live_')) {
|
|
174
|
+
throw new Error('The browser did not return a Better Auth run.cloud API key.');
|
|
175
|
+
}
|
|
133
176
|
saveCredentials({
|
|
134
177
|
token: parsed.token,
|
|
135
178
|
apiUrl: parsed.apiUrl,
|
|
136
179
|
appUrl: opts.appUrl,
|
|
137
|
-
expiresAt:
|
|
180
|
+
expiresAt: isRunCloud
|
|
181
|
+
? Number.MAX_SAFE_INTEGER
|
|
182
|
+
: Math.floor(Date.now() / 1000) + parsed.expiresIn,
|
|
138
183
|
});
|
|
139
184
|
console.log('Logged in.');
|
|
140
185
|
return;
|
|
@@ -142,17 +187,22 @@ export function registerLogin(program) {
|
|
|
142
187
|
const state = crypto.randomBytes(16).toString('hex');
|
|
143
188
|
const appOrigin = new URL(opts.appUrl).origin;
|
|
144
189
|
const { port, waitForToken, close } = await startCallbackServer(state, appOrigin, LOGIN_TIMEOUT_MS);
|
|
145
|
-
const loginUrl =
|
|
190
|
+
const loginUrl = cliAuthUrl(opts.appUrl, port, state);
|
|
146
191
|
console.log(`Opening your browser to authorize the CLI…\n ${loginUrl}\n`);
|
|
147
192
|
console.log("If the browser doesn't open, paste the URL above into it manually.");
|
|
148
193
|
openBrowser(loginUrl);
|
|
149
194
|
try {
|
|
150
195
|
const payload = await waitForToken();
|
|
196
|
+
if (isRunCloud && !payload.token.startsWith('rc_live_')) {
|
|
197
|
+
throw new Error('The browser did not return a Better Auth run.cloud API key.');
|
|
198
|
+
}
|
|
151
199
|
saveCredentials({
|
|
152
200
|
token: payload.token,
|
|
153
201
|
apiUrl: payload.apiUrl,
|
|
154
202
|
appUrl: opts.appUrl,
|
|
155
|
-
expiresAt:
|
|
203
|
+
expiresAt: isRunCloud
|
|
204
|
+
? Number.MAX_SAFE_INTEGER
|
|
205
|
+
: Math.floor(Date.now() / 1000) + payload.expiresIn,
|
|
156
206
|
});
|
|
157
207
|
console.log(`Logged in. Credentials saved to ${credentialsDisplayPath()}`);
|
|
158
208
|
}
|
package/dist/commands/sandbox.js
CHANGED
|
@@ -1,37 +1,8 @@
|
|
|
1
|
-
import { friendlyApiError } from '../api.js';
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
this.baseUrl = baseUrl;
|
|
7
|
-
this.token = token;
|
|
8
|
-
}
|
|
9
|
-
async request(method, path, body) {
|
|
10
|
-
const res = await fetch(`${this.baseUrl}${path}`, {
|
|
11
|
-
method,
|
|
12
|
-
headers: { Authorization: `Bearer ${this.token}`, 'Content-Type': 'application/json' },
|
|
13
|
-
body: body !== undefined ? JSON.stringify(body) : undefined,
|
|
14
|
-
});
|
|
15
|
-
const text = await res.text();
|
|
16
|
-
if (!res.ok) {
|
|
17
|
-
let msg = text;
|
|
18
|
-
try {
|
|
19
|
-
msg = JSON.parse(text).error ?? text;
|
|
20
|
-
}
|
|
21
|
-
catch {
|
|
22
|
-
}
|
|
23
|
-
throw new Error(`control plane ${res.status}: ${msg}`);
|
|
24
|
-
}
|
|
25
|
-
return text ? JSON.parse(text) : null;
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
function cp(opts) {
|
|
29
|
-
const baseUrl = (opts.cpUrl || process.env.SBX_API_URL || 'http://127.0.0.1:8080').replace(/\/+$/, '');
|
|
30
|
-
const token = opts.cpToken || process.env.SBX_TOKEN;
|
|
31
|
-
if (!token) {
|
|
32
|
-
throw new Error('No control-plane token: set SBX_TOKEN or pass --cp-token.');
|
|
33
|
-
}
|
|
34
|
-
return new CpClient(baseUrl, token);
|
|
1
|
+
import { ApiClient, friendlyApiError } from '../api.js';
|
|
2
|
+
import { requireCredentials } from '../config.js';
|
|
3
|
+
function runCloudApi() {
|
|
4
|
+
const credentials = requireCredentials();
|
|
5
|
+
return new ApiClient(credentials.apiUrl, credentials.token);
|
|
35
6
|
}
|
|
36
7
|
function print(value, opts) {
|
|
37
8
|
if (opts.json) {
|
|
@@ -62,39 +33,39 @@ async function run(fn) {
|
|
|
62
33
|
process.exitCode = 1;
|
|
63
34
|
}
|
|
64
35
|
}
|
|
65
|
-
function
|
|
66
|
-
return cmd
|
|
67
|
-
.option('--cp-url <url>', 'control-plane base URL (default $SBX_API_URL or http://127.0.0.1:8080)')
|
|
68
|
-
.option('--cp-token <token>', 'control-plane org token (or $SBX_TOKEN; required)')
|
|
69
|
-
.option('--json', 'output JSON', false);
|
|
36
|
+
function withOutput(cmd) {
|
|
37
|
+
return cmd.option('--json', 'output JSON', false);
|
|
70
38
|
}
|
|
71
39
|
export function registerSandbox(program) {
|
|
72
40
|
const sandbox = program.command('sandbox').description('Spawn and control microVM sandboxes');
|
|
73
|
-
|
|
41
|
+
withOutput(sandbox
|
|
74
42
|
.command('create')
|
|
75
43
|
.description('Spawn a microVM sandbox')
|
|
76
44
|
.option('--image <ref>', 'base image', 'newly/agent-base')
|
|
77
45
|
.option('--size <class>', 'size preset', 's-2')
|
|
78
46
|
.option('--region <region>', 'placement region')
|
|
79
|
-
.option('--name <name>', 'human-readable name')
|
|
80
|
-
|
|
47
|
+
.option('--name <name>', 'human-readable name')
|
|
48
|
+
.option('--org <id>', 'billing and ownership organization')).action((opts) => run(async () => {
|
|
49
|
+
const body = { size: opts.size, image: opts.image };
|
|
81
50
|
if (opts.name)
|
|
82
51
|
body.name = opts.name;
|
|
83
52
|
if (opts.region)
|
|
84
53
|
body.region = opts.region;
|
|
85
|
-
|
|
54
|
+
if (opts.org)
|
|
55
|
+
body.orgId = opts.org;
|
|
56
|
+
print(await runCloudApi().post('/run-cloud/sandboxes', body), opts);
|
|
86
57
|
}));
|
|
87
|
-
|
|
58
|
+
withOutput(sandbox.command('list').description('List sandboxes').option('--state <state>', 'filter by state')).action((opts) => run(async () => {
|
|
88
59
|
const qs = opts.state ? `?state=${encodeURIComponent(opts.state)}` : '';
|
|
89
|
-
const data = await
|
|
60
|
+
const data = await runCloudApi().get(`/run-cloud/sandboxes${qs}`);
|
|
90
61
|
print(data?.items ?? [], opts);
|
|
91
62
|
}));
|
|
92
|
-
|
|
63
|
+
withOutput(sandbox
|
|
93
64
|
.command('exec')
|
|
94
65
|
.description('Run a command in a sandbox (via /bin/sh -c)')
|
|
95
66
|
.argument('<id>', 'sandbox id')
|
|
96
67
|
.argument('<cmd...>', 'command to run, e.g. npm run build')).action((id, cmdParts, opts) => run(async () => {
|
|
97
|
-
const r = await
|
|
68
|
+
const r = await runCloudApi().post(`/run-cloud/sandboxes/${encodeURIComponent(id)}/exec`, {
|
|
98
69
|
cmd: ['/bin/sh', '-c', cmdParts.join(' ')],
|
|
99
70
|
});
|
|
100
71
|
if (opts.json) {
|
|
@@ -108,13 +79,13 @@ export function registerSandbox(program) {
|
|
|
108
79
|
}
|
|
109
80
|
process.exitCode = r.exit_code ?? 0;
|
|
110
81
|
}));
|
|
111
|
-
|
|
82
|
+
withOutput(sandbox
|
|
112
83
|
.command('logs')
|
|
113
84
|
.description('Show recent sandbox logs (one-shot journalctl tail; streaming is a follow-up)')
|
|
114
85
|
.argument('<id>', 'sandbox id')
|
|
115
86
|
.option('--lines <n>', 'number of lines to tail', '200')).action((id, opts) => run(async () => {
|
|
116
87
|
const n = Number(opts.lines) || 200;
|
|
117
|
-
const r = await
|
|
88
|
+
const r = await runCloudApi().post(`/run-cloud/sandboxes/${encodeURIComponent(id)}/exec`, {
|
|
118
89
|
cmd: [
|
|
119
90
|
'/bin/sh',
|
|
120
91
|
'-c',
|
|
@@ -128,8 +99,8 @@ export function registerSandbox(program) {
|
|
|
128
99
|
process.stdout.write(r.stdout ?? '');
|
|
129
100
|
process.stderr.write(r.stderr ?? '');
|
|
130
101
|
}));
|
|
131
|
-
|
|
132
|
-
await
|
|
102
|
+
withOutput(sandbox.command('rm').description('Destroy a sandbox').argument('<id>', 'sandbox id')).action((id, opts) => run(async () => {
|
|
103
|
+
await runCloudApi().delete(`/run-cloud/sandboxes/${encodeURIComponent(id)}`);
|
|
133
104
|
if (!opts.json)
|
|
134
105
|
console.log(`destroyed ${id}`);
|
|
135
106
|
}));
|
package/dist/config.js
CHANGED
|
@@ -41,10 +41,10 @@ export function loadCredentials() {
|
|
|
41
41
|
const envApiUrl = isRunCloud()
|
|
42
42
|
? process.env.RUN_CLOUD_API_URL
|
|
43
43
|
: process.env.NEWLY_API_URL;
|
|
44
|
-
if (envToken?.trim() && envApiUrl?.trim()) {
|
|
44
|
+
if (envToken?.trim() && (isRunCloud() || envApiUrl?.trim())) {
|
|
45
45
|
return {
|
|
46
46
|
token: envToken.trim(),
|
|
47
|
-
apiUrl: normalizeApiUrl(envApiUrl
|
|
47
|
+
apiUrl: normalizeApiUrl(envApiUrl?.trim() || 'https://api.newly.app'),
|
|
48
48
|
appUrl: isRunCloud()
|
|
49
49
|
? process.env.RUN_CLOUD_APP_URL ?? 'https://run.cloud'
|
|
50
50
|
: process.env.NEWLY_APP_URL ?? 'https://newly.app',
|
|
@@ -105,6 +105,10 @@ export function requireCredentials() {
|
|
|
105
105
|
if (!creds) {
|
|
106
106
|
throw new Error(`Not logged in (or session expired). Run: ${loginCommandName()}`);
|
|
107
107
|
}
|
|
108
|
+
if (isRunCloud() && !creds.token.startsWith('rc_live_')) {
|
|
109
|
+
throw new Error('Legacy run.cloud credentials are no longer supported. Run `runcloud login` to sign in ' +
|
|
110
|
+
'with Better Auth.');
|
|
111
|
+
}
|
|
108
112
|
return creds;
|
|
109
113
|
}
|
|
110
114
|
export function writeProjectLink(dir, link) {
|