@newly/cli 0.1.0 → 0.1.2
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 +69 -10
- package/dist/api.js +26 -0
- package/dist/commands/build.js +250 -0
- package/dist/commands/create.js +1 -1
- package/dist/commands/creds.js +103 -0
- package/dist/commands/deploy.js +55 -16
- package/dist/commands/dns.js +185 -0
- package/dist/commands/domains.js +201 -0
- package/dist/commands/email.js +86 -0
- package/dist/commands/login.js +27 -3
- package/dist/commands/misc.js +38 -11
- package/dist/commands/run-cloud.js +272 -0
- package/dist/commands/services.js +39 -0
- package/dist/commands/stack.js +13 -11
- package/dist/commands/status.js +10 -10
- package/dist/commands/website.js +218 -0
- package/dist/config.js +31 -5
- package/dist/diagnose.js +159 -0
- package/dist/index.js +16 -2
- package/dist/newly-dev.js +10 -0
- package/dist/run-cloud.js +10 -0
- package/dist/services-manifest.js +63 -0
- package/package.json +15 -4
- package/dist/commands/up.js +0 -85
package/dist/diagnose.js
ADDED
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
const DEFAULT_URLS = {
|
|
2
|
+
dev: {
|
|
3
|
+
apiOps: 'https://cp-api-ops-dev.newly.app',
|
|
4
|
+
worker: 'https://cp-worker-dev.newly.app',
|
|
5
|
+
},
|
|
6
|
+
prod: {
|
|
7
|
+
apiOps: 'https://cp-api-ops-prod.newly.app',
|
|
8
|
+
worker: 'https://cp-worker-prod.newly.app',
|
|
9
|
+
},
|
|
10
|
+
};
|
|
11
|
+
function stripSlash(url) {
|
|
12
|
+
return url.replace(/\/+$/, '');
|
|
13
|
+
}
|
|
14
|
+
export function resolveTargets(env) {
|
|
15
|
+
if (env !== 'dev' && env !== 'prod') {
|
|
16
|
+
throw new Error(`--env must be dev or prod (got "${env}")`);
|
|
17
|
+
}
|
|
18
|
+
const upper = env.toUpperCase();
|
|
19
|
+
const apiOps = process.env[`DIAGNOSE_CP_API_OPS_URL_${upper}`] ??
|
|
20
|
+
process.env.DIAGNOSE_CP_API_OPS_URL ??
|
|
21
|
+
DEFAULT_URLS[env].apiOps;
|
|
22
|
+
const worker = process.env[`DIAGNOSE_CP_WORKER_URL_${upper}`] ??
|
|
23
|
+
process.env.DIAGNOSE_CP_WORKER_URL ??
|
|
24
|
+
DEFAULT_URLS[env].worker;
|
|
25
|
+
return [
|
|
26
|
+
{ service: 'cp-api', url: stripSlash(apiOps) },
|
|
27
|
+
{ service: 'cp-worker', url: stripSlash(worker) },
|
|
28
|
+
];
|
|
29
|
+
}
|
|
30
|
+
function extractAuthorization(headers) {
|
|
31
|
+
if (headers instanceof Headers) {
|
|
32
|
+
return headers.get('authorization') ?? undefined;
|
|
33
|
+
}
|
|
34
|
+
const h = headers;
|
|
35
|
+
return h?.Authorization ?? h?.authorization;
|
|
36
|
+
}
|
|
37
|
+
export async function resolveOpsAuthHeader(env = process.env) {
|
|
38
|
+
const audience = env.DIAGNOSE_OIDC_AUDIENCE ?? 'newly-diagnose-ops';
|
|
39
|
+
try {
|
|
40
|
+
const { GoogleAuth } = await import('google-auth-library');
|
|
41
|
+
const auth = new GoogleAuth();
|
|
42
|
+
const client = await auth.getIdTokenClient(audience);
|
|
43
|
+
const authz = extractAuthorization(await client.getRequestHeaders());
|
|
44
|
+
if (authz) {
|
|
45
|
+
return authz;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
}
|
|
50
|
+
const token = env.DIAGNOSE_OPS_TOKEN;
|
|
51
|
+
if (token) {
|
|
52
|
+
return `Bearer ${token}`;
|
|
53
|
+
}
|
|
54
|
+
throw new Error('No ops credential available. Run `gcloud auth application-default login` (ADC/OIDC), ' +
|
|
55
|
+
'or export DIAGNOSE_OPS_TOKEN with the break-glass ops token.');
|
|
56
|
+
}
|
|
57
|
+
function truncate(value, max = 200) {
|
|
58
|
+
const trimmed = value.trim();
|
|
59
|
+
return trimmed.length > max ? `${trimmed.slice(0, max)}...` : trimmed;
|
|
60
|
+
}
|
|
61
|
+
export async function fetchDiagnose(target, authHeader) {
|
|
62
|
+
try {
|
|
63
|
+
const res = await fetch(`${target.url}/diagnose`, { headers: { Authorization: authHeader } });
|
|
64
|
+
if (!res.ok) {
|
|
65
|
+
const body = await res.text().catch(() => '');
|
|
66
|
+
return {
|
|
67
|
+
service: target.service,
|
|
68
|
+
error: `HTTP ${res.status}${body ? `: ${truncate(body)}` : ''}`,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
return (await res.json());
|
|
72
|
+
}
|
|
73
|
+
catch (e) {
|
|
74
|
+
return { service: target.service, error: e instanceof Error ? e.message : String(e) };
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
function isDoc(result) {
|
|
78
|
+
return !('error' in result);
|
|
79
|
+
}
|
|
80
|
+
export function rollupVerdict(services) {
|
|
81
|
+
const names = Object.keys(services);
|
|
82
|
+
const down = names.filter((name) => !isDoc(services[name]));
|
|
83
|
+
if (down.length) {
|
|
84
|
+
return `DOWN - unreachable: ${down.join(', ')}`;
|
|
85
|
+
}
|
|
86
|
+
const degraded = [];
|
|
87
|
+
for (const name of names) {
|
|
88
|
+
const doc = services[name];
|
|
89
|
+
const bad = Object.entries(doc.health)
|
|
90
|
+
.filter(([, probe]) => !probe.ok)
|
|
91
|
+
.map(([probeName]) => probeName);
|
|
92
|
+
if (bad.length) {
|
|
93
|
+
degraded.push(`${name} (${bad.join(', ')})`);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
if (degraded.length) {
|
|
97
|
+
return `DEGRADED - ${degraded.join('; ')}`;
|
|
98
|
+
}
|
|
99
|
+
return `OK - ${names.length} service(s) healthy`;
|
|
100
|
+
}
|
|
101
|
+
export async function fanoutDiagnose(env) {
|
|
102
|
+
const targets = resolveTargets(env);
|
|
103
|
+
const authHeader = await resolveOpsAuthHeader();
|
|
104
|
+
const results = await Promise.all(targets.map((target) => fetchDiagnose(target, authHeader)));
|
|
105
|
+
const services = {};
|
|
106
|
+
results.forEach((result, index) => {
|
|
107
|
+
services[targets[index].service] = result;
|
|
108
|
+
});
|
|
109
|
+
return { rollup: rollupVerdict(services), services };
|
|
110
|
+
}
|
|
111
|
+
export function renderRollup(result, session) {
|
|
112
|
+
const lines = [result.rollup];
|
|
113
|
+
if (session) {
|
|
114
|
+
lines.push(`session: ${session}`);
|
|
115
|
+
}
|
|
116
|
+
lines.push('');
|
|
117
|
+
for (const [name, service] of Object.entries(result.services)) {
|
|
118
|
+
if (!isDoc(service)) {
|
|
119
|
+
lines.push(`${name}: UNREACHABLE - ${service.error}`);
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
const sha = service.deploy.git_sha ?? '(sha unknown)';
|
|
123
|
+
const revision = service.deploy.revision ?? '(revision unknown)';
|
|
124
|
+
lines.push(`${name}: ${sha} @ ${revision} [env=${service.env}]`);
|
|
125
|
+
const badProbes = Object.entries(service.health)
|
|
126
|
+
.filter(([, probe]) => !probe.ok)
|
|
127
|
+
.map(([probeName, probe]) => `${probeName}${probe.detail ? ` (${probe.detail})` : ''}`);
|
|
128
|
+
lines.push(` health: ${badProbes.length ? `DEGRADED - ${badProbes.join(', ')}` : 'all ok'}`);
|
|
129
|
+
for (const verdict of service.diagnosis) {
|
|
130
|
+
lines.push(` - ${verdict}`);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
return lines.join('\n');
|
|
134
|
+
}
|
|
135
|
+
export function registerDiagnose(program) {
|
|
136
|
+
program
|
|
137
|
+
.command('diagnose')
|
|
138
|
+
.description('Fan out /diagnose across control-plane services and roll up a verdict (dev/ops only)')
|
|
139
|
+
.option('--env <env>', 'dev or prod', 'dev')
|
|
140
|
+
.option('--session <id>', 'annotate the rollup with a session id you are investigating')
|
|
141
|
+
.option('--json', 'emit the merged JSON document instead of the human rollup')
|
|
142
|
+
.action(async (opts) => {
|
|
143
|
+
try {
|
|
144
|
+
if (opts.env !== 'dev' && opts.env !== 'prod') {
|
|
145
|
+
throw new Error('--env must be dev or prod');
|
|
146
|
+
}
|
|
147
|
+
const result = await fanoutDiagnose(opts.env);
|
|
148
|
+
if (opts.json) {
|
|
149
|
+
console.log(JSON.stringify({ session: opts.session ?? null, ...result }, null, 2));
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
console.log(renderRollup(result, opts.session));
|
|
153
|
+
}
|
|
154
|
+
catch (e) {
|
|
155
|
+
console.error(e instanceof Error ? e.message : String(e));
|
|
156
|
+
process.exitCode = 1;
|
|
157
|
+
}
|
|
158
|
+
});
|
|
159
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -6,9 +6,16 @@ import { registerLink } from './commands/link.js';
|
|
|
6
6
|
import { registerCreate } from './commands/create.js';
|
|
7
7
|
import { registerPull } from './commands/pull.js';
|
|
8
8
|
import { registerDeploy } from './commands/deploy.js';
|
|
9
|
-
import { registerUp } from './commands/up.js';
|
|
10
9
|
import { registerMisc } from './commands/misc.js';
|
|
11
10
|
import { registerStack } from './commands/stack.js';
|
|
11
|
+
import { registerCreds } from './commands/creds.js';
|
|
12
|
+
import { registerBuild } from './commands/build.js';
|
|
13
|
+
import { registerServices } from './commands/services.js';
|
|
14
|
+
import { registerEmail } from './commands/email.js';
|
|
15
|
+
import { registerWebsite } from './commands/website.js';
|
|
16
|
+
import { registerDomains } from './commands/domains.js';
|
|
17
|
+
import { registerDns } from './commands/dns.js';
|
|
18
|
+
import { registerDiagnose } from './diagnose.js';
|
|
12
19
|
const program = new Command('newly')
|
|
13
20
|
.description('Develop, provision, and deploy Newly backends')
|
|
14
21
|
.version('0.1.0');
|
|
@@ -18,7 +25,14 @@ registerLink(program);
|
|
|
18
25
|
registerCreate(program);
|
|
19
26
|
registerPull(program);
|
|
20
27
|
registerDeploy(program);
|
|
21
|
-
registerUp(program);
|
|
22
28
|
registerMisc(program);
|
|
23
29
|
registerStack(program);
|
|
30
|
+
registerCreds(program);
|
|
31
|
+
registerBuild(program);
|
|
32
|
+
registerServices(program);
|
|
33
|
+
registerEmail(program);
|
|
34
|
+
registerWebsite(program);
|
|
35
|
+
registerDomains(program);
|
|
36
|
+
registerDns(program);
|
|
37
|
+
registerDiagnose(program);
|
|
24
38
|
await program.parseAsync(process.argv);
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { homedir } from 'node:os';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
if (!process.env.NEWLY_HOME) {
|
|
5
|
+
process.env.NEWLY_HOME = process.env.NEWLY_DEV_HOME ?? join(homedir(), '.newly-dev');
|
|
6
|
+
}
|
|
7
|
+
if (!process.env.NEWLY_APP_URL && process.env.NEWLY_DEV_APP_URL) {
|
|
8
|
+
process.env.NEWLY_APP_URL = process.env.NEWLY_DEV_APP_URL;
|
|
9
|
+
}
|
|
10
|
+
await import('./index.js');
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { Command } from 'commander';
|
|
3
|
+
import { registerLogin } from './commands/login.js';
|
|
4
|
+
import { registerRunCloud } from './commands/run-cloud.js';
|
|
5
|
+
const program = new Command('runcloud')
|
|
6
|
+
.description('Create and control remote iOS simulator sessions')
|
|
7
|
+
.version('0.1.0');
|
|
8
|
+
registerLogin(program);
|
|
9
|
+
registerRunCloud(program);
|
|
10
|
+
await program.parseAsync(process.argv);
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
export const DEFAULT_BACKEND_SERVICES = [
|
|
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
|
+
];
|
|
7
|
+
const SERVICE_NAME = /^[a-z][a-z0-9-]{0,31}$/;
|
|
8
|
+
export function readBackendServices(dir) {
|
|
9
|
+
const manifestPath = join(dir, 'backend', 'newly.services.json');
|
|
10
|
+
if (!existsSync(manifestPath))
|
|
11
|
+
return DEFAULT_BACKEND_SERVICES;
|
|
12
|
+
const parsed = JSON.parse(readFileSync(manifestPath, 'utf8'));
|
|
13
|
+
if (!Array.isArray(parsed.services) || parsed.services.length === 0) {
|
|
14
|
+
throw new Error('backend/newly.services.json must contain a non-empty services array');
|
|
15
|
+
}
|
|
16
|
+
return parsed.services.map((raw) => {
|
|
17
|
+
if (!raw || typeof raw !== 'object')
|
|
18
|
+
throw new Error('Invalid service entry in newly.services.json');
|
|
19
|
+
const entry = raw;
|
|
20
|
+
const name = String(entry.name ?? '');
|
|
21
|
+
const path = String(entry.path ?? '');
|
|
22
|
+
const type = String(entry.type ?? 'worker');
|
|
23
|
+
if (!SERVICE_NAME.test(name))
|
|
24
|
+
throw new Error(`Invalid service name in newly.services.json: ${name}`);
|
|
25
|
+
if (!path)
|
|
26
|
+
throw new Error(`Service ${name} is missing path`);
|
|
27
|
+
if (type !== 'worker')
|
|
28
|
+
throw new Error(`Service ${name} has unsupported type: ${type}`);
|
|
29
|
+
return {
|
|
30
|
+
name,
|
|
31
|
+
path,
|
|
32
|
+
type,
|
|
33
|
+
schema: entry.schema === undefined ? undefined : entry.schema === null ? null : String(entry.schema),
|
|
34
|
+
secrets: entry.secrets === undefined ? undefined : String(entry.secrets),
|
|
35
|
+
public: entry.public === undefined ? true : Boolean(entry.public),
|
|
36
|
+
};
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
export function parseServiceFilter(value) {
|
|
40
|
+
if (!value)
|
|
41
|
+
return [];
|
|
42
|
+
const names = value
|
|
43
|
+
.split(',')
|
|
44
|
+
.map((s) => s.trim())
|
|
45
|
+
.filter(Boolean);
|
|
46
|
+
for (const name of names) {
|
|
47
|
+
if (!SERVICE_NAME.test(name))
|
|
48
|
+
throw new Error(`Invalid service name: ${name}`);
|
|
49
|
+
}
|
|
50
|
+
return names;
|
|
51
|
+
}
|
|
52
|
+
export function assertKnownServices(services, selected) {
|
|
53
|
+
if (selected.length === 0)
|
|
54
|
+
return;
|
|
55
|
+
const known = new Set(services.map((s) => s.name));
|
|
56
|
+
const unknown = selected.filter((s) => !known.has(s));
|
|
57
|
+
if (unknown.length)
|
|
58
|
+
throw new Error(`Unknown service(s): ${unknown.join(', ')}`);
|
|
59
|
+
}
|
|
60
|
+
export function serviceUrl(projectId, env, service) {
|
|
61
|
+
const suffix = env === 'prod' ? 'api.newlyapp.com' : 'api-dev.newlyapp.com';
|
|
62
|
+
return `https://${service}-${projectId}-${env}.${suffix}`;
|
|
63
|
+
}
|
package/package.json
CHANGED
|
@@ -1,14 +1,24 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@newly/cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"description": "Develop, provision, and deploy Newly backends from your terminal",
|
|
5
5
|
"license": "UNLICENSED",
|
|
6
6
|
"type": "module",
|
|
7
|
-
"bin": {
|
|
8
|
-
|
|
9
|
-
|
|
7
|
+
"bin": {
|
|
8
|
+
"newly": "dist/index.js",
|
|
9
|
+
"newly-dev": "dist/newly-dev.js",
|
|
10
|
+
"runcloud": "dist/run-cloud.js"
|
|
11
|
+
},
|
|
12
|
+
"files": [
|
|
13
|
+
"dist"
|
|
14
|
+
],
|
|
15
|
+
"engines": {
|
|
16
|
+
"node": ">=20"
|
|
17
|
+
},
|
|
10
18
|
"scripts": {
|
|
19
|
+
"prebuild": "node -e \"require('node:fs').rmSync('dist', { recursive: true, force: true })\"",
|
|
11
20
|
"build": "tsc",
|
|
21
|
+
"build:run-cloud": "tsc && node dist/run-cloud.js --help >/dev/null",
|
|
12
22
|
"dev": "tsx src/index.ts",
|
|
13
23
|
"lint": "eslint src",
|
|
14
24
|
"test": "vitest run",
|
|
@@ -16,6 +26,7 @@
|
|
|
16
26
|
},
|
|
17
27
|
"dependencies": {
|
|
18
28
|
"commander": "^12.1.0",
|
|
29
|
+
"google-auth-library": "^9.15.0",
|
|
19
30
|
"tar": "^7.4.3"
|
|
20
31
|
},
|
|
21
32
|
"devDependencies": {
|
package/dist/commands/up.js
DELETED
|
@@ -1,85 +0,0 @@
|
|
|
1
|
-
import { existsSync } from 'node:fs';
|
|
2
|
-
import { join } from 'node:path';
|
|
3
|
-
import { ApiClient, ApiError, friendlyApiError } from '../api.js';
|
|
4
|
-
import { requireCredentials, resolveProject } from '../config.js';
|
|
5
|
-
import { runDevDeploy, runProdDeploy } from './deploy.js';
|
|
6
|
-
function defaultHasLocalBackend(dir) {
|
|
7
|
-
return existsSync(join(dir, 'backend', 'scripts', 'deploy.sh'));
|
|
8
|
-
}
|
|
9
|
-
export async function runUp(deps) {
|
|
10
|
-
const { client, projectId, env, region, cwd, print, hasLocalBackend = defaultHasLocalBackend, deployDev = runDevDeploy, deployProd = runProdDeploy, } = deps;
|
|
11
|
-
const stackInfo = await client.get(`/newly_backend/stack/${projectId}`);
|
|
12
|
-
if (!stackInfo.cli_deployable) {
|
|
13
|
-
throw new Error('This project is on the Specular stack (a single Node/drizzle backend), which `newly up` does not support. ' +
|
|
14
|
-
'Only newly-backend-stack projects (Python api-service + Node auth-service) are CLI-deployable.');
|
|
15
|
-
}
|
|
16
|
-
const status = await client.get(`/newly_backend/status/${projectId}`);
|
|
17
|
-
if (status[env]) {
|
|
18
|
-
print(`${env} backend already provisioned (status: ${status[env].status}).`);
|
|
19
|
-
}
|
|
20
|
-
else {
|
|
21
|
-
print(`Provisioning ${env} backend…`);
|
|
22
|
-
if (env === 'dev') {
|
|
23
|
-
await client.post(`/newly_backend/provision_dev/${projectId}`, {
|
|
24
|
-
dev_region: region,
|
|
25
|
-
});
|
|
26
|
-
print('Provisioned dev backend.');
|
|
27
|
-
}
|
|
28
|
-
else {
|
|
29
|
-
await client.post(`/newly_backend/provision_prod/${projectId}`);
|
|
30
|
-
print('Provisioned prod backend.');
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
if (env === 'dev') {
|
|
34
|
-
if (!hasLocalBackend(cwd)) {
|
|
35
|
-
print('Provisioned. No backend code in this directory — nothing to deploy yet.\n' +
|
|
36
|
-
'Run `newly pull ' + projectId + '` to get the project code, or develop on the ' +
|
|
37
|
-
'platform; the backend goes active on its first deploy.');
|
|
38
|
-
return;
|
|
39
|
-
}
|
|
40
|
-
await deployDev(projectId, client, cwd);
|
|
41
|
-
}
|
|
42
|
-
else {
|
|
43
|
-
await deployProd(projectId, client);
|
|
44
|
-
}
|
|
45
|
-
const finalStatus = await client.get(`/newly_backend/status/${projectId}`);
|
|
46
|
-
const s = finalStatus[env];
|
|
47
|
-
if (s) {
|
|
48
|
-
print(`\n${env} backend: ${s.status}`);
|
|
49
|
-
print(` auth: ${s.auth_hostname}`);
|
|
50
|
-
print(` api: ${s.api_hostname}`);
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
export function registerUp(program) {
|
|
54
|
-
program
|
|
55
|
-
.command('up')
|
|
56
|
-
.description('Provision (if needed) and deploy the backend (dev only; prod ships via Promote in the web app)')
|
|
57
|
-
.option('--env <env>', 'dev (prod is not supported via the CLI yet)', 'dev')
|
|
58
|
-
.option('--region <region>', 'region for dev provisioning', 'us-central1')
|
|
59
|
-
.option('--project <id>', 'project id (defaults to the linked directory)')
|
|
60
|
-
.option('--yes', 'skip the prod confirmation prompt', false)
|
|
61
|
-
.action(async (opts) => {
|
|
62
|
-
try {
|
|
63
|
-
if (opts.env !== 'dev' && opts.env !== 'prod')
|
|
64
|
-
throw new Error('--env must be dev or prod');
|
|
65
|
-
if (opts.env === 'prod') {
|
|
66
|
-
throw new Error('Prod provisioning/deploys are not supported via the CLI yet — use Promote in the Newly web app.');
|
|
67
|
-
}
|
|
68
|
-
const creds = requireCredentials();
|
|
69
|
-
const projectId = resolveProject(process.cwd(), opts.project);
|
|
70
|
-
const client = new ApiClient(creds.apiUrl, creds.token);
|
|
71
|
-
await runUp({
|
|
72
|
-
client,
|
|
73
|
-
projectId,
|
|
74
|
-
env: opts.env,
|
|
75
|
-
region: opts.region,
|
|
76
|
-
cwd: process.cwd(),
|
|
77
|
-
print: console.log,
|
|
78
|
-
});
|
|
79
|
-
}
|
|
80
|
-
catch (e) {
|
|
81
|
-
console.error(e instanceof ApiError ? friendlyApiError(e) : String(e instanceof Error ? e.message : e));
|
|
82
|
-
process.exitCode = 1;
|
|
83
|
-
}
|
|
84
|
-
});
|
|
85
|
-
}
|