899028e20f
- RomConfig/RomCache on the kit's ConfigService/CacheStore; RomSync wires the pure domain into the kit SyncEngine (poll/watch/coalesce/fingerprint) and ProviderClient; EngineStatus assembly shared by REST + SSE - makeApi: HttpApiBuilder groups over live service values (handler runtime shares the plugin runtime's singletons) + sseRoute status feed - index.ts: definePluginKit entry (async-main boundary); headless-first (UI serve failure logged, engine continues) - cli.ts on the kit dispatcher: scan/detect/preview offline, sync/uninstall online; set-password is gone with the standalone server - dev.ts: auth-free loopback API server (:5885) — the dev:live proxy target; never bundled - verified live host-less: raw config round-trip on disk, status/platforms/ preview endpoints, soft-fail reconcile without a host - CI: workspace install, per-package typecheck, publish from plugin/ Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
98 lines
2.8 KiB
TypeScript
98 lines
2.8 KiB
TypeScript
#!/usr/bin/env bun
|
|
// Ops CLI on the kit's dispatcher — the same layer graph as the plugin entry, so every
|
|
// command sees the exact services the runner runs. scan/detect/preview work offline;
|
|
// sync/uninstall talk to the host.
|
|
import {
|
|
type CliCommand,
|
|
ProviderClient,
|
|
runPluginCli,
|
|
} from "@punktfunk/plugin-kit";
|
|
import { Effect } from "effect";
|
|
import pkg from "../package.json" with { type: "json" };
|
|
import { PLUGIN_NAME } from "./index.js";
|
|
import { type RomServices, romLayer } from "./layer.js";
|
|
import { PROVIDER_ID, RomSync } from "./services/engine.js";
|
|
|
|
const print = (line: string) => Effect.sync(() => console.log(line));
|
|
|
|
const commands: Record<string, CliCommand<RomServices>> = {
|
|
scan: {
|
|
summary: "Scan configured ROM roots and list candidates",
|
|
offline: true,
|
|
run: () =>
|
|
Effect.gen(function* () {
|
|
const sync = yield* RomSync;
|
|
const { report } = yield* sync.preview;
|
|
yield* print(
|
|
`${report.considered} candidate(s), ${report.included} would be included`,
|
|
);
|
|
for (const [platform, n] of Object.entries(report.perPlatform)) {
|
|
yield* print(` ${platform}: ${n}`);
|
|
}
|
|
}),
|
|
},
|
|
detect: {
|
|
summary: "Probe for installed emulators",
|
|
offline: true,
|
|
run: () =>
|
|
Effect.gen(function* () {
|
|
const sync = yield* RomSync;
|
|
const detected = yield* sync.detect(true);
|
|
for (const d of detected) {
|
|
yield* print(
|
|
`${d.id.padEnd(14)} ${d.via.padEnd(8)} ${d.exePath ?? d.exeToken}${d.cores ? ` (${d.cores.length} cores)` : ""}`,
|
|
);
|
|
}
|
|
if (detected.length === 0) yield* print("no emulators found");
|
|
}),
|
|
},
|
|
preview: {
|
|
summary: "Dry-run: show the entries a sync would reconcile",
|
|
offline: true,
|
|
run: () =>
|
|
Effect.gen(function* () {
|
|
const sync = yield* RomSync;
|
|
const { entries, report } = yield* sync.preview;
|
|
for (const e of entries) yield* print(`${e.external_id} ${e.title}`);
|
|
for (const s of report.skipped) {
|
|
yield* print(`SKIP ${s.external_id}: ${s.reason}`);
|
|
}
|
|
yield* print(
|
|
`${report.included} included, ${report.skipped.length} skipped`,
|
|
);
|
|
}),
|
|
},
|
|
sync: {
|
|
summary: "Reconcile the library provider now",
|
|
run: () =>
|
|
Effect.gen(function* () {
|
|
const sync = yield* RomSync;
|
|
const outcome = yield* sync.engine.sync("manual");
|
|
yield* print(
|
|
outcome._tag === "AlreadyRunning"
|
|
? "a sync is already running"
|
|
: `${outcome._tag}: ${outcome.report.included} entries`,
|
|
);
|
|
}),
|
|
},
|
|
uninstall: {
|
|
summary: "Remove every rom-manager entry from the host library",
|
|
run: () =>
|
|
Effect.gen(function* () {
|
|
const provider = yield* ProviderClient;
|
|
yield* provider.remove(PROVIDER_ID);
|
|
yield* print("provider entries removed");
|
|
}),
|
|
},
|
|
};
|
|
|
|
await runPluginCli({
|
|
def: {
|
|
name: PLUGIN_NAME,
|
|
version: pkg.version,
|
|
layer: romLayer,
|
|
main: Effect.void,
|
|
},
|
|
commands,
|
|
});
|