Share connected WP.com sites between Studio app and CLI#3227
Share connected WP.com sites between Studio app and CLI#3227youknowriad wants to merge 6 commits intotrunkfrom
Conversation
…member them
Connected sites used to live in app.json as a top-level
`connectedWpcomSites: { [userId]: SyncSite[] }` — Desktop-only state that the
CLI could not read or write. This moves the list into per-site entries in
cli.json so both Desktop and the CLI share a single source of truth, and wires
up the CLI push/pull flows to auto-connect on success.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
📊 Performance Test ResultsComparing dd14d49 vs trunk app-size
site-editor
site-startup
Results are median values from multiple test runs. Legend: 🟢 Improvement (faster) | 🔴 Regression (slower) | ⚪ No change (<50ms diff) |
Treat the move of connected sites out of app.json as a breaking change. `loadUserData` now throws `AppConfigVersionMismatchError` if it finds a version higher than this build supports, and the main process surfaces a blocking "Please upgrade Studio" dialog before boot continues. Mirrors the `SharedConfigVersionMismatchError` pattern used for shared.json. Migration 04 now stamps version 2 on app.json and strips the legacy top-level `connectedWpcomSites` field, so once it runs, older Studio builds reading the same `~/.studio/` directory are forced to upgrade before they can clobber the migrated data. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
bcotrim
left a comment
There was a problem hiding this comment.
Great concept and the data model decision makes sense. Left a couple of suggestions, mainly questioning whether shared.json might be a cleaner home for this data than cli.json, and a tweak to the migration strategy.
Using the shared.json file also brings the solution closer to your suggestion of keeping the data shared between Studio app and cli.
Happy to discuss if any of it needs more context!
| import { syncSiteSchema, type SyncSite } from '../types/sync'; | ||
| import { lockFileAsync, unlockFileAsync } from './lockfile'; | ||
| import { getCurrentUserId } from './shared-config'; | ||
| import { getCliConfigPath, getConfigDirectory } from './well-known-paths'; |
There was a problem hiding this comment.
Did you consider using shared.json instead for storing this data?
The purpose of that file is exactly cases like this one, where both CLI and App want to manage some data. It would give us the existing lockSharedConfig/saveSharedConfig/unlockSharedConfig making this lib a lot cleaner.
There was a problem hiding this comment.
I agree it's shared but it's also a site property and "sites" is in cli.json so unless we move sites to shared, for me it makes more sense for this to be under "sites" than being a separate property.
And to be honest, for me everything is shared almost at this point. There's very little arguments to hold onto the split files.
There was a problem hiding this comment.
Fair point on sites being a CLI concept, but I think connectedWpcomSites is more of a sync/UI concern than a site property, the App needs it for the publish picker, the CLI needs it for push/pull targeting. Neither process really "owns" it.
There's also a precedent that cuts against the "site property → cli.json" argument: sortOrder and themeDetails are site data too, but they live in app.json because the CLI doesn't need, or should be aware of them.
When we introduced the CLI as the mechanism of running site commands in Studio app, one of the side effects is to make the CLI owner of that implementation. I believe Studio shouldn't be aware or using parts of the CLI implementation. Today the CLI stores its data in a file, tomorrow it can be a database, remote server, etc... the app shouldn't have to adapt to it.
There was a problem hiding this comment.
Fair point on sites being a CLI concept, but I think connectedWpcomSites is more of a sync/UI concern than a site property, the App needs it for the publish picker, the CLI needs it for push/pull targeting. Neither process really "owns" it.
Same for the whole "sites" object, both need them. It' still a site property for me, which sites are connected to my local site.
There was a problem hiding this comment.
There's also a precedent that cuts against the "site property → cli.json" argument: sortOrder and themeDetails are site data too, but they live in app.json because the CLI doesn't need, or should be aware of them.
sort can be useful to sort the sites in CLI too. themeDetails is a cache that probably doesn't make sense in our config.
| @@ -0,0 +1,139 @@ | |||
| /** | |||
| * Moves `connectedWpcomSites` from `app.json` into per-site entries in | |||
There was a problem hiding this comment.
I would suggest doing this migration in 2 steps.
- CLI –> Moves data over to
shared.json/cli.jsonif necessary, and continues, no data cleanup - App –> Moves data over to
shared.json/cli.jsonif necessary, does the data cleanup afterwards.
The existing file locking mechanism will prevent these migrations to collide with each other. I think this gives the process a good safety in case the app isn't updated.
There was a problem hiding this comment.
I don't understand the current behavior enough to make this change, it's so confusing to me to be honest. I think we need a big simplification but feel free to push changes.
|
I'm taking a look at this PR now |
|
I implemented a new strategy here. Here's what the code does now:
|
|
I've asked @bcotrim to review my changes to this PR |
| } | ||
|
|
||
| const { connectedWpcomSites: _legacy, ...rest } = parsed; | ||
| await writeFile( getAppConfigPath(), JSON.stringify( rest, null, 2 ) + '\n', { |
There was a problem hiding this comment.
Should we use the lock/unlock pattern here?
|
|
||
| // Remember this connection so future push/pull runs (and the Desktop UI) | ||
| // can surface it without re-selecting from the full site list. | ||
| await addConnectedWpcomSite( site.id, { ...remoteSite, localSiteId: site.id } ); |
There was a problem hiding this comment.
Should we try...catch here? The pull was still successful and the user will see the reflected site changes.
|
|
||
| // Remember this connection so future push/pull runs (and the Desktop UI) | ||
| // can surface it without re-selecting from the full site list. | ||
| await addConnectedWpcomSite( site.id, { ...remoteSite, localSiteId: site.id } ); |
There was a problem hiding this comment.
Similar to https://github.com/Automattic/studio/pull/3227/changes#r3161883090 we could add a try...catch here
bcotrim
left a comment
There was a problem hiding this comment.
Added a couple of suggestions that I think we could address before merging.
Code LGTM and works as described 👍
Yes, I think the solution for this is to have a single version of config files that both app and cli can read, and if it's higher than the supported one, it asks you to upgrade. |
Related issues
How AI was used in this PR
Opus 4.7 scoped the refactor, wrote the new shared helpers, the migration, and the Desktop/CLI wiring. I (the author) steered the data-model decision (cli.json sites property, per-user keying preserved) and reviewed each hunk. The diff is roughly 520 lines added / 135 removed across 12 files — mostly mechanical once the data layout was settled.
Proposed Changes
Connected WP.com sites used to live in
app.jsonas a top-levelconnectedWpcomSites: { [userId]: SyncSite[] }— Desktop-only state. This moves the list into per-site entries incli.jsonso both Desktop and the CLI share a single source of truth, and wires up the CLI push/pull flows to auto-connect on success.tools/common/)syncSiteSchemazod schema added next to theSyncSitetype, which is now inferred from it.tools/common/lib/connected-sites.tswith read/write helpers againstcli.json:getConnectedWpcomSitesForLocalSite,getAllConnectedWpcomSitesForCurrentUser,addConnectedWpcomSite,removeConnectedWpcomSite,updateConnectedWpcomSites,markConnectedWpcomSiteSynced. Uses the existingcli.json.locklockfile and a permissive passthrough schema so Desktop and CLI can evolve theircli.jsonsite entries independently without corrupting each other's fields.siteSchemanow carries an optionalconnectedWpcomSites: { [userId]: SyncSite[] }map per site.connectedWpcomSitesremoved from theapp.jsonUserDatatype.app.json.connectedWpcomSites[userId][].{localSiteId}intocli.json sites[].connectedWpcomSites[userId], deduping by remote id. It stampsconnectedWpcomSitesMigratedToCli: trueonapp.jsonand leaves the legacy field in place for this release so older Studio versions keep working — a follow-up migration will strip it.app.json:connectWpcomSites,disconnectWpcomSites,updateConnectedWpcomSites,getConnectedWpcomSitesinapps/studio/src/modules/sync/lib/ipc-handlers.tsreconcileSessionEnvironmentBeforeRunandsetSessionEnvironmentinapps/studio/src/ipc-handlers.tsapps/cli/commands/push.tsandapps/cli/commands/pull.tscalladdConnectedWpcomSite+markConnectedWpcomSiteSyncedafter a successful run.studio codeagent gained asite_connected_remote_sitesMCP tool plus a new system-prompt section instructing it to resolve the target before pushing — 1 attached site → confirm, many →AskUserQuestionlist, 0 → open-ended question.Testing Instructions
~/.studio/app.jsonand~/.studio/cli.json.app.json.connectedWpcomSitespopulated, launch the Desktop app. Expectcli.json sites[].connectedWpcomSites[userId]to be populated, andapp.json.connectedWpcomSitesMigratedToCli: trueto be added. The legacyapp.json.connectedWpcomSitesshould still be present.cli.jsonwithout further app.json writes.studio pushorstudio pullfor a site with no connections yet — after success, verify the remote site shows up undercli.json sites[].connectedWpcomSitesand that Desktop's site dropdown reflects it.studio code, ask it to "push my site". It should callsite_connected_remote_sites, then either confirm (single attached), show a picker (many), or ask open-ended (none) before callingsite_push.Pre-merge Checklist
npm run typecheckclean across workspaces;npm test -- apps/cli apps/studio— 1042 passing)🤖 Generated with Claude Code