Luigit
repositories / pi-ext

pi-ext

bugabingas pi extensions

owned by admin

extensions/the-system/system-command.ts

Raw
import { lstatSync } from "node:fs";
import path from "node:path";
import type {
	ExtensionAPI,
	ExtensionCommandContext,
	RegisteredCommand,
} from "@earendil-works/pi-coding-agent";
import { boardSummaryBlock } from "./board-summary.ts";
import { runDoctor } from "./doctor.ts";
import { applyInitialization, inspectInitialization } from "./init.ts";
import { loadSystemRenderer } from "./loaders.js";
import { openHtmlFile } from "./open-browser.ts";
import { openEditableFile } from "./open-editor.ts";
import { loadProjectIndex } from "./project-index.ts";
import { systemArgumentCompletions } from "./system-command-meta.ts";
import { systemPromptFor } from "./system-prompts.ts";

const LIFECYCLE = new Set([
	"research",
	"spec",
	"spec-verify",
	"assess",
	"phase",
	"plan",
	"issue",
	"exec",
]);
const BUNDLE_SUBCOMMANDS = new Set([
	"research",
	"spec",
	"phase",
	"plan",
	"issue",
]);
const NON_LIFECYCLE = new Set([
	"init",
	"doctor",
	"view",
	"book",
	"board",
	"edit",
]);
type CommandOptions = Omit<RegisteredCommand, "name" | "sourceInfo">;
type SystemCommandHooks = Readonly<{
	beginLifecycle?: () => void;
	activateBundleTool?: () => void;
	activateBoardProposal?: (cwd: string) => void | Promise<void>;
	activateBoardUpdate?: (cwd: string) => void | Promise<void>;
	activateBundleMigration?: () => void;
	openBoard?: (cwd: string) => Promise<string>;
}>;

export function createSystemCommand(
	pi: ExtensionAPI,
	hooks: SystemCommandHooks = {},
): CommandOptions {
	return {
		description: "The System project governance",
		getArgumentCompletions: systemArgumentCompletions,
		handler: async (args: string, ctx: ExtensionCommandContext) => {
			const parsed = parseSystemArgs(args);
			if (!parsed.subcommand) {
				ctx.ui.notify(systemHelp(), "info");
				return;
			}
			if (parsed.subcommand === "init") {
				await runSystemInit(
					pi,
					hooks.activateBundleMigration,
					parsed.rest,
					ctx,
				);
				return;
			}
			if (parsed.subcommand === "view") {
				const selectors = parsed.rest.trim().split(/\s+/).filter(Boolean);
				if (selectors.length !== 1) {
					ctx.ui.notify("Usage: /system view <path|id|slug>", "error");
					return;
				}
				const output = await renderSystemView(ctx.cwd, selectors[0] as string);
				ctx.ui.notify(await openNotice("System view", output), "info");
				return;
			}
			if (parsed.subcommand === "edit") {
				const selectors = parsed.rest.trim().split(/\s+/).filter(Boolean);
				if (selectors.length !== 1) {
					ctx.ui.notify("Usage: /system edit <path|id|slug>", "error");
					return;
				}
				const output = await editSystemDocument(
					ctx.cwd,
					selectors[0] as string,
				);
				ctx.ui.notify(output, output.includes("opened:") ? "info" : "error");
				return;
			}
			if (parsed.subcommand === "book") {
				if (parsed.rest.trim()) {
					ctx.ui.notify("Usage: /system book", "error");
					return;
				}
				const output = await renderSystemBook(ctx.cwd);
				ctx.ui.notify(await openNotice("System book", output), "info");
				return;
			}
			if (parsed.subcommand === "board") {
				if (parsed.rest.trim()) {
					ctx.ui.notify("Usage: /system board", "error");
					return;
				}
				if (!hooks.openBoard)
					throw new Error("System board runtime is unavailable");
				ctx.ui.notify(await hooks.openBoard(ctx.cwd), "info");
				return;
			}
			if (parsed.subcommand === "doctor") {
				const result = runDoctor(ctx.cwd);
				ctx.ui.notify(
					formatDoctor(result),
					result.exitCode === 0 ? "info" : "warning",
				);
				return;
			}
			hooks.beginLifecycle?.();
			if (BUNDLE_SUBCOMMANDS.has(parsed.subcommand))
				hooks.activateBundleTool?.();
			if (parsed.subcommand === "assess")
				await hooks.activateBoardProposal?.(ctx.cwd);
			if (parsed.subcommand === "exec")
				await hooks.activateBoardUpdate?.(ctx.cwd);
			pi.sendUserMessage(
				systemPromptFor(
					parsed.subcommand,
					parsed.rest,
					boardSummaryBlock(ctx.cwd),
				),
			);
			if (ctx.mode === "print" || ctx.mode === "json") {
				await new Promise((resolve) => setTimeout(resolve, 0));
				await ctx.waitForIdle();
			}
		},
	};
}

function parseSystemArgs(args: string): { subcommand?: string; rest: string } {
	const trimmed = args.trim();
	if (!trimmed) return { rest: "" };
	const [raw = "", ...rest] = trimmed.split(/\s+/);
	const subcommand = raw.toLowerCase();
	if (!LIFECYCLE.has(subcommand) && !NON_LIFECYCLE.has(subcommand))
		return { rest: "" };
	return { subcommand, rest: rest.join(" ") };
}

function systemHelp(): string {
	return [
		"/system <subcommand>",
		"init [PROJECT-PREFIX]",
		"doctor",
		"view <path|id|slug>",
		"edit <path|id|slug>",
		"book",
		"board",
		"research [instructions]",
		"spec [instructions]",
		"spec-verify [instructions]",
		"assess [instructions]",
		"phase [instructions]",
		"plan [instructions]",
		"issue [instructions]",
		"exec [instructions]",
	].join("\n");
}

async function runSystemInit(
	pi: ExtensionAPI,
	activateBundleMigration: (() => void) | undefined,
	args: string,
	ctx: ExtensionCommandContext,
): Promise<void> {
	let requested = args.trim() || undefined;
	let inspection = inspectInitialization(ctx.cwd, requested);
	requested ??= inspection.prefix;
	if (!requested) {
		requested = await ctx.ui.input("Project prefix", "SYS");
		if (!requested) {
			ctx.ui.notify("/system init cancelled", "info");
			return;
		}
		inspection = inspectInitialization(ctx.cwd, requested);
	}
	const configMigration = inspection.rewrites.find((item) =>
		item.startsWith(".system/config.json "),
	);
	const summary = [
		`Root: ${inspection.root}`,
		`Prefix: ${inspection.prefix}`,
		...inspection.creates.map((item) => `Create: ${item}`),
		...inspection.cores
			.filter((item) => item.state === "missing")
			.map((item) => `Create: ${item.path}`),
		...inspection.rewrites.map((item) => `Update: ${item}`),
	].join("\n");
	if (!(await ctx.ui.confirm("Initialize The System?", summary))) {
		ctx.ui.notify("/system init cancelled", "info");
		return;
	}
	const reloadRequired =
		inspection.creates.length > 0 ||
		inspection.rewrites.length > 0 ||
		inspection.cores.some((core) => core.state === "missing");
	const result = applyInitialization(ctx.cwd, requested, true);
	ctx.ui.notify(
		[
			`Initialized The System at ${result.root}`,
			`Prefix: ${result.prefix}`,
			...result.changed.map((item) => `Wrote: ${item}`),
			...(reloadRequired
				? ["Run /reload to activate System format skills."]
				: []),
		].join("\n"),
		"info",
	);
	if (
		!configMigration ||
		!(await ctx.ui.confirm(
			"Should System bundles be migrated by agent?",
			`Machine schema updated mechanically.\n${configMigration}`,
		))
	)
		return;
	await ctx.waitForIdle();
	activateBundleMigration?.();
	pi.sendUserMessage(
		systemPromptFor(
			"init",
			[
				"The human approved agent-led bundle migration.",
				"Machine config and board schema markers are already current; do not edit them.",
				`Source: ${configMigration}`,
				"Inspect existing bundles and migrate obsolete structure and content to the current contracts.",
			].join("\n"),
		),
	);
}

function formatDoctor(result: ReturnType<typeof runDoctor>): string {
	if (result.exitCode === 0) return `System doctor OK\nRoot: ${result.root}`;
	return [
		`System doctor found ${result.diagnostics.length} issue(s)`,
		`Root: ${result.root}`,
		...result.references.map((item) => `Ref: ${item.path} -> ${item.spec}`),
		...result.diagnostics.map(
			(item) => `${item.code} ${item.path}: ${item.message}`,
		),
	].join("\n");
}

async function renderSystemView(
	cwd: string,
	selector: string,
): Promise<string> {
	const index = loadProjectIndex(cwd);
	const document = resolveDocument(index.root, index.documents, selector);
	const { renderSystemViewSite } = await loadSystemRenderer();
	return renderSystemViewSite(index, document).file;
}

async function renderSystemBook(cwd: string): Promise<string> {
	const { renderSystemBookSite } = await loadSystemRenderer();
	return renderSystemBookSite(loadProjectIndex(cwd)).file;
}

async function editSystemDocument(
	cwd: string,
	selector: string,
): Promise<string> {
	const index = loadProjectIndex(cwd);
	const file = editableSystemPath(index.root, index.documents, selector);
	const opened = await openEditableFile(file);
	if (opened.ok)
		return `System document opened: ${file}\nEditor: ${opened.command}`;
	return `System document not opened: ${file}\nEditor: ${opened.error}`;
}

function editableSystemPath(
	root: string,
	documents: ReturnType<typeof loadProjectIndex>["documents"],
	selector: string,
): string {
	const normalized = selector.split(path.sep).join("/");
	if (normalized.includes("/") || normalized.endsWith(".md")) {
		const target = path.resolve(root, selector);
		const relative = path.relative(root, target).split(path.sep).join("/");
		if (
			relative.startsWith("../") ||
			relative === ".." ||
			!relative.startsWith(".system/")
		)
			throw new Error(`System edit path must be inside .system: ${selector}`);
		const stats = lstatSync(target, { throwIfNoEntry: false });
		if (!stats?.isFile() || stats.isSymbolicLink())
			throw new Error(`No editable System file: ${selector}`);
		return target;
	}
	return path.join(root, resolveDocument(root, documents, selector).path);
}

function resolveDocument(
	root: string,
	documents: ReturnType<typeof loadProjectIndex>["documents"],
	selector: string,
) {
	const normalized = selector.split(path.sep).join("/");
	if (normalized.includes("/") || normalized.endsWith(".md")) {
		const relative = path
			.relative(root, path.resolve(root, selector))
			.split(path.sep)
			.join("/");
		const found = documents.find((document) => document.path === relative);
		if (!found) throw new Error(`No System document path: ${selector}`);
		return found;
	}
	const byId = documents.find((document) => document.metadata.id === selector);
	if (byId) return byId;
	const bySlug = documents.filter((document) => {
		const bundle = document.path.split("/")[2] ?? "";
		return bundle.endsWith(`-${selector}`);
	});
	if (bySlug.length === 1 && bySlug[0]) return bySlug[0];
	if (bySlug.length > 1) throw new Error(`Ambiguous System slug: ${selector}`);
	throw new Error(`No System document: ${selector}`);
}

async function openNotice(label: string, output: string): Promise<string> {
	const opened = await openHtmlFile(output);
	if (opened.ok)
		return `${label} opened: ${opened.url}\nFile: ${output}\nBrowser: ${opened.command}`;
	return `${label} written: ${output}\nOpen: ${opened.url}\nBrowser: ${opened.error}`;
}