Developer Guide
Technical documentation for developers who want to understand, modify, or extend the Auto One-Click Admin addon.
Architecture Overview
The addon follows Local’s standard two-process architecture:
┌─────────────────────────────────────────────────────────────┐
│ Main Process │
│ ┌─────────────────┐ ┌─────────────────┐ │
│ │ index.ts │ │ lifecycle-hooks │ │
│ │ (entry point) │ │ (siteAdded) │ │
│ └────────┬────────┘ └────────┬────────┘ │
│ │ │ │
│ ▼ ▼ │
│ ┌─────────────────┐ ┌─────────────────┐ │
│ │ ipc-handlers.ts │ │ WP-CLI │ │
│ │ (settings IPC) │ │ siteData │ │
│ └────────┬────────┘ └────────┬────────┘ │
└───────────┼────────────────────┼────────────────────────────┘
│ IPC │ IPC (CONFIGURED event)
▼ ▼
┌─────────────────────────────────────────────────────────────┐
│ Renderer Process │
│ ┌─────────────────┐ ┌─────────────────┐ │
│ │ index.tsx │ │ PreferencesPanel│ │
│ │ (entry point) │ │ (settings UI) │ │
│ └────────┬────────┘ └─────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────┐ │
│ │ Apollo Cache │ │
│ │ Eviction │ │
│ └─────────────────┘ │
└─────────────────────────────────────────────────────────────┘
File Structure
local-addon-oneclick-admin/
├── package.json # Addon manifest and dependencies
├── tsconfig.json # TypeScript configuration
├── README.md # Quick reference
├── docs/
│ ├── USER_GUIDE.md # End-user documentation
│ ├── DEVELOPER_GUIDE.md # This file
│ └── TROUBLESHOOTING.md # Common issues
├── src/
│ ├── common/
│ │ └── constants.ts # Shared constants (IPC channels, storage keys)
│ ├── main/
│ │ ├── index.ts # Main process entry point
│ │ ├── ipc-handlers.ts # IPC handlers for settings
│ │ └── lifecycle-hooks.ts # Site lifecycle hooks
│ └── renderer/
│ ├── index.tsx # Renderer entry point
│ └── PreferencesPanel.tsx # Settings UI component
└── lib/ # Compiled JavaScript output
Key Technical Patterns
1. Two Event Mechanisms for Site Events
Critical Discovery: Local has TWO different mechanisms for site events:
HooksMain.doActions('siteAdded')- The addon hooks API (context.hooks.addAction)sendIPCEvent('siteAdded')- IPC event that emits onipcMain
Which services use which:
| Service | HooksMain.doActions | sendIPCEvent |
|---|---|---|
| AddSiteService (regular create) | ✅ Line 249 | ✅ Line 250 |
| CloneSiteService (clone) | ✅ Line 214 | ✅ Line 215 |
| ImporterLocalExport (import/blueprint) | ❌ | ✅ Line 64 |
| ImporterGeneric (generic import) | ❌ | ✅ Line 57 |
The Problem: If you use context.hooks.addAction('siteAdded', ...), your addon will NOT receive events for imported sites or sites created from blueprints!
The Solution: Use ipcMain.on('siteAdded', ...) because Local’s sendIPCEvent() function emits on ipcMain (see app/shared/helpers/send-ipc-event.ts line 36):
import { ipcMain } from 'electron';
// This fires for ALL site creation flows
ipcMain.on('siteAdded', async (_event, site) => {
if (!isEnabled()) return;
if (site.oneClickAdminID) return; // Already configured
// Site is already running - safe to use WP-CLI
const users = await wpCli.run(site, [
'user', 'list', '--role=administrator', '--format=json'
]);
// ... configure one-click admin
});
2. Why siteAdded Instead of siteStarted
Additional Discovery: For new sites, the siteStarted hook is NEVER called!
When Local creates a new site, the flow is:
- Site record created
- Site provisioned
- WordPress installed
- Site finalized
- Status set to ‘running’
siteAddedfires (site is already running!)
The siteStarted hook only fires when you manually start an existing site. New site creation has its own flow that doesn’t use SiteProcessManagerService.
3. Apollo Cache Eviction for UI Refresh
Problem: When we call siteData.updateSite(), the data is persisted but Local’s UI doesn’t update because:
- The
SiteInfoOneClickAdmincomponent uses Apollo GraphQL - The
sitesUpdatedsubscription does NOT includeoneClickAdminIDfields - Apollo’s
InMemoryCachenever receives the updated values
Solution: Access Apollo Client directly and evict the cache:
function findApolloClient(): any {
// Local exposes Apollo via connectToDevTools: true
if ((window as any).__APOLLO_CLIENT__) {
return (window as any).__APOLLO_CLIENT__;
}
return null;
}
// On receiving CONFIGURED IPC event:
const apolloClient = findApolloClient();
if (apolloClient?.cache) {
apolloClient.cache.evict({
id: 'ROOT_QUERY',
fieldName: 'site',
args: { id: siteId },
});
apolloClient.cache.gc();
apolloClient.refetchQueries({ include: 'active' });
}
4. Class Components (No React Hooks)
Local’s addon system does NOT support React hooks. All components must be class-based:
// WRONG - will crash
const MyComponent = () => {
const [state, setState] = useState(); // Error!
};
// CORRECT
class MyComponent extends React.Component {
state = { enabled: true };
componentDidMount() {
// Load data here
}
render() {
return React.createElement('div', null, 'Content');
}
}
5. IPC Pattern with ipcMain.handle()
Always use ipcMain.handle() for IPC handlers, NOT LocalMain.addIpcAsyncListener:
// Main process
import { ipcMain } from 'electron';
ipcMain.handle('addon:get-settings', async () => {
const enabled = userData.get(STORAGE_KEYS.ENABLED, true);
return { success: true, data: { enabled } };
});
// Renderer process
const electron = context.electron || (window as any).electron;
const response = await electron.ipcRenderer.invoke('addon:get-settings');
Data Flow
Site Creation Flow
User creates site
│
▼
Local provisions site & installs WordPress
│
▼
`siteAdded` hook fires (site already running)
│
▼
Check if addon enabled & site not configured
│
▼
WP-CLI: `wp user list --role=administrator --format=json`
│
▼
Parse JSON, get first admin user
│
▼
siteData.updateSite(siteId, { oneClickAdminID, oneClickAdminDisplayName })
│
▼
Send IPC CONFIGURED event to renderer
│
▼
Renderer receives event, checks if viewing this site
│
▼
Apollo cache evict + refetch
│
▼
UI updates with one-click admin button
Settings Flow
User opens Settings > Auto One-Click Admin
│
▼
PreferencesPanel.componentDidMount()
│
▼
IPC: invoke('oneclick-admin:get-settings')
│
▼
Main process: userData.get(STORAGE_KEYS.ENABLED, true)
│
▼
Response: { success: true, data: { enabled: true } }
│
▼
User toggles checkbox → setApplyButtonDisabled(false)
│
▼
User clicks Apply → onApply() callback
│
▼
IPC: invoke('oneclick-admin:save-settings', { enabled })
│
▼
Main process: userData.set(STORAGE_KEYS.ENABLED, enabled)
│
▼
Apply button auto-disables
Key Discoveries / Learnings
From Local Source Code Analysis
-
Two Event Mechanisms: Local has
HooksMain.doActions()for addon hooks ANDsendIPCEvent()for IPC. ImporterLocalExport (imports/blueprints) ONLY usessendIPCEvent, not hooks! UseipcMain.on()to catch all events. -
siteStartedvssiteAdded: ThesiteStartedhook only fires for manual starts of existing sites, not new site creation - Apollo Subscription Gap: The
sitesUpdatedGraphQL subscription includes:subscription sitesUpdated { sitesUpdated { id, name, domain, status, siteLastStartedTimestamp, path } # oneClickAdminID NOT included! } -
Apollo Client Access: Local’s Apollo Client is created with
connectToDevTools: true, exposing it viawindow.__APOLLO_CLIENT__ -
MobX Legacy:
refreshSitesIPC only updates MobX store, not Apollo cache - sendIPCEvent internals (from
app/shared/helpers/send-ipc-event.ts):- From main process: sends to renderer via
webContents.send()AND emits onipcMain.emit() - This is why
ipcMain.on('siteAdded', ...)works for catching all site creation events
- From main process: sends to renderer via
- IPC site objects are serialized: When receiving site data via IPC, the object is serialized (via
simpleSerialize). It’s missing:- Methods like
getSiteServiceByRole() - Some properties like
paths - Always fetch the full site from
siteData.getSite(siteId)before using services likewpCli.run()
- Methods like
How to Modify
Change User Selection Logic
Edit src/main/lifecycle-hooks.ts:
// Current: First admin user
const firstAdmin = users[0];
// Alternative: User with specific role
const superAdmin = users.find(u => u.roles.includes('administrator'));
// Alternative: Most recently created admin
const newestAdmin = users.sort((a, b) =>
parseInt(b.ID) - parseInt(a.ID)
)[0];
Add More Preferences
- Update
src/common/constants.ts:export const STORAGE_KEYS = { ENABLED: `${ADDON_SLUG}_enabled`, PREFER_ROLE: `${ADDON_SLUG}_prefer_role`, // New }; -
Update
src/main/ipc-handlers.tsto handle new settings - Update
src/renderer/PreferencesPanel.tsxwith new UI controls
Listen for Additional Events
Edit src/main/lifecycle-hooks.ts to add more event listeners. Remember to check if the event uses HooksMain.doActions() or sendIPCEvent():
// For events that use sendIPCEvent (like siteAdded)
ipcMain.on('someEvent', async (_event, data) => {
// Handle event
});
// For events that only use HooksMain.doActions (like siteStarted)
context.hooks.addAction('siteStarted', async (site: any) => {
// Handle event
});
Building
# Install dependencies
npm install
# Build TypeScript to JavaScript
npm run build
# Output in lib/ directory
After building, restart Local to load the updated addon.
Testing
Manual Testing Checklist
- Create new site - one-click admin appears
- Import site - one-click admin configured
- Clone site - one-click admin on clone
- Disable addon - new sites not configured
- Settings persist after restart
- UI updates immediately after site creation
Debug Commands (in DevTools Console)
// Check if addon loaded
console.log(window.__APOLLO_CLIENT__);
// Check current hash (for site ID matching)
console.log(window.location.hash);
// Manually trigger cache eviction
window.__APOLLO_CLIENT__.cache.evict({ id: 'ROOT_QUERY' });
window.__APOLLO_CLIENT__.refetchQueries({ include: 'active' });
Dependencies
@getflywheel/local- Local addon API typeselectron- IPC communicationtypescript- Type checking and compilation
Resources
- Local Addon Documentation
- Kitchen Sink Reference Addon
- Local Source Code (for hook discovery)