no ecosystem files in git; officer-setup generates one

Deleted all four — ecosystem.config.cjs, .light., .mac.light. and the
.profile. they derived from. The repository now contains no ecosystem file at
all, and .gitignore keeps it that way.

officer-setup writes one at the end, describing exactly the six processes a core
install runs: officer, officer-anthropic-proxy, officer-agent, officer-opencode,
officer-pty and officer-headscale. No profiles, no derivation, no plugins.

The four existed because a profile has to subtract from something, so the full
list had to name every plugin's process whether or not anybody installed it —
and a test then had to assert the two files still agreed. Generating one file
removes the subtraction, the second list and the test that policed them.

It is .cjs, not the .js PM2's docs use, and that is not a preference:
package.json declares "type": "module", so a .js file here is ESM and
`module.exports` throws. PM2 require()s the config.

Sections 10 (Services) and 11 (Verify) are built on top of it — write,
startOrRestart, save, optional boot hook, then check every process is online with
a sane restart count AND that the API actually answers on PORT. A process can be
`online` and serving nothing, so the port is asked directly rather than inferred.
That completes all eleven sections.

catalogue.test.ts required both deleted files at import, so it could not even
load. Its central assertion — "the store offers exactly what light leaves out" —
has no meaning without a full list to subtract from, which is the point of the
change. Replaced by two weaker but real checks: the store must not offer a core
process, and every process it names must have a sidecar directory to run. The
second catches the same typo the old one did without needing a manifest of
everything; verified it holds for all 15 catalogue entries.

app-store/pm2.ts starts a sidecar with `--only` against this file, which now
holds core alone — so it can stop a plugin but cannot start one that was never
written in. Marked `[open]` there rather than left to be discovered: appending a
plugin's entry is the plugin system's job.

Generated one in a scratch directory and required it with node: six apps, correct
cwd on each, valid CommonJS.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-13 02:35:12 +00:00
co-authored by Claude Opus 5
parent b075f1f882
commit 62cbf510cb
10 changed files with 260 additions and 387 deletions
+37 -18
View File
@@ -1,34 +1,53 @@
import { describe, expect, it } from 'bun:test';
import { existsSync } from 'node:fs';
import { join } from 'node:path';
import { CATALOGUE, byId } from './catalogue';
import { CAPABILITIES } from '../capabilities/registry';
// The catalogue is a hand-written list describing machinery that lives elsewhere, which is the shape of
// thing that rots silently. These tests pin it to the three sources it claims to agree with:
// ecosystem.config.cjs, the light profile, and the capability registry.
// thing that rots silently. These tests pin it to what it claims to agree with.
//
// The intent is that adding a sidecar to the estate and forgetting the app store FAILS HERE, rather than
// the sidecar being quietly uninstallable and nobody noticing for a release.
//
// ── What changed on 2026-08-13 ──
//
// Two of these tests derived the catalogue from `ecosystem.config.cjs` minus `ecosystem.light.config.cjs`
// — "the app store offers exactly what light leaves out". That was the right check while those files
// existed, and they do not any more: the ecosystem file is GENERATED at setup, describes only the six
// core processes, and is not in git. There is no longer a list of every possible process to subtract
// from, which is the point — a plugin's process is described when it is installed, not before.
//
// So the derivation is gone and two weaker but still real checks replace it: the store must not offer a
// CORE process, and every process it names must have a sidecar directory to run. The second is the
// stronger of the two — it catches the typo the old test caught, without needing a manifest of
// everything.
const full = (require('../../../ecosystem.config.cjs') as { apps: { name: string }[] }).apps.map((a) => a.name);
const light = (require('../../../ecosystem.light.config.cjs') as { apps: { name: string }[] }).apps.map((a) => a.name);
// The processes a core install runs, mirroring CORE_PROCESSES in
// scripts/setup/officer-setup/lib/services.sh. Duplicated deliberately: the generator is shell and this
// is a test, and the alternative is the test reading a file the repository does not contain.
const CORE = [
'officer',
'officer-anthropic-proxy',
'officer-agent',
'officer-opencode',
'officer-pty',
'officer-headscale',
];
describe('the catalogue against the real estate', () => {
it('offers exactly the processes the light profile leaves out', () => {
// This is the definition of the app store: light is the baseline, everything else is installable.
const notInLight = full.filter((name) => !light.includes(name)).sort();
const offered = CATALOGUE.map((e) => e.process).sort();
expect(offered).toEqual(notInLight);
});
it('names a process that actually exists in the ecosystem', () => {
// A typo here would install nothing and report success.
for (const entry of CATALOGUE) expect(full).toContain(entry.process);
});
it('does not offer to install the baseline', () => {
// "Uninstall chat" is not a thing the store should be able to express.
for (const entry of CATALOGUE) expect(light).not.toContain(entry.process);
for (const entry of CATALOGUE) expect(CORE).not.toContain(entry.process);
});
it('names a process that has a sidecar to run', () => {
// A typo here would install nothing and report success. Every process is `officer-<dir>` under
// src/servers/sidecar/, which is what the generated ecosystem entry would point `args` at.
for (const entry of CATALOGUE) {
const dir = entry.process.replace(/^officer-/, '');
expect(existsSync(join(import.meta.dir, '../sidecar', dir))).toBe(true);
}
});
});
+7
View File
@@ -20,6 +20,13 @@
export type Pm2Result = { ok: true } | { ok: false; error: string };
// The file officer-setup GENERATES at $OFFICER_ROOT/platform. It is not in the repository.
//
// `[open]` As of 2026-08-13 it contains the SIX core processes and nothing else, so
// `pm2 start ecosystem.config.cjs --only officer-jellyfin` finds no such app and does nothing. Installing
// a plugin has to append its entry here before starting it — that is the plugin system's job and it is
// not built. Until it is, the app store can stop a running sidecar but cannot start one that was never
// in the file.
const ECOSYSTEM = 'ecosystem.config.cjs';
async function pm2(args: string[], cwd: string): Promise<{ code: number; out: string }> {