add gen:ecosystem script to rebuild ecosystem.config.cjs from the database

bun gen:ecosystem writes the whole file from three sources: CORE_PROCESSES read
directly out of scripts/setup/officer-setup/lib/services.sh (shelled out to bash,
so there is exactly one list rather than a TS copy to keep in sync by hand),
marketplace plugins that are installed AND enabled in Postgres and actually ship a
sidecar/, and app-store services that are installed AND enabled, using each
catalogue entry's `process` name against the src/servers/sidecar/<id>/index.ts
convention.

Extracted `pluginPm2Entry` (a pure DiscoveredPlugin -> Pm2App) out of
addPluginToEcosystem so the live install/uninstall path and this from-scratch
rebuild share one definition of what a plugin's entry looks like, rather than two
that can drift apart. addPluginToEcosystem's own behavior is unchanged.

Complements, not replaces, the existing incremental add/remove in
servers/plugins/ecosystem.ts — that edits the live file at install time; this
regenerates the whole thing, for a fresh clone, drift, or restoring from a backup.

Found while testing against this machine's real file, worth flagging rather than
silently working around: officer-marketplace (the marketplace.officer.dev process,
src/servers/marketplace/server.tsx) is in this machine's ecosystem.config.cjs by
hand and is modeled by neither the plugin system nor the app-store catalogue, so
this script does not know about it and would drop its entry on a real run. Left
alone rather than guessed at.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-18 09:23:25 +00:00
co-authored by Claude Opus 5
parent 9e641bc228
commit 7b7d8bbf36
3 changed files with 148 additions and 11 deletions
+23 -11
View File
@@ -31,7 +31,7 @@ import type { DiscoveredPlugin } from './manifest';
const ECOSYSTEM_PATH = join(PLATFORM_DIR, 'ecosystem.config.cjs');
/** One PM2 app entry. Matches what `services.sh` emits for the core processes. */
type Pm2App = { name: string; script: string; args?: string; cwd: string; watch: boolean };
export type Pm2App = { name: string; script: string; args?: string; cwd: string; watch: boolean };
/**
* The header to write when there is no file to take one from.
@@ -91,13 +91,15 @@ function writeEcosystemApps(apps: Pm2App[]): void {
}
/**
* Ensure the plugin's sidecar has an entry, replacing one that is already there.
* The PM2 entry a plugin's sidecar would get, or null when it has none — most plugins won't.
*
* Idempotent, because re-installing is how a plugin is upgraded and the script path may have moved.
* Returns false when the plugin has no sidecar at all, which is not a failure — most plugins won't.
* Pure, deliberately: `addPluginToEcosystem` below is the read-modify-write that touches the file for a
* live install, and `scripts/gen-ecosystem.ts` is the other caller, rebuilding the whole file from what
* the database says is installed and enabled. Neither should duplicate the other's idea of what a
* plugin's entry looks like.
*/
export function addPluginToEcosystem(plugin: DiscoveredPlugin): boolean {
if (!plugin.sidecar) return false;
export function pluginPm2Entry(plugin: DiscoveredPlugin): Pm2App | null {
if (!plugin.sidecar) return null;
const name = pluginProcessName(plugin.appName);
// Relative to the platform, because `cwd` is pinned to it and an absolute path would break the moment
@@ -106,12 +108,22 @@ export function addPluginToEcosystem(plugin: DiscoveredPlugin): boolean {
? plugin.sidecar.script.slice(PLATFORM_DIR.length + 1)
: plugin.sidecar.script;
const entry: Pm2App =
plugin.sidecar.runtime === 'node'
? { name, script: 'node', args: script, cwd: PLATFORM_DIR, watch: false }
: { name, script: 'bun', args: `run ${script}`, cwd: PLATFORM_DIR, watch: false };
return plugin.sidecar.runtime === 'node'
? { name, script: 'node', args: script, cwd: PLATFORM_DIR, watch: false }
: { name, script: 'bun', args: `run ${script}`, cwd: PLATFORM_DIR, watch: false };
}
const apps = readEcosystemApps().filter((a) => a.name !== name);
/**
* Ensure the plugin's sidecar has an entry, replacing one that is already there.
*
* Idempotent, because re-installing is how a plugin is upgraded and the script path may have moved.
* Returns false when the plugin has no sidecar at all, which is not a failure — most plugins won't.
*/
export function addPluginToEcosystem(plugin: DiscoveredPlugin): boolean {
const entry = pluginPm2Entry(plugin);
if (!entry) return false;
const apps = readEcosystemApps().filter((a) => a.name !== entry.name);
apps.push(entry);
writeEcosystemApps(apps);
return true;