entrify-deploy 1.2.0 → 1.3.0
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 +59 -2
- package/dist/api.d.ts +34 -0
- package/dist/api.d.ts.map +1 -1
- package/dist/api.js +31 -3
- package/dist/api.js.map +1 -1
- package/dist/cli.js +446 -198
- package/dist/cli.js.map +1 -1
- package/dist/util.d.ts +38 -0
- package/dist/util.d.ts.map +1 -1
- package/dist/util.js +65 -1
- package/dist/util.js.map +1 -1
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { createInterface } from 'node:readline';
|
|
3
|
+
import { readFile } from 'node:fs/promises';
|
|
4
|
+
import { resolve } from 'node:path';
|
|
3
5
|
import { cwd as processCwd } from 'node:process';
|
|
4
6
|
import { readCredentials, clearCredentials, login, PRODUCTION_URL } from './auth.js';
|
|
5
7
|
import { EntrifyApi } from './api.js';
|
|
6
8
|
import { getGitInfo, validateLocal } from './detector.js';
|
|
7
|
-
import { buildDeployUrl, cliName, normaliseRepoUrl, parseArgs } from './util.js';
|
|
9
|
+
import { CliError, EXIT, buildDeployUrl, cliName, normaliseRepoUrl, parseArgs, parseDotenv, previousDeployed, projectNameFromRepo, } from './util.js';
|
|
8
10
|
/** `entrify` or `dropdeploy`, whichever was typed. */
|
|
9
11
|
const BIN = cliName();
|
|
10
12
|
// ── ANSI colours (no external dep) ──────────────────────────────────────────
|
|
@@ -16,6 +18,30 @@ const c = {
|
|
|
16
18
|
bold: (s) => `\x1b[1m${s}\x1b[0m`,
|
|
17
19
|
dim: (s) => `\x1b[2m${s}\x1b[0m`,
|
|
18
20
|
};
|
|
21
|
+
// ── Output: human text, or exactly one JSON object with --json ──────────────
|
|
22
|
+
let jsonMode = false;
|
|
23
|
+
/** Human-readable output. Silent with --json, so stdout carries only the JSON result. */
|
|
24
|
+
function say(...args) {
|
|
25
|
+
if (!jsonMode)
|
|
26
|
+
console.log(...args);
|
|
27
|
+
}
|
|
28
|
+
function sayInline(text) {
|
|
29
|
+
if (!jsonMode)
|
|
30
|
+
process.stdout.write(text);
|
|
31
|
+
}
|
|
32
|
+
/** A command's result. With --json it is printed as `{ "ok": true, ... }`; humans have already been told. */
|
|
33
|
+
function emit(result) {
|
|
34
|
+
if (jsonMode)
|
|
35
|
+
process.stdout.write(JSON.stringify({ ok: true, ...result }) + '\n');
|
|
36
|
+
}
|
|
37
|
+
function colourStatus(status) {
|
|
38
|
+
if (!status)
|
|
39
|
+
return c.dim('no deployments');
|
|
40
|
+
if (status === 'DEPLOYED')
|
|
41
|
+
return c.green(status);
|
|
42
|
+
return status === 'FAILED' ? c.red(status) : c.yellow(status);
|
|
43
|
+
}
|
|
44
|
+
const publicProject = (p) => ({ id: p.id, slug: p.slug, name: p.name });
|
|
19
45
|
// ── Interactive prompt ───────────────────────────────────────────────────────
|
|
20
46
|
async function prompt(question) {
|
|
21
47
|
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
@@ -102,17 +128,32 @@ function renderBar(percent, label) {
|
|
|
102
128
|
}
|
|
103
129
|
const isTTY = process.stdout.isTTY ?? false;
|
|
104
130
|
function printProgress(percent, label) {
|
|
105
|
-
if (!isTTY)
|
|
131
|
+
if (!isTTY || jsonMode)
|
|
106
132
|
return;
|
|
107
133
|
process.stdout.write('\r' + renderBar(percent, label) + ' ');
|
|
108
134
|
}
|
|
109
135
|
function clearProgress() {
|
|
110
|
-
if (!isTTY)
|
|
136
|
+
if (!isTTY || jsonMode)
|
|
111
137
|
return;
|
|
112
138
|
process.stdout.write('\r' + ' '.repeat(72) + '\r');
|
|
113
139
|
}
|
|
114
140
|
// ── Error diagnosis from build log tail ─────────────────────────────────────
|
|
115
141
|
const ERROR_PATTERNS = [
|
|
142
|
+
{
|
|
143
|
+
re: /no space left on device/i,
|
|
144
|
+
cause: 'The Entrify server ran out of disk space',
|
|
145
|
+
fix: 'Not a problem in your code: ask the Entrify admin to free space, then redeploy',
|
|
146
|
+
},
|
|
147
|
+
{
|
|
148
|
+
re: /did not start listening/i,
|
|
149
|
+
cause: 'The app never accepted connections on its port',
|
|
150
|
+
fix: 'Listen on 0.0.0.0 (not 127.0.0.1 or localhost) and on the PORT environment variable',
|
|
151
|
+
},
|
|
152
|
+
{
|
|
153
|
+
re: /exited during startup/i,
|
|
154
|
+
cause: 'The app crashed while starting',
|
|
155
|
+
fix: `Read the error above; if it asks for a setting, add it with \`${BIN} env set KEY=VALUE\` and redeploy`,
|
|
156
|
+
},
|
|
116
157
|
{
|
|
117
158
|
re: /Cannot find module|Module not found/i,
|
|
118
159
|
cause: 'Missing dependency',
|
|
@@ -157,164 +198,82 @@ function diagnose(log) {
|
|
|
157
198
|
}
|
|
158
199
|
return null;
|
|
159
200
|
}
|
|
160
|
-
// ──
|
|
161
|
-
async function
|
|
201
|
+
// ── Auth ─────────────────────────────────────────────────────────────────────
|
|
202
|
+
async function requireCreds() {
|
|
203
|
+
const creds = await readCredentials();
|
|
204
|
+
if (!creds)
|
|
205
|
+
throw new CliError(`Not logged in. Run: ${BIN} auth login`, 'NOT_LOGGED_IN', EXIT.AUTH);
|
|
206
|
+
return creds;
|
|
207
|
+
}
|
|
208
|
+
async function createProjectFromRemote(api, { remoteUrl, branch, detectedType }) {
|
|
209
|
+
const repo = normaliseRepoUrl(remoteUrl);
|
|
210
|
+
const host = repo.split('/')[0];
|
|
211
|
+
const source = host === 'github.com' ? 'GITHUB' : host.includes('gitlab') ? 'GITLAB' : null;
|
|
212
|
+
if (!source) {
|
|
213
|
+
throw new CliError(`Only GitHub and GitLab repositories can be deployed (this remote is on ${host}).`, 'UNSUPPORTED_REMOTE', EXIT.NEEDS_INPUT);
|
|
214
|
+
}
|
|
215
|
+
return api.createProject({
|
|
216
|
+
name: projectNameFromRepo(remoteUrl),
|
|
217
|
+
source,
|
|
218
|
+
githubUrl: `https://${repo}`,
|
|
219
|
+
type: detectedType ?? undefined,
|
|
220
|
+
branch: branch === 'HEAD' ? undefined : branch,
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
async function pickProject(api, query) {
|
|
162
224
|
const projects = await api.listProjects();
|
|
163
|
-
if (
|
|
164
|
-
const p = projects.find((p) => p.id ===
|
|
165
|
-
if (!p)
|
|
166
|
-
throw new
|
|
167
|
-
|
|
225
|
+
if (query.projectId) {
|
|
226
|
+
const p = projects.find((p) => p.id === query.projectId || p.slug === query.projectId);
|
|
227
|
+
if (!p) {
|
|
228
|
+
throw new CliError(`Project "${query.projectId}" not found. List them with: ${BIN} projects`, 'PROJECT_NOT_FOUND', EXIT.NEEDS_INPUT);
|
|
229
|
+
}
|
|
230
|
+
return { project: p, created: false };
|
|
168
231
|
}
|
|
169
232
|
// Auto-match by git remote URL (HTTPS or SSH); with several projects on one repo, prefer this branch's
|
|
170
|
-
if (remoteUrl) {
|
|
171
|
-
const needle = normaliseRepoUrl(remoteUrl);
|
|
233
|
+
if (query.remoteUrl) {
|
|
234
|
+
const needle = normaliseRepoUrl(query.remoteUrl);
|
|
172
235
|
const matches = projects.filter((p) => p.githubUrl && normaliseRepoUrl(p.githubUrl) === needle);
|
|
173
|
-
if (matches.length > 0)
|
|
174
|
-
return matches.find((p) => p.branch ===
|
|
236
|
+
if (matches.length > 0) {
|
|
237
|
+
return { project: matches.find((p) => p.branch === query.branch) ?? matches[0], created: false };
|
|
238
|
+
}
|
|
239
|
+
if (query.create) {
|
|
240
|
+
const project = await createProjectFromRemote(api, { remoteUrl: query.remoteUrl, branch: query.branch, ...query.create });
|
|
241
|
+
return { project, created: true };
|
|
242
|
+
}
|
|
175
243
|
}
|
|
244
|
+
const hint = `Pass --project-id <id|slug> (list them with: ${BIN} projects), or deploy with --create to make a new project from this repository.`;
|
|
176
245
|
if (projects.length === 0) {
|
|
177
|
-
throw new
|
|
246
|
+
throw new CliError(`No projects found on Entrify. ${hint}`, 'NO_PROJECTS', EXIT.NEEDS_INPUT);
|
|
178
247
|
}
|
|
179
|
-
// Without a terminal (CI) the prompt below
|
|
180
|
-
if (!process.stdin.isTTY) {
|
|
181
|
-
throw new
|
|
248
|
+
// Without a terminal (CI, coding agents) or with --json, nobody can answer the prompt below.
|
|
249
|
+
if (!process.stdin.isTTY || jsonMode) {
|
|
250
|
+
throw new CliError(`No project matches this repository. ${hint}`, 'NO_PROJECT_MATCH', EXIT.NEEDS_INPUT);
|
|
182
251
|
}
|
|
183
|
-
|
|
184
|
-
console.log(`\n${c.bold('Select a project:')}`);
|
|
252
|
+
say(`\n${c.bold('Select a project:')}`);
|
|
185
253
|
projects.forEach((p, i) => {
|
|
186
|
-
|
|
187
|
-
const status = latest
|
|
188
|
-
? latest.status === 'DEPLOYED'
|
|
189
|
-
? c.green(latest.status)
|
|
190
|
-
: latest.status === 'FAILED'
|
|
191
|
-
? c.red(latest.status)
|
|
192
|
-
: c.yellow(latest.status)
|
|
193
|
-
: c.dim('no deployments');
|
|
194
|
-
console.log(` ${c.cyan(String(i + 1))}) ${p.name} ${c.dim(`(${p.slug})`)} ${status}`);
|
|
254
|
+
say(` ${c.cyan(String(i + 1))}) ${p.name} ${c.dim(`(${p.slug})`)} ${colourStatus(p.deployments?.[0]?.status)}`);
|
|
195
255
|
});
|
|
196
256
|
const answer = await prompt(`\nEnter number [1-${projects.length}]: `);
|
|
197
257
|
const idx = parseInt(answer, 10) - 1;
|
|
198
258
|
if (isNaN(idx) || idx < 0 || idx >= projects.length) {
|
|
199
|
-
throw new
|
|
259
|
+
throw new CliError('Invalid selection', 'INVALID_SELECTION', EXIT.NEEDS_INPUT);
|
|
200
260
|
}
|
|
201
|
-
return projects[idx];
|
|
261
|
+
return { project: projects[idx], created: false };
|
|
202
262
|
}
|
|
203
|
-
|
|
204
|
-
async function
|
|
205
|
-
const
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
const password = await promptHidden('Password: ');
|
|
209
|
-
process.stdout.write('Logging in… ');
|
|
210
|
-
const creds = await login(apiUrl, email, password);
|
|
211
|
-
console.log(c.green('✓'));
|
|
212
|
-
console.log(`Logged in as ${c.bold(creds.email)}`);
|
|
213
|
-
}
|
|
214
|
-
async function cmdAuthStatus() {
|
|
215
|
-
const creds = await readCredentials();
|
|
216
|
-
if (!creds) {
|
|
217
|
-
console.log(`${c.yellow('Not logged in.')} Run: ${BIN} auth login`);
|
|
218
|
-
process.exit(1);
|
|
219
|
-
}
|
|
220
|
-
console.log(`${c.green('✓')} Logged in as ${c.bold(creds.email)}`);
|
|
221
|
-
console.log(` API: ${c.dim(creds.apiUrl)}`);
|
|
222
|
-
}
|
|
223
|
-
async function cmdAuthLogout() {
|
|
224
|
-
await clearCredentials();
|
|
225
|
-
console.log(`${c.green('✓')} Logged out`);
|
|
226
|
-
}
|
|
227
|
-
async function cmdProjects() {
|
|
228
|
-
const creds = await readCredentials();
|
|
229
|
-
if (!creds) {
|
|
230
|
-
console.error(c.red('Not logged in.') + ` Run: ${BIN} auth login`);
|
|
231
|
-
process.exit(1);
|
|
232
|
-
}
|
|
233
|
-
const api = new EntrifyApi(creds);
|
|
234
|
-
const projects = await api.listProjects();
|
|
235
|
-
if (projects.length === 0) {
|
|
236
|
-
console.log('No projects found.');
|
|
237
|
-
return;
|
|
238
|
-
}
|
|
239
|
-
console.log(`\n${c.bold('Your projects:')}`);
|
|
240
|
-
for (const p of projects) {
|
|
241
|
-
const latest = p.deployments?.[0];
|
|
242
|
-
const statusStr = latest
|
|
243
|
-
? latest.status === 'DEPLOYED'
|
|
244
|
-
? c.green(latest.status)
|
|
245
|
-
: latest.status === 'FAILED'
|
|
246
|
-
? c.red(latest.status)
|
|
247
|
-
: c.yellow(latest.status)
|
|
248
|
-
: c.dim('no deployments');
|
|
249
|
-
console.log(` ${c.cyan(p.slug)} ${p.name} ${statusStr}`);
|
|
250
|
-
if (latest?.subdomain) {
|
|
251
|
-
console.log(` ${c.dim('→ ' + buildDeployUrl(creds.apiUrl, latest.subdomain))}`);
|
|
252
|
-
}
|
|
253
|
-
}
|
|
254
|
-
console.log();
|
|
263
|
+
/** The project for the repository in --dir (or the current directory), or --project-id. Never creates one. */
|
|
264
|
+
async function resolveProject(api, flags) {
|
|
265
|
+
const git = await getGitInfo(flags['dir'] ?? processCwd());
|
|
266
|
+
const { project } = await pickProject(api, { remoteUrl: git.remoteUrl, branch: git.branch, projectId: flags['project-id'] ?? null });
|
|
267
|
+
return project;
|
|
255
268
|
}
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
// ── 1. Auth check ──────────────────────────────────────────────────────────
|
|
259
|
-
const creds = await readCredentials();
|
|
260
|
-
if (!creds) {
|
|
261
|
-
console.error(c.red('Not logged in.') + ` Run: ${BIN} auth login`);
|
|
262
|
-
process.exit(1);
|
|
263
|
-
}
|
|
264
|
-
const api = new EntrifyApi(creds);
|
|
265
|
-
console.log(`\n${c.bold('▶ Entrify')}\n`);
|
|
266
|
-
// ── 2. Git info ───────────────────────────────────────────────────────────
|
|
267
|
-
process.stdout.write(' Checking repository… ');
|
|
268
|
-
const gitInfo = await getGitInfo(dir);
|
|
269
|
-
console.log(c.green('✓'));
|
|
270
|
-
if (gitInfo.isDirty) {
|
|
271
|
-
console.log(` ${c.yellow('⚠')} Working tree has uncommitted changes`);
|
|
272
|
-
}
|
|
273
|
-
// ── 3. Validate ───────────────────────────────────────────────────────────
|
|
274
|
-
process.stdout.write(' Validating project… ');
|
|
275
|
-
const validation = await validateLocal(dir, gitInfo);
|
|
276
|
-
if (!validation.ok) {
|
|
277
|
-
console.log(c.red('✗'));
|
|
278
|
-
for (const e of validation.errors) {
|
|
279
|
-
console.error(` ${c.red('✗')} ${e}`);
|
|
280
|
-
}
|
|
281
|
-
process.exit(1);
|
|
282
|
-
}
|
|
283
|
-
console.log(c.green('✓'));
|
|
284
|
-
for (const w of validation.warnings) {
|
|
285
|
-
console.log(` ${c.yellow('⚠')} ${w}`);
|
|
286
|
-
}
|
|
287
|
-
// ── 4. Framework detection ────────────────────────────────────────────────
|
|
288
|
-
if (gitInfo.remoteUrl) {
|
|
289
|
-
process.stdout.write(' Detecting framework… ');
|
|
290
|
-
try {
|
|
291
|
-
const detected = await api.detectType(gitInfo.remoteUrl, gitInfo.branch);
|
|
292
|
-
console.log(`${c.green('✓')} ${c.bold(detected.type)} ${c.dim(`(${detected.hint})`)}`);
|
|
293
|
-
}
|
|
294
|
-
catch {
|
|
295
|
-
console.log(c.dim('skipped'));
|
|
296
|
-
}
|
|
297
|
-
}
|
|
298
|
-
// ── 5. Project resolution ──────────────────────────────────────────────────
|
|
299
|
-
process.stdout.write(' Resolving project… ');
|
|
300
|
-
const project = await pickProject(api, gitInfo.remoteUrl, gitInfo.branch, flags['project-id'] ?? null);
|
|
301
|
-
console.log(`${c.green('✓')} ${c.bold(project.name)} ${c.dim(`(${project.slug})`)}`);
|
|
302
|
-
// The server builds the project's branch, not whatever is checked out here.
|
|
303
|
-
if (project.branch && gitInfo.branch !== 'HEAD' && gitInfo.branch !== project.branch) {
|
|
304
|
-
console.log(` ${c.yellow('⚠')} This project deploys branch ${c.bold(project.branch)}; you are on ${c.bold(gitInfo.branch)}.`);
|
|
305
|
-
}
|
|
306
|
-
// ── 6. Trigger deployment ──────────────────────────────────────────────────
|
|
307
|
-
process.stdout.write(' Triggering deployment… ');
|
|
308
|
-
const { deploymentId, queued } = await api.triggerDeploy(project.id);
|
|
309
|
-
console.log(c.green('✓') + (queued ? c.dim(' (queued behind active build)') : ''));
|
|
310
|
-
console.log();
|
|
311
|
-
// ── 7. Stream logs + live progress bar ────────────────────────────────────
|
|
269
|
+
/** Streams the build log (with a progress bar for humans), then waits for the final status. */
|
|
270
|
+
async function followDeployment(api, projectId, deploymentId) {
|
|
312
271
|
const logLines = [];
|
|
313
|
-
let currentStep =
|
|
272
|
+
let currentStep = null;
|
|
314
273
|
let currentPercent = 5;
|
|
315
274
|
printProgress(currentPercent, 'Waiting in queue…');
|
|
316
275
|
try {
|
|
317
|
-
for await (const evt of api.streamLogs(
|
|
276
|
+
for await (const evt of api.streamLogs(projectId, deploymentId)) {
|
|
318
277
|
if (evt.type === 'line') {
|
|
319
278
|
logLines.push(evt.text);
|
|
320
279
|
const step = detectBuildStep(evt.text);
|
|
@@ -322,12 +281,12 @@ async function cmdDeploy(flags) {
|
|
|
322
281
|
currentStep = step;
|
|
323
282
|
currentPercent = STEP_PERCENT[step];
|
|
324
283
|
clearProgress();
|
|
325
|
-
|
|
284
|
+
say(`\n ${c.cyan('›')} ${c.bold(STEP_LABEL[step])}`);
|
|
326
285
|
printProgress(currentPercent, STEP_LABEL[step] + '…');
|
|
327
286
|
}
|
|
328
287
|
else {
|
|
329
288
|
clearProgress();
|
|
330
|
-
|
|
289
|
+
say(` ${c.dim(evt.text)}`);
|
|
331
290
|
printProgress(currentPercent, currentStep ? STEP_LABEL[currentStep] + '…' : 'Building…');
|
|
332
291
|
}
|
|
333
292
|
}
|
|
@@ -348,26 +307,23 @@ async function cmdDeploy(flags) {
|
|
|
348
307
|
}
|
|
349
308
|
catch {
|
|
350
309
|
clearProgress();
|
|
351
|
-
|
|
310
|
+
say(` ${c.yellow('⚠')} Log stream disconnected — polling for result…`);
|
|
352
311
|
}
|
|
353
|
-
// ── 8. Fetch final status ──────────────────────────────────────────────────
|
|
354
312
|
clearProgress();
|
|
355
|
-
|
|
313
|
+
// SSE ends slightly before DB is updated — poll with exponential backoff
|
|
356
314
|
let finalLog = null;
|
|
357
315
|
let subdomain = null;
|
|
358
|
-
// SSE ends slightly before DB is updated — poll with exponential backoff
|
|
359
316
|
let consecutiveErrors = 0;
|
|
360
317
|
for (let attempt = 0; attempt < 20; attempt++) {
|
|
361
318
|
const delay = Math.min(1000 * 2 ** attempt, 10_000);
|
|
362
319
|
await new Promise((r) => setTimeout(r, delay));
|
|
363
320
|
try {
|
|
364
|
-
const s = await api.getDeploymentStatus(
|
|
321
|
+
const s = await api.getDeploymentStatus(projectId, deploymentId);
|
|
365
322
|
consecutiveErrors = 0;
|
|
366
323
|
finalLog = s.buildLog;
|
|
367
324
|
subdomain = s.subdomain;
|
|
368
|
-
if (
|
|
369
|
-
|
|
370
|
-
break;
|
|
325
|
+
if (s.status === 'DEPLOYED' || s.status === 'FAILED' || s.status === 'CANCELLED') {
|
|
326
|
+
return { status: s.status, subdomain, log: finalLog ?? logLines.join(''), step: currentStep };
|
|
371
327
|
}
|
|
372
328
|
}
|
|
373
329
|
catch {
|
|
@@ -375,48 +331,305 @@ async function cmdDeploy(flags) {
|
|
|
375
331
|
break;
|
|
376
332
|
}
|
|
377
333
|
}
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
334
|
+
return { status: 'TIMEOUT', subdomain, log: finalLog ?? logLines.join(''), step: currentStep };
|
|
335
|
+
}
|
|
336
|
+
/** Prints the result of a deploy or rollback; a deployment that did not go live is thrown as a CliError. */
|
|
337
|
+
function reportDeployment({ creds, project, deploymentId, outcome, details }) {
|
|
338
|
+
const base = { project: publicProject(project), deploymentId, status: outcome.status, ...details };
|
|
339
|
+
say();
|
|
340
|
+
if (outcome.status === 'DEPLOYED') {
|
|
341
|
+
const url = outcome.subdomain ? buildDeployUrl(creds.apiUrl, outcome.subdomain) : null;
|
|
342
|
+
say(c.bold(c.green('✓ Deployed successfully')));
|
|
343
|
+
if (url)
|
|
344
|
+
say(`\n ${c.bold('Live URL')} → ${c.cyan(url)}`);
|
|
345
|
+
say();
|
|
346
|
+
emit({ ...base, url });
|
|
347
|
+
return;
|
|
348
|
+
}
|
|
349
|
+
if (outcome.status === 'FAILED') {
|
|
350
|
+
say(c.bold(c.red(`✗ Deployment failed${outcome.step ? ` at: ${c.bold(outcome.step)}` : ''}`)));
|
|
351
|
+
say();
|
|
352
|
+
const lines = outcome.log.split('\n').filter(Boolean);
|
|
353
|
+
const diagnosis = outcome.log ? diagnose(outcome.log) : null;
|
|
354
|
+
if (lines.length > 0) {
|
|
355
|
+
say(` ${c.dim('Last 20 lines of build log:')}`);
|
|
356
|
+
say(' ' + '─'.repeat(52));
|
|
357
|
+
for (const l of lines.slice(-20))
|
|
358
|
+
say(` ${c.dim(l)}`);
|
|
359
|
+
say(' ' + '─'.repeat(52));
|
|
360
|
+
say();
|
|
361
|
+
if (diagnosis) {
|
|
362
|
+
say(` ${c.yellow('Probable cause:')} ${diagnosis.cause}`);
|
|
363
|
+
say(` ${c.cyan('Suggested fix:')} ${diagnosis.fix}`);
|
|
364
|
+
say();
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
throw new CliError(`Deployment failed${outcome.step ? ` at ${outcome.step}` : ''}`, 'DEPLOY_FAILED', EXIT.FAILED, {
|
|
368
|
+
...base,
|
|
369
|
+
step: outcome.step,
|
|
370
|
+
cause: diagnosis?.cause ?? null,
|
|
371
|
+
fix: diagnosis?.fix ?? null,
|
|
372
|
+
logTail: lines.slice(-40),
|
|
373
|
+
});
|
|
374
|
+
}
|
|
375
|
+
if (outcome.status === 'CANCELLED') {
|
|
376
|
+
throw new CliError('The deployment was cancelled.', 'DEPLOY_CANCELLED', EXIT.FAILED, base);
|
|
377
|
+
}
|
|
378
|
+
const dashboard = `${creds.apiUrl}/projects/${project.id}`;
|
|
379
|
+
throw new CliError(`Timed out waiting for the deployment to finish. Check: ${BIN} status, or ${dashboard}`, 'DEPLOY_TIMEOUT', EXIT.FAILED, { ...base, dashboard });
|
|
380
|
+
}
|
|
381
|
+
// ── Commands ─────────────────────────────────────────────────────────────────
|
|
382
|
+
async function cmdAuthLogin(flags) {
|
|
383
|
+
const apiUrl = flags['api-url'] ?? PRODUCTION_URL;
|
|
384
|
+
console.log(`Connecting to ${c.cyan(apiUrl)}`);
|
|
385
|
+
const email = flags['email'] ?? (await prompt('Email: '));
|
|
386
|
+
const password = await promptHidden('Password: ');
|
|
387
|
+
process.stdout.write('Logging in… ');
|
|
388
|
+
const creds = await login(apiUrl, email, password);
|
|
389
|
+
console.log(c.green('✓'));
|
|
390
|
+
console.log(`Logged in as ${c.bold(creds.email)}`);
|
|
391
|
+
}
|
|
392
|
+
async function cmdAuthStatus() {
|
|
393
|
+
const creds = await requireCreds();
|
|
394
|
+
say(`${c.green('✓')} Logged in as ${c.bold(creds.email)}`);
|
|
395
|
+
say(` API: ${c.dim(creds.apiUrl)}`);
|
|
396
|
+
emit({ email: creds.email, apiUrl: creds.apiUrl });
|
|
397
|
+
}
|
|
398
|
+
async function cmdAuthLogout() {
|
|
399
|
+
await clearCredentials();
|
|
400
|
+
say(`${c.green('✓')} Logged out`);
|
|
401
|
+
emit({});
|
|
402
|
+
}
|
|
403
|
+
async function cmdProjects() {
|
|
404
|
+
const creds = await requireCreds();
|
|
405
|
+
const projects = await new EntrifyApi(creds).listProjects();
|
|
406
|
+
const rows = projects.map((p) => {
|
|
407
|
+
const latest = p.deployments?.[0];
|
|
408
|
+
return { ...publicProject(p), type: p.type, status: latest?.status ?? null, url: latest?.subdomain ? buildDeployUrl(creds.apiUrl, latest.subdomain) : null };
|
|
409
|
+
});
|
|
410
|
+
if (rows.length === 0)
|
|
411
|
+
say('No projects found.');
|
|
412
|
+
else {
|
|
413
|
+
say(`\n${c.bold('Your projects:')}`);
|
|
414
|
+
for (const r of rows) {
|
|
415
|
+
say(` ${c.cyan(r.slug)} ${r.name} ${colourStatus(r.status ?? undefined)}`);
|
|
416
|
+
if (r.url)
|
|
417
|
+
say(` ${c.dim('→ ' + r.url)}`);
|
|
418
|
+
}
|
|
419
|
+
say();
|
|
420
|
+
}
|
|
421
|
+
emit({ projects: rows });
|
|
422
|
+
}
|
|
423
|
+
async function cmdDeploy(flags) {
|
|
424
|
+
const dir = flags['dir'] ?? processCwd();
|
|
425
|
+
const creds = await requireCreds();
|
|
426
|
+
const api = new EntrifyApi(creds);
|
|
427
|
+
const warnings = [];
|
|
428
|
+
const warn = (w) => {
|
|
429
|
+
warnings.push(w);
|
|
430
|
+
say(` ${c.yellow('⚠')} ${w}`);
|
|
431
|
+
};
|
|
432
|
+
say(`\n${c.bold('▶ Entrify')}\n`);
|
|
433
|
+
sayInline(' Checking repository… ');
|
|
434
|
+
const gitInfo = await getGitInfo(dir);
|
|
435
|
+
say(c.green('✓'));
|
|
436
|
+
if (gitInfo.isDirty)
|
|
437
|
+
warn('Working tree has uncommitted changes. Entrify deploys what is pushed, not local files.');
|
|
438
|
+
sayInline(' Validating project… ');
|
|
439
|
+
const validation = await validateLocal(dir, gitInfo);
|
|
440
|
+
if (!validation.ok) {
|
|
441
|
+
say(c.red('✗'));
|
|
442
|
+
for (const e of validation.errors)
|
|
443
|
+
say(` ${c.red('✗')} ${e}`);
|
|
444
|
+
throw new CliError(validation.errors.join(' '), 'INVALID_PROJECT', EXIT.NEEDS_INPUT, { errors: validation.errors });
|
|
445
|
+
}
|
|
446
|
+
say(c.green('✓'));
|
|
447
|
+
validation.warnings.forEach(warn);
|
|
448
|
+
let detectedType = null;
|
|
449
|
+
if (gitInfo.remoteUrl) {
|
|
450
|
+
sayInline(' Detecting framework… ');
|
|
451
|
+
try {
|
|
452
|
+
const detected = await api.detectType(gitInfo.remoteUrl, gitInfo.branch);
|
|
453
|
+
detectedType = detected.type;
|
|
454
|
+
say(`${c.green('✓')} ${c.bold(detected.type)} ${c.dim(`(${detected.hint})`)}`);
|
|
385
455
|
}
|
|
386
|
-
|
|
456
|
+
catch {
|
|
457
|
+
say(c.dim('skipped'));
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
sayInline(' Resolving project… ');
|
|
461
|
+
const { project, created } = await pickProject(api, {
|
|
462
|
+
remoteUrl: gitInfo.remoteUrl,
|
|
463
|
+
branch: gitInfo.branch,
|
|
464
|
+
projectId: flags['project-id'] ?? null,
|
|
465
|
+
create: flags['create'] === 'true' ? { detectedType } : undefined,
|
|
466
|
+
});
|
|
467
|
+
say(`${c.green('✓')} ${c.bold(project.name)} ${c.dim(`(${project.slug})`)}${created ? c.dim(' (new project)') : ''}`);
|
|
468
|
+
if (project.source === 'UPLOAD') {
|
|
469
|
+
throw new CliError(`"${project.slug}" was created by uploading files, so there is no repository to build. Upload a new version from the dashboard: ${creds.apiUrl}/projects/${project.id}`, 'UPLOAD_PROJECT', EXIT.NEEDS_INPUT);
|
|
470
|
+
}
|
|
471
|
+
// The server builds the project's branch, not whatever is checked out here.
|
|
472
|
+
if (project.branch && gitInfo.branch !== 'HEAD' && gitInfo.branch !== project.branch) {
|
|
473
|
+
warn(`This project deploys branch ${project.branch}; you are on ${gitInfo.branch}.`);
|
|
474
|
+
}
|
|
475
|
+
sayInline(' Triggering deployment… ');
|
|
476
|
+
const { deploymentId, queued } = await api.triggerDeploy(project.id);
|
|
477
|
+
say(c.green('✓') + (queued ? c.dim(' (queued behind active build)') : ''));
|
|
478
|
+
say();
|
|
479
|
+
const outcome = await followDeployment(api, project.id, deploymentId);
|
|
480
|
+
reportDeployment({ creds, project, deploymentId, outcome, details: { created, warnings } });
|
|
481
|
+
}
|
|
482
|
+
async function cmdStatus(flags) {
|
|
483
|
+
const creds = await requireCreds();
|
|
484
|
+
const api = new EntrifyApi(creds);
|
|
485
|
+
const project = await resolveProject(api, flags);
|
|
486
|
+
const deployments = await api.listDeployments(project.id, 10);
|
|
487
|
+
const latest = deployments[0] ?? null;
|
|
488
|
+
const live = deployments.find((d) => d.status === 'DEPLOYED' && d.subdomain);
|
|
489
|
+
const url = live?.subdomain ? buildDeployUrl(creds.apiUrl, live.subdomain) : null;
|
|
490
|
+
say(`\n${c.bold(project.name)} ${c.dim(`(${project.slug})`)}`);
|
|
491
|
+
say(` Latest: ${colourStatus(latest?.status)}${latest ? c.dim(` ${latest.createdAt} ${latest.id}`) : ''}`);
|
|
492
|
+
say(` Live: ${url ? c.cyan(url) : c.dim('not live')}\n`);
|
|
493
|
+
emit({
|
|
494
|
+
project: publicProject(project),
|
|
495
|
+
latest: latest && { id: latest.id, status: latest.status, createdAt: latest.createdAt, commitHash: latest.commitHash ?? null },
|
|
496
|
+
url,
|
|
497
|
+
});
|
|
498
|
+
}
|
|
499
|
+
async function cmdLogs(flags) {
|
|
500
|
+
const creds = await requireCreds();
|
|
501
|
+
const api = new EntrifyApi(creds);
|
|
502
|
+
const project = await resolveProject(api, flags);
|
|
503
|
+
const deploymentId = flags['deployment'] ?? (await api.listDeployments(project.id, 1))[0]?.id;
|
|
504
|
+
if (!deploymentId)
|
|
505
|
+
throw new CliError('This project has no deployments yet.', 'NO_DEPLOYMENTS', EXIT.NEEDS_INPUT);
|
|
506
|
+
const lineLimit = flags['lines'] === 'all' ? Infinity : Number(flags['lines'] ?? 200);
|
|
507
|
+
if (!(lineLimit > 0))
|
|
508
|
+
throw new CliError('--lines takes a positive number or "all".', 'USAGE', EXIT.NEEDS_INPUT);
|
|
509
|
+
const tailOf = (text) => {
|
|
510
|
+
const lines = text.split('\n').filter(Boolean);
|
|
511
|
+
return Number.isFinite(lineLimit) ? lines.slice(-lineLimit) : lines;
|
|
512
|
+
};
|
|
513
|
+
const current = await api.getDeploymentStatus(project.id, deploymentId);
|
|
514
|
+
let status = current.status;
|
|
515
|
+
let tail = tailOf(current.buildLog ?? '');
|
|
516
|
+
for (const l of tail)
|
|
517
|
+
say(l);
|
|
518
|
+
if (flags['follow'] === 'true' && (status === 'QUEUED' || status === 'BUILDING')) {
|
|
519
|
+
// Prints each new line as it arrives, after the lines above.
|
|
520
|
+
const outcome = await followDeployment(api, project.id, deploymentId);
|
|
521
|
+
status = outcome.status;
|
|
522
|
+
tail = tailOf(outcome.log);
|
|
523
|
+
}
|
|
524
|
+
say(`\n${c.dim('Deployment')} ${deploymentId} ${colourStatus(status)}`);
|
|
525
|
+
emit({ project: publicProject(project), deploymentId, status, lines: tail });
|
|
526
|
+
}
|
|
527
|
+
/** `KEY=VALUE` to a pair. Errors name the key only, never the value. */
|
|
528
|
+
function parsePair(arg) {
|
|
529
|
+
const eq = arg.indexOf('=');
|
|
530
|
+
if (eq < 1)
|
|
531
|
+
throw new CliError(`Expected KEY=VALUE, got "${arg.split('=')[0]}"`, 'USAGE', EXIT.NEEDS_INPUT);
|
|
532
|
+
return [arg.slice(0, eq), arg.slice(eq + 1)];
|
|
533
|
+
}
|
|
534
|
+
async function cmdEnv(sub, args, flags) {
|
|
535
|
+
const creds = await requireCreds();
|
|
536
|
+
const api = new EntrifyApi(creds);
|
|
537
|
+
const project = await resolveProject(api, flags);
|
|
538
|
+
const existing = await api.listEnv(project.id);
|
|
539
|
+
const applyHint = `Redeploy to apply: ${BIN} deploy`;
|
|
540
|
+
if (sub === undefined || sub === 'list') {
|
|
541
|
+
if (existing.length === 0)
|
|
542
|
+
say('No environment variables set.');
|
|
543
|
+
for (const v of existing)
|
|
544
|
+
say(` ${c.cyan(v.key)} ${c.dim(v.environment)}`);
|
|
545
|
+
emit({ project: publicProject(project), variables: existing.map((v) => ({ key: v.key, environment: v.environment })) });
|
|
387
546
|
return;
|
|
388
547
|
}
|
|
389
|
-
if (
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
548
|
+
if (sub === 'set' || sub === 'push') {
|
|
549
|
+
let pairs;
|
|
550
|
+
if (sub === 'push') {
|
|
551
|
+
const file = resolve(flags['dir'] ?? processCwd(), args[0] ?? '.env');
|
|
552
|
+
const text = await readFile(file, 'utf8').catch(() => {
|
|
553
|
+
throw new CliError(`Cannot read ${file}`, 'FILE_NOT_FOUND', EXIT.NEEDS_INPUT);
|
|
554
|
+
});
|
|
555
|
+
pairs = parseDotenv(text);
|
|
556
|
+
}
|
|
557
|
+
else {
|
|
558
|
+
pairs = args.map(parsePair);
|
|
559
|
+
}
|
|
560
|
+
if (pairs.length === 0) {
|
|
561
|
+
throw new CliError(`Nothing to set. Use: ${BIN} env set KEY=VALUE … or ${BIN} env push [.env file]`, 'USAGE', EXIT.NEEDS_INPUT);
|
|
562
|
+
}
|
|
563
|
+
// A key given twice keeps its last value, as in a .env file.
|
|
564
|
+
pairs = [...new Map(pairs)];
|
|
565
|
+
const created = [];
|
|
566
|
+
const updated = [];
|
|
567
|
+
for (const [key, value] of pairs) {
|
|
568
|
+
const found = existing.find((v) => v.key === key);
|
|
569
|
+
if (found) {
|
|
570
|
+
await api.updateEnv(project.id, found.id, value);
|
|
571
|
+
updated.push(key);
|
|
404
572
|
}
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
573
|
+
else {
|
|
574
|
+
await api.createEnv(project.id, key, value);
|
|
575
|
+
created.push(key);
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
say(`${c.green('✓')} Set ${pairs.length} variable(s) on ${c.bold(project.slug)}: ${[...created, ...updated].join(', ')}`);
|
|
579
|
+
say(c.dim(applyHint));
|
|
580
|
+
emit({ project: publicProject(project), created, updated, hint: applyHint });
|
|
581
|
+
return;
|
|
582
|
+
}
|
|
583
|
+
if (sub === 'unset') {
|
|
584
|
+
if (args.length === 0)
|
|
585
|
+
throw new CliError(`Use: ${BIN} env unset KEY …`, 'USAGE', EXIT.NEEDS_INPUT);
|
|
586
|
+
const removed = [];
|
|
587
|
+
const missing = [];
|
|
588
|
+
for (const key of args) {
|
|
589
|
+
const found = existing.find((v) => v.key === key);
|
|
590
|
+
if (!found) {
|
|
591
|
+
missing.push(key);
|
|
592
|
+
continue;
|
|
412
593
|
}
|
|
594
|
+
await api.deleteEnv(project.id, found.id);
|
|
595
|
+
removed.push(key);
|
|
413
596
|
}
|
|
414
|
-
|
|
597
|
+
if (removed.length)
|
|
598
|
+
say(`${c.green('✓')} Removed: ${removed.join(', ')}`);
|
|
599
|
+
if (missing.length)
|
|
600
|
+
say(`${c.yellow('⚠')} Not set: ${missing.join(', ')}`);
|
|
601
|
+
if (removed.length)
|
|
602
|
+
say(c.dim(applyHint));
|
|
603
|
+
emit({ project: publicProject(project), removed, missing, hint: applyHint });
|
|
604
|
+
return;
|
|
415
605
|
}
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
606
|
+
throw new CliError('Unknown env subcommand. Use: list | set KEY=VALUE … | push [file] | unset KEY …', 'USAGE', EXIT.NEEDS_INPUT);
|
|
607
|
+
}
|
|
608
|
+
async function cmdRollback(flags) {
|
|
609
|
+
const creds = await requireCreds();
|
|
610
|
+
const api = new EntrifyApi(creds);
|
|
611
|
+
const project = await resolveProject(api, flags);
|
|
612
|
+
const targetId = flags['to'] ?? previousDeployed(await api.listDeployments(project.id, 50))?.id;
|
|
613
|
+
if (!targetId) {
|
|
614
|
+
throw new CliError('There is no earlier successful deployment to roll back to.', 'NO_ROLLBACK_TARGET', EXIT.NEEDS_INPUT);
|
|
615
|
+
}
|
|
616
|
+
sayInline(` Rolling back ${c.bold(project.slug)} to ${c.dim(targetId)}… `);
|
|
617
|
+
const { deploymentId } = await api.rollback(project.id, targetId);
|
|
618
|
+
say(c.green('✓'));
|
|
619
|
+
const outcome = await followDeployment(api, project.id, deploymentId);
|
|
620
|
+
reportDeployment({ creds, project, deploymentId, outcome, details: { rolledBackTo: targetId } });
|
|
621
|
+
}
|
|
622
|
+
async function cmdCancel(flags) {
|
|
623
|
+
const creds = await requireCreds();
|
|
624
|
+
const api = new EntrifyApi(creds);
|
|
625
|
+
const project = await resolveProject(api, flags);
|
|
626
|
+
const active = flags['deployment']
|
|
627
|
+
?? (await api.listDeployments(project.id, 10)).find((d) => d.status === 'QUEUED' || d.status === 'BUILDING')?.id;
|
|
628
|
+
if (!active)
|
|
629
|
+
throw new CliError('No deployment is queued or building.', 'NOTHING_TO_CANCEL', EXIT.NEEDS_INPUT);
|
|
630
|
+
await api.cancelDeployment(project.id, active);
|
|
631
|
+
say(`${c.green('✓')} Cancelled ${active}`);
|
|
632
|
+
emit({ project: publicProject(project), cancelled: active });
|
|
420
633
|
}
|
|
421
634
|
// ── Help ─────────────────────────────────────────────────────────────────────
|
|
422
635
|
function cmdHelp() {
|
|
@@ -429,8 +642,20 @@ ${c.bold('USAGE')}
|
|
|
429
642
|
${c.bold('COMMANDS')}
|
|
430
643
|
${c.cyan('deploy')} Deploy the current project ${c.dim('(default)')}
|
|
431
644
|
${c.dim('--project-id <id|slug>')} Skip project selection and deploy to a specific project
|
|
645
|
+
${c.dim('--create')} Create a project from this repository if none matches
|
|
432
646
|
${c.dim('--dir <path>')} Use a different working directory
|
|
433
|
-
|
|
647
|
+
${c.cyan('status')} Latest deployment and live URL of the current project
|
|
648
|
+
${c.cyan('logs')} Build log of the latest deployment
|
|
649
|
+
${c.dim('--deployment <id>')} A specific deployment instead of the latest
|
|
650
|
+
${c.dim('--lines <n|all>')} How many lines from the end ${c.dim('(default 200)')}
|
|
651
|
+
${c.dim('--follow')} Stream a deployment that is still building
|
|
652
|
+
${c.cyan('env list')} Environment variable names ${c.dim('(values are never shown)')}
|
|
653
|
+
${c.cyan('env set KEY=VALUE …')} Add or change variables
|
|
654
|
+
${c.cyan('env push [file]')} Add or change every variable in a .env file ${c.dim('(default .env)')}
|
|
655
|
+
${c.cyan('env unset KEY …')} Remove variables
|
|
656
|
+
${c.cyan('rollback')} Go back to the previous successful deployment
|
|
657
|
+
${c.dim('--to <deployment-id>')} A specific earlier deployment
|
|
658
|
+
${c.cyan('cancel')} Cancel the deployment that is queued or building
|
|
434
659
|
${c.cyan('projects')} List all your projects and their status
|
|
435
660
|
|
|
436
661
|
${c.cyan('auth login')} Log in to Entrify ${c.dim('(default: entrify.site)')}
|
|
@@ -443,6 +668,14 @@ ${c.bold('COMMANDS')}
|
|
|
443
668
|
${c.cyan('help')} Show this help message
|
|
444
669
|
${c.cyan('--version')} Print the CLI version
|
|
445
670
|
|
|
671
|
+
The project is matched from the git remote of the current directory; pass
|
|
672
|
+
${c.dim('--project-id <id|slug>')} to any project command to choose one instead.
|
|
673
|
+
|
|
674
|
+
${c.bold('FOR SCRIPTS AND AI AGENTS')}
|
|
675
|
+
${c.dim('--json')} Print one JSON object: ${c.dim('{ "ok": true, ... }')} or ${c.dim('{ "ok": false, "error": { "code", "message" } }')}
|
|
676
|
+
Exit codes: ${c.dim('0')} ok · ${c.dim('1')} failed · ${c.dim('2')} needs input (flag, project, file) · ${c.dim('3')} not logged in
|
|
677
|
+
Commands never prompt without a terminal or with --json.
|
|
678
|
+
|
|
446
679
|
${c.bold('ENVIRONMENT')} ${c.dim('(the DROPDEPLOY_* names still work)')}
|
|
447
680
|
${c.dim('ENTRIFY_TOKEN')} Bearer token ${c.dim('(skips auth login in CI)')}
|
|
448
681
|
${c.dim('ENTRIFY_URL')} Entrify URL, e.g. https://entrify.site
|
|
@@ -451,18 +684,20 @@ ${c.bold('ENVIRONMENT')} ${c.dim('(the DROPDEPLOY_* names still work)')}
|
|
|
451
684
|
${c.bold('EXAMPLES')}
|
|
452
685
|
${c.dim('$')} ${BIN} auth login
|
|
453
686
|
${c.dim('$')} ${BIN} deploy
|
|
454
|
-
${c.dim('$')} ${BIN} deploy --
|
|
455
|
-
${c.dim('$')} ${BIN}
|
|
687
|
+
${c.dim('$')} ${BIN} deploy --create --json
|
|
688
|
+
${c.dim('$')} ${BIN} logs --follow
|
|
689
|
+
${c.dim('$')} ${BIN} env push .env.production
|
|
690
|
+
${c.dim('$')} ${BIN} rollback
|
|
456
691
|
`);
|
|
457
692
|
}
|
|
458
693
|
// ── Entry point ───────────────────────────────────────────────────────────────
|
|
459
694
|
async function main() {
|
|
460
695
|
const { command, flags } = parseArgs(process.argv.slice(2));
|
|
461
|
-
const [cmd, sub] = command;
|
|
696
|
+
const [cmd, sub, ...rest] = command;
|
|
697
|
+
jsonMode = flags['json'] === 'true';
|
|
462
698
|
if (flags['help'] === 'true')
|
|
463
699
|
return cmdHelp();
|
|
464
700
|
if (cmd === 'version' || flags['version'] === 'true') {
|
|
465
|
-
const { readFile } = await import('node:fs/promises');
|
|
466
701
|
const { join, dirname } = await import('node:path');
|
|
467
702
|
const { fileURLToPath } = await import('node:url');
|
|
468
703
|
const pkgPath = join(dirname(fileURLToPath(import.meta.url)), '..', 'package.json');
|
|
@@ -477,21 +712,34 @@ async function main() {
|
|
|
477
712
|
return cmdAuthStatus();
|
|
478
713
|
if (sub === 'logout')
|
|
479
714
|
return cmdAuthLogout();
|
|
480
|
-
|
|
481
|
-
process.exit(1);
|
|
715
|
+
throw new CliError('Unknown auth subcommand. Use: login | status | logout', 'USAGE', EXIT.NEEDS_INPUT);
|
|
482
716
|
}
|
|
483
717
|
if (cmd === 'projects')
|
|
484
718
|
return cmdProjects();
|
|
485
719
|
if (cmd === 'deploy' || cmd === undefined)
|
|
486
720
|
return cmdDeploy(flags);
|
|
721
|
+
if (cmd === 'status')
|
|
722
|
+
return cmdStatus(flags);
|
|
723
|
+
if (cmd === 'logs')
|
|
724
|
+
return cmdLogs(flags);
|
|
725
|
+
if (cmd === 'env')
|
|
726
|
+
return cmdEnv(sub, rest, flags);
|
|
727
|
+
if (cmd === 'rollback')
|
|
728
|
+
return cmdRollback(flags);
|
|
729
|
+
if (cmd === 'cancel')
|
|
730
|
+
return cmdCancel(flags);
|
|
487
731
|
if (cmd === 'help' || cmd === '--help' || cmd === '-h')
|
|
488
732
|
return cmdHelp();
|
|
489
|
-
|
|
490
|
-
console.error(`Run ${BIN} help for usage.`);
|
|
491
|
-
process.exit(1);
|
|
733
|
+
throw new CliError(`Unknown command: ${cmd}. Run ${BIN} help for usage.`, 'USAGE', EXIT.NEEDS_INPUT);
|
|
492
734
|
}
|
|
493
735
|
main().catch((err) => {
|
|
494
|
-
|
|
495
|
-
|
|
736
|
+
const e = err instanceof CliError ? err : new CliError(err instanceof Error ? err.message : String(err), 'ERROR');
|
|
737
|
+
if (jsonMode) {
|
|
738
|
+
process.stdout.write(JSON.stringify({ ok: false, error: { code: e.code, message: e.message }, ...e.details }) + '\n');
|
|
739
|
+
}
|
|
740
|
+
else {
|
|
741
|
+
console.error(`\n${c.red('Error:')} ${e.message}`);
|
|
742
|
+
}
|
|
743
|
+
process.exit(e.exitCode);
|
|
496
744
|
});
|
|
497
745
|
//# sourceMappingURL=cli.js.map
|