@newly/cli 0.1.93 → 0.1.95

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
@@ -27,30 +27,47 @@ newly email status # inspect Newly Email setup
27
27
  ## Auth
28
28
 
29
29
  ```bash
30
- newly login # opens a browser, stores a session token in ~/.newly/credentials
31
- newly login --manual # paste the credential instead of using the browser callback
32
- newly whoami # show the stored credential: account, API, expiry, file
33
- newly logout # delete stored credentials
30
+ newly login # pick a method in the terminal, store the credential in ~/.newly/credentials
31
+ newly login --email # emailed six-digit code; never opens a browser
32
+ newly login --google # or --github: shows a code, you confirm it in the browser
33
+ newly whoami # show the stored credential: account, API, expiry, file
34
+ newly logout # revoke the credential and delete it locally
34
35
  ```
35
36
 
36
- The token expires. Re-run `newly login` when commands report you're logged out —
37
- it reports the existing session first and offers to keep it, so running it out of
38
- habit no longer throws away a working credential.
37
+ Sign-in goes through Better Auth on the Newly control plane (`https://api.newly.app`).
38
+ `--email` finishes entirely in the terminal, so it works over SSH and in
39
+ containers. Google and GitHub use the device flow: the CLI prints a short code,
40
+ you confirm it at `newly.app/cli-auth`, and the credential is minted server-side —
41
+ it never passes through the browser and nothing is posted to localhost.
42
+
43
+ Re-run `newly login` when commands report you're logged out — it reports the
44
+ existing session first and offers to keep it, so running it out of habit no
45
+ longer throws away a working credential.
46
+
47
+ `newly logout` revokes the credential server-side as well as deleting it, unless
48
+ you supplied it yourself with `--token` (that one is yours, often shared with CI,
49
+ so it is only removed locally).
39
50
 
40
51
  `newly whoami` reads the stored credential without calling the API, and exits
41
52
  non-zero when there is none or it has expired:
42
53
 
43
54
  ```
44
55
  Account you@example.com
45
- API https://api.newly.app
46
- Expires 8/30/2026, 5:02:58 PM
56
+ API https://your-backend.modal.run/api
57
+ Issued by https://api.newly.app
58
+ Expires never
47
59
  Credential ~/.newly/credentials
48
60
  ```
49
61
 
62
+ `API` is where commands go; `Issued by` is where the credential came from, and
63
+ only appears when the two differ — which they do for Newly, because sign-in lives
64
+ on the control plane and the commands do not. `logout` revokes against the
65
+ issuer.
66
+
50
67
  It reports whatever would actually be sent, including a credential supplied
51
68
  through `NEWLY_API_TOKEN`, which takes precedence over the saved file. The
52
69
  account line is only filled in when the sign-in method revealed an address; the
53
- browser handoff never sees one.
70
+ device flow does not always see one.
54
71
 
55
72
  ## Project linking
56
73
 
@@ -167,6 +184,7 @@ newly-dev login \
167
184
  | `NEWLY_DEV_HOME` | Credentials directory used by `newly-dev` (default: `~/.newly-dev`). |
168
185
  | `NEWLY_APP_URL` | Default for `--app-url` at module load. |
169
186
  | `NEWLY_DEV_APP_URL` | When set, `newly-dev` surfaces this as `NEWLY_APP_URL`. |
187
+ | `NEWLY_AUTH_API_URL` | Control plane to sign in against (default `https://api.newly.app`). Point it at a dev control plane to log in there. Note this is the issuer, not the API — `--api-url` still names the API. |
170
188
 
171
189
  ### Typical dev workflow
172
190
 
@@ -1,13 +1,17 @@
1
- import http from 'node:http';
2
- import crypto from 'node:crypto';
3
1
  import { hostname } from 'node:os';
4
2
  import { spawn } from 'node:child_process';
5
3
  import { ApiClient, ApiError } from '../api.js';
6
- import { clearCredentials, credentialsDisplayPath, loadCredentials, loginCommandName, normalizeApiUrl, sanitizeAccount, saveCredentials, } from '../config.js';
4
+ import { clearCredentials, credentialsDisplayPath, loadCredentials, loginCommandName, normalizeApiUrl, saveCredentials, } from '../config.js';
7
5
  import { acquirePromptIO, ask, PromptAbortedError, select, } from '../prompt.js';
8
6
  import { AuthError, createApiKey, fetchProviders, isValidEmail, isValidOtp, normalizeOtp, sendEmailCode, verifyEmailCode, } from '../runCloudAuth.js';
9
- const LOGIN_TIMEOUT_MS = 5 * 60 * 1000;
10
7
  export const DEFAULT_RUN_CLOUD_API_URL = 'https://api.run.cloud';
8
+ export const DEFAULT_NEWLY_AUTH_API_URL = 'https://api.newly.app';
9
+ export function authApiUrlFor(isRunCloud, explicitApiUrl) {
10
+ if (isRunCloud) {
11
+ return explicitApiUrl ?? process.env.RUN_CLOUD_API_URL ?? DEFAULT_RUN_CLOUD_API_URL;
12
+ }
13
+ return process.env.NEWLY_AUTH_API_URL ?? DEFAULT_NEWLY_AUTH_API_URL;
14
+ }
11
15
  export function loginChoices(providers) {
12
16
  const choices = [];
13
17
  if (providers.emailOtp) {
@@ -34,92 +38,6 @@ export function methodFromFlags(flags) {
34
38
  }
35
39
  return chosen[0] ?? null;
36
40
  }
37
- export async function startCallbackServer(state, appOrigin, timeoutMs) {
38
- let resolvePayload;
39
- let rejectPayload;
40
- const payloadPromise = new Promise((resolve, reject) => {
41
- resolvePayload = resolve;
42
- rejectPayload = reject;
43
- });
44
- const timer = setTimeout(() => rejectPayload(new Error(`Login timed out after ${timeoutMs / 1000}s.\n` +
45
- "If the browser said the local handoff was blocked, Chrome gates HTTPS→localhost behind a permission: " +
46
- 'click the icon at the left of the address bar, allow "access to apps on this device", ' +
47
- 'then run login again (or use --manual).')), timeoutMs);
48
- const corsHeaders = {
49
- 'Access-Control-Allow-Origin': appOrigin,
50
- 'Access-Control-Allow-Methods': 'POST, OPTIONS',
51
- 'Access-Control-Allow-Headers': 'content-type',
52
- 'Access-Control-Allow-Private-Network': 'true',
53
- };
54
- const server = http.createServer((req, res) => {
55
- if (req.method === 'OPTIONS') {
56
- res.writeHead(204, corsHeaders);
57
- res.end();
58
- return;
59
- }
60
- if (req.method !== 'POST') {
61
- res.writeHead(405, corsHeaders);
62
- res.end();
63
- return;
64
- }
65
- let body = '';
66
- req.on('data', (chunk) => (body += chunk));
67
- req.on('end', () => {
68
- try {
69
- const parsed = JSON.parse(body);
70
- if (parsed.state !== state) {
71
- res.writeHead(403, corsHeaders);
72
- res.end('state mismatch');
73
- return;
74
- }
75
- if (typeof parsed.token !== 'string' ||
76
- parsed.token.length === 0 ||
77
- typeof parsed.expiresIn !== 'number' ||
78
- !Number.isFinite(parsed.expiresIn) ||
79
- parsed.expiresIn <= 0 ||
80
- typeof parsed.apiUrl !== 'string') {
81
- res.writeHead(400, corsHeaders);
82
- res.end('invalid credential payload');
83
- return;
84
- }
85
- const apiUrl = new URL(parsed.apiUrl);
86
- if (!['http:', 'https:'].includes(apiUrl.protocol)) {
87
- res.writeHead(400, corsHeaders);
88
- res.end('invalid API URL');
89
- return;
90
- }
91
- res.writeHead(200, { ...corsHeaders, 'Content-Type': 'application/json' });
92
- res.end(JSON.stringify({ ok: true }));
93
- clearTimeout(timer);
94
- const account = sanitizeAccount(parsed.account);
95
- resolvePayload({
96
- token: parsed.token,
97
- expiresIn: parsed.expiresIn,
98
- apiUrl: parsed.apiUrl,
99
- ...(account ? { account } : {}),
100
- });
101
- }
102
- catch {
103
- res.writeHead(400, corsHeaders);
104
- res.end('bad request');
105
- }
106
- });
107
- });
108
- const port = await new Promise((resolve) => {
109
- server.listen(0, '127.0.0.1', () => {
110
- const addr = server.address();
111
- resolve(typeof addr === 'object' && addr ? addr.port : 0);
112
- });
113
- });
114
- return {
115
- port,
116
- waitForToken: () => payloadPromise,
117
- close: () => {
118
- clearTimeout(timer);
119
- server.close();
120
- },
121
- };
122
- }
123
41
  export function openBrowser(url) {
124
42
  const [cmd, args] = process.platform === 'darwin'
125
43
  ? ['open', [url]]
@@ -131,13 +49,9 @@ export function openBrowser(url) {
131
49
  });
132
50
  child.unref();
133
51
  }
134
- export function cliAuthUrl(appUrl, port, state, provider) {
135
- const url = new URL('/cli-auth', appUrl);
136
- url.searchParams.set('port', String(port));
137
- url.searchParams.set('state', state);
138
- if (provider)
139
- url.searchParams.set('provider', provider);
140
- return url.toString();
52
+ export function commandApiUrls(apiUrl, authApiUrl) {
53
+ const same = normalizeApiUrl(apiUrl) === normalizeApiUrl(authApiUrl);
54
+ return { apiUrl, ...(same ? {} : { authApiUrl }) };
141
55
  }
142
56
  export async function emailLogin(apiUrl, presetEmail, io) {
143
57
  const email = presetEmail?.trim()
@@ -179,27 +93,6 @@ function hostLabel() {
179
93
  return 'cli';
180
94
  }
181
95
  }
182
- export function parseManualCredential(blob) {
183
- let parsed;
184
- try {
185
- parsed = JSON.parse(blob);
186
- }
187
- catch {
188
- throw new Error('Could not parse the pasted credential as JSON. Copy the entire blob shown in the browser and try again.');
189
- }
190
- const c = parsed;
191
- if (typeof c.token !== 'string' || typeof c.expiresIn !== 'number' || typeof c.apiUrl !== 'string') {
192
- throw new Error('Pasted credential is missing required fields (expected JSON with token, expiresIn, apiUrl). ' +
193
- 'Copy the entire blob shown in the browser.');
194
- }
195
- const account = sanitizeAccount(c.account);
196
- return {
197
- token: c.token,
198
- expiresIn: c.expiresIn,
199
- apiUrl: c.apiUrl,
200
- ...(account ? { account } : {}),
201
- };
202
- }
203
96
  export async function verifyRunCloudCredential(apiUrl, token) {
204
97
  try {
205
98
  await new ApiClient(normalizeApiUrl(apiUrl), token).get('/run-cloud/account');
@@ -241,6 +134,7 @@ export function whoamiReport(creds) {
241
134
  const rows = [
242
135
  ['Account', creds.account ?? 'not recorded (browser sign-in never sees an address)'],
243
136
  ['API', creds.apiUrl],
137
+ ...(creds.authApiUrl ? [['Issued by', creds.authApiUrl]] : []),
244
138
  ['Expires', expiresAtLabel(creds) ?? 'never'],
245
139
  ['Credential', credentialsDisplayPath()],
246
140
  ];
@@ -274,9 +168,9 @@ export function registerLogin(program) {
274
168
  .command('login')
275
169
  .description(isRunCloud
276
170
  ? 'Log in with your run.cloud account: email, Google, or GitHub'
277
- : 'Log in with your Newly account via the browser')
171
+ : 'Log in with your Newly account: email, Google, or GitHub')
278
172
  .option('--app-url <url>', 'product web app URL', defaultAppUrl)
279
- .option('--manual', 'newly skin only: paste the credential from the browser instead of using the local callback', false)
173
+ .option('--manual', 'deprecated no-op: the browser→localhost paste fallback is gone (device flow works headless)', false)
280
174
  .option('--api-url <url>', 'explicit API URL (skips browser; use with --token)')
281
175
  .option('--token <token>', 'explicit credential token (skips browser; use with --api-url)')
282
176
  .option('--expires-in <seconds>', 'session lifetime when using --token (default 7 days)', (v) => Number(v), 604800)
@@ -319,30 +213,18 @@ export function registerLogin(program) {
319
213
  console.log(` Keeping the credential at ${credentialsDisplayPath()}\n`);
320
214
  return;
321
215
  }
322
- if (opts.manual && !isRunCloud) {
323
- const { createInterface } = await import('node:readline/promises');
324
- const rl = createInterface({ input: process.stdin, output: process.stdout });
325
- const blob = await rl.question('Paste the credential from the browser: ');
326
- rl.close();
327
- const parsed = parseManualCredential(blob);
328
- saveCredentials({
329
- token: parsed.token,
330
- apiUrl: parsed.apiUrl,
331
- appUrl: opts.appUrl,
332
- expiresAt: Math.floor(Date.now() / 1000) + parsed.expiresIn,
333
- ...(parsed.account ? { account: parsed.account } : {}),
334
- });
335
- console.log('Logged in.');
336
- return;
216
+ if (opts.manual) {
217
+ console.log('Note: --manual is a no-op now — sign-in never posts to localhost, so it works over SSH and headless already.');
337
218
  }
219
+ const authApiUrl = authApiUrlFor(isRunCloud, opts.apiUrl);
338
220
  let socialProvider;
339
- if (isRunCloud && !opts.browser) {
340
- const authApiUrl = opts.apiUrl ?? process.env.RUN_CLOUD_API_URL ?? DEFAULT_RUN_CLOUD_API_URL;
221
+ if (!opts.browser) {
341
222
  let method = methodFromFlags(opts);
342
223
  const terminal = method === 'email' || !method ? acquirePromptIO() : null;
343
224
  try {
225
+ let providers = null;
344
226
  if (!method && terminal) {
345
- const providers = await fetchProviders(authApiUrl);
227
+ providers = await fetchProviders(authApiUrl);
346
228
  const choices = loginChoices(providers);
347
229
  if (choices.length === 0) {
348
230
  throw new Error(`${authApiUrl} has no sign-in methods enabled.`);
@@ -351,20 +233,27 @@ export function registerLogin(program) {
351
233
  }
352
234
  if (method === 'email') {
353
235
  const preset = typeof opts.email === 'string' ? opts.email : undefined;
236
+ providers ??= await fetchProviders(authApiUrl);
354
237
  const { email, token } = await emailLogin(authApiUrl, preset, terminal?.io);
355
238
  saveCredentials({
356
239
  token,
357
- apiUrl: authApiUrl,
358
240
  appUrl: opts.appUrl,
359
241
  expiresAt: Number.MAX_SAFE_INTEGER,
360
242
  revocable: true,
361
243
  account: email,
244
+ ...commandApiUrls(providers.cliApiUrl ?? authApiUrl, authApiUrl),
362
245
  });
363
246
  console.log(`\n Signed in as ${email}`);
364
247
  console.log(` Credential saved to ${credentialsDisplayPath()}\n`);
365
248
  console.log(' Try:');
366
- console.log(' runcloud ios create --open');
367
- console.log(' runcloud sandbox run "echo hello"');
249
+ if (isRunCloud) {
250
+ console.log(' runcloud ios create --open');
251
+ console.log(' runcloud sandbox run "echo hello"');
252
+ }
253
+ else {
254
+ console.log(' newly whoami');
255
+ console.log(' newly status');
256
+ }
368
257
  return;
369
258
  }
370
259
  }
@@ -374,54 +263,24 @@ export function registerLogin(program) {
374
263
  if (method === 'google' || method === 'github')
375
264
  socialProvider = method;
376
265
  }
377
- if (isRunCloud) {
378
- if (opts.manual) {
379
- console.log('Note: --manual is no longer needed for run.cloud — `runcloud login` works over SSH and headless via device flow.');
380
- }
381
- const apiUrl = opts.apiUrl ?? process.env.RUN_CLOUD_API_URL ?? DEFAULT_RUN_CLOUD_API_URL;
382
- const { runDeviceLogin } = await import('../deviceLogin.js');
383
- const payload = await runDeviceLogin({
384
- apiUrl,
385
- appUrl: opts.appUrl,
386
- provider: socialProvider,
387
- openBrowser,
388
- });
389
- await verifyRunCloudCredential(payload.apiUrl, payload.token);
390
- saveCredentials({
391
- token: payload.token,
392
- apiUrl: payload.apiUrl,
393
- appUrl: opts.appUrl,
394
- expiresAt: Math.floor(Date.now() / 1000) + payload.expiresIn,
395
- revocable: true,
396
- ...(payload.account ? { account: payload.account } : {}),
397
- });
398
- console.log(`Logged in. Credentials saved to ${credentialsDisplayPath()}`);
399
- return;
400
- }
401
- const state = crypto.randomBytes(16).toString('hex');
402
- const appOrigin = new URL(opts.appUrl).origin;
403
- const { port, waitForToken, close } = await startCallbackServer(state, appOrigin, LOGIN_TIMEOUT_MS);
404
- const loginUrl = cliAuthUrl(opts.appUrl, port, state, socialProvider);
405
- console.log(`Opening your browser to authorize the CLI…\n ${loginUrl}\n`);
406
- console.log("If the browser doesn't open, paste the URL above into it manually.");
407
- openBrowser(loginUrl);
408
- try {
409
- const payload = await waitForToken();
410
- if (isRunCloud)
411
- await verifyRunCloudCredential(payload.apiUrl, payload.token);
412
- saveCredentials({
413
- token: payload.token,
414
- apiUrl: payload.apiUrl,
415
- appUrl: opts.appUrl,
416
- expiresAt: Math.floor(Date.now() / 1000) + payload.expiresIn,
417
- ...(payload.account ? { account: payload.account } : {}),
418
- });
419
- console.log(`Logged in${payload.account ? ` as ${payload.account}` : ''}. ` +
420
- `Credentials saved to ${credentialsDisplayPath()}`);
421
- }
422
- finally {
423
- close();
424
- }
266
+ const { runDeviceLogin } = await import('../deviceLogin.js');
267
+ const payload = await runDeviceLogin({
268
+ apiUrl: authApiUrl,
269
+ appUrl: opts.appUrl,
270
+ provider: socialProvider,
271
+ openBrowser,
272
+ });
273
+ await verifyRunCloudCredential(authApiUrl, payload.token);
274
+ saveCredentials({
275
+ token: payload.token,
276
+ appUrl: opts.appUrl,
277
+ expiresAt: Math.floor(Date.now() / 1000) + payload.expiresIn,
278
+ revocable: true,
279
+ ...(payload.account ? { account: payload.account } : {}),
280
+ ...commandApiUrls(payload.apiUrl, authApiUrl),
281
+ });
282
+ console.log(`Logged in${payload.account ? ` as ${payload.account}` : ''}. ` +
283
+ `Credentials saved to ${credentialsDisplayPath()}`);
425
284
  }
426
285
  catch (e) {
427
286
  if (e instanceof PromptAbortedError) {
@@ -449,13 +308,11 @@ export function registerLogin(program) {
449
308
  });
450
309
  program
451
310
  .command('logout')
452
- .description(isRunCloud
453
- ? 'Revoke this machine’s credential and delete it locally'
454
- : 'Delete the stored CLI credential')
311
+ .description('Revoke this machine’s credential and delete it locally')
455
312
  .action(async () => {
456
313
  const creds = loadCredentials();
457
- if (isRunCloud && creds?.revocable) {
458
- console.log(await revokeRunCloudCredential(creds.apiUrl, creds.token));
314
+ if (creds?.revocable) {
315
+ console.log(await revokeRunCloudCredential(creds.authApiUrl ?? creds.apiUrl, creds.token));
459
316
  }
460
317
  clearCredentials();
461
318
  console.log('Logged out.');
@@ -358,8 +358,17 @@ export function registerSandbox(program) {
358
358
  else
359
359
  console.log(renderSandbox(exposedRecord, 'created'));
360
360
  }));
361
- withOutput(sandbox.command('list').description('List sandboxes').option('--state <state>', 'filter by state')).action((opts) => run(async () => {
362
- const qs = opts.state ? `?state=${encodeURIComponent(opts.state)}` : '';
361
+ withOutput(sandbox
362
+ .command('list')
363
+ .description('List sandboxes')
364
+ .option('--state <state>', 'filter by state')
365
+ .option('--name <name>', 'filter by name (exact)')).action((opts) => run(async () => {
366
+ const query = new URLSearchParams();
367
+ if (opts.state)
368
+ query.set('state', opts.state);
369
+ if (opts.name)
370
+ query.set('name', opts.name);
371
+ const qs = query.size ? `?${query}` : '';
363
372
  const api = runCloudApi();
364
373
  const [data, boxes] = await Promise.all([
365
374
  api.get(`/run-cloud/sandboxes${qs}`),
@@ -525,6 +534,17 @@ export function registerSandbox(program) {
525
534
  console.log(`${style.green('✓')} ${lifecycle.pastTense} ${id}`);
526
535
  }));
527
536
  }
537
+ withOutput(sandbox
538
+ .command('rename')
539
+ .description('Change a sandbox name (the label `list --name` filters on)')
540
+ .argument('<id>', 'sandbox id')
541
+ .argument('<name>', 'new name; pass "" to clear it')).action((id, name, opts) => run(async () => {
542
+ const record = await runCloudApi().post(`/run-cloud/sandboxes/${encodeURIComponent(id)}/name`, { name });
543
+ if (opts.json)
544
+ printJson(record);
545
+ else
546
+ console.log(`${style.green('✓')} Renamed ${id} to ${name || '(no name)'}`);
547
+ }));
528
548
  withOutput(sandbox
529
549
  .command('logs')
530
550
  .description('Show saved console logs (available after a sandbox is deleted)')
@@ -1,10 +1,11 @@
1
- import { normalizeApiUrl, sanitizeAccount } from "./config.js";
1
+ import { loginCommandName, normalizeApiUrl, sanitizeAccount } from "./config.js";
2
2
  export async function runDeviceLogin(deps) {
3
3
  const fetchFn = deps.fetchFn ?? fetch;
4
4
  const log = deps.log ?? ((m) => console.log(m));
5
5
  const sleep = deps.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
6
6
  const now = deps.now ?? (() => Date.now());
7
7
  const base = normalizeApiUrl(deps.apiUrl);
8
+ const retry = loginCommandName();
8
9
  const startRes = await fetchFn(`${base}/run-cloud/cli-auth/device`, {
9
10
  method: "POST",
10
11
  headers: { "content-type": "application/json" },
@@ -23,7 +24,7 @@ export async function runDeviceLogin(deps) {
23
24
  const deadline = now() + start.expires_in * 1000;
24
25
  for (;;) {
25
26
  if (now() >= deadline)
26
- throw new Error("Login timed out. Run `runcloud login` again.");
27
+ throw new Error(`Login timed out. Run \`${retry}\` again.`);
27
28
  await sleep(intervalMs);
28
29
  const res = await fetchFn(`${base}/run-cloud/cli-auth/token`, {
29
30
  method: "POST",
@@ -48,7 +49,7 @@ export async function runDeviceLogin(deps) {
48
49
  continue;
49
50
  }
50
51
  if (error === "access_denied")
51
- throw new Error("Authorization was denied. Run `runcloud login` again.");
52
- throw new Error("The login code expired. Run `runcloud login` again.");
52
+ throw new Error(`Authorization was denied. Run \`${retry}\` again.`);
53
+ throw new Error(`The login code expired. Run \`${retry}\` again.`);
53
54
  }
54
55
  }
@@ -41,11 +41,13 @@ export async function fetchProviders(apiUrl) {
41
41
  if (!response.ok)
42
42
  throw new AuthError(await readError(response), response.status);
43
43
  const payload = (await response.json());
44
+ const cliApiUrl = typeof payload.cliApiUrl === 'string' ? payload.cliApiUrl.trim() : '';
44
45
  return {
45
46
  emailOtp: payload.emailOtp === true,
46
47
  google: payload.google === true,
47
48
  github: payload.github === true,
48
49
  apple: payload.apple === true,
50
+ ...(cliApiUrl ? { cliApiUrl } : {}),
49
51
  };
50
52
  }
51
53
  export async function sendEmailCode(apiUrl, email) {
package/dist/version.js CHANGED
@@ -1 +1 @@
1
- export const CLI_VERSION = '0.1.93';
1
+ export const CLI_VERSION = '0.1.95';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@newly/cli",
3
- "version": "0.1.93",
3
+ "version": "0.1.95",
4
4
  "description": "Develop, provision, and deploy Newly backends from your terminal",
5
5
  "license": "UNLICENSED",
6
6
  "type": "module",