Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,15 @@
# Changelog

## 0.0.1-beta.25 (2026-06-25)

### 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
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -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": {
Expand Down
19 changes: 18 additions & 1 deletion src/__tests__/output.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
});
});
});
18 changes: 17 additions & 1 deletion src/__tests__/ssh-connection.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down Expand Up @@ -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 });
Expand Down
8 changes: 8 additions & 0 deletions src/commands/db.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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}...`);
Expand Down Expand Up @@ -418,13 +421,18 @@ 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,
restored_from: file,
size_bytes: localSize,
rewrote_prefix: remapFromPrefix ? `${remapFromPrefix} -> ${remotePrefix}` : null,
search_replaced: srFrom && srTo ? `${srFrom} -> ${srTo}` : null,
elapsed: elapsedStr,
});

if (!isJsonMode() && takeBackup) {
Expand Down
30 changes: 21 additions & 9 deletions src/lib/output.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,20 +57,32 @@ export function table(headers: string[], rows: Record<string, any>[]): 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 {
Expand Down
1 change: 1 addition & 0 deletions src/lib/ssh-connection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
Loading