Skip to content

Commit 60bd278

Browse files
feat: add local development with WordPress Playground and cloud sync
- Local dev: create, clone, start, stop, push, pull, list, delete - Clone flow: full site clone from InstaWP cloud (files + MySQL→SQLite DB) - Background mode: --background flag with PID tracking and local stop - Teams: client-side team switch with team_id injection on all API calls - Sites list: 50 per page default, --all flag, pagination hints - Site resolver: 10-minute cache for name→ID lookups - Rsync: --itemize-changes (only shows changed files) - Auto-login: finds first admin user, works with cloned sites - AST SQLite driver: WP_SQLITE_AST_DRIVER=true for WooCommerce compat - Vendor: mysql2sqlite (MIT, dumblob/mysql2sqlite) for DB conversion - Terminal: stty sane after Playground exits Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent aafd5e2 commit 60bd278

15 files changed

Lines changed: 1706 additions & 43 deletions

CLAUDE.md

Lines changed: 87 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -22,34 +22,55 @@ src/
2222
│ ├── exec.ts # exec + wp commands (merged, --api/--ssh transport)
2323
│ ├── ssh.ts # Interactive SSH shell
2424
│ ├── sync.ts # rsync push/pull via SSH
25-
│ └── teams.ts # teams list/members
25+
│ ├── teams.ts # teams list/switch/members
26+
│ └── local.ts # local create/clone/start/stop/push/pull/list/delete
2627
├── lib/
27-
│ ├── api.ts # Axios client, auth interceptor, 401/429 handling
28+
│ ├── api.ts # Axios client, auth interceptor, team_id injection
2829
│ ├── auth.ts # OAuth flow (local HTTP server for callback)
2930
│ ├── config.ts # Conf-based persistent config (~/.config/instawp/)
31+
│ ├── local-env.ts # Playground server management, background mode
3032
│ ├── output.ts # chalk/ora output helpers, --json mode
31-
│ ├── site-resolver.ts # Resolve site by ID, name, or domain
33+
│ ├── site-resolver.ts # Resolve site by ID/name/domain with caching
3234
│ ├── ssh-keys.ts # SSH key generation, upload, caching
3335
│ └── ssh-connection.ts # SSH/rsync spawn helpers
34-
└── __tests__/ # Vitest tests (148 tests)
36+
├── __tests__/ # Vitest tests (148 tests)
37+
scripts/
38+
└── mysql2sqlite # MySQL→SQLite dump converter (vendored)
3539
```
3640

3741
## Commands
3842

3943
```
44+
# Auth
4045
instawp login [--token <t>] [--api-url <url>]
4146
instawp whoami
42-
instawp sites list [--status <s>] [--page <n>]
43-
instawp sites create --name <n> [--php <v>] [--config <id>]
44-
instawp create --name <n> # alias for sites create
47+
48+
# Sites (cloud)
49+
instawp sites list [--status <s>] [--page <n>] [--per-page <n>] [--all]
50+
instawp create --name <n> [--php <v>] [--config <id>]
4551
instawp sites delete <site> [--force]
52+
53+
# Remote access
4654
instawp exec <site> <cmd...> [--api] [--timeout <s>]
47-
instawp wp <site> <args...> [--api] # shorthand: prepends `wp` to args
55+
instawp wp <site> <args...> [--api]
4856
instawp ssh <site>
4957
instawp sync push <site> [--path] [--exclude] [--dry-run]
5058
instawp sync pull <site> [--path] [--exclude] [--dry-run]
59+
60+
# Teams
5161
instawp teams list
62+
instawp teams switch [team] # client-side team context
5263
instawp teams members <team>
64+
65+
# Local development (powered by WordPress Playground)
66+
instawp local create [--name <n>] [--wp <v>] [--php <v>] [--background] [--no-open]
67+
instawp local clone <cloud-site> [--name <n>] [--no-start]
68+
instawp local start [name] [--background] [--no-open]
69+
instawp local stop [name]
70+
instawp local push <local-name> [cloud-site] [--dry-run]
71+
instawp local pull <local-name> <cloud-site> [--dry-run]
72+
instawp local list
73+
instawp local delete <name> [--force]
5374
```
5475

5576
All commands support `--json` for machine-readable output.
@@ -62,11 +83,41 @@ All commands support `--json` for machine-readable output.
6283
- `--api`: uses `POST /sites/{id}/run-cmd` API → cloud-app → InstaCP `v-instawp-run-cmd`
6384
- Both transports can run arbitrary commands (API is not WP-only despite the name)
6485

65-
### Site resolution
86+
### Site resolution + caching
6687
- `resolveSite()` accepts ID (numeric), name, or domain
6788
- Numeric → direct `GET /sites/{id}/details`
6889
- String → fetches list, matches by name/sub_domain/domain, then fetches details
69-
- Errors on zero matches or ambiguous multiple matches
90+
- **Caches** name→ID mappings for 10 minutes (avoids list call on repeat lookups)
91+
92+
### Team context
93+
- `teams switch` stores team_id locally (no server-side change)
94+
- API interceptor injects `team_id` as query param on all requests
95+
- Client-app `SiteService::getList()` already accepts `team_id` parameter
96+
97+
### Local development architecture
98+
- Uses **WordPress Playground** (`@wp-playground/cli`) — WASM PHP + SQLite, no Docker needed
99+
- NOT a hard dependency — auto-downloaded via `npx`, faster if installed globally (`npm i -g @wp-playground/cli`)
100+
- Instance data stored at `~/.instawp/local/<name>/`
101+
- Fresh sites: mount entire `wp-content` before install (`--mount-before-install`)
102+
- Cloned sites: mount subdirs individually after install (`--mount`) so Playground sets up `db.php` internally
103+
104+
### Clone flow (local clone)
105+
1. Export MySQL dump via SSH (`wp db export`)
106+
2. Strip SSH MOTD from dump output
107+
3. Convert MySQL → SQLite using `mysql2sqlite` (awk script)
108+
4. Import directly into `.ht.sqlite` via `sqlite3` CLI
109+
5. Rename table prefix to `wp_` (tables + meta keys + option names)
110+
6. Search-replace cloud URL → `http://127.0.0.1:<port>` across all tables
111+
7. Pull wp-content via rsync (plugins, themes, uploads)
112+
8. Pull non-core root files (CLAUDE.md, .htaccess, etc.)
113+
9. Generate blueprint with `WP_SQLITE_AST_DRIVER=true` + `login` step with actual admin username
114+
10. Write error suppression mu-plugin
115+
116+
### Background mode
117+
- `--background` flag spawns detached process, polls until server responds, returns immediately
118+
- PID stored at `<instance>/server.pid`, logs at `<instance>/server.log`
119+
- `local stop` kills the background process
120+
- `local list` shows `running`/`stopped` status
70121

71122
### SSH key management
72123
- Auto-generates RSA 4096 key at `~/.instawp/cli_key` if needed
@@ -77,15 +128,33 @@ All commands support `--json` for machine-readable output.
77128
### Config storage
78129
- Uses `conf` package → `~/.config/instawp/config.json`
79130
- Env overrides: `INSTAWP_TOKEN`, `INSTAWP_API_URL`
80-
- SSH cache with TTL stored alongside auth config
131+
- Stores: auth, SSH cache, site cache, team_id, local instances
132+
133+
## Vendored Dependencies
134+
135+
### `scripts/mysql2sqlite`
136+
- **Source**: https://github.com/dumblob/mysql2sqlite
137+
- **License**: MIT
138+
- **What**: AWK script that converts MySQL dump files to SQLite-compatible SQL
139+
- **Used by**: `local clone` for database import
140+
- **Version**: Vendored from master branch (2026-03-23)
141+
- **Update procedure**: Download latest from `https://raw.githubusercontent.com/dumblob/mysql2sqlite/master/mysql2sqlite` and replace `scripts/mysql2sqlite`. Test with `instawp local clone` on a WooCommerce site to verify compatibility.
142+
143+
## Known Limitations
144+
145+
### Local clone + SQLite
146+
- **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.
147+
- Some MySQL-specific queries may still fail at runtime (rare edge cases in complex plugins)
148+
- PHP deprecation warnings can crash WASM PHP — suppressed via mu-plugin (`error_reporting(E_ERROR | E_PARSE)`)
149+
- `downloads.w.org` is unreachable on some networks — connectivity pre-check warns the user
81150

82151
## API Endpoints Used
83152

84153
| Endpoint | Used By |
85154
|----------|---------|
86155
| `GET /api/v2/sites` | sites list, site resolver |
87156
| `GET /api/v2/sites/{id}/details` | site resolver |
88-
| `POST /api/v2/sites` | sites create |
157+
| `POST /api/v2/sites` | sites create, local push (auto-create) |
89158
| `DELETE /api/v2/sites/{id}` | sites delete |
90159
| `POST /api/v2/sites/{id}/run-cmd` | exec --api, wp --api |
91160
| `GET /api/v2/tasks/{id}/status` | create (poll provisioning) |
@@ -113,6 +182,8 @@ npm run build
113182
node dist/index.js --help
114183
node dist/index.js login --token <test-token>
115184
node dist/index.js sites list
185+
node dist/index.js local create --name test --background --no-open
186+
node dist/index.js local stop test
116187
```
117188

118189
Or link globally:
@@ -127,8 +198,8 @@ instawp --help
127198

128199
```bash
129200
# Bump version in package.json, then:
130-
git tag v0.0.1-beta.1
131-
git push origin v0.0.1-beta.1
201+
git tag v0.0.1-beta.2
202+
git push origin v0.0.1-beta.2
132203
```
133204

134205
- Publishes with `--tag beta` (install via `npm i -g @instawp/cli@beta`)
@@ -142,3 +213,5 @@ git push origin v0.0.1-beta.1
142213
- Spinners stop before printing output (no interleaved text)
143214
- JSON mode returns `{ success, data }` or `{ success: false, error }`
144215
- Version reads from package.json at runtime (single source of truth)
216+
- rsync uses `--itemize-changes` (only shows actually changed files)
217+
- Terminal restored with `stty sane` after Playground exits

package.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,15 @@
11
{
22
"name": "@instawp/cli",
3-
"version": "0.0.1-beta.1",
3+
"version": "0.0.1-beta.2",
44
"description": "InstaWP CLI - Create and manage WordPress sites from the terminal",
55
"type": "module",
66
"bin": {
77
"instawp": "./dist/index.js"
88
},
99
"files": [
1010
"dist",
11-
"!dist/__tests__"
11+
"!dist/__tests__",
12+
"scripts/mysql2sqlite"
1213
],
1314
"scripts": {
1415
"build": "tsc",

0 commit comments

Comments
 (0)