TypeScript CLI for InstaWP. Lets users create/manage WordPress sites from the terminal.
- Stack: TypeScript, ESM, Commander.js, Axios, Node 18+
- Package:
@instawp/clion npm (scoped, public) - Entry:
src/index.ts→ compiled todist/index.js - Binary:
instawp(registered viabinin package.json)
src/
├── index.ts # CLI entry point, command registration
├── types.ts # All TypeScript interfaces
├── commands/
│ ├── login.ts # OAuth browser flow + --token
│ ├── whoami.ts # Show current session
│ ├── sites.ts # sites list/create/delete + top-level create alias
│ ├── exec.ts # exec + wp commands (merged, --api/--ssh transport)
│ ├── ssh.ts # Interactive SSH shell
│ ├── sync.ts # rsync push/pull via SSH
│ ├── teams.ts # teams list/switch/members
│ └── local.ts # local create/clone/start/stop/push/pull/list/delete
├── lib/
│ ├── api.ts # Axios client, auth interceptor, team_id injection
│ ├── auth.ts # OAuth flow (local HTTP server for callback)
│ ├── config.ts # Conf-based persistent config (~/.config/instawp/)
│ ├── local-env.ts # Playground server management, background mode
│ ├── output.ts # chalk/ora output helpers, --json mode
│ ├── site-resolver.ts # Resolve site by ID/name/domain with caching
│ ├── ssh-keys.ts # SSH key generation, upload, caching
│ └── ssh-connection.ts # SSH/rsync spawn helpers
├── __tests__/ # Vitest tests (148 tests)
scripts/
└── mysql2sqlite # MySQL→SQLite dump converter (vendored)
# Auth
instawp login [--token <t>] [--api-url <url>]
instawp whoami
# Sites (cloud)
instawp sites list [--status <s>] [--page <n>] [--per-page <n>] [--all]
instawp sites creds <site>
instawp create --name <n> [--php <v>] [--config <id>]
instawp sites delete <site> [--force]
instawp open <site> [--admin] [--magic] [--print]
# Remote access (wp is the primary command; exec is the escape hatch)
instawp wp <site> <args...> [--api] # WP-CLI on the site
instawp ssh <site> # Interactive shell
instawp sync push <site> [--path] [--exclude] [--dry-run]
instawp sync pull <site> [--path] [--exclude] [--dry-run]
instawp db push <site> <file> [--force] [--no-backup]
instawp db pull <site> [--output <path>] [--no-compress]
instawp logs <site> [--wp] [--php] [--nginx] [--follow] [--lines <n>]
instawp exec <site> <cmd...> [--api] [--timeout <s>] # Raw shell (non-WP)
# Teams
instawp teams list
instawp teams switch [team] # client-side team context
instawp teams members <team>
# Local development (powered by WordPress Playground)
instawp local create [--name <n>] [--wp <v>] [--php <v>] [--background] [--no-open]
instawp local clone <cloud-site> [--name <n>] [--no-start]
instawp local start [name] [--background] [--no-open]
instawp local stop [name]
instawp local push <local-name> [cloud-site] [--dry-run]
instawp local pull <local-name> <cloud-site> [--dry-run]
instawp local list
instawp local delete <name> [--force]
All commands support --json for machine-readable output.
wp <site>is the canonical way to run anything on a remote site — prependswpto args, integrates with WP-CLI.exec <site>is the escape hatch for non-WP shell commands (ls,tail,ps, …). Same transport, nowpprefix.- Both accept
--as a POSIX end-of-options marker so users can forward raw args:instawp wp my-site -- post list --post_type=page - Both shell-escape each arg via single-quote wrapping before piping to remote stdin — fixes the
eval '...'parens issue. --ssh(default): real SSH connection, proper exit codes, real-time output--api: usesPOST /sites/{id}/run-cmdAPI → cloud-app → InstaCPv-instawp-run-cmd- Both transports can run arbitrary commands (API is not WP-only despite the name)
resolveSite()accepts ID (numeric), name, or domain- Numeric → direct
GET /sites/{id}/details - String → fetches list, matches by name/sub_domain/domain, then fetches details
- Caches name→ID mappings for 10 minutes (avoids list call on repeat lookups)
teams switchstores team_id locally (no server-side change)- API interceptor injects
team_idas query param on all requests - Client-app
SiteService::getList()already acceptsteam_idparameter
- Uses WordPress Playground (
@wp-playground/cli) — WASM PHP + SQLite, no Docker needed - NOT a hard dependency — auto-downloaded via
npx, faster if installed globally (npm i -g @wp-playground/cli) - Instance data stored at
~/.instawp/local/<name>/ - Fresh sites: mount entire
wp-contentbefore install (--mount-before-install) - Cloned sites: mount subdirs individually after install (
--mount) so Playground sets updb.phpinternally
- Export MySQL dump via SSH (
wp db export) - Strip SSH MOTD from dump output
- Convert MySQL → SQLite using
mysql2sqlite(awk script) - Import directly into
.ht.sqliteviasqlite3CLI - Rename table prefix to
wp_(tables + meta keys + option names) - Search-replace cloud URL →
http://127.0.0.1:<port>across all tables - Pull wp-content via rsync (plugins, themes, uploads)
- Pull non-core root files (CLAUDE.md, .htaccess, etc.)
- Generate blueprint with
WP_SQLITE_AST_DRIVER=true+loginstep with actual admin username - Write error suppression mu-plugin
--backgroundflag spawns detached process, polls until server responds, returns immediately- PID stored at
<instance>/server.pid, logs at<instance>/server.log local stopkills the background processlocal listshowsrunning/stoppedstatus
- Auto-generates RSA 4096 key at
~/.instawp/cli_keyif needed - Checks existing keys (
~/.ssh/id_rsa,id_ed25519) first - Uploads to InstaWP API, enables SSH+SFTP on site, attaches key
- Caches connection details for 1 hour in conf store
- Uses
confpackage →~/.config/instawp/config.json - Env overrides:
INSTAWP_TOKEN,INSTAWP_API_URL - Stores: auth, SSH cache, site cache, team_id, local instances
- Source: https://github.com/dumblob/mysql2sqlite
- License: MIT
- What: AWK script that converts MySQL dump files to SQLite-compatible SQL
- Used by:
local clonefor database import - Version: Vendored from master branch (2026-03-23)
- Update procedure: Download latest from
https://raw.githubusercontent.com/dumblob/mysql2sqlite/master/mysql2sqliteand replacescripts/mysql2sqlite. Test withinstawp local cloneon a WooCommerce site to verify compatibility.
Windows ships with ssh/scp but not rsync, awk, or sqlite3. The CLI works on Windows with zero extra installs via:
better-sqlite3(npm dep) — replaces the sqlite3 CLI. Native module, prebuilt binaries for win32-x64.vendor/win32/busybox.exe— providesawkfor themysql2sqlitescript (invoked asbusybox awk -f ...). Statically linked, no DLLs. This is the only bundled binary.- Pure-JS SFTP (
ssh2-sftp-client) for file transfers — see below. cross-spawnfor launching WordPress Playground (local create/start/clone).npx/wp-playground-cliare.cmdshims on Windows; Node won't spawn.cmdwithoutshell:true(CVE-2024-27980), solocal-env.tsusescross-spawn(resolves the shim + quotes mount-path args safely). Never use barechild_process.spawnfor npx/.cmd on Windows.
syncFiles()insrc/lib/ssh-connection.tsis the dispatcher. On macOS/Linux it shells out torsync(delta sync). On Windows it callssyncViaSftp()(src/lib/sftp-sync.ts).- Why not bundle rsync on Windows? We tried (betas 4–9). msys2 rsync.exe cannot drive native Windows OpenSSH — incompatible pipe/signal semantics produce
connection unexpectedly closed (0 bytes)+sigpacket: Suppressing signal 30. Bundling an msys ssh too would drag in the whole Heimdal/Kerberos DLL chain (~3.5 MB, ~15 brittle DLLs). Pure-JS SFTP sidesteps all of it. - Trade-off: SFTP does full-file copy (no rsync delta). Fine for wp-content; slower on large repeat syncs.
syncViaSftpmirrors rsync's exclude/include patterns viamakeMatcher(exact names match at any depth;*globs don't cross/; patterns with/are anchored to the relative path).
findAwk()(src/commands/local.ts) prefers bundledbusybox.exeon Windows, thenawk/gawkin PATH, then common Git-for-Windows dirs.bundledBusybox()(src/lib/windows-binaries.ts) returns the path only on win32 when the file exists.
# Maintainer-only — just downloads busybox64u.exe (requires curl)
bash scripts/fetch-windows-binaries.sh
git add vendor/win32 && git commit -m 'chore: refresh busybox'See vendor/win32/NOTICE.md for source + license (BusyBox is GPL-2.0).
src/lib/paths.ts→toRsyncPath()convertsC:\foo\bar→/c/foo/bar. Retained for completeness, but only exercised byrsyncViaSshwhich no longer runs on Windows.
- WP_SQLITE_AST_DRIVER=true is required for complex plugins (WooCommerce). The new AST-based SQLite driver (v2.2.1+) handles 99% of MySQL queries.
- Some MySQL-specific queries may still fail at runtime (rare edge cases in complex plugins)
- PHP deprecation warnings can crash WASM PHP — suppressed via mu-plugin (
error_reporting(E_ERROR | E_PARSE)) downloads.w.orgis unreachable on some networks — connectivity pre-check warns the user
| Endpoint | Used By |
|---|---|
GET /api/v2/sites |
sites list, site resolver |
GET /api/v2/sites/{id}/details |
site resolver |
POST /api/v2/sites |
sites create, local push (auto-create) |
DELETE /api/v2/sites/{id} |
sites delete |
POST /api/v2/sites/{id}/run-cmd |
exec --api, wp --api |
GET /api/v2/tasks/{id}/status |
create (poll provisioning) |
GET /api/v2/ssh-keys |
SSH key matching |
POST /api/v2/ssh-keys |
SSH key upload |
POST /api/v2/sites/{id}/ssh-keys/{keyId} |
attach key to site |
POST /api/v2/sites/{id}/update-ssh-status |
enable SSH |
POST /api/v2/sites/{id}/update-sftp-status |
enable SFTP |
GET /api/v2/teams |
teams list, whoami |
GET /api/v2/teams/{id}/members |
teams members |
npm install # Install deps
npm run dev # Watch mode (tsc --watch)
npm run build # Build to dist/
npm test # Run vitest (148 tests)
npm run test:watch # Watch mode testsnpm run build
node dist/index.js --help
node dist/index.js login --token <test-token>
node dist/index.js sites list
node dist/index.js local create --name test --background --no-open
node dist/index.js local stop testOr link globally:
npm link
instawp --helpWorkflow: Push a v* tag → GitHub Actions builds, tests, publishes to npm.
# Bump version in package.json, then:
git tag v0.0.1-beta.2
git push origin v0.0.1-beta.2- Publishes with
--tag beta(install vianpm i -g @instawp/cli@beta) - Uses
NPM_TOKENsecret (generated from vikas@instawp.com npm account) - Remove
--tag betafrom workflow for stable releases
- All imports use
.jsextension (ESM requirement) process.exit(1)on fatal errors after printing message viaerror()- Spinners stop before printing output (no interleaved text)
- JSON mode returns
{ success, data }or{ success: false, error } - Version reads from package.json at runtime (single source of truth)
- rsync uses
--itemize-changes(only shows actually changed files) - Terminal restored with
stty saneafter Playground exits