From 01195ac5c7cf2d7c83c98a1786b5f7ff0cfb2d70 Mon Sep 17 00:00:00 2001 From: Vikas Singhal Date: Thu, 25 Jun 2026 18:03:56 +0530 Subject: [PATCH 1/2] feat(cli): db push transport + progress polish (large-DB feedback #13-#15) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit From a 166 MB full-parity push (round 3): - #13 scp uploads with -C (compress in transit); SQL compresses ~5-10x - #14 non-TTY runs (pipe/CI/background) emit one-line phase markers to stderr instead of going silent for the whole push (ora spinners are TTY-only) - #15 db push summary reports elapsed time + throughput (e.g. "144s (1.2 MB/s)") #16 (search-replace table scoping, --verify readiness check, backup retention) deferred — larger/new surface. Co-Authored-By: Claude Opus 4.8 (1M context) --- CHANGELOG.md | 10 ++++++++++ src/__tests__/output.test.ts | 19 +++++++++++++++++- src/__tests__/ssh-connection.test.ts | 18 ++++++++++++++++- src/commands/db.ts | 8 ++++++++ src/lib/output.ts | 30 +++++++++++++++++++--------- src/lib/ssh-connection.ts | 1 + 6 files changed, 75 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 977940e..4f914b5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,15 @@ # Changelog +## Unreleased + +### Improved — db push transport & progress (large-DB path) + +From round-3 feedback (a 166 MB full-parity push — the transform logic held; transport/progress needed polish): + +- **`db push` compresses the dump in transit** — scp now uses `-C` (SQL compresses ~5–10×), cutting upload time on large dumps. (#13) +- **Non-interactive runs show progress** — when stdout isn't a TTY (pipe / CI / background), each phase prints a one-line marker to **stderr** (Backing up… / Uploading… / Importing… / Rewriting URLs…) instead of going silent until the final summary. (#14) +- **`db push` summary reports timing** — adds an `elapsed: 144s (1.2 MB/s)` line. (#15) + ## 0.0.1-beta.24 (2026-06-23) ### Added — db push prefix-safety, URL remap, sync mirroring & webroot, exec shell diff --git a/src/__tests__/output.test.ts b/src/__tests__/output.test.ts index 4999089..a8747f7 100644 --- a/src/__tests__/output.test.ts +++ b/src/__tests__/output.test.ts @@ -132,10 +132,27 @@ describe('output', () => { s.stop(); }); - it('returns an ora spinner in human mode', () => { + it('returns a working spinner in human mode', () => { const s = spinner('Loading...'); expect(s.start).toBeTypeOf('function'); expect(s.succeed).toBeTypeOf('function'); }); + + it('emits a phase marker to stderr in non-TTY mode (heartbeat for long ops)', () => { + // The vitest runner is non-TTY, so spinner() returns the marker stub. + const spy = vi.spyOn(console, 'error').mockImplementation(() => {}); + spinner('Uploading...').start(); + expect(spy).toHaveBeenCalled(); + expect(spy.mock.calls[0][0]).toContain('Uploading...'); + }); + + it('emits no marker in json mode (machine output stays clean)', () => { + setJsonMode(true); + const spy = vi.spyOn(console, 'error').mockImplementation(() => {}); + const s = spinner('Uploading...'); + s.start(); + s.succeed('done'); + expect(spy).not.toHaveBeenCalled(); + }); }); }); diff --git a/src/__tests__/ssh-connection.test.ts b/src/__tests__/ssh-connection.test.ts index f5faf68..8f01518 100644 --- a/src/__tests__/ssh-connection.test.ts +++ b/src/__tests__/ssh-connection.test.ts @@ -16,7 +16,7 @@ vi.mock('node:fs', async (importOriginal) => { }; }); -const { spawnInteractiveSsh, execViaSsh, execViaSshStreamStdin, rsyncViaSsh } = await import('../lib/ssh-connection.js'); +const { spawnInteractiveSsh, execViaSsh, execViaSshStreamStdin, scpUpload, rsyncViaSsh } = await import('../lib/ssh-connection.js'); const conn: SshConnection = { host: 'test.example.com', @@ -140,6 +140,22 @@ describe('ssh-connection', () => { }); }); + describe('scpUpload', () => { + it('enables compression (-C) and returns the exit code', () => { + mockSpawnSync.mockReturnValue({ status: 0, stdout: '', stderr: '' }); + + const code = scpUpload(conn, '/local/dump.sql', '/tmp/dump.sql'); + + expect(code).toBe(0); + const cmd = mockSpawnSync.mock.calls[0][0]; + const args = mockSpawnSync.mock.calls[0][1] as string[]; + expect(cmd).toBe('scp'); + expect(args).toContain('-C'); + expect(args).toContain('/local/dump.sql'); + expect(args[args.length - 1]).toBe('testuser@test.example.com:/tmp/dump.sql'); + }); + }); + describe('rsyncViaSsh', () => { it('builds correct rsync command for push', () => { mockSpawnSync.mockReturnValue({ status: 0 }); diff --git a/src/commands/db.ts b/src/commands/db.ts index 4896445..5c794ae 100644 --- a/src/commands/db.ts +++ b/src/commands/db.ts @@ -221,6 +221,9 @@ Examples: } } + // Start the clock once the actual work begins (excludes prompt think-time). + const startedAt = Date.now(); + // Step 1: Backup if (takeBackup) { const backupSpin = spinner(`Backing up remote database to ~/${backupFilename}...`); @@ -418,6 +421,10 @@ Examples: cleanupSpin.succeed('Cleanup complete'); } + const elapsedSec = (Date.now() - startedAt) / 1000; + const rate = elapsedSec > 0 ? `${formatBytes(localSize / elapsedSec)}/s` : 'n/a'; + const elapsedStr = `${elapsedSec < 10 ? elapsedSec.toFixed(1) : Math.round(elapsedSec)}s (${rate})`; + success('Push complete', { site_id: site.id, backup_path: takeBackup ? backupRemotePath : null, @@ -425,6 +432,7 @@ Examples: size_bytes: localSize, rewrote_prefix: remapFromPrefix ? `${remapFromPrefix} -> ${remotePrefix}` : null, search_replaced: srFrom && srTo ? `${srFrom} -> ${srTo}` : null, + elapsed: elapsedStr, }); if (!isJsonMode() && takeBackup) { diff --git a/src/lib/output.ts b/src/lib/output.ts index 2e72e78..65ef03a 100644 --- a/src/lib/output.ts +++ b/src/lib/output.ts @@ -57,20 +57,32 @@ export function table(headers: string[], rows: Record[]): void { console.log(t.toString()); } -function shouldSuppressSpinner(): boolean { - if (jsonMode) return true; - if (!process.stdout.isTTY) return true; - if (process.env.CI) return true; - if (process.env.INSTAWP_QUIET) return true; - if (process.env.NO_COLOR) return true; - return false; +/** Truly silent: machine output (--json) or an explicit quiet request. */ +function spinnerSilent(): boolean { + return jsonMode || !!process.env.INSTAWP_QUIET; } export function spinner(text: string): Ora | { text: string; start: () => any; succeed: (t?: string) => void; fail: (t?: string) => void; stop: () => void } { - if (shouldSuppressSpinner()) { + if (spinnerSilent()) { return { text: '', start() { return this; }, succeed() {}, fail() {}, stop() {} }; } - return ora(text); + + // Animated spinner only on an interactive TTY (and when colors aren't disabled). + if (process.stdout.isTTY && !process.env.CI && !process.env.NO_COLOR) { + return ora(text); + } + + // Non-interactive (pipe / CI / redirected / NO_COLOR): no animation, but emit + // one-line phase markers to STDERR so long operations show a heartbeat instead + // of going silent (e.g. a 144s db push looked hung). stderr keeps stdout/the + // final summary clean for capture. ora itself writes to stderr, so this matches. + return { + text, + start() { console.error(chalk.cyan('→') + ' ' + this.text); return this; }, + succeed(t?: string) { console.error(chalk.green('✓') + ' ' + (t || this.text)); }, + fail(t?: string) { console.error(chalk.red('✗') + ' ' + (t || this.text)); }, + stop() {}, + }; } export function info(msg: string): void { diff --git a/src/lib/ssh-connection.ts b/src/lib/ssh-connection.ts index 53bba3e..77ac4dd 100644 --- a/src/lib/ssh-connection.ts +++ b/src/lib/ssh-connection.ts @@ -40,6 +40,7 @@ function sshTarget(conn: SshConnection): string { function scpArgs(conn: SshConnection): string[] { ensureKnownHosts(); return [ + '-C', // compress in transit — SQL dumps compress ~5–10×; near-free on already-compressed inputs '-i', conn.privateKeyPath, '-P', String(conn.port), '-o', 'StrictHostKeyChecking=accept-new', From 8ec7f3a19446f1da9ad771ce59513aa3b61427e2 Mon Sep 17 00:00:00 2001 From: Vikas Singhal Date: Thu, 25 Jun 2026 18:08:19 +0530 Subject: [PATCH 2/2] chore(release): 0.0.1-beta.25 Finalize CHANGELOG header + bump for db push transport/progress polish (scp -C compression, non-TTY phase markers, elapsed/throughput in summary). Co-Authored-By: Claude Opus 4.8 (1M context) --- CHANGELOG.md | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4f914b5..54785cb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -## Unreleased +## 0.0.1-beta.25 (2026-06-25) ### Improved — db push transport & progress (large-DB path) diff --git a/package.json b/package.json index 71f8939..751cb3b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@instawp/cli", - "version": "0.0.1-beta.24", + "version": "0.0.1-beta.25", "description": "InstaWP CLI - Create and manage WordPress sites from the terminal", "type": "module", "bin": {