Behavior-preserving cleanups: - extract shared resolveBaseUrl helper for jira/confluence base-URL logic - extract shared registerTool helper; port jira/gitlab/confluence tools to it - make scanSecrets declarative (filter/map) - collapse ok/warn pair into status() and drop double existsSync in kit-doctor Typecheck clean; npm test 71 PASS, 0 FAIL.
126 lines
5.8 KiB
TypeScript
126 lines
5.8 KiB
TypeScript
/**
|
|
* Kit CLI Extension
|
|
*
|
|
* WHAT: developer-experience commands for the corporate kit.
|
|
* /kit-doctor — environment health check (node, kit version, config, env vars,
|
|
* env.sh, loaded commands/skills). Read-only, offline-safe.
|
|
* /kit-help — catalog of everything this package provides (commands, skills,
|
|
* guardrails).
|
|
*
|
|
* CONFIGURE: nothing to configure. Values are read from config/company.json and
|
|
* the environment. (`/kit-config` — in company-context.ts — shows the
|
|
* effective corporate config.)
|
|
*
|
|
* DISABLE: remove/rename this file from the package `extensions/` folder.
|
|
*/
|
|
|
|
import * as fs from "node:fs";
|
|
import * as os from "node:os";
|
|
import * as path from "node:path";
|
|
import { fileURLToPath } from "node:url";
|
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
|
|
const HERE = path.dirname(fileURLToPath(import.meta.url));
|
|
const PKG_ROOT = path.join(HERE, "..");
|
|
const CONFIG_PATH = path.join(PKG_ROOT, "config", "company.json");
|
|
const ENV_FILE = path.join(os.homedir(), ".config", "pi-kit", "env.sh");
|
|
|
|
const PROVIDER_KEYS = ["ANTHROPIC_API_KEY", "OPENAI_API_KEY", "OPENROUTER_API_KEY", "GEMINI_API_KEY", "GOOGLE_API_KEY"];
|
|
|
|
// Guardrail extensions don't register commands, so list them statically here.
|
|
const GUARDRAILS: Array<[string, string]> = [
|
|
["protected-paths", "блок записи в секреты/ключи/.git/прод-конфиги (по пути)"],
|
|
["permission-gate", "подтверждение опасных bash (rm -rf, force push, release-*/v* деплой, sudo …)"],
|
|
["secret-scanner", "блок записи контента, похожего на секрет"],
|
|
["commit-guard", "Conventional Commits + скан секретов в сообщении коммита"],
|
|
["llm-redaction", "редакция секретов из payload перед отправкой в LLM"],
|
|
["audit-log", "локальный аудит (~/.config/pi-kit/audit.jsonl)"],
|
|
];
|
|
|
|
function readVersion(): string {
|
|
try {
|
|
return JSON.parse(fs.readFileSync(path.join(PKG_ROOT, "package.json"), "utf8")).version ?? "?";
|
|
} catch {
|
|
return "?";
|
|
}
|
|
}
|
|
|
|
function readConfig(): Record<string, unknown> {
|
|
try {
|
|
return JSON.parse(fs.readFileSync(CONFIG_PATH, "utf8"));
|
|
} catch {
|
|
return {};
|
|
}
|
|
}
|
|
|
|
/** Show lines in a dismissable selector (UI) or as a notification (no UI). */
|
|
async function show(ctx: any, title: string, lines: string[]): Promise<void> {
|
|
if (ctx.hasUI) await ctx.ui.select(title, lines);
|
|
else ctx.ui.notify(`${title}\n${lines.join("\n")}`, "info");
|
|
}
|
|
|
|
export default function kitCliExtension(pi: ExtensionAPI) {
|
|
pi.registerCommand("kit-doctor", {
|
|
description: "Проверка окружения pi-kit (node, версия, конфиг, env, токены)",
|
|
handler: async (_args, ctx) => {
|
|
const status = (ok: boolean, text: string) => `${ok ? "✓" : "⚠"} ${text}`;
|
|
const cfg = readConfig();
|
|
const lines: string[] = [];
|
|
|
|
lines.push(status(true, `Node.js ${process.version}`));
|
|
lines.push(status(true, `pi-kit v${readVersion()}${process.env.PI_KIT_CHANNEL ? ` (канал: ${process.env.PI_KIT_CHANNEL})` : ""}`));
|
|
|
|
// Config
|
|
const cfgOk = typeof cfg.gitHost === "string" && cfg.gitHost !== "TODO";
|
|
lines.push(status(cfgOk, `config: gitHost=${cfg.gitHost ?? "?"}, tracker=${cfg.trackerUrl ?? "?"}, docs=${cfg.docsUrl ?? "?"}`));
|
|
if (cfg.remoteConfigUrl) lines.push(status(true, `remoteConfigUrl задан`));
|
|
// TODO-placeholder values are a readiness signal, not an error: warn so
|
|
// maintainers notice they haven't filled the single source of truth yet.
|
|
for (const [field, value] of Object.entries({ repoMap: cfg.repoMap, trackerUrl: cfg.trackerUrl, docsUrl: cfg.docsUrl })) {
|
|
if (typeof value !== "string" || value === "TODO" || value.trim() === "" || value.startsWith("TODO")) {
|
|
lines.push(status(false, `config: ${field} не заполнен (TODO) — скиллы repo-map/docs-map/jira будут просить уточнить`));
|
|
}
|
|
}
|
|
|
|
// Env vars (presence only, never values)
|
|
for (const v of ["TRACKER_URL", "JIRA_TOKEN", "CONFLUENCE_TOKEN", "GITLAB_TOKEN"]) {
|
|
lines.push(status(Boolean(process.env[v]), `${v} ${process.env[v] ? "задан" : "не задан"}`));
|
|
}
|
|
const provider = PROVIDER_KEYS.find((k) => process.env[k]);
|
|
lines.push(status(Boolean(provider), provider ? `LLM-ключ: ${provider}` : "LLM-ключ провайдера не найден"));
|
|
|
|
// env.sh
|
|
const envShExists = fs.existsSync(ENV_FILE);
|
|
lines.push(status(envShExists, `env.sh ${envShExists ? "есть" : "нет"} (${ENV_FILE})`));
|
|
|
|
// Loaded commands/skills
|
|
const cmds = pi.getCommands();
|
|
const skills = cmds.filter((c) => c.source === "skill").length;
|
|
lines.push(status(true, `команд загружено: ${cmds.length} (скилов: ${skills})`));
|
|
|
|
const warns = lines.filter((l) => l.startsWith("⚠")).length;
|
|
lines.push("", warns === 0 ? "Итог: всё ок." : `Итог: предупреждений — ${warns}.`);
|
|
await show(ctx, "pi-kit doctor", lines);
|
|
},
|
|
});
|
|
|
|
pi.registerCommand("kit-help", {
|
|
description: "Каталог возможностей pi-kit (команды, скилы, guardrails)",
|
|
handler: async (_args, ctx) => {
|
|
const cmds = pi.getCommands();
|
|
const mine = cmds.filter((c) => c.sourceInfo?.path?.startsWith(PKG_ROOT));
|
|
const bySource = (src: string) =>
|
|
mine
|
|
.filter((c) => c.source === src)
|
|
.sort((a, b) => a.name.localeCompare(b.name))
|
|
.map((c) => ` /${c.name}${c.description ? ` — ${c.description}` : ""}`);
|
|
|
|
const lines: string[] = [];
|
|
lines.push("— Команды —", ...bySource("extension"), ...bySource("prompt"));
|
|
lines.push("", "— Скилы —", ...bySource("skill"));
|
|
lines.push("", "— Guardrails (расширения) —", ...GUARDRAILS.map(([n, d]) => ` ${n} — ${d}`));
|
|
await show(ctx, "pi-kit help", lines);
|
|
},
|
|
});
|
|
}
|