stricter checks
CI / verify (pull_request) Successful in 12s

This commit is contained in:
2026-10-10 15:16:53 +02:00
parent ced43fbb11
commit 513d51f702
30 changed files with 984 additions and 470 deletions
+118 -105
View File
@@ -50,11 +50,118 @@ const nativePlugin = z.union([
z.object({ id: z.string(), effect: z.function() }),
]);
type DirectoryExports = { ".": string; "./tui"?: string };
type Name = (typeof names)[number];
async function prompt(retained: Name[]) {
const input = createInterface({ input: process.stdin, output: process.stdout });
try {
const defaults = retained.length > 0 ? retained : names;
const chosen = (
(
await input.question(`Plugins (agents,multi-work,quota) [${defaults.join(",")}]: `)
).trim() || defaults.join(",")
).split(",");
if (
chosen.includes("multi-work") &&
!chosen.includes("agents") &&
/^(y|yes)?$/iu.test(
(await input.question("Multi-work requires Agents. Add Agents? [Y/n]: ")).trim(),
)
)
chosen.push("agents");
return chosen;
} finally {
input.close();
}
}
async function choose(retained: Name[], selection?: string) {
if (selection !== undefined) return selection.split(",");
if (process.stdin.isTTY) return prompt(retained);
if (retained.length === 0)
throw new Error(
"Noninteractive installation requires --plugins agents,multi-work,quota (or a valid subset)",
);
return [...retained];
}
async function stage(source: string, staged: string, name: Name) {
const file = name === "quota" ? "quota-plugin" : name;
const entrypoint = join(source, "dist", `${file}.js`);
const { default: plugin } = z.object({ default: nativePlugin }).parse(await import(entrypoint));
if (plugin.id !== `stanp-opencode.${name}`)
throw new Error(`Invalid native OpenCode plugin: ${name}`);
const directory = join(staged, name);
await mkdir(directory);
await copyFile(entrypoint, join(directory, "index.js"));
const exports: DirectoryExports = { ".": "./index.js" };
if (name === "quota") {
const tui = join(source, "dist", "tui.js");
if (
!new Bun.Transpiler({ loader: "js" })
.scan(await Bun.file(tui).text())
.exports.includes("default")
)
throw new Error("Quota lacks its TUI entrypoint");
await copyFile(tui, join(directory, "tui.js"));
exports["./tui"] = "./tui.js";
}
await Bun.write(
join(directory, "package.json"),
JSON.stringify({
name: `stanp-opencode-${name}`,
type: "module",
main: "./index.js",
exports,
}),
);
}
async function activate(
source: string,
config: string,
selected: Name[],
updated: z.infer<typeof configSchema>,
) {
const configFile = join(config, "opencode.json");
const parent = join(config, "plugins");
await mkdir(parent, { recursive: true });
const staged = join(parent, `.stanp-opencode.new.${process.pid}`);
const backup = join(parent, `.stanp-opencode.old.${process.pid}`);
const target = join(parent, "stanp-opencode");
const configStage = join(config, `.stanp-opencode.config.${process.pid}`);
let saved = false;
let activated = false;
try {
await mkdir(staged);
for (const name of selected) await stage(source, staged, name);
await Bun.write(configStage, JSON.stringify(updated, null, 4) + "\n");
try {
await rename(target, backup);
saved = true;
} catch (error) {
if (!(error instanceof Error && "code" in error && error.code === "ENOENT"))
throw error;
}
await rename(staged, target);
activated = true;
await rename(configStage, configFile);
} catch (error) {
if (activated) await rm(target, { recursive: true, force: true });
if (saved) await rename(backup, target);
throw error;
} finally {
await rm(staged, { recursive: true, force: true });
await rm(configStage, { force: true });
}
if (saved) await rm(backup, { recursive: true, force: true });
await rm(join(parent, "stanp-opencode.js"), { force: true });
}
export async function install(source: string, config: string, selection?: string) {
const absoluteBase = join(config, "plugins", "stanp-opencode");
const monolith = (target: string) =>
target === base || target === absoluteBase || /^stanp-opencode(?:@[^/]+)?$/.test(target);
target === base || target === absoluteBase || /^stanp-opencode(?:@[^/]+)?$/u.test(target);
const owned = new Set(
names.flatMap((name) => [
`${base}/${name}`,
@@ -82,39 +189,7 @@ export async function install(source: string, config: string, selection?: string
entry.package === `stanp-opencode/${name}`,
),
);
let chosen: string[];
if (selection !== undefined) chosen = selection.split(",");
else if (process.stdin.isTTY) {
const input = createInterface({ input: process.stdin, output: process.stdout });
try {
const defaults = retained.length ? retained : names;
chosen = (
(
await input.question(
`Plugins (agents,multi-work,quota) [${defaults.join(",")}]: `,
)
).trim() || defaults.join(",")
).split(",");
if (chosen.includes("multi-work") && !chosen.includes("agents")) {
if (
/^(y|yes)?$/i.test(
(
await input.question("Multi-work requires Agents. Add Agents? [Y/n]: ")
).trim(),
)
)
chosen.push("agents");
}
} finally {
input.close();
}
} else {
if (!retained.length)
throw new Error(
"Noninteractive installation requires --plugins agents,multi-work,quota (or a valid subset)",
);
chosen = [...retained];
}
const chosen = await choose(retained, selection);
if (
!z.array(z.enum(names)).min(1).safeParse(chosen).success ||
new Set(chosen).size !== chosen.length
@@ -131,17 +206,16 @@ export async function install(source: string, config: string, selection?: string
entry.package === `stanp-opencode/${name}` ||
(name === "agents" && monolith(entry.package)),
);
const options: NonNullable<Entry["options"]> = {};
Object.assign(options, prior?.options);
delete options.playwright;
const options: NonNullable<Entry["options"]> = { ...prior?.options };
delete options["playwright"];
fill(options, skeleton(name));
const entry = entrySchema.parse({ ...prior, package: `${base}/${name}` });
delete entry.options;
if (Object.keys(options).length) entry.options = options;
if (Object.keys(options).length > 0) entry.options = options;
return entry;
});
const first = configured.findIndex((entry) => isOwned(entry.package));
const next = entries.filter((_entry, index) => !isOwned(configured[index]!.package));
const next = entries.filter((_entry, index) => !isOwned(configured[index]?.package ?? ""));
next.splice(first < 0 ? next.length : first, 0, ...registrations);
const updated: z.infer<typeof configSchema> = {
...current,
@@ -149,75 +223,13 @@ export async function install(source: string, config: string, selection?: string
plugins: next,
};
// Coordinate delegates to workers from Multi-work children, so routing needs depth >= 2.
const experimental = experimentalSchema.safeParse(current.experimental ?? {});
const experimental = experimentalSchema.safeParse(current["experimental"] ?? {});
if (selected.includes("agents") && experimental.success)
updated.experimental = {
updated["experimental"] = {
...experimental.data,
subagent_depth: Math.max(experimental.data.subagent_depth ?? 0, 2),
};
const parent = join(config, "plugins");
await mkdir(parent, { recursive: true });
const staged = join(parent, `.stanp-opencode.new.${process.pid}`);
const backup = join(parent, `.stanp-opencode.old.${process.pid}`);
const target = join(parent, "stanp-opencode");
const configStage = join(config, `.stanp-opencode.config.${process.pid}`);
let saved = false;
let activated = false;
try {
await mkdir(staged);
for (const name of selected) {
const file = name === "quota" ? "quota-plugin" : name;
const entrypoint = join(source, "dist", `${file}.js`);
const { default: definition } = await import(entrypoint);
const plugin = nativePlugin.parse(definition);
if (plugin.id !== `stanp-opencode.${name}`)
throw new Error(`Invalid native OpenCode plugin: ${name}`);
const directory = join(staged, name);
await mkdir(directory);
await copyFile(entrypoint, join(directory, "index.js"));
const exports: DirectoryExports = { ".": "./index.js" };
if (name === "quota") {
const tui = join(source, "dist", "tui.js");
if (
!new Bun.Transpiler({ loader: "js" })
.scan(await Bun.file(tui).text())
.exports.includes("default")
)
throw new Error("Quota lacks its TUI entrypoint");
await copyFile(tui, join(directory, "tui.js"));
exports["./tui"] = "./tui.js";
}
await Bun.write(
join(directory, "package.json"),
JSON.stringify({
name: `stanp-opencode-${name}`,
type: "module",
main: "./index.js",
exports,
}),
);
}
await Bun.write(configStage, JSON.stringify(updated, null, 4) + "\n");
try {
await rename(target, backup);
saved = true;
} catch (error) {
if (!(error instanceof Error && "code" in error && error.code === "ENOENT"))
throw error;
}
await rename(staged, target);
activated = true;
await rename(configStage, configFile);
} catch (error) {
if (activated) await rm(target, { recursive: true, force: true });
if (saved) await rename(backup, target);
throw error;
} finally {
await rm(staged, { recursive: true, force: true });
await rm(configStage, { force: true });
}
if (saved) await rm(backup, { recursive: true, force: true });
await rm(join(parent, "stanp-opencode.js"), { force: true });
await activate(source, config, selected, updated);
console.log(`Installed ${selected.join(", ")}. Restart OpenCode.`);
if (oldMonolith)
console.log(
@@ -227,6 +239,7 @@ export async function install(source: string, config: string, selection?: string
if (import.meta.main) {
const [source, config, selection] = process.argv.slice(2);
if (!source || !config) throw new Error("Expected package directory and config directory");
if (source === undefined || source === "" || config === undefined || config === "")
throw new Error("Expected package directory and config directory");
await install(source, config, selection);
}
+48 -37
View File
@@ -8,14 +8,15 @@ import { OpenCode } from "@opencode/client";
import { command } from "../src/multi-work/herdr.js";
import { z } from "zod";
const binary = process.env.OPENCODE_2022;
if (!binary) throw new Error("Set OPENCODE_2022 to an isolated OpenCode 2.0.22 binary");
const binary = process.env["OPENCODE_2022"];
if (binary === undefined || binary === "")
throw new Error("Set OPENCODE_2022 to an isolated OpenCode 2.0.22 binary");
const temp = await mkdtemp(join(tmpdir(), "native-multi-work-"));
try {
const root = join(temp, "root"),
destination = join(temp, "child"),
bin = join(temp, "bin"),
config = join(temp, "config");
const root = join(temp, "root");
const destination = join(temp, "child");
const bin = join(temp, "bin");
const config = join(temp, "config");
const herdrCreation = join(temp, "herdr-creation.txt");
for (const path of [root, bin, config, join(temp, "plugin")]) await mkdir(path);
const signal = AbortSignal.timeout(120_000);
@@ -82,7 +83,7 @@ try {
target: "bun",
format: "esm",
});
if (!built.success) throw new Error(String(built.logs));
if (!built.success) throw new Error(built.logs.map((log) => log.message).join(","));
}
await writeFile(
join(config, "opencode.json"),
@@ -134,12 +135,18 @@ try {
},
});
const socket = createServer();
await new Promise<void>((done) => socket.listen(0, "127.0.0.1", done));
await new Promise<void>((done) => {
socket.listen(0, "127.0.0.1", done);
});
const { port } = z.object({ port: z.number() }).parse(socket.address());
await new Promise<void>((done) => socket.close(() => done()));
await new Promise<void>((done) => {
socket.close(() => {
done();
});
});
// Do not inherit user credentials, discovery URLs, Herdr identity, or config overrides.
const env = {
PATH: `${bin}:${process.env.PATH}`,
PATH: `${bin}:${process.env["PATH"]}`,
HOME: join(temp, "home"),
XDG_CONFIG_HOME: config,
XDG_DATA_HOME: join(temp, "data"),
@@ -160,13 +167,13 @@ try {
let server = start();
const headers = { authorization: "" };
const client = OpenCode.make({ baseUrl: `http://127.0.0.1:${port}`, headers });
async function ready() {
const ready = async () => {
for (let attempt = 0; attempt < 200; attempt++) {
try {
const password = (await readFile(join(temp, "server.log"), "utf8")).match(
/server password (\S+)/,
/server password (\S+)/u,
)?.[1];
if (password)
if (password !== undefined)
headers.authorization = `Basic ${Buffer.from(`opencode:${password}`).toString("base64")}`;
await client.server.info();
return;
@@ -177,7 +184,7 @@ try {
throw new Error(
`Host did not start: ${await readFile(join(temp, "server-errors.log"), "utf8")}`,
);
}
};
try {
await ready();
const location = { directory: root };
@@ -228,7 +235,7 @@ try {
(await readFile(env.FIXTURE_TRACE, "utf8"))
.trim()
.split("\n")
.map((line) => JSON.parse(line)),
.map((line) => z.json().parse(JSON.parse(line))),
);
if (
!admission.some((entry) => entry.id === child.id && entry.directory === destination) ||
@@ -305,21 +312,21 @@ try {
pending.then(() => {
throw new Error("Foreground unexpectedly finished before cancellation");
}),
new Promise<never>((_resolve, reject) =>
new Promise<never>((_resolve, reject) => {
deadline.addEventListener(
"abort",
() => reject(new Error("Model was not reached")),
{
once: true,
() => {
reject(new Error("Model was not reached"));
},
),
),
{ once: true },
);
}),
]);
cancel.abort();
let rejected = false;
await pending.catch(() => {
rejected = true;
});
const rejected = await pending.then(
() => false,
() => true,
);
if (!rejected) throw new Error("Cancelled RPC did not reject");
for (let attempt = 0; attempt < 100; attempt++) {
if ((await client.session.get({ sessionID: child.id })).outcome === "interrupted")
@@ -329,6 +336,18 @@ try {
}
releaseModel.resolve();
holdModel = false;
const multiWorkFailed = async () => {
for (let attempt = 0; attempt < 100; attempt++) {
const { data } = await client.plugin.list({ location });
const multi = data.find((plugin) => plugin.id === "stanp-opencode.multi-work");
if (multi?.state.status === "failed") return;
if (attempt === 99 || multi?.state.status === "active")
throw new Error(
`Missing or late Agents did not fail Multi-work activation: ${JSON.stringify(data)}`,
);
await Bun.sleep(100);
}
};
for (const plugins of [
[{ package: join(temp, "plugin", "multi-work") }],
[
@@ -344,16 +363,7 @@ try {
);
server = start();
await ready();
for (let attempt = 0; attempt < 100; attempt++) {
const { data } = await client.plugin.list({ location });
const multi = data.find((plugin) => plugin.id === "stanp-opencode.multi-work");
if (multi?.state.status === "failed") break;
if (attempt === 99 || multi?.state.status === "active")
throw new Error(
`Missing or late Agents did not fail Multi-work activation: ${JSON.stringify(data)}`,
);
await Bun.sleep(100);
}
await multiWorkFailed();
}
server.kill();
await server.exited;
@@ -364,7 +374,8 @@ try {
target: "bun",
format: "esm",
});
if (!quotaBuild.success) throw new Error(String(quotaBuild.logs));
if (!quotaBuild.success)
throw new Error(quotaBuild.logs.map((log) => log.message).join(","));
await writeFile(
join(config, "opencode.json"),
JSON.stringify({
@@ -409,7 +420,7 @@ try {
releaseModel.resolve();
server.kill();
await server.exited;
model.stop(true);
void model.stop(true);
}
} finally {
await rm(temp, { recursive: true, force: true });