@newly/cli 0.1.2 → 0.1.91
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 +20 -19
- package/README.runcloud.md +253 -0
- package/dist/api.js +123 -43
- package/dist/commands/appStore.js +162 -0
- package/dist/commands/appStoreEvaluate.js +515 -0
- package/dist/commands/boxAccess.js +349 -0
- package/dist/commands/build.js +261 -13
- package/dist/commands/create.js +106 -30
- package/dist/commands/deploy.js +100 -129
- package/dist/commands/desktop.js +55 -0
- package/dist/commands/environment.js +290 -0
- package/dist/commands/image.js +145 -0
- package/dist/commands/login.js +265 -18
- package/dist/commands/run-cloud.js +318 -108
- package/dist/commands/sandbox.js +787 -0
- package/dist/commands/sandboxOutput.js +70 -0
- package/dist/commands/secrets.js +203 -0
- package/dist/config.js +56 -17
- package/dist/deviceLogin.js +48 -0
- package/dist/index.js +6 -0
- package/dist/prompt.js +196 -0
- package/dist/run-cloud.js +9 -2
- package/dist/runCloudAuth.js +79 -0
- package/dist/sandboxExec.js +91 -0
- package/dist/services-manifest.js +30 -1
- package/dist/shellSession.js +86 -0
- package/dist/terminal.js +158 -0
- package/dist/version.js +1 -0
- package/package.json +13 -7
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import WebSocket from 'ws';
|
|
2
|
+
export function streamExitCode(frame) {
|
|
3
|
+
const code = frame.code ?? frame.exit_code ?? 0;
|
|
4
|
+
return Number.isSafeInteger(code) ? code : undefined;
|
|
5
|
+
}
|
|
6
|
+
export function shellCommand(argv) {
|
|
7
|
+
if (argv.length === 1)
|
|
8
|
+
return argv[0] ?? '';
|
|
9
|
+
return argv
|
|
10
|
+
.map((part) => `'${part.replaceAll("'", "'\"'\"'")}'`)
|
|
11
|
+
.join(' ');
|
|
12
|
+
}
|
|
13
|
+
export function runSandboxCommand(input) {
|
|
14
|
+
const url = new URL(`/run-cloud/sandboxes/${encodeURIComponent(input.sandboxId)}/exec/stream?mode=command`, input.apiUrl);
|
|
15
|
+
url.protocol = url.protocol === 'https:' ? 'wss:' : 'ws:';
|
|
16
|
+
const socket = input.createSocket?.(url.toString(), {
|
|
17
|
+
headers: { Authorization: `Bearer ${input.token}` },
|
|
18
|
+
}) ??
|
|
19
|
+
new WebSocket(url, {
|
|
20
|
+
headers: { Authorization: `Bearer ${input.token}` },
|
|
21
|
+
});
|
|
22
|
+
return new Promise((resolve, reject) => {
|
|
23
|
+
const stdout = [];
|
|
24
|
+
const stderr = [];
|
|
25
|
+
let exitCode;
|
|
26
|
+
let settled = false;
|
|
27
|
+
const finish = (error) => {
|
|
28
|
+
if (settled)
|
|
29
|
+
return;
|
|
30
|
+
settled = true;
|
|
31
|
+
if (error) {
|
|
32
|
+
reject(error);
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
if (exitCode === undefined) {
|
|
36
|
+
reject(new Error('sandbox exec stream closed before reporting an exit code'));
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
resolve({
|
|
40
|
+
exit_code: exitCode,
|
|
41
|
+
stdout: Buffer.concat(stdout).toString('utf8'),
|
|
42
|
+
stderr: Buffer.concat(stderr).toString('utf8'),
|
|
43
|
+
});
|
|
44
|
+
};
|
|
45
|
+
socket.once('open', () => socket.send(JSON.stringify(input.request)));
|
|
46
|
+
socket.on('message', (raw) => {
|
|
47
|
+
let frame;
|
|
48
|
+
try {
|
|
49
|
+
frame = JSON.parse(raw.toString());
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
socket.close(1002, 'invalid exec frame');
|
|
53
|
+
finish(new Error('sandbox exec stream returned invalid JSON'));
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
if (frame.stream === 'stdout' || frame.stream === 'stderr') {
|
|
57
|
+
if (typeof frame.data !== 'string')
|
|
58
|
+
return;
|
|
59
|
+
const chunk = Buffer.from(frame.data, 'base64');
|
|
60
|
+
if (frame.stream === 'stdout') {
|
|
61
|
+
stdout.push(chunk);
|
|
62
|
+
input.onStdout?.(chunk);
|
|
63
|
+
}
|
|
64
|
+
else {
|
|
65
|
+
stderr.push(chunk);
|
|
66
|
+
input.onStderr?.(chunk);
|
|
67
|
+
}
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
if (frame.stream === 'exit') {
|
|
71
|
+
const code = streamExitCode(frame);
|
|
72
|
+
if (code === undefined) {
|
|
73
|
+
socket.close(1002, 'invalid exit code');
|
|
74
|
+
finish(new Error('sandbox exec stream returned an invalid exit code'));
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
exitCode = code;
|
|
78
|
+
socket.close(1000);
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
socket.once('error', (error) => finish(error));
|
|
82
|
+
socket.once('close', (code, reason) => {
|
|
83
|
+
if (exitCode !== undefined)
|
|
84
|
+
finish();
|
|
85
|
+
else {
|
|
86
|
+
finish(new Error(reason.toString() ||
|
|
87
|
+
`sandbox exec stream closed before completion (${code})`));
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
}
|
|
@@ -2,7 +2,6 @@ import { existsSync, readFileSync } from 'node:fs';
|
|
|
2
2
|
import { join } from 'node:path';
|
|
3
3
|
export const DEFAULT_BACKEND_SERVICES = [
|
|
4
4
|
{ name: 'api', path: 'api-service', type: 'worker', schema: 'api', secrets: 'api', public: true },
|
|
5
|
-
{ name: 'auth', path: 'auth-service', type: 'worker', schema: 'auth', secrets: 'auth', public: true },
|
|
6
5
|
];
|
|
7
6
|
const SERVICE_NAME = /^[a-z][a-z0-9-]{0,31}$/;
|
|
8
7
|
export function readBackendServices(dir) {
|
|
@@ -33,9 +32,39 @@ export function readBackendServices(dir) {
|
|
|
33
32
|
schema: entry.schema === undefined ? undefined : entry.schema === null ? null : String(entry.schema),
|
|
34
33
|
secrets: entry.secrets === undefined ? undefined : String(entry.secrets),
|
|
35
34
|
public: entry.public === undefined ? true : Boolean(entry.public),
|
|
35
|
+
bindings: parseBindings(name, entry.bindings),
|
|
36
36
|
};
|
|
37
37
|
});
|
|
38
38
|
}
|
|
39
|
+
function parseBindings(serviceName, value) {
|
|
40
|
+
if (value === undefined)
|
|
41
|
+
return undefined;
|
|
42
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
43
|
+
throw new Error(`Service ${serviceName} bindings must be an object`);
|
|
44
|
+
}
|
|
45
|
+
const bindings = {};
|
|
46
|
+
for (const [bindingName, raw] of Object.entries(value)) {
|
|
47
|
+
if (!/^[A-Z][A-Z0-9_]{0,127}$/.test(bindingName)) {
|
|
48
|
+
throw new Error(`Invalid binding name for service ${serviceName}: ${bindingName}`);
|
|
49
|
+
}
|
|
50
|
+
if (bindingName.startsWith('NEWLY_')) {
|
|
51
|
+
throw new Error(`Service ${serviceName} cannot override reserved binding ${bindingName}`);
|
|
52
|
+
}
|
|
53
|
+
if (!raw || typeof raw !== 'object' || Array.isArray(raw)) {
|
|
54
|
+
throw new Error(`Binding ${bindingName} for service ${serviceName} must be an object`);
|
|
55
|
+
}
|
|
56
|
+
const entries = Object.entries(raw);
|
|
57
|
+
if (entries.length !== 1 || !['database', 'secret', 'storage'].includes(entries[0][0])) {
|
|
58
|
+
throw new Error(`Binding ${bindingName} for service ${serviceName} must declare exactly one database, secret, or storage`);
|
|
59
|
+
}
|
|
60
|
+
const [kind, logicalName] = entries[0];
|
|
61
|
+
if (typeof logicalName !== 'string' || logicalName.length === 0) {
|
|
62
|
+
throw new Error(`Binding ${bindingName} for service ${serviceName} has an invalid logical name`);
|
|
63
|
+
}
|
|
64
|
+
bindings[bindingName] = { [kind]: logicalName };
|
|
65
|
+
}
|
|
66
|
+
return bindings;
|
|
67
|
+
}
|
|
39
68
|
export function parseServiceFilter(value) {
|
|
40
69
|
if (!value)
|
|
41
70
|
return [];
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import WebSocket from 'ws';
|
|
2
|
+
export async function runShellSession(input) {
|
|
3
|
+
const stdin = input.stdin ?? process.stdin;
|
|
4
|
+
const stdout = input.stdout ?? process.stdout;
|
|
5
|
+
const stderr = input.stderr ?? process.stderr;
|
|
6
|
+
const url = new URL(input.path, input.apiUrl);
|
|
7
|
+
url.protocol = url.protocol === 'https:' ? 'wss:' : 'ws:';
|
|
8
|
+
const createWebSocket = input.createWebSocket ?? ((address, options) => new WebSocket(address, options));
|
|
9
|
+
const socket = createWebSocket(url.toString(), {
|
|
10
|
+
headers: { Authorization: `Bearer ${input.token}` },
|
|
11
|
+
});
|
|
12
|
+
const ttyInput = stdin;
|
|
13
|
+
const ttyOutput = stdout;
|
|
14
|
+
const wasRaw = ttyInput.isRaw;
|
|
15
|
+
let exitCode = 1;
|
|
16
|
+
const sendResize = () => {
|
|
17
|
+
if (socket.readyState === WebSocket.OPEN && ttyOutput.rows && ttyOutput.columns) {
|
|
18
|
+
socket.send(JSON.stringify({ stream: 'resize', rows: ttyOutput.rows, cols: ttyOutput.columns }));
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
const onInput = (data) => {
|
|
22
|
+
if (socket.readyState === WebSocket.OPEN) {
|
|
23
|
+
socket.send(JSON.stringify({ stream: 'stdin', data: Buffer.from(data).toString('base64') }));
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
const restore = () => {
|
|
27
|
+
stdin.off('data', onInput);
|
|
28
|
+
ttyOutput.off?.('resize', sendResize);
|
|
29
|
+
if (ttyInput.isTTY && typeof ttyInput.setRawMode === 'function')
|
|
30
|
+
ttyInput.setRawMode(Boolean(wasRaw));
|
|
31
|
+
stdin.pause();
|
|
32
|
+
};
|
|
33
|
+
return await new Promise((resolve, reject) => {
|
|
34
|
+
socket.once('open', () => {
|
|
35
|
+
if (ttyInput.isTTY && typeof ttyInput.setRawMode === 'function')
|
|
36
|
+
ttyInput.setRawMode(true);
|
|
37
|
+
stdin.resume();
|
|
38
|
+
stdin.on('data', onInput);
|
|
39
|
+
ttyOutput.on?.('resize', sendResize);
|
|
40
|
+
sendResize();
|
|
41
|
+
});
|
|
42
|
+
socket.on('message', (raw) => {
|
|
43
|
+
try {
|
|
44
|
+
const frame = JSON.parse(raw.toString());
|
|
45
|
+
if ((frame.stream === 'stdout' || frame.stream === 'stderr') && typeof frame.data === 'string') {
|
|
46
|
+
const target = frame.stream === 'stderr' ? stderr : stdout;
|
|
47
|
+
target.write(Buffer.from(frame.data, 'base64'));
|
|
48
|
+
}
|
|
49
|
+
else if (frame.stream === 'exit') {
|
|
50
|
+
const code = frame.code ?? frame.exit_code ?? 0;
|
|
51
|
+
if (Number.isSafeInteger(code))
|
|
52
|
+
exitCode = code;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
socket.once('error', (error) => {
|
|
59
|
+
restore();
|
|
60
|
+
reject(error);
|
|
61
|
+
});
|
|
62
|
+
socket.once('close', (code, reason) => {
|
|
63
|
+
restore();
|
|
64
|
+
if (code >= 4400) {
|
|
65
|
+
reject(new Error(reason.toString() || `${input.label} rejected (${code})`));
|
|
66
|
+
}
|
|
67
|
+
else {
|
|
68
|
+
resolve(exitCode);
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
export function runEnvironmentDatabaseShell(input) {
|
|
74
|
+
return runShellSession({
|
|
75
|
+
...input,
|
|
76
|
+
path: `/environment-databases/${encodeURIComponent(input.databaseId)}/shell`,
|
|
77
|
+
label: 'Database shell',
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
export function runSandboxShell(input) {
|
|
81
|
+
return runShellSession({
|
|
82
|
+
...input,
|
|
83
|
+
path: `/run-cloud/sandboxes/${encodeURIComponent(input.sandboxId)}/exec/stream`,
|
|
84
|
+
label: 'Sandbox shell',
|
|
85
|
+
});
|
|
86
|
+
}
|
package/dist/terminal.js
ADDED
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
const ANSI_PATTERN = /\u001b\[[0-9;]*m/g;
|
|
2
|
+
function supportsColor(stream) {
|
|
3
|
+
return stream.isTTY === true && process.env.NO_COLOR === undefined;
|
|
4
|
+
}
|
|
5
|
+
function paint(code, value, stream = process.stdout) {
|
|
6
|
+
return supportsColor(stream) ? `\u001b[${code}m${value}\u001b[0m` : value;
|
|
7
|
+
}
|
|
8
|
+
export const style = {
|
|
9
|
+
bold: (value, stream) => paint(1, value, stream),
|
|
10
|
+
dim: (value, stream) => paint(2, value, stream),
|
|
11
|
+
red: (value, stream) => paint(31, value, stream),
|
|
12
|
+
green: (value, stream) => paint(32, value, stream),
|
|
13
|
+
yellow: (value, stream) => paint(33, value, stream),
|
|
14
|
+
blue: (value, stream) => paint(34, value, stream),
|
|
15
|
+
cyan: (value, stream) => paint(36, value, stream),
|
|
16
|
+
};
|
|
17
|
+
function visibleLength(value) {
|
|
18
|
+
return value.replace(ANSI_PATTERN, '').length;
|
|
19
|
+
}
|
|
20
|
+
function truncate(value, maxWidth) {
|
|
21
|
+
if (!maxWidth || visibleLength(value) <= maxWidth)
|
|
22
|
+
return value;
|
|
23
|
+
if (maxWidth <= 1)
|
|
24
|
+
return '…';
|
|
25
|
+
return `${value.slice(0, maxWidth - 1)}…`;
|
|
26
|
+
}
|
|
27
|
+
function pad(value, width) {
|
|
28
|
+
return `${value}${' '.repeat(Math.max(0, width - visibleLength(value)))}`;
|
|
29
|
+
}
|
|
30
|
+
export function renderTable(title, columns, rows) {
|
|
31
|
+
if (rows.length === 0)
|
|
32
|
+
return title;
|
|
33
|
+
const cells = rows.map((row) => Object.fromEntries(columns.map((column) => [
|
|
34
|
+
column.key,
|
|
35
|
+
truncate(row[column.key] === undefined || row[column.key] === null ? '—' : String(row[column.key]), column.maxWidth),
|
|
36
|
+
])));
|
|
37
|
+
const widths = Object.fromEntries(columns.map((column) => [
|
|
38
|
+
column.key,
|
|
39
|
+
Math.max(visibleLength(column.label), ...cells.map((row) => visibleLength(row[column.key]))),
|
|
40
|
+
]));
|
|
41
|
+
const line = (row) => columns
|
|
42
|
+
.map((column, index) => {
|
|
43
|
+
const value = row[column.key];
|
|
44
|
+
return index === columns.length - 1 ? value : pad(value, widths[column.key]);
|
|
45
|
+
})
|
|
46
|
+
.join(' ')
|
|
47
|
+
.trimEnd();
|
|
48
|
+
const header = Object.fromEntries(columns.map((column) => [column.key, style.dim(column.label)]));
|
|
49
|
+
return `${style.bold(title)}\n\n${line(header)}\n${cells.map((row) => line(row)).join('\n')}`;
|
|
50
|
+
}
|
|
51
|
+
export function renderDetails(title, fields, footer) {
|
|
52
|
+
const visibleFields = fields.filter(({ value }) => value !== undefined && value !== null && value !== '');
|
|
53
|
+
const labelWidth = Math.max(...visibleFields.map(({ label }) => label.length), 0);
|
|
54
|
+
const lines = visibleFields.map(({ label, value }) => ` ${style.dim(pad(label, labelWidth))} ${String(value)}`);
|
|
55
|
+
return [style.bold(title), ...lines, footer ? `\n${footer}` : undefined]
|
|
56
|
+
.filter((value) => value !== undefined)
|
|
57
|
+
.join('\n');
|
|
58
|
+
}
|
|
59
|
+
export function formatState(state) {
|
|
60
|
+
const value = typeof state === 'string' && state ? state : 'unknown';
|
|
61
|
+
if (value === 'running' || value === 'ready')
|
|
62
|
+
return style.green(value);
|
|
63
|
+
if (['building', 'building_image', 'pending', 'starting', 'destroying'].includes(value)) {
|
|
64
|
+
return style.yellow(value.replaceAll('_', ' '));
|
|
65
|
+
}
|
|
66
|
+
if (value === 'paused' || value === 'archived' || value === 'stopped') {
|
|
67
|
+
return style.blue(value);
|
|
68
|
+
}
|
|
69
|
+
if (value === 'failed' || value === 'interrupted' || value === 'destroyed') {
|
|
70
|
+
return style.red(value);
|
|
71
|
+
}
|
|
72
|
+
return value.replaceAll('_', ' ');
|
|
73
|
+
}
|
|
74
|
+
function rounded(value, fractionDigits = 1) {
|
|
75
|
+
return Number.isInteger(value) ? String(value) : value.toFixed(fractionDigits).replace(/\.0$/, '');
|
|
76
|
+
}
|
|
77
|
+
export function formatCpu(milliCpu) {
|
|
78
|
+
if (typeof milliCpu !== 'number' || !Number.isFinite(milliCpu))
|
|
79
|
+
return undefined;
|
|
80
|
+
return `${rounded(milliCpu / 1000, 3)} CPU`;
|
|
81
|
+
}
|
|
82
|
+
export function formatMib(mebibytes) {
|
|
83
|
+
if (typeof mebibytes !== 'number' || !Number.isFinite(mebibytes))
|
|
84
|
+
return undefined;
|
|
85
|
+
if (mebibytes >= 1024)
|
|
86
|
+
return `${rounded(mebibytes / 1024)} GiB`;
|
|
87
|
+
return `${rounded(mebibytes)} MiB`;
|
|
88
|
+
}
|
|
89
|
+
export function formatSeconds(seconds) {
|
|
90
|
+
if (typeof seconds !== 'number' || !Number.isFinite(seconds))
|
|
91
|
+
return undefined;
|
|
92
|
+
if (seconds === 0)
|
|
93
|
+
return 'never';
|
|
94
|
+
if (seconds < 60)
|
|
95
|
+
return `${rounded(seconds)}s`;
|
|
96
|
+
if (seconds < 3600)
|
|
97
|
+
return `${rounded(seconds / 60)}m`;
|
|
98
|
+
if (seconds < 86_400)
|
|
99
|
+
return `${rounded(seconds / 3600)}h`;
|
|
100
|
+
return `${rounded(seconds / 86_400)}d`;
|
|
101
|
+
}
|
|
102
|
+
export function formatAge(value, now = Date.now()) {
|
|
103
|
+
if (typeof value !== 'string')
|
|
104
|
+
return undefined;
|
|
105
|
+
const timestamp = Date.parse(value);
|
|
106
|
+
if (!Number.isFinite(timestamp))
|
|
107
|
+
return value;
|
|
108
|
+
const seconds = Math.max(0, Math.floor((now - timestamp) / 1000));
|
|
109
|
+
if (seconds < 5)
|
|
110
|
+
return 'just now';
|
|
111
|
+
return `${formatSeconds(seconds)} ago`;
|
|
112
|
+
}
|
|
113
|
+
export function shortDigest(value) {
|
|
114
|
+
if (typeof value !== 'string' || !value)
|
|
115
|
+
return undefined;
|
|
116
|
+
const digest = value.startsWith('sha256:') ? value.slice(7) : value;
|
|
117
|
+
return digest.slice(0, 12);
|
|
118
|
+
}
|
|
119
|
+
export function printJson(value) {
|
|
120
|
+
console.log(JSON.stringify(value, null, 2));
|
|
121
|
+
}
|
|
122
|
+
export class StatusLine {
|
|
123
|
+
lastMessage;
|
|
124
|
+
frame = 0;
|
|
125
|
+
update(message) {
|
|
126
|
+
if (process.stderr.isTTY) {
|
|
127
|
+
const frames = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
|
|
128
|
+
const prefix = style.cyan(frames[this.frame++ % frames.length], process.stderr);
|
|
129
|
+
process.stderr.write(`\r\u001b[2K${prefix} ${message}`);
|
|
130
|
+
this.lastMessage = message;
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
if (message !== this.lastMessage) {
|
|
134
|
+
process.stderr.write(`→ ${message}\n`);
|
|
135
|
+
this.lastMessage = message;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
succeed(message) {
|
|
139
|
+
this.finish(`${style.green('✓', process.stderr)} ${message}`);
|
|
140
|
+
}
|
|
141
|
+
fail(message) {
|
|
142
|
+
this.finish(`${style.red('✗', process.stderr)} ${message}`);
|
|
143
|
+
}
|
|
144
|
+
clear() {
|
|
145
|
+
if (process.stderr.isTTY && this.lastMessage)
|
|
146
|
+
process.stderr.write('\r\u001b[2K');
|
|
147
|
+
this.lastMessage = undefined;
|
|
148
|
+
}
|
|
149
|
+
finish(message) {
|
|
150
|
+
if (process.stderr.isTTY && this.lastMessage) {
|
|
151
|
+
process.stderr.write(`\r\u001b[2K${message}\n`);
|
|
152
|
+
}
|
|
153
|
+
else {
|
|
154
|
+
process.stderr.write(`${message}\n`);
|
|
155
|
+
}
|
|
156
|
+
this.lastMessage = undefined;
|
|
157
|
+
}
|
|
158
|
+
}
|
package/dist/version.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const CLI_VERSION = '0.1.91';
|
package/package.json
CHANGED
|
@@ -1,13 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@newly/cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.91",
|
|
4
4
|
"description": "Develop, provision, and deploy Newly backends from your terminal",
|
|
5
5
|
"license": "UNLICENSED",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"bin": {
|
|
8
8
|
"newly": "dist/index.js",
|
|
9
|
-
"newly-dev": "dist/newly-dev.js"
|
|
10
|
-
"runcloud": "dist/run-cloud.js"
|
|
9
|
+
"newly-dev": "dist/newly-dev.js"
|
|
11
10
|
},
|
|
12
11
|
"files": [
|
|
13
12
|
"dist"
|
|
@@ -16,22 +15,29 @@
|
|
|
16
15
|
"node": ">=20"
|
|
17
16
|
},
|
|
18
17
|
"scripts": {
|
|
19
|
-
"prebuild": "node -e \"require('node:fs').rmSync('dist', { recursive: true, force: true })\"",
|
|
18
|
+
"prebuild": "node scripts/sync-version.mjs && node -e \"require('node:fs').rmSync('dist', { recursive: true, force: true })\"",
|
|
20
19
|
"build": "tsc",
|
|
21
|
-
"build:run-cloud": "
|
|
20
|
+
"build:run-cloud": "bun run build && node dist/run-cloud.js --help >/dev/null",
|
|
21
|
+
"package:runcloud": "bun run build:run-cloud && node scripts/prepare-runcloud-package.mjs",
|
|
22
|
+
"publish:runcloud": "bun run lint && bun run test && bun run package:runcloud && npm publish .publish/runcloud --access public",
|
|
22
23
|
"dev": "tsx src/index.ts",
|
|
23
24
|
"lint": "eslint src",
|
|
24
25
|
"test": "vitest run",
|
|
25
|
-
"prepublishOnly": "
|
|
26
|
+
"prepublishOnly": "bun run lint && bun run test && bun run build",
|
|
27
|
+
"sync-version": "node scripts/sync-version.mjs",
|
|
28
|
+
"test:tty": "node scripts/test-tty-login.mjs"
|
|
26
29
|
},
|
|
27
30
|
"dependencies": {
|
|
28
31
|
"commander": "^12.1.0",
|
|
32
|
+
"esbuild": "0.25.12",
|
|
29
33
|
"google-auth-library": "^9.15.0",
|
|
30
|
-
"tar": "^7.4.3"
|
|
34
|
+
"tar": "^7.4.3",
|
|
35
|
+
"ws": "^8.21.0"
|
|
31
36
|
},
|
|
32
37
|
"devDependencies": {
|
|
33
38
|
"@eslint/js": "^9.0.0",
|
|
34
39
|
"@types/node": "^20.0.0",
|
|
40
|
+
"@types/ws": "^8.18.1",
|
|
35
41
|
"eslint": "^9.0.0",
|
|
36
42
|
"tsx": "^4.0.0",
|
|
37
43
|
"typescript": "^5.0.0",
|