+118
-105
@@ -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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user