@glash/cli 0.2.39 → 0.2.40

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
@@ -4,7 +4,7 @@ The official command-line interface for [glashDB](https://glashdb.com).
4
4
 
5
5
  ## Install
6
6
 
7
- Pick whichever install style you prefer — all four produce the same `glash` command:
7
+ Pick whichever install style you prefer - all four produce the same `glash` command:
8
8
 
9
9
  ```bash
10
10
  # Short-name (recommended)
@@ -18,7 +18,7 @@ pnpm add -g glashdb
18
18
  yarn global add glashdb
19
19
  bun add -g glashdb
20
20
 
21
- # No install — run on demand
21
+ # No install - run on demand
22
22
  npx -y glashdb deploy
23
23
  ```
24
24
 
@@ -44,7 +44,7 @@ glash --version
44
44
  ```
45
45
 
46
46
  On macOS with Homebrew Node, the global prefix is usually
47
- `/opt/homebrew/bin` (Apple Silicon) or `/usr/local/bin` (Intel) — both are
47
+ `/opt/homebrew/bin` (Apple Silicon) or `/usr/local/bin` (Intel) - both are
48
48
  typically already on `PATH`. With `nvm`, re-run `nvm use` in new shells or
49
49
  add `nvm use --default` to your shell init.
50
50
 
@@ -109,6 +109,14 @@ glash deploy --asset-mode auto --asset-threshold 100mb
109
109
  Every deploy prints a live monitor link. Open it to watch the black-screen deployment flow while the CLI is packaging, uploading, building, routing, and checking the live URL.
110
110
  Public asset offload prepares assets in small batches and uses multipart uploads for large files, so image/video-heavy `public/` folders can upload without forcing the source bundle over the platform limit.
111
111
 
112
+ Before a local source upload, `glash deploy` automatically runs the project's
113
+ `build` script with the package manager declared in `package.json` or detected
114
+ from its lockfile. The progress display keeps the progress bar on top and shows
115
+ the active step and latest build output underneath it. A failed local build
116
+ stops the deployment before source is uploaded. Use `--no-build` only when you
117
+ intentionally need to bypass this pre-deploy check. Remote Git deployments
118
+ continue to build on the Glash build worker.
119
+
112
120
  Framework/runtime checks:
113
121
 
114
122
  `glash deploy` detects both the project framework and the runtime scripts in `package.json`. If they disagree, for example the project looks like Next.js but `start` calls `glash serve`, the CLI stops before upload and asks you to align the scripts or explicitly continue. Use `--ignore-framework-mismatch` only when the mismatch is intentional.
@@ -142,7 +150,8 @@ For glashjs projects, `glash deploy` detects `glashjs` or `glash.config.mjs`, ma
142
150
  | `glash projects:rm <slug>` | Delete a project |
143
151
  | `glash link <slug>` | Link the current directory to an existing project |
144
152
  | `glash open` | Open the linked project's primary domain |
145
- | `glash deploy [--prod] [--detach]` | Deploy the linked project; streams the pipeline until live |
153
+ | `glash deploy [--prod] [--detach]` | Build and deploy the linked project; streams the pipeline until live |
154
+ | `glash deploy --no-build` | Skip the automatic local build before uploading source |
146
155
  | `glash deploy --asset-mode offload` | Upload `public/` assets to object storage/CDN before zipping source |
147
156
  | `glash deploy --source-upload chunked` | Stage the source zip through multipart object storage to bypass Cloudflare's single-request cap |
148
157
  | `glash deployments` | List recent deployments |
@@ -200,7 +209,7 @@ The commands require PostgreSQL client tools locally: `pg_dump` for dumps,
200
209
  | --- | --- | --- |
201
210
  | `GLASH_API_URL` | `https://api.glashdb.com/api` | Override the API base (self-hosted, staging) |
202
211
  | `GLASH_APP_URL` | `https://glashdb.com` | Override the dashboard base used for live monitor links |
203
- | `GLASH_TOKEN` | — | Use this token instead of `~/.glash/credentials.json` (useful in CI) |
212
+ | `GLASH_TOKEN` | - | Use this token instead of `~/.glash/credentials.json` (useful in CI) |
204
213
  | `GLASH_ASSET_OFFLOAD_THRESHOLD` | `100mb` | Default threshold for `glash deploy --asset-mode auto` |
205
214
  | `GLASH_ASSET_BATCH_SIZE` | `50` | Number of public assets prepared per request during `--asset-mode offload` |
206
215
  | `GLASH_SOURCE_UPLOAD_MODE` | `auto` | Source upload mode: `auto`, `direct`, or `chunked` |
package/bin/glash.mjs CHANGED
@@ -49,7 +49,7 @@ const commands = {
49
49
  };
50
50
 
51
51
  function usage() {
52
- info(`${color.bold('glash')} ${color.dim(`v${VERSION}`)} — glashDB command line
52
+ info(`${color.bold('glash')} ${color.dim(`v${VERSION}`)} - glashDB command line
53
53
 
54
54
  ${color.bold('USAGE')}
55
55
  glash <command> [args] [flags]
@@ -68,7 +68,7 @@ ${color.bold('PROJECTS')}
68
68
  open Open the project's primary domain in a browser
69
69
 
70
70
  ${color.bold('DEPLOY')}
71
- deploy [--prod] [--detach] Deploy the linked project, syncing .env files first
71
+ deploy [--prod] [--detach] Build and deploy the linked project, syncing .env files
72
72
  new deploy Deploy to a brand new project, ignoring any existing link
73
73
  deployments List recent deployments
74
74
  status <id> Show deployment pipeline + events
@@ -98,6 +98,7 @@ ${color.bold('FLAGS')}
98
98
  --environment <name> Env-var environment (default: production)
99
99
  --env-file <file[,file]> Env file(s) to sync before deploy
100
100
  --no-env Skip automatic env sync during deploy
101
+ --no-build Skip the automatic local build before source upload
101
102
  --asset-mode <mode> Public asset handling: inline, auto, offload
102
103
  --asset-threshold <size> Auto-offload threshold (default: 100mb)
103
104
  --source-upload <mode> Source upload: auto, direct, chunked
@@ -5,11 +5,12 @@ import fg from 'fast-glob';
5
5
  import { execSync } from 'node:child_process';
6
6
  import { createHash } from 'node:crypto';
7
7
  import { zipDirectory } from '../lib/zip.mjs';
8
+ import { buildProjectBeforeDeploy, compactBuildOutputLine } from '../lib/project-build.mjs';
8
9
  import { join, basename, extname } from 'node:path';
9
10
  import { tmpdir } from 'node:os';
10
11
  import { createReadStream, openAsBlob } from 'node:fs';
11
12
  import { access, readFile, rm, stat } from 'node:fs/promises';
12
- import { stdin, stdout } from 'node:process';
13
+ import { stdin, stdout, stderr } from 'node:process';
13
14
 
14
15
  function currentCommit() {
15
16
  try {
@@ -147,6 +148,9 @@ export async function deploy({ flags }) {
147
148
  let dep;
148
149
  let envSummary = { count: 0, skipped: Boolean(flags['no-env']) };
149
150
  let assetSummary = { mode: requestedAssetMode, status: requestedAssetMode === 'inline' ? 'inline' : 'pending', offloaded: false, count: 0, bytes: 0, skipped: 0 };
151
+ let localBuild = remoteDeploy
152
+ ? { status: 'remote' }
153
+ : { status: 'pending' };
150
154
  let currentPhase = 'doing';
151
155
  const start = Date.now();
152
156
  const indicator = createDeployIndicator();
@@ -158,6 +162,7 @@ export async function deploy({ flags }) {
158
162
  source: remoteDeploy ? 'remote git deploy' : 'local upload',
159
163
  framework: projectProfile.framework,
160
164
  runtime: projectProfile.runtime?.summary || '',
165
+ localBuild,
161
166
  sourceUpload: { mode: remoteDeploy ? 'remote' : 'pending', parts: 0 },
162
167
  packagesUploaded: 0,
163
168
  filesIncluded: 0,
@@ -172,6 +177,33 @@ export async function deploy({ flags }) {
172
177
  info(`Live status: ${terminalLink(initialStatusUrl)}`);
173
178
  indicator.start(currentPhase);
174
179
 
180
+ if (!remoteDeploy) {
181
+ try {
182
+ localBuild = await buildProjectBeforeDeploy(process.cwd(), {
183
+ packageJson: projectProfile.runtime?.pkg,
184
+ skip: Boolean(flags['no-build']) || process.env.GLASH_CLI_BUILD_RUNNING === '1',
185
+ onStart: (plan) => {
186
+ indicator.setPhase('prebuilding', `Building project (${plan.displayCommand})`);
187
+ },
188
+ onLine: ({ line }) => {
189
+ const detail = compactBuildOutputLine(line);
190
+ if (detail) indicator.setDetail(`Building project — ${detail}`);
191
+ },
192
+ });
193
+ report.localBuild = localBuild;
194
+ } catch (e) {
195
+ localBuild = {
196
+ status: 'failed',
197
+ command: e.buildPlan?.displayCommand || '',
198
+ };
199
+ report.localBuild = localBuild;
200
+ indicator.fail('failed', 'Project build failed');
201
+ printBuildFailureOutput(e.buildOutput);
202
+ printDeployReport({ ...report, status: 'failed', error: `Project build failed: ${e.message}` });
203
+ return fail(`Project build failed: ${e.message}`);
204
+ }
205
+ }
206
+
175
207
  if (!flags['no-env']) {
176
208
  indicator.setPhase('syncing');
177
209
  try {
@@ -711,7 +743,7 @@ function sourceUploadPartSize(value) {
711
743
  }
712
744
 
713
745
  function sourceUploadConcurrency(flags = {}) {
714
- // Default 4 — parts upload simultaneously and 4 is the measured throughput
746
+ // Default 4 - parts upload simultaneously and 4 is the measured throughput
715
747
  // sweet spot: a single connection can't saturate a typical uplink (~2.5x
716
748
  // speedup from parallelism), but past ~4 connections contention makes it
717
749
  // slower and unreliable. Override with --source-concurrency or
@@ -1243,6 +1275,7 @@ function printDeployReport(report) {
1243
1275
  ['Source', report.source || '-'],
1244
1276
  ['Framework', report.framework || 'auto'],
1245
1277
  ['Runtime', report.runtime || '-'],
1278
+ ['Local build', formatLocalBuildSummary(report.localBuild)],
1246
1279
  ['Packages uploaded', formatPackagesUploaded(report)],
1247
1280
  ['Public assets', formatAssetSummary(report.assetSummary)],
1248
1281
  ['Deployment size', formatDeploymentSize(report)],
@@ -1269,6 +1302,27 @@ function formatReportStatus(status) {
1269
1302
  return status || '-';
1270
1303
  }
1271
1304
 
1305
+ function formatLocalBuildSummary(build) {
1306
+ if (!build || build.status === 'pending') return 'pending';
1307
+ if (build.status === 'remote') return 'handled by the Glash build worker';
1308
+ if (build.status === 'built') {
1309
+ const duration = Number.isFinite(build.durationMs) ? ` in ${formatDuration(build.durationMs)}` : '';
1310
+ return `${build.displayCommand || 'build completed'}${duration}`;
1311
+ }
1312
+ if (build.status === 'failed') return `${build.command || 'build command'} failed`;
1313
+ if (build.reason === 'disabled') return 'skipped (--no-build)';
1314
+ if (build.reason === 'no-package-json') return 'skipped (no package.json)';
1315
+ if (build.reason === 'no-build-script') return 'skipped (no build script)';
1316
+ return build.status || '-';
1317
+ }
1318
+
1319
+ function printBuildFailureOutput(output) {
1320
+ const lines = String(output || '').trim().split(/\r?\n/).filter(Boolean).slice(-80);
1321
+ if (!lines.length) return;
1322
+ warn(`Build output (last ${lines.length} line${lines.length === 1 ? '' : 's'}):`);
1323
+ stderr.write(`${lines.join('\n')}\n`);
1324
+ }
1325
+
1272
1326
  function formatPackagesUploaded(report) {
1273
1327
  const count = report.packagesUploaded ?? 0;
1274
1328
  if (report.sourceUpload?.mode === 'chunked') {
@@ -1347,27 +1401,38 @@ function createDeployIndicator() {
1347
1401
  const interactive = Boolean(stdout.isTTY);
1348
1402
  let timer;
1349
1403
  let phase = 'doing';
1404
+ let detail = detailFromPhase(phase);
1350
1405
  let progress = 0;
1351
1406
  let lastPrinted = '';
1407
+ let rendered = false;
1352
1408
 
1353
- function message() {
1409
+ function messages() {
1354
1410
  const width = 24;
1355
1411
  const filled = Math.round((progress / 100) * width);
1356
1412
  const bar = `${'#'.repeat(filled)}${'-'.repeat(width - filled)}`;
1357
1413
  const percent = `${String(Math.round(progress)).padStart(3, ' ')}%`;
1358
- return `${color.dim(`[${bar}]`)} ${percent} ${phase}`;
1414
+ return {
1415
+ bar: `${color.dim(`[${bar}]`)} ${percent}`,
1416
+ status: ` ${color.cyan('→')} ${detail}`,
1417
+ };
1359
1418
  }
1360
1419
 
1361
1420
  function render({ force = false } = {}) {
1362
- const next = message();
1421
+ const next = messages();
1422
+ const signature = `${next.bar}\n${next.status}`;
1423
+ if (signature === lastPrinted) return;
1363
1424
  if (!interactive) {
1364
- if (force && next !== lastPrinted) {
1365
- lastPrinted = next;
1366
- info(next);
1425
+ if (force) {
1426
+ lastPrinted = signature;
1427
+ info(next.bar);
1428
+ info(next.status);
1367
1429
  }
1368
1430
  return;
1369
1431
  }
1370
- stdout.write(`\r\x1b[2K${next}`);
1432
+ if (rendered) stdout.write('\r\x1b[2K\x1b[1A\r\x1b[2K');
1433
+ stdout.write(`${next.bar}\n${next.status}`);
1434
+ lastPrinted = signature;
1435
+ rendered = true;
1371
1436
  }
1372
1437
 
1373
1438
  function stopTimer() {
@@ -1375,45 +1440,59 @@ function createDeployIndicator() {
1375
1440
  timer = undefined;
1376
1441
  }
1377
1442
 
1378
- function setProgress(nextProgress, nextPhase = phase) {
1443
+ function updateProgress(nextProgress, nextPhase = phase, nextDetail) {
1379
1444
  const bounded = Math.max(0, Math.min(100, Number(nextProgress) || 0));
1380
1445
  const advanced = Math.max(progress, bounded);
1381
- const changed = nextPhase !== phase || advanced !== progress;
1446
+ const resolvedDetail = nextDetail || (nextPhase !== phase ? detailFromPhase(nextPhase) : detail);
1447
+ const changed = nextPhase !== phase || advanced !== progress || resolvedDetail !== detail;
1382
1448
  phase = nextPhase;
1383
1449
  progress = advanced;
1450
+ detail = resolvedDetail;
1384
1451
  if (changed) render({ force: true });
1385
1452
  }
1386
1453
 
1387
1454
  return {
1388
1455
  start(initialPhase = 'doing') {
1389
1456
  phase = initialPhase;
1457
+ detail = detailFromPhase(initialPhase);
1390
1458
  progress = 0;
1459
+ lastPrinted = '';
1460
+ rendered = false;
1391
1461
  render({ force: true });
1392
1462
  timer = setInterval(render, 1000);
1393
1463
  },
1394
- setPhase(nextPhase) {
1464
+ setPhase(nextPhase, nextDetail) {
1395
1465
  if (!nextPhase) return;
1396
- setProgress(progressFromPhase(nextPhase), nextPhase);
1466
+ updateProgress(progressFromPhase(nextPhase), nextPhase, nextDetail);
1397
1467
  },
1398
- setProgress(nextProgress, nextPhase = phase) {
1399
- setProgress(nextProgress, nextPhase);
1468
+ setProgress(nextProgress, nextPhase = phase, nextDetail) {
1469
+ updateProgress(nextProgress, nextPhase, nextDetail);
1400
1470
  },
1401
- finish(finalPhase = 'complete') {
1471
+ setDetail(nextDetail) {
1472
+ const value = String(nextDetail || '').trim();
1473
+ if (!value || value === detail) return;
1474
+ detail = value;
1475
+ render();
1476
+ },
1477
+ finish(finalPhase = 'complete', finalDetail = detailFromPhase(finalPhase)) {
1402
1478
  stopTimer();
1403
1479
  phase = finalPhase;
1480
+ detail = finalDetail;
1404
1481
  progress = 100;
1405
1482
  render({ force: true });
1406
1483
  if (interactive) stdout.write('\n');
1407
1484
  },
1408
- fail(finalPhase = 'failed') {
1485
+ fail(finalPhase = 'failed', finalDetail = detailFromPhase(finalPhase)) {
1409
1486
  stopTimer();
1410
1487
  phase = finalPhase;
1488
+ detail = finalDetail;
1411
1489
  render({ force: true });
1412
1490
  if (interactive) stdout.write('\n');
1413
1491
  },
1414
1492
  stop() {
1415
1493
  stopTimer();
1416
- if (interactive) stdout.write('\r\x1b[2K');
1494
+ if (interactive && rendered) stdout.write('\r\x1b[2K\x1b[1A\r\x1b[2K');
1495
+ rendered = false;
1417
1496
  },
1418
1497
  wait(ms) {
1419
1498
  return sleep(ms);
@@ -1421,14 +1500,38 @@ function createDeployIndicator() {
1421
1500
  };
1422
1501
  }
1423
1502
 
1503
+ function detailFromPhase(phase) {
1504
+ const details = {
1505
+ doing: 'Preparing deployment',
1506
+ prebuilding: 'Building project',
1507
+ syncing: 'Syncing environment variables',
1508
+ assets: 'Preparing public assets',
1509
+ packaging: 'Packaging source files',
1510
+ uploading: 'Uploading source chunks',
1511
+ glashing: 'Uploading source bundle',
1512
+ hatching: 'Waiting for a Glash build worker',
1513
+ waiting: 'Waiting for deployment updates',
1514
+ building: 'Building project on Glash',
1515
+ deploying: 'Deploying application',
1516
+ routing: 'Configuring domains and routing',
1517
+ checking: 'Checking the live deployment',
1518
+ complete: 'Deployment complete',
1519
+ queued: 'Deployment queued',
1520
+ failed: 'Deployment failed',
1521
+ 'timed out': 'Deployment timed out',
1522
+ };
1523
+ return details[phase] || String(phase || 'Working');
1524
+ }
1525
+
1424
1526
  function progressFromPhase(phase) {
1425
1527
  const progressByPhase = {
1426
1528
  doing: 2,
1427
- syncing: 8,
1428
- assets: 15,
1429
- packaging: 22,
1430
- uploading: 28,
1431
- glashing: 35,
1529
+ prebuilding: 7,
1530
+ syncing: 12,
1531
+ assets: 18,
1532
+ packaging: 25,
1533
+ uploading: 32,
1534
+ glashing: 38,
1432
1535
  hatching: 45,
1433
1536
  waiting: 48,
1434
1537
  building: 65,
@@ -42,7 +42,7 @@ async function browserLogin(flags) {
42
42
  try {
43
43
  start = await identityPost('/cli/device/start', { deviceName: hostname() });
44
44
  } catch (e) {
45
- // Older backend without device auth — fall back to password login.
45
+ // Older backend without device auth - fall back to password login.
46
46
  warn('Browser login is unavailable; falling back to email/password.');
47
47
  return passwordLogin(flags);
48
48
  }
@@ -61,7 +61,7 @@ async function browserLogin(flags) {
61
61
  } else if (openBrowser(approveUrl)) {
62
62
  info(color.dim(' Opened your browser… waiting for approval.'));
63
63
  } else {
64
- info(' Could not open a browser automatically — open the link above.');
64
+ info(' Could not open a browser automatically - open the link above.');
65
65
  }
66
66
 
67
67
  const deadline = Date.now() + expiresIn * 1000;
@@ -11,7 +11,7 @@ export async function logout() {
11
11
  try {
12
12
  await apiPost('/auth/logout', {}, { token });
13
13
  } catch (e) {
14
- info(color.dim(`(server-side revoke failed: ${e?.message || e} — clearing local creds anyway)`));
14
+ info(color.dim(`(server-side revoke failed: ${e?.message || e} - clearing local creds anyway)`));
15
15
  }
16
16
  }
17
17
  await clearCreds();
package/commands/open.mjs CHANGED
@@ -12,7 +12,7 @@ export async function open({ flags }) {
12
12
  try {
13
13
  const p = await apiGet(`/projects/${projectId}`);
14
14
  const primary = p.domains?.find((d) => d.isPrimary) ?? p.domains?.[0];
15
- if (!primary) return fail('Project has no domains yet — deploy first.');
15
+ if (!primary) return fail('Project has no domains yet - deploy first.');
16
16
  url = `https://${primary.domain}`;
17
17
  } catch (e) {
18
18
  return fail(e.message);
@@ -10,6 +10,6 @@ export async function whoami() {
10
10
  info(`${color.bold(me.email)} ${color.dim(`(${me.role})`)}`);
11
11
  info(color.dim(`API: ${API_URL}`));
12
12
  } catch (e) {
13
- fail(`${e.message} — try \`glash login\` again`);
13
+ fail(`${e.message} - try \`glash login\` again`);
14
14
  }
15
15
  }
package/lib/api.mjs CHANGED
@@ -65,7 +65,7 @@ export async function api(method, path, { body, token, query, retries = DEFAULT_
65
65
  }
66
66
 
67
67
  // FormData bodies carry file Blobs backed by the on-disk zip, so they are
68
- // re-streamable — fetch reads them fresh on each attempt, which lets us
68
+ // re-streamable - fetch reads them fresh on each attempt, which lets us
69
69
  // retry uploads without re-zipping.
70
70
  const requestBody = isForm ? body : (body === undefined ? undefined : JSON.stringify(body));
71
71
  const effectiveTimeout = timeoutMs ?? (isForm ? UPLOAD_TIMEOUT_MS : DEFAULT_TIMEOUT_MS);
@@ -0,0 +1,146 @@
1
+ import { spawn } from 'node:child_process';
2
+ import { access, readFile } from 'node:fs/promises';
3
+ import { join } from 'node:path';
4
+
5
+ const PACKAGE_MANAGERS = new Set(['npm', 'pnpm', 'yarn', 'bun']);
6
+ const LOCKFILES = [
7
+ ['pnpm-lock.yaml', 'pnpm'],
8
+ ['yarn.lock', 'yarn'],
9
+ ['bun.lock', 'bun'],
10
+ ['bun.lockb', 'bun'],
11
+ ['package-lock.json', 'npm'],
12
+ ['npm-shrinkwrap.json', 'npm'],
13
+ ];
14
+ const MAX_CAPTURED_OUTPUT = 64 * 1024;
15
+
16
+ export async function planProjectBuild(cwd, { packageJson } = {}) {
17
+ const pkg = packageJson ?? await readPackageJson(cwd);
18
+ if (!pkg) {
19
+ return { status: 'skipped', reason: 'no-package-json' };
20
+ }
21
+
22
+ const script = String(pkg.scripts?.build || '').trim();
23
+ if (!script) {
24
+ return { status: 'skipped', reason: 'no-build-script' };
25
+ }
26
+
27
+ const packageManager = await detectPackageManager(cwd, pkg);
28
+ const args = ['run', 'build'];
29
+ return {
30
+ status: 'ready',
31
+ command: packageManager,
32
+ args,
33
+ displayCommand: `${packageManager} ${args.join(' ')}`,
34
+ script,
35
+ };
36
+ }
37
+
38
+ export async function buildProjectBeforeDeploy(cwd, {
39
+ packageJson,
40
+ skip = false,
41
+ onStart,
42
+ onLine,
43
+ spawnImpl = spawn,
44
+ env = process.env,
45
+ } = {}) {
46
+ if (skip) return { status: 'skipped', reason: 'disabled' };
47
+
48
+ const plan = await planProjectBuild(cwd, { packageJson });
49
+ if (plan.status !== 'ready') return plan;
50
+
51
+ onStart?.(plan);
52
+ const startedAt = Date.now();
53
+ let capturedOutput = '';
54
+
55
+ const appendOutput = (stream, line) => {
56
+ const text = String(line || '').trimEnd();
57
+ if (!text) return;
58
+ capturedOutput += `${stream === 'stderr' ? '[stderr] ' : ''}${text}\n`;
59
+ if (capturedOutput.length > MAX_CAPTURED_OUTPUT) {
60
+ capturedOutput = capturedOutput.slice(-MAX_CAPTURED_OUTPUT);
61
+ }
62
+ try { onLine?.({ stream, line: text }); } catch {}
63
+ };
64
+
65
+ await new Promise((resolve, reject) => {
66
+ const child = spawnImpl(plan.command, plan.args, {
67
+ cwd,
68
+ env: { ...env, GLASH_CLI_BUILD_RUNNING: '1' },
69
+ stdio: ['inherit', 'pipe', 'pipe'],
70
+ });
71
+
72
+ streamLines(child.stdout, (line) => appendOutput('stdout', line));
73
+ streamLines(child.stderr, (line) => appendOutput('stderr', line));
74
+
75
+ child.once('error', (cause) => {
76
+ const error = new Error(`Could not start ${plan.displayCommand}: ${cause.message}`, { cause });
77
+ error.buildOutput = capturedOutput;
78
+ error.buildPlan = plan;
79
+ reject(error);
80
+ });
81
+ child.once('close', (code, signal) => {
82
+ if (code === 0) return resolve();
83
+ const outcome = signal ? `signal ${signal}` : `exit code ${code ?? 'unknown'}`;
84
+ const error = new Error(`${plan.displayCommand} failed with ${outcome}`);
85
+ error.exitCode = code;
86
+ error.signal = signal;
87
+ error.buildOutput = capturedOutput;
88
+ error.buildPlan = plan;
89
+ reject(error);
90
+ });
91
+ });
92
+
93
+ return {
94
+ ...plan,
95
+ status: 'built',
96
+ durationMs: Date.now() - startedAt,
97
+ output: capturedOutput,
98
+ };
99
+ }
100
+
101
+ export function compactBuildOutputLine(line, maxLength = 110) {
102
+ const compact = String(line || '')
103
+ .replace(/\x1B\][^\x07]*(?:\x07|\x1B\\)/g, '')
104
+ .replace(/\x1B\[[0-?]*[ -/]*[@-~]/g, '')
105
+ .replace(/[\u0000-\u0008\u000B\u000C\u000E-\u001F\u007F]/g, '')
106
+ .replace(/\s+/g, ' ')
107
+ .trim();
108
+ if (!compact) return '';
109
+ return compact.length > maxLength ? `${compact.slice(0, maxLength - 1)}…` : compact;
110
+ }
111
+
112
+ async function detectPackageManager(cwd, pkg) {
113
+ const declared = String(pkg.packageManager || '').trim().toLowerCase().match(/^([a-z]+)(?:@|$)/)?.[1];
114
+ if (declared && PACKAGE_MANAGERS.has(declared)) return declared;
115
+
116
+ for (const [filename, packageManager] of LOCKFILES) {
117
+ try {
118
+ await access(join(cwd, filename));
119
+ return packageManager;
120
+ } catch {}
121
+ }
122
+ return 'npm';
123
+ }
124
+
125
+ async function readPackageJson(cwd) {
126
+ try {
127
+ return JSON.parse(await readFile(join(cwd, 'package.json'), 'utf8'));
128
+ } catch {
129
+ return null;
130
+ }
131
+ }
132
+
133
+ function streamLines(stream, onLine) {
134
+ if (!stream) return;
135
+ let pending = '';
136
+ stream.setEncoding('utf8');
137
+ stream.on('data', (chunk) => {
138
+ pending += chunk;
139
+ const lines = pending.split(/\r?\n|\r/);
140
+ pending = lines.pop() ?? '';
141
+ for (const line of lines) onLine(line);
142
+ });
143
+ stream.on('end', () => {
144
+ if (pending) onLine(pending);
145
+ });
146
+ }
package/lib/ui.mjs CHANGED
@@ -25,7 +25,7 @@ export async function prompt(question, { mask = false } = {}) {
25
25
  rl.close();
26
26
  return a.trim();
27
27
  }
28
- // Masked input — write * for each typed char. readline doesn't natively
28
+ // Masked input - write * for each typed char. readline doesn't natively
29
29
  // mask, so we monkey-patch the output stream for this prompt only.
30
30
  const orig = rl._writeToOutput?.bind(rl);
31
31
  rl._writeToOutput = (s) => {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@glash/cli",
3
- "version": "0.2.39",
4
- "description": "glashDB command-line interface — deploy projects, pull source + env, manage env vars.",
3
+ "version": "0.2.40",
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",
7
7
  "repository": {
@@ -30,13 +30,9 @@
30
30
  "database"
31
31
  ],
32
32
  "dependencies": {
33
- "chalk": "^5.3.0",
34
- "commander": "^12.1.0",
35
33
  "dotenv": "^16.4.5",
36
34
  "fast-glob": "^3.3.3",
37
35
  "fs-extra": "^11.2.0",
38
- "open": "^10.1.0",
39
- "ora": "^8.0.1",
40
36
  "yazl": "^3.3.1"
41
37
  },
42
38
  "publishConfig": {