@instawp/cli 0.0.1-beta.2 → 0.0.1-beta.4

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/CHANGELOG.md ADDED
@@ -0,0 +1,69 @@
1
+ # Changelog
2
+
3
+ ## 0.0.1-beta.4 (2026-05-22)
4
+
5
+ ### Windows — Zero-Install Support
6
+ - Bundled `rsync.exe` (with msys2 runtime DLLs) and BusyBox-w64 (`awk` provider) in `bin/win32/`. No more "install Git for Windows / cwRsync" prerequisite — `instawp local clone`, `local push/pull`, and `sync push/pull` work out of the box on Windows.
7
+ - Replaced the external `sqlite3` CLI dependency with the `better-sqlite3` Node module.
8
+ - New `src/lib/windows-binaries.ts` resolves bundled binaries; falls back to PATH then common Git-for-Windows install dirs.
9
+
10
+ ### Bug Fixes (Windows)
11
+ - `instawp local clone` now resolves the bundled `mysql2sqlite` script correctly (was broken by `new URL(import.meta.url).pathname` returning `/C:/...`).
12
+ - `mysql2sqlite` is invoked as `awk -f script` explicitly; no longer relies on shebang interpretation.
13
+ - `rsync` no longer treats Windows drive paths (`C:\...`) as remote hostnames — paths are converted to msys style (`/c/...`) inside `rsyncViaSsh`.
14
+ - `-e ssh -i <key>` argument uses forward slashes + quoted paths so msys/cygwin sh inside rsync parses the key path correctly.
15
+ - Eliminated the SQL injection risk in `local clone`'s URL search-replace (now uses bound parameters via better-sqlite3).
16
+
17
+ ### Internals
18
+ - New `scripts/fetch-windows-binaries.sh` (maintainer-only) refreshes the Windows bundle from MSYS2 + frippery.org.
19
+ - 32 new tests covering path conversion and bundled-binary resolution.
20
+
21
+ ## 0.0.1-beta.3 (2026-04-12)
22
+
23
+ ### New Commands
24
+ - `local create` — Create local WordPress sites (powered by WordPress Playground, no Docker needed)
25
+ - `local clone <site>` — Clone an InstaWP cloud site to local (files + database)
26
+ - `local start/stop` — Start in foreground or `--background` mode
27
+ - `local push/pull` — Sync wp-content between local and cloud (incremental rsync)
28
+ - `local list` — Show local sites with running/stopped status
29
+ - `local delete` — Remove local sites
30
+ - `sites php <site>` — View or update PHP version and settings
31
+ - `sites update <site>` — Update site label, description, or expiration
32
+ - `teams switch <team>` — Switch active team context
33
+
34
+ ### Improvements
35
+ - `create --wp <version>` — Specify WordPress version when creating sites
36
+ - `sites list` — 50 per page default, `--all` flag, pagination hints
37
+ - Login now shows user name and team after success
38
+ - Site resolver caches name-to-ID lookups for 10 minutes
39
+ - rsync only shows actually changed files (`--itemize-changes`)
40
+ - Magic login URL fixed to use correct `/wordpress-auto-login` endpoint
41
+
42
+ ### Bug Fixes
43
+ - Windows: SSH key generation now works (removed Unix-specific shell commands)
44
+ - Windows: command detection uses `where` instead of `which`
45
+ - `exec/wp --api` flag now works at any position in the command
46
+ - Terminal restored after local site Ctrl+C (`stty sane`)
47
+
48
+ ## 0.0.1-beta.2 (2026-03-23)
49
+
50
+ ### New Commands
51
+ - `local create/clone/start/stop/push/pull/list/delete` — Full local development workflow
52
+ - `teams switch` — Client-side team context
53
+
54
+ ### Improvements
55
+ - Site resolver caching
56
+ - Incremental rsync output
57
+
58
+ ## 0.0.1-beta.1 (2026-03-02)
59
+
60
+ ### Initial Release
61
+ - `login` — OAuth browser flow or `--token`
62
+ - `whoami` — Show current session
63
+ - `create` — Create WordPress sites with provisioning progress
64
+ - `sites list/delete` — Manage sites
65
+ - `exec/wp` — Run commands via SSH or API
66
+ - `ssh` — Interactive SSH sessions
67
+ - `sync push/pull` — rsync wp-content via SSH
68
+ - `teams list/members` — View teams
69
+ - `--json` mode for all commands
@@ -0,0 +1,38 @@
1
+ # Bundled Windows binaries
2
+
3
+ Files in this directory are third-party binaries bundled into the npm package
4
+ so that `instawp local clone`, `instawp local push/pull`, and `instawp sync`
5
+ work on Windows without requiring users to install rsync, awk, or sqlite3.
6
+
7
+ Populate this directory by running `scripts/fetch-windows-binaries.sh`.
8
+
9
+ ## Components and licenses
10
+
11
+ ### busybox.exe
12
+ - **Source**: BusyBox-w64 by Ron Yorston — https://frippery.org/busybox/
13
+ - **License**: GPL-2.0
14
+ - **Used for**: provides `awk` (invoked as `busybox.exe awk -f ...`) for
15
+ converting MySQL dumps to SQLite via the vendored `scripts/mysql2sqlite`
16
+ awk script.
17
+
18
+ ### rsync.exe + msys-*.dll
19
+ - **Source**: Git for Windows portable distribution — https://gitforwindows.org/
20
+ - **License**: rsync is GPL-3.0; msys2-runtime DLLs are mixed (mostly LGPL/MIT)
21
+ - **Used for**: file sync between local and remote sites in `sync push/pull`
22
+ and `local push/pull/clone`.
23
+
24
+ The `msys-2.0.dll` and other `msys-*.dll` files must remain colocated with
25
+ rsync.exe — rsync.exe links against them at runtime.
26
+
27
+ ## License compliance
28
+
29
+ The CLI is MIT-licensed, but the bundled GPL binaries impose obligations on
30
+ **redistribution**:
31
+
32
+ - Users who receive the binaries are entitled to the corresponding source.
33
+ - BusyBox source: https://busybox.net/downloads/
34
+ - rsync source: https://download.samba.org/pub/rsync/src/
35
+ - Git for Windows source: https://github.com/git-for-windows/git
36
+
37
+ The maintainer's responsibility is to keep this NOTICE.md shipped alongside
38
+ the binaries in the npm tarball.
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
@@ -108,6 +108,19 @@ export function registerExecCommand(program) {
108
108
  .option('--api', 'Use API transport instead of SSH')
109
109
  .option('--timeout <seconds>', 'Command timeout in seconds (API mode only)', '30')
110
110
  .action(async (siteIdentifier, args, opts) => {
111
+ // passThroughOptions may swallow --api/--timeout into args — extract them
112
+ const extractedApi = args.includes('--api');
113
+ const timeoutIdx = args.indexOf('--timeout');
114
+ let extractedTimeout;
115
+ if (timeoutIdx !== -1 && args[timeoutIdx + 1]) {
116
+ extractedTimeout = args[timeoutIdx + 1];
117
+ args = args.filter((_, i) => i !== timeoutIdx && i !== timeoutIdx + 1);
118
+ }
119
+ args = args.filter(a => a !== '--api');
120
+ if (extractedApi)
121
+ opts.api = true;
122
+ if (extractedTimeout)
123
+ opts.timeout = extractedTimeout;
111
124
  await execAction(siteIdentifier, args, opts);
112
125
  });
113
126
  }
@@ -120,6 +133,19 @@ export function registerWpCommand(program) {
120
133
  .option('--api', 'Use API transport instead of SSH')
121
134
  .option('--timeout <seconds>', 'Command timeout in seconds (API mode only)', '30')
122
135
  .action(async (siteIdentifier, args, opts) => {
136
+ // passThroughOptions may swallow --api/--timeout into args — extract them
137
+ const extractedApi = args.includes('--api');
138
+ const timeoutIdx = args.indexOf('--timeout');
139
+ let extractedTimeout;
140
+ if (timeoutIdx !== -1 && args[timeoutIdx + 1]) {
141
+ extractedTimeout = args[timeoutIdx + 1];
142
+ args = args.filter((_, i) => i !== timeoutIdx && i !== timeoutIdx + 1);
143
+ }
144
+ args = args.filter(a => a !== '--api');
145
+ if (extractedApi)
146
+ opts.api = true;
147
+ if (extractedTimeout)
148
+ opts.timeout = extractedTimeout;
123
149
  await execAction(siteIdentifier, ['wp', ...args], opts);
124
150
  });
125
151
  }
@@ -1 +1 @@
1
- {"version":3,"file":"exec.js","sourceRoot":"","sources":["../../src/commands/exec.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"exec.js","sourceRoot":"","sources":["../../src/commands/exec.ts"],"names":[],"mappings":"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"}
@@ -1,8 +1,11 @@
1
1
  import { spawnSync } from 'node:child_process';
2
- import { join, resolve } from 'node:path';
2
+ import { join } from 'node:path';
3
3
  import { existsSync, mkdirSync, readFileSync, rmSync, statSync, writeFileSync } from 'node:fs';
4
4
  import chalk from 'chalk';
5
5
  import open from 'open';
6
+ import Database from 'better-sqlite3';
7
+ import { resolveFromModule } from '../lib/paths.js';
8
+ import { bundledBusybox, bundledRsync } from '../lib/windows-binaries.js';
6
9
  import { getLocalInstances, getLocalInstance, setLocalInstance, removeLocalInstance, } from '../lib/config.js';
7
10
  import { getNextPort, createInstanceDir, deleteInstanceDir, startServer, startServerBackground, stopServer as stopServerProcess, isServerRunning, checkPlaygroundConnectivity, ensureAutoLogin, } from '../lib/local-env.js';
8
11
  import { requireAuth, getClient } from '../lib/api.js';
@@ -54,11 +57,11 @@ export function registerLocalCommand(program) {
54
57
  createdAt: new Date().toISOString(),
55
58
  };
56
59
  setLocalInstance(instance);
57
- success(`Instance "${name}" created`);
58
- console.log(`
59
- ${chalk.dim('#')} Starting WordPress ${opts.wp} with PHP ${opts.php}...
60
- ${chalk.dim('#')} Data stored at: ${chalk.dim(dir)}
61
- `);
60
+ if (!isJsonMode()) {
61
+ success(`Instance "${name}" created`);
62
+ console.log(`\n${chalk.dim('#')} Starting WordPress ${opts.wp} with PHP ${opts.php}...`);
63
+ console.log(`${chalk.dim('#')} Data stored at: ${chalk.dim(dir)}\n`);
64
+ }
62
65
  await launchServer(instance, opts);
63
66
  }
64
67
  catch (err) {
@@ -176,6 +179,7 @@ ${chalk.dim('#')} Data stored at: ${chalk.dim(dir)}
176
179
  local
177
180
  .command('push <local-name> [cloud-site]')
178
181
  .description('Push local wp-content to an InstaWP cloud site')
182
+ .option('--include <pattern...>', 'Include patterns (e.g. .git)')
179
183
  .option('--exclude <pattern...>', 'Additional exclude patterns')
180
184
  .option('--dry-run', 'Show what would be transferred')
181
185
  .action(async (localName, cloudSiteArg, opts) => {
@@ -186,7 +190,7 @@ ${chalk.dim('#')} Data stored at: ${chalk.dim(dir)}
186
190
  process.exit(1);
187
191
  }
188
192
  if (!checkRsync()) {
189
- error('rsync is required. Install: brew install rsync');
193
+ error('rsync is required.' + (process.platform === 'win32' ? ' Reinstall the CLI — the bundled rsync.exe is missing.' : ' Install: brew install rsync'));
190
194
  process.exit(1);
191
195
  }
192
196
  const localWpContent = join(instance.path, 'wp-content') + '/';
@@ -282,6 +286,7 @@ ${chalk.dim('#')} Data stored at: ${chalk.dim(dir)}
282
286
  local
283
287
  .command('pull <local-name> <cloud-site>')
284
288
  .description('Pull wp-content from an InstaWP cloud site to local')
289
+ .option('--include <pattern...>', 'Include patterns (e.g. .git)')
285
290
  .option('--exclude <pattern...>', 'Additional exclude patterns')
286
291
  .option('--dry-run', 'Show what would be transferred')
287
292
  .action(async (localName, cloudSiteArg, opts) => {
@@ -292,7 +297,7 @@ ${chalk.dim('#')} Data stored at: ${chalk.dim(dir)}
292
297
  process.exit(1);
293
298
  }
294
299
  if (!checkRsync()) {
295
- error('rsync is required. Install: brew install rsync');
300
+ error('rsync is required.' + (process.platform === 'win32' ? ' Reinstall the CLI — the bundled rsync.exe is missing.' : ' Install: brew install rsync'));
296
301
  process.exit(1);
297
302
  }
298
303
  const localWpContent = join(instance.path, 'wp-content') + '/';
@@ -309,11 +314,14 @@ ${chalk.dim('#')} Data stored at: ${chalk.dim(dir)}
309
314
  }
310
315
  const conn = await ensureSshAccess(site.id);
311
316
  const remotePath = `/home/${conn.username}/web/${conn.domain}/public_html/wp-content/`;
312
- const extraArgs = [
313
- '--exclude=database', // Don't overwrite local SQLite database
314
- '--exclude=db.php',
315
- '--exclude=mu-plugins',
316
- ];
317
+ const extraArgs = [];
318
+ if (opts.include) {
319
+ for (const pattern of opts.include) {
320
+ extraArgs.push(`--include=${pattern}`);
321
+ }
322
+ }
323
+ extraArgs.push('--exclude=database', // Don't overwrite local SQLite database
324
+ '--exclude=db.php', '--exclude=mu-plugins');
317
325
  if (opts.exclude) {
318
326
  for (const pattern of opts.exclude) {
319
327
  extraArgs.push(`--exclude=${pattern}`);
@@ -338,10 +346,12 @@ ${chalk.dim('#')} Data stored at: ${chalk.dim(dir)}
338
346
  .description('Clone a complete InstaWP cloud site to local')
339
347
  .option('--name <name>', 'Local instance name (defaults to cloud site name)')
340
348
  .option('--no-start', 'Do not start the local site after cloning')
349
+ .option('--force', 'Overwrite existing local instance')
350
+ .option('--include <pattern...>', 'Include patterns for rsync (e.g. .git)')
341
351
  .action(async (cloudSiteArg, opts) => {
342
352
  requireAuth();
343
353
  if (!checkRsync()) {
344
- error('rsync is required. Install: brew install rsync');
354
+ error('rsync is required.' + (process.platform === 'win32' ? ' Reinstall the CLI — the bundled rsync.exe is missing.' : ' Install: brew install rsync'));
345
355
  process.exit(1);
346
356
  }
347
357
  // 1. Resolve cloud site
@@ -360,8 +370,15 @@ ${chalk.dim('#')} Data stored at: ${chalk.dim(dir)}
360
370
  const instances = getLocalInstances();
361
371
  const name = sanitizeName(opts.name || site.name || site.sub_domain || `site-${site.id}`);
362
372
  if (instances[name]) {
363
- error(`Local instance "${name}" already exists. Use --name to pick a different name.`);
364
- process.exit(1);
373
+ if (!opts.force) {
374
+ error(`Local instance "${name}" already exists. Use --force to overwrite or --name to pick a different name.`);
375
+ process.exit(1);
376
+ }
377
+ // Force: delete existing instance first
378
+ stopServerProcess(instances[name]);
379
+ deleteInstanceDir(name);
380
+ removeLocalInstance(name);
381
+ info(`Existing instance "${name}" removed.`);
365
382
  }
366
383
  const port = await getNextPort(instances);
367
384
  const dir = createInstanceDir(name);
@@ -402,7 +419,14 @@ ${chalk.dim('#')} Data stored at: ${chalk.dim(dir)}
402
419
  const remotePath = `/home/${conn.username}/web/${conn.domain}/public_html/wp-content/`;
403
420
  const remoteSource = `${conn.username}@${conn.host}:${remotePath}`;
404
421
  info(`Pulling wp-content from ${chalk.dim(conn.domain)}...`);
422
+ const includeArgs = [];
423
+ if (opts.include) {
424
+ for (const pattern of opts.include) {
425
+ includeArgs.push(`--include=${pattern}`);
426
+ }
427
+ }
405
428
  const rsyncExit = rsyncViaSsh(conn, remoteSource, localWpContent, [
429
+ ...includeArgs,
406
430
  '--exclude=cache',
407
431
  '--exclude=upgrade',
408
432
  '--exclude=wflogs',
@@ -433,7 +457,7 @@ ${chalk.dim('#')} Data stored at: ${chalk.dim(dir)}
433
457
  const dbSpin2 = spinner('Importing database...');
434
458
  dbSpin2.start();
435
459
  try {
436
- const mysql2sqlitePath = resolve(join(new URL(import.meta.url).pathname, '..', '..', '..', 'scripts', 'mysql2sqlite'));
460
+ const mysql2sqlitePath = resolveFromModule(import.meta.url, '..', '..', 'scripts', 'mysql2sqlite');
437
461
  const dbDir = join(dir, 'wp-content', 'database');
438
462
  const sqliteDbPath = join(dbDir, '.ht.sqlite');
439
463
  // Clean slate for database dir
@@ -446,8 +470,15 @@ ${chalk.dim('#')} Data stored at: ${chalk.dim(dir)}
446
470
  if (sqlStart > 0) {
447
471
  writeFileSync(dumpPath, rawDump.substring(sqlStart));
448
472
  }
449
- // Convert MySQL → SQLite
450
- const convertResult = spawnSync(mysql2sqlitePath, [dumpPath], {
473
+ // Convert MySQL → SQLite via awk (mysql2sqlite is an awk script).
474
+ // Windows doesn't honor shebangs, so invoke awk explicitly.
475
+ const awk = findAwk();
476
+ if (!awk) {
477
+ throw new Error('awk not found. ' + (process.platform === 'win32'
478
+ ? 'Reinstall the CLI — the bundled busybox.exe is missing.'
479
+ : 'Install awk/gawk.'));
480
+ }
481
+ const convertResult = spawnSync(awk.cmd, [...awk.prefixArgs, '-f', mysql2sqlitePath, dumpPath], {
451
482
  encoding: 'utf-8',
452
483
  maxBuffer: 500 * 1024 * 1024,
453
484
  timeout: 120000,
@@ -458,69 +489,54 @@ ${chalk.dim('#')} Data stored at: ${chalk.dim(dir)}
458
489
  // Add DROP TABLE before each CREATE TABLE
459
490
  let sqliteSql = convertResult.stdout;
460
491
  sqliteSql = sqliteSql.replace(/^(CREATE TABLE `([^`]+)`)/gm, 'DROP TABLE IF EXISTS `$2`;\n$1');
461
- // Write and import into SQLite
462
- const tmpSql = join(dir, 'sqlite-import.sql');
463
- writeFileSync(tmpSql, sqliteSql);
464
- spawnSync('sqlite3', [sqliteDbPath], {
465
- input: `.read ${tmpSql}\n`,
466
- encoding: 'utf-8',
467
- timeout: 120000,
468
- });
469
- // Find the table prefix and rename to wp_
470
- const tablesResult = spawnSync('sqlite3', [sqliteDbPath, '.tables'], { encoding: 'utf-8' });
471
- const allTables = (tablesResult.stdout || '').split(/\s+/).filter(Boolean);
472
- const optionsTable = allTables.find((t) => t.endsWith('_options'));
473
- const oldPrefix = optionsTable ? optionsTable.replace('options', '') : 'wp_';
474
- if (oldPrefix !== 'wp_') {
475
- // Rename tables
476
- const renameStatements = allTables
477
- .filter((t) => t.startsWith(oldPrefix))
478
- .map((t) => `ALTER TABLE \`${t}\` RENAME TO \`wp_${t.substring(oldPrefix.length)}\`;`)
479
- .join('\n');
480
- spawnSync('sqlite3', [sqliteDbPath, renameStatements], { encoding: 'utf-8' });
481
- // Rename meta keys and option names that contain the old prefix
482
- const fixPrefixSql = [
483
- `UPDATE wp_usermeta SET meta_key = REPLACE(meta_key, '${oldPrefix}', 'wp_') WHERE meta_key LIKE '${oldPrefix}%';`,
484
- `UPDATE wp_options SET option_name = REPLACE(option_name, '${oldPrefix}', 'wp_') WHERE option_name LIKE '${oldPrefix}%';`,
485
- ].join('\n');
486
- spawnSync('sqlite3', [sqliteDbPath, fixPrefixSql], { encoding: 'utf-8' });
487
- }
488
- // Search-replace old cloud URL → localhost
489
- const localUrl = `http://127.0.0.1:${instance.port}`;
490
- const oldDomain = site.url || site.sub_domain || '';
491
- const oldUrls = [
492
- oldDomain,
493
- oldDomain.replace('https://', 'http://'),
494
- ].filter(Boolean);
495
- for (const oldUrl of oldUrls) {
496
- const replaceSql = [
497
- `UPDATE wp_options SET option_value = REPLACE(option_value, '${oldUrl}', '${localUrl}') WHERE option_value LIKE '%${oldUrl}%';`,
498
- `UPDATE wp_posts SET post_content = REPLACE(post_content, '${oldUrl}', '${localUrl}') WHERE post_content LIKE '%${oldUrl}%';`,
499
- `UPDATE wp_posts SET guid = REPLACE(guid, '${oldUrl}', '${localUrl}') WHERE guid LIKE '%${oldUrl}%';`,
500
- `UPDATE wp_postmeta SET meta_value = REPLACE(meta_value, '${oldUrl}', '${localUrl}') WHERE meta_value LIKE '%${oldUrl}%';`,
501
- `UPDATE wp_comments SET comment_content = REPLACE(comment_content, '${oldUrl}', '${localUrl}') WHERE comment_content LIKE '%${oldUrl}%';`,
502
- ].join('\n');
503
- spawnSync('sqlite3', [sqliteDbPath, replaceSql], { encoding: 'utf-8' });
504
- }
505
- // Ensure siteurl/home are correct
506
- spawnSync('sqlite3', [sqliteDbPath,
507
- `UPDATE wp_options SET option_value='${localUrl}' WHERE option_name IN ('siteurl','home');`,
508
- ], { encoding: 'utf-8' });
509
- // Get admin username for blueprint login step
510
- const adminResult = spawnSync('sqlite3', [sqliteDbPath,
511
- "SELECT user_login FROM wp_users WHERE ID = (SELECT user_id FROM wp_usermeta WHERE meta_key = 'wp_capabilities' AND meta_value LIKE '%administrator%' LIMIT 1);",
512
- ], { encoding: 'utf-8' });
513
- adminUsername = (adminResult.stdout || '').trim() || 'admin';
514
- // Count tables for output
515
- const countResult = spawnSync('sqlite3', [sqliteDbPath,
516
- "SELECT COUNT(*) FROM sqlite_master WHERE type='table';",
517
- ], { encoding: 'utf-8' });
518
- // Clean up temp file
492
+ // Import directly via better-sqlite3 (no external sqlite3 CLI needed)
493
+ const db = new Database(sqliteDbPath);
519
494
  try {
520
- rmSync(tmpSql);
495
+ db.exec(sqliteSql);
496
+ // Find the table prefix and rename to wp_
497
+ const tableRows = db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'").all();
498
+ const allTables = tableRows.map(r => r.name);
499
+ const optionsTable = allTables.find(t => t.endsWith('_options'));
500
+ const oldPrefix = optionsTable ? optionsTable.replace('options', '') : 'wp_';
501
+ if (oldPrefix !== 'wp_') {
502
+ const renames = allTables
503
+ .filter(t => t.startsWith(oldPrefix))
504
+ .map(t => `ALTER TABLE \`${t}\` RENAME TO \`wp_${t.substring(oldPrefix.length)}\``);
505
+ db.exec(renames.join(';\n') + ';');
506
+ // Rename meta keys and option names that contain the old prefix
507
+ db.prepare('UPDATE wp_usermeta SET meta_key = REPLACE(meta_key, ?, ?) WHERE meta_key LIKE ?').run(oldPrefix, 'wp_', oldPrefix + '%');
508
+ db.prepare('UPDATE wp_options SET option_name = REPLACE(option_name, ?, ?) WHERE option_name LIKE ?').run(oldPrefix, 'wp_', oldPrefix + '%');
509
+ }
510
+ // Search-replace old cloud URL → localhost (bound params: no SQL injection)
511
+ const localUrl = `http://127.0.0.1:${instance.port}`;
512
+ const oldDomain = site.url || site.sub_domain || '';
513
+ const oldUrls = [
514
+ oldDomain,
515
+ oldDomain.replace('https://', 'http://'),
516
+ ].filter(Boolean);
517
+ const replaceStmts = [
518
+ 'UPDATE wp_options SET option_value = REPLACE(option_value, ?, ?) WHERE option_value LIKE ?',
519
+ 'UPDATE wp_posts SET post_content = REPLACE(post_content, ?, ?) WHERE post_content LIKE ?',
520
+ 'UPDATE wp_posts SET guid = REPLACE(guid, ?, ?) WHERE guid LIKE ?',
521
+ 'UPDATE wp_postmeta SET meta_value = REPLACE(meta_value, ?, ?) WHERE meta_value LIKE ?',
522
+ 'UPDATE wp_comments SET comment_content = REPLACE(comment_content, ?, ?) WHERE comment_content LIKE ?',
523
+ ].map(s => db.prepare(s));
524
+ for (const oldUrl of oldUrls) {
525
+ const likePattern = '%' + oldUrl + '%';
526
+ for (const stmt of replaceStmts) {
527
+ stmt.run(oldUrl, localUrl, likePattern);
528
+ }
529
+ }
530
+ db.prepare("UPDATE wp_options SET option_value = ? WHERE option_name IN ('siteurl','home')").run(localUrl);
531
+ // Get admin username for blueprint login step
532
+ const adminRow = db.prepare("SELECT user_login FROM wp_users WHERE ID = (SELECT user_id FROM wp_usermeta WHERE meta_key = 'wp_capabilities' AND meta_value LIKE '%administrator%' LIMIT 1)").get();
533
+ adminUsername = adminRow?.user_login || 'admin';
534
+ const tableCount = db.prepare("SELECT COUNT(*) AS c FROM sqlite_master WHERE type='table'").get().c;
535
+ dbSpin2.succeed(`Database imported (${tableCount} tables, admin: ${adminUsername})`);
536
+ }
537
+ finally {
538
+ db.close();
521
539
  }
522
- catch { }
523
- dbSpin2.succeed(`Database imported (${(countResult.stdout || '').trim()} tables, admin: ${adminUsername})`);
524
540
  }
525
541
  catch (err) {
526
542
  dbSpin2.fail('Database import failed: ' + err.message);
@@ -579,9 +595,46 @@ ${chalk.bold.green('Clone complete!')}
579
595
  });
580
596
  }
581
597
  function checkRsync() {
582
- const result = spawnSync('which', ['rsync'], { stdio: 'ignore' });
598
+ if (bundledRsync())
599
+ return true;
600
+ const cmd = process.platform === 'win32' ? 'where' : 'which';
601
+ const result = spawnSync(cmd, ['rsync'], { stdio: 'ignore' });
583
602
  return result.status === 0;
584
603
  }
604
+ /**
605
+ * Locate an awk-compatible interpreter. Resolution order:
606
+ * 1. Bundled BusyBox-w64 in bin/win32/ (Windows only — invoked as `busybox awk`)
607
+ * 2. `awk` or `gawk` in PATH
608
+ * 3. Common Git-for-Windows install dirs (Windows only)
609
+ *
610
+ * Returns the command path plus any arg-prefix that must precede the awk
611
+ * arguments (busybox uses `busybox awk -f script input`).
612
+ */
613
+ function findAwk() {
614
+ const bb = bundledBusybox();
615
+ if (bb)
616
+ return { cmd: bb, prefixArgs: ['awk'] };
617
+ const cmd = process.platform === 'win32' ? 'where' : 'which';
618
+ for (const name of ['awk', 'gawk']) {
619
+ const r = spawnSync(cmd, [name], { stdio: 'pipe' });
620
+ if (r.status === 0)
621
+ return { cmd: name, prefixArgs: [] };
622
+ }
623
+ if (process.platform === 'win32') {
624
+ const candidates = [
625
+ 'C:\\Program Files\\Git\\usr\\bin\\awk.exe',
626
+ 'C:\\Program Files (x86)\\Git\\usr\\bin\\awk.exe',
627
+ ];
628
+ if (process.env.PROGRAMFILES) {
629
+ candidates.push(process.env.PROGRAMFILES + '\\Git\\usr\\bin\\awk.exe');
630
+ }
631
+ for (const c of candidates) {
632
+ if (existsSync(c))
633
+ return { cmd: c, prefixArgs: [] };
634
+ }
635
+ }
636
+ return null;
637
+ }
585
638
  function sanitizeName(name) {
586
639
  return name.replace(/[^a-zA-Z0-9_-]/g, '-').toLowerCase();
587
640
  }
@@ -611,27 +664,51 @@ function printUrls(port) {
611
664
  }
612
665
  async function launchServer(instance, opts) {
613
666
  const shouldOpen = opts.open !== false;
667
+ const json = isJsonMode();
614
668
  if (opts.background) {
615
- const spin = spinner(`Starting "${instance.name}" in background...`);
616
- spin.start();
669
+ const spin = json ? null : spinner(`Starting "${instance.name}" in background...`);
670
+ spin?.start();
617
671
  try {
618
672
  const { pid, url } = await startServerBackground(instance, opts.blueprint);
619
- spin.succeed(`Running in background (PID: ${pid})`);
620
- printUrls(instance.port);
673
+ if (json) {
674
+ console.log(JSON.stringify({
675
+ success: true,
676
+ data: {
677
+ name: instance.name,
678
+ url,
679
+ port: instance.port,
680
+ pid,
681
+ wp: instance.wp,
682
+ php: instance.php,
683
+ path: instance.path,
684
+ },
685
+ }));
686
+ }
687
+ else {
688
+ spin?.succeed(`Running in background (PID: ${pid})`);
689
+ printUrls(instance.port);
690
+ info(`Stop with: instawp local stop ${instance.name}`);
691
+ info(`Logs: ${instance.path}/server.log`);
692
+ }
621
693
  if (shouldOpen)
622
694
  await openWpAdmin(url);
623
- info(`Stop with: instawp local stop ${instance.name}`);
624
- info(`Logs: ${instance.path}/server.log`);
625
695
  }
626
696
  catch (err) {
627
- spin.fail('Failed to start');
628
- error(err.message);
697
+ if (json) {
698
+ console.log(JSON.stringify({ success: false, error: err.message }));
699
+ }
700
+ else {
701
+ spin?.fail('Failed to start');
702
+ error(err.message);
703
+ }
629
704
  process.exit(1);
630
705
  }
631
706
  }
632
707
  else {
633
- printUrls(instance.port);
634
- console.log(chalk.dim('\nPress Ctrl+C to stop.\n'));
708
+ if (!json) {
709
+ printUrls(instance.port);
710
+ console.log(chalk.dim('\nPress Ctrl+C to stop.\n'));
711
+ }
635
712
  try {
636
713
  await startServer(instance, {
637
714
  blueprint: opts.blueprint,