@glash/cli 0.2.22 → 0.2.23

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.
Files changed (2) hide show
  1. package/commands/deploy.mjs +87 -46
  2. package/package.json +1 -1
@@ -129,22 +129,28 @@ export async function deploy({ flags }) {
129
129
  const target = flags.prod || flags.production ? 'production' : (flags.target || 'production');
130
130
 
131
131
  let dep;
132
+ let envSummary = { count: 0, skipped: Boolean(flags['no-env']) };
133
+ let currentPhase = 'doing';
134
+ const start = Date.now();
135
+ const indicator = createDeployIndicator();
136
+ indicator.start(currentPhase);
132
137
 
133
138
  if (!flags['no-env']) {
139
+ indicator.setPhase('syncing');
134
140
  try {
135
- await syncEnvBeforeDeploy(projectId, target, flags);
141
+ envSummary = await syncEnvBeforeDeploy(projectId, target, flags);
136
142
  } catch (e) {
143
+ indicator.stop();
137
144
  return fail(`Env sync failed: ${e.message}`);
138
145
  }
139
146
  }
140
147
 
141
148
  if (!remoteDeploy) {
142
- info(color.dim(`→ uploading local source for project ${projectId} (${target})`));
149
+ indicator.setPhase('packaging');
143
150
  const zipPath = join(tmpdir(), `glash-deploy-${Date.now()}.zip`);
144
151
  try {
145
152
  await zipDirectory(process.cwd(), zipPath);
146
- const zipStat = await stat(zipPath);
147
- info(color.dim(`→ source bundle ${(zipStat.size / 1024 / 1024).toFixed(1)} MB`));
153
+ await stat(zipPath);
148
154
  const formData = new FormData();
149
155
  formData.append('projectId', projectId);
150
156
  formData.append('file', await openAsBlob(zipPath, { type: 'application/zip' }), 'project.zip');
@@ -153,39 +159,37 @@ export async function deploy({ flags }) {
153
159
  formData.append('gitBranch', gitBranch || '');
154
160
  formData.append('target', target);
155
161
 
156
- info(color.dim('→ uploading bundle to glashDB…'));
162
+ indicator.setPhase('glashing');
157
163
  dep = await apiPost('/deployments/upload', formData);
158
164
  await rm(zipPath, { force: true }).catch(() => {});
159
165
  } catch (e) {
160
166
  await rm(zipPath, { force: true }).catch(() => {});
167
+ indicator.stop();
161
168
  return fail(`Upload failed: ${e.message}`);
162
169
  }
163
170
  } else {
164
- info(color.dim(`→ deploying connected repo for project ${projectId}${commitHash ? ` @ ${commitHash.slice(0, 7)}` : ''} (${target})`));
171
+ indicator.setPhase('hatching');
165
172
  try {
166
173
  dep = await apiPost('/deployments', { projectId, commitHash, gitBranch, target });
167
174
  } catch (e) {
175
+ indicator.stop();
168
176
  return fail(e.message);
169
177
  }
170
178
  }
171
179
 
172
- ok(`Deployment ${color.bold(dep.id)} queued`);
173
-
174
180
  if (flags.detach || flags.D) {
181
+ indicator.stop();
182
+ info(`Queued: ${color.dim(dep.id)}`);
175
183
  info(`Track it with: ${color.cyan(`glash status ${dep.id}`)}`);
176
184
  return;
177
185
  }
178
186
 
179
187
  // Poll the pipeline until it terminates.
180
188
  const seen = new Set();
181
- let lastStatus = '';
182
- const start = Date.now();
183
189
  const timeoutMs = deployTimeoutMs(flags);
184
190
  let pollDelayMs = 5000;
185
191
  let lastProgressAt = Date.now();
186
- let currentPhase = 'queued';
187
- const indicator = createDeployIndicator({ start, getPhase: () => currentPhase });
188
- indicator.start();
192
+ indicator.setPhase('hatching');
189
193
  while (Date.now() - start < timeoutMs) {
190
194
  let pipe;
191
195
  try {
@@ -198,7 +202,7 @@ export async function deploy({ flags }) {
198
202
  continue;
199
203
  }
200
204
  if (Date.now() - lastProgressAt > 60_000) {
201
- info(color.dim(` • deploy Still working; waiting for the next deployment update…`));
205
+ indicator.setPhase('waiting');
202
206
  lastProgressAt = Date.now();
203
207
  }
204
208
  pollDelayMs = Math.min(Math.max(pollDelayMs * 1.5, 5000), 30000);
@@ -210,19 +214,22 @@ export async function deploy({ flags }) {
210
214
  const key = `${ev.id ?? ev.createdAt}:${ev.step}:${ev.status}`;
211
215
  if (seen.has(key)) continue;
212
216
  seen.add(key);
213
- indicator.clear();
214
- const tag = stepBadge(ev.status);
215
- info(` ${tag} ${color.dim(ev.step.padEnd(8))} ${ev.message ?? ''}`);
216
217
  lastProgressAt = Date.now();
217
218
  currentPhase = phaseFromEvent(ev);
219
+ indicator.setPhase(currentPhase);
218
220
  }
219
221
 
220
222
  const status = pipe.deployStatus ?? pipe.buildStatus ?? '';
221
- if (status && status !== lastStatus) lastStatus = status;
222
223
  if (['ready', 'live', 'success'].includes(status)) {
223
224
  indicator.stop();
224
225
  const url = pipe.hostings?.[0]?.domains?.[0]?.domain;
225
- ok(`Live${url ? ` → ${color.cyan(`https://${url.replace(/^https?:\/\//, '')}`)}` : ''}`);
226
+ if (url) {
227
+ const problem = await detectLiveUrlProblem(`https://${url.replace(/^https?:\/\//, '')}`);
228
+ if (problem) {
229
+ return fail(`Deployment finished, but the hostname is still not serving your app: ${problem}. Run \`glash logs ${dep.id}\` for details.`);
230
+ }
231
+ }
232
+ printDeploySuccess({ url, envSummary });
226
233
  return;
227
234
  }
228
235
  if (status === 'placeholder') {
@@ -239,6 +246,7 @@ export async function deploy({ flags }) {
239
246
  return fail(`Deployment failed (${status})${detail}. Run \`glash logs ${dep.id}\` for details.`);
240
247
  }
241
248
  currentPhase = phaseFromPipeline(pipe, currentPhase);
249
+ indicator.setPhase(currentPhase);
242
250
  pollDelayMs = nextPollDelay(pipe);
243
251
  await indicator.wait(pollDelayMs);
244
252
  }
@@ -248,7 +256,7 @@ export async function deploy({ flags }) {
248
256
 
249
257
  async function syncEnvBeforeDeploy(projectId, target, flags) {
250
258
  const files = await resolveEnvFiles(target, flags);
251
- if (!files.length) return;
259
+ if (!files.length) return { count: 0, files: [] };
252
260
 
253
261
  const merged = new Map();
254
262
  for (const file of files) {
@@ -259,9 +267,9 @@ async function syncEnvBeforeDeploy(projectId, target, flags) {
259
267
  .filter(([key]) => !key.startsWith('GLASH_'))
260
268
  .map(([key, value]) => ({ key, value, environment: target }));
261
269
 
262
- if (!vars.length) return;
270
+ if (!vars.length) return { count: 0, files };
263
271
  await apiPost(`/projects/${projectId}/env-vars/bulk`, { vars });
264
- info(color.dim(`→ synced ${vars.length} env vars from ${files.join(', ')}`));
272
+ return { count: vars.length, files };
265
273
  }
266
274
 
267
275
  async function resolveEnvFiles(target, flags) {
@@ -306,13 +314,6 @@ function parseDotenv(text) {
306
314
  return out;
307
315
  }
308
316
 
309
- function stepBadge(status) {
310
- if (status === 'success' || status === 'issued' || status === 'active' || status === 'ready') return color.green('✓');
311
- if (status === 'error' || status === 'failed') return color.red('✗');
312
- if (status === 'warning') return color.yellow('!');
313
- return color.cyan('•');
314
- }
315
-
316
317
  function nextPollDelay(pipe) {
317
318
  const status = pipe.deployStatus ?? pipe.buildStatus ?? '';
318
319
  const last = [...(pipe.events ?? [])].reverse().find(Boolean);
@@ -340,19 +341,19 @@ function formatDuration(ms) {
340
341
  }
341
342
 
342
343
  function phaseFromEvent(ev) {
343
- if (ev.step === 'detect') return 'detecting';
344
- if (ev.step === 'host') return 'assigning host';
345
- if (ev.step === 'package') return ev.status === 'queued' ? 'queued' : 'packaging';
344
+ if (ev.step === 'detect') return 'doing';
345
+ if (ev.step === 'host') return 'hatching';
346
+ if (ev.step === 'package') return ev.status === 'queued' ? 'hatching' : 'packaging';
346
347
  if (ev.step === 'build') return 'building';
347
- if (ev.step === 'deploy') return ev.status === 'ready' ? 'finalizing' : 'deploying';
348
+ if (ev.step === 'deploy') return ev.status === 'ready' ? 'checking' : 'deploying';
348
349
  if (ev.step === 'route' || ev.step === 'edge' || ev.step === 'ssl') return 'routing';
349
- if (ev.step === 'runtime') return 'checking runtime';
350
- return ev.step || 'deploying';
350
+ if (ev.step === 'runtime') return 'checking';
351
+ return ev.step || 'glashing';
351
352
  }
352
353
 
353
354
  function phaseFromPipeline(pipe, fallback) {
354
355
  const status = pipe.deployStatus ?? pipe.buildStatus ?? '';
355
- if (status === 'queued') return 'queued';
356
+ if (status === 'queued') return 'hatching';
356
357
  if (status === 'building') return 'building';
357
358
  if (status === 'rolling-out') return 'deploying';
358
359
  const last = [...(pipe.events ?? [])].reverse().find(Boolean);
@@ -369,23 +370,60 @@ function deploymentFailureHint(message = '') {
369
370
  return '';
370
371
  }
371
372
 
372
- function createDeployIndicator({ start, getPhase }) {
373
+ async function detectLiveUrlProblem(url) {
374
+ try {
375
+ const res = await fetch(url, {
376
+ redirect: 'manual',
377
+ signal: AbortSignal.timeout(10_000),
378
+ headers: { 'User-Agent': `glash-cli-live-check` },
379
+ });
380
+ const text = await res.clone().text().catch(() => '');
381
+ if (/glashDB:\s*no application is deployed at this hostname/i.test(text)) {
382
+ return 'the edge reached the host, but no nginx route exists for this hostname';
383
+ }
384
+ if (/Glash\s+Site reserved|Site reserved/i.test(text)) {
385
+ return 'the hostname is still showing the glashDB parked page';
386
+ }
387
+ if (res.status >= 500) {
388
+ return `the live URL returned HTTP ${res.status}`;
389
+ }
390
+ return '';
391
+ } catch (e) {
392
+ return `the live URL check failed (${e.message})`;
393
+ }
394
+ }
395
+
396
+ function printDeploySuccess({ url, envSummary }) {
397
+ const liveUrl = url ? `https://${url.replace(/^https?:\/\//, '')}` : '';
398
+ if (liveUrl) info(`Live: ${terminalLink(liveUrl)}`);
399
+ const envText = envSummary?.skipped ? 'skipped' : `${envSummary?.count ?? 0} moved to glash`;
400
+ info(`Env: ${envText}`);
401
+ ok('Success');
402
+ }
403
+
404
+ function terminalLink(url) {
405
+ const label = color.cyan(url);
406
+ if (!stdout.isTTY) return label;
407
+ return `\x1b]8;;${url}\x07${label}\x1b]8;;\x07`;
408
+ }
409
+
410
+ function createDeployIndicator() {
373
411
  const interactive = Boolean(stdout.isTTY);
374
412
  let frame = 0;
375
413
  let timer;
376
- let plainLast = 0;
414
+ let phase = 'doing';
415
+ let printedPhase = '';
377
416
  const frames = ['.', '..', '...'];
378
417
 
379
418
  function message() {
380
- const elapsed = Math.floor((Date.now() - start) / 1000);
381
419
  const dots = frames[frame++ % frames.length].padEnd(3, ' ');
382
- return ` ${color.cyan('•')} ${color.dim('deploy'.padEnd(8))} ${getPhase()}${dots} ${color.dim(`${elapsed}s elapsed`)}`;
420
+ return color.dim(`${phase}${dots}`);
383
421
  }
384
422
 
385
- function render() {
423
+ function render({ force = false } = {}) {
386
424
  if (!interactive) {
387
- if (Date.now() - plainLast > 30_000) {
388
- plainLast = Date.now();
425
+ if (force && phase !== printedPhase) {
426
+ printedPhase = phase;
389
427
  info(message().trimEnd());
390
428
  }
391
429
  return;
@@ -394,12 +432,15 @@ function createDeployIndicator({ start, getPhase }) {
394
432
  }
395
433
 
396
434
  return {
397
- start() {
435
+ start(initialPhase = 'doing') {
436
+ phase = initialPhase;
398
437
  render();
399
438
  timer = setInterval(render, 1000);
400
439
  },
401
- clear() {
402
- if (interactive) stdout.write('\r\x1b[2K');
440
+ setPhase(nextPhase) {
441
+ if (!nextPhase || nextPhase === phase) return;
442
+ phase = nextPhase;
443
+ render({ force: true });
403
444
  },
404
445
  stop() {
405
446
  if (timer) clearInterval(timer);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@glash/cli",
3
- "version": "0.2.22",
3
+ "version": "0.2.23",
4
4
  "description": "glashDB command-line interface — deploy projects, pull source + env, manage env vars.",
5
5
  "license": "Apache-2.0",
6
6
  "homepage": "https://glashdb.com",