import { readFileSync, writeFileSync } from "node:fs"; import { Command } from "commander"; import chalk from "chalk"; import { ensureAuth } from "../lib/config.js"; import { apiRequest } from "../lib/api-client.js"; import { POLICY_MODES, orgPolicyDocSchema, type OrgPolicyDoc, } from "@skills-hub-ai/shared"; interface ApplyResult { dryRun: boolean; modeChanged: boolean; governanceChanged: boolean; added: string[]; removed: string[]; } interface PolicyEntry { id: string; skill: { slug: string; name: string }; // An entry is org-level when both are null; a dept/team-scoped entry is // ALWAYS a block for that scope regardless of the org's policy mode. department: { slug: string; name: string } | null; team: { slug: string; name: string; department: { slug: string; name: string }; } | null; createdAt: string; } function scopeLabel(e: PolicyEntry): string { if (e.team) return `blocked for team:${e.team.department.slug}/${e.team.slug}`; if (e.department) return `blocked for dept:${e.department.slug}`; return ""; } interface OrgDetail { slug: string; policyMode: "OPEN" | "ALLOWLIST" | "BLOCKLIST"; } const getCommand = new Command("get") .description("Show the org's install policy mode and entries") .argument("", "organization slug") .option("--json", "output raw JSON") .action(async (org: string, opts: { json?: boolean }) => { ensureAuth(); try { const [detail, entries] = await Promise.all([ apiRequest(`/api/v1/orgs/${encodeURIComponent(org)}`), apiRequest( `/api/v1/orgs/${encodeURIComponent(org)}/policy`, ), ]); if (opts.json) { console.log( JSON.stringify({ policyMode: detail.policyMode, entries }, null, 2), ); return; } const modeColor = detail.policyMode === "OPEN" ? chalk.green : detail.policyMode === "ALLOWLIST" ? chalk.yellow : chalk.red; console.log( `\nInstall policy for ${chalk.bold(org)}: ${modeColor(detail.policyMode)}`, ); const orgLevel = entries.filter((e) => !e.department && !e.team); const scoped = entries.filter((e) => e.department || e.team); if (detail.policyMode === "OPEN") { console.log( chalk.gray(" Members can install any skill from the catalog."), ); } else { const verb = detail.policyMode === "ALLOWLIST" ? "allowed" : "blocked"; console.log( chalk.bold(`\n${orgLevel.length} org-level ${verb} skill(s):`), ); for (const e of orgLevel) { console.log(` ${e.skill.slug} ${chalk.gray(`(${e.skill.name})`)}`); } } // Scoped blocks apply in every mode (including OPEN) — always show them. if (scoped.length > 0) { console.log(chalk.bold(`\n${scoped.length} department/team block(s):`)); for (const e of scoped) { console.log( ` ${e.skill.slug} ${chalk.gray(`(${e.skill.name})`)} ${chalk.yellow(scopeLabel(e))}`, ); } } } catch (err) { console.error( chalk.red(err instanceof Error ? err.message : "Failed to get policy"), ); process.exit(1); } }); const setModeCommand = new Command("set-mode") .description("Set the install policy mode (admin)") .argument("", "organization slug") .argument("", `one of: ${POLICY_MODES.join(", ")}`) .action(async (org: string, mode: string) => { ensureAuth(); const upper = mode.toUpperCase(); if (!(POLICY_MODES as readonly string[]).includes(upper)) { console.error( chalk.red(`Invalid mode "${mode}" — use ${POLICY_MODES.join(", ")}`), ); process.exit(1); } try { await apiRequest(`/api/v1/orgs/${encodeURIComponent(org)}/policy`, { method: "PATCH", body: JSON.stringify({ mode: upper }), }); console.log(chalk.green(`Policy mode set to ${upper}.`)); } catch (err) { console.error( chalk.red(err instanceof Error ? err.message : "Failed to set mode"), ); process.exit(1); } }); /** --team requires --department (teams are scoped to a department). */ function validateScope(opts: { department?: string; team?: string }): void { if (opts.team && !opts.department) { console.error( chalk.red("--team requires --department (teams belong to a department)"), ); process.exit(1); } } const SCOPE_HELP = "Without --department/--team the entry is org-level. A dept/team-scoped entry is always a block for that scope, in any policy mode."; const addCommand = new Command("add") .description(`Add a skill to the policy (admin). ${SCOPE_HELP}`) .argument("", "organization slug") .argument("", "skill slug to add") .option("--department ", "scope the entry to a department (a block)") .option("--team ", "scope the entry to a team (requires --department)") .action( async ( org: string, skillSlug: string, opts: { department?: string; team?: string }, ) => { ensureAuth(); validateScope(opts); try { await apiRequest(`/api/v1/orgs/${encodeURIComponent(org)}/policy`, { method: "POST", body: JSON.stringify({ skillSlug, ...(opts.department ? { departmentSlug: opts.department } : {}), ...(opts.team ? { teamSlug: opts.team } : {}), }), }); const where = opts.team ? ` (blocked for team ${opts.department}/${opts.team})` : opts.department ? ` (blocked for dept ${opts.department})` : ""; console.log( chalk.green(`Added ${skillSlug} to the policy list.${where}`), ); } catch (err) { console.error( chalk.red(err instanceof Error ? err.message : "Failed to add entry"), ); process.exit(1); } }, ); const removeCommand = new Command("remove") .alias("rm") .description(`Remove a skill from the policy (admin). ${SCOPE_HELP}`) .argument("", "organization slug") .argument("", "skill slug to remove") .option("--department ", "target a department-scoped entry") .option("--team ", "target a team-scoped entry (requires --department)") .action( async ( org: string, skillSlug: string, opts: { department?: string; team?: string }, ) => { ensureAuth(); validateScope(opts); try { const params = new URLSearchParams(); if (opts.department) params.set("department", opts.department); if (opts.team) params.set("team", opts.team); const qs = params.toString(); await apiRequest( `/api/v1/orgs/${encodeURIComponent(org)}/policy/${encodeURIComponent(skillSlug)}${qs ? `?${qs}` : ""}`, { method: "DELETE" }, ); console.log(chalk.green(`Removed ${skillSlug} from the policy list.`)); } catch (err) { console.error( chalk.red( err instanceof Error ? err.message : "Failed to remove entry", ), ); process.exit(1); } }, ); const exportCommand = new Command("export") .description( "Export the org's full install policy as a versioned JSON document (admin)", ) .argument("", "organization slug") .option("--out ", "write to a file instead of stdout") .action(async (org: string, opts: { out?: string }) => { ensureAuth(); try { const doc = await apiRequest( `/api/v1/orgs/${encodeURIComponent(org)}/policy/export`, ); const json = JSON.stringify(doc, null, 2) + "\n"; if (opts.out) { writeFileSync(opts.out, json); console.log(chalk.green(`Wrote policy to ${opts.out}`)); } else { process.stdout.write(json); } } catch (err) { console.error( chalk.red( err instanceof Error ? err.message : "Failed to export policy", ), ); process.exit(1); } }); const applyCommand = new Command("apply") .description( "Apply a policy JSON document to the org, reconciling mode + entries (admin). Use --dry-run for CI checks.", ) .argument("", "organization slug") .argument("", "path to a policy JSON document (from `policy export`)") .option( "--dry-run", "show the diff without applying (exits 1 if drift exists)", ) .action(async (org: string, file: string, opts: { dryRun?: boolean }) => { ensureAuth(); let doc: OrgPolicyDoc; try { const parsed = orgPolicyDocSchema.safeParse( JSON.parse(readFileSync(file, "utf-8")), ); if (!parsed.success) { console.error( chalk.red( `Invalid policy document: ${parsed.error.issues[0].message}`, ), ); process.exit(1); } doc = parsed.data; } catch (err) { console.error( chalk.red( err instanceof Error ? `Could not read ${file}: ${err.message}` : "Bad file", ), ); process.exit(1); return; } try { const qs = opts.dryRun ? "?dryRun=1" : ""; const result = await apiRequest( `/api/v1/orgs/${encodeURIComponent(org)}/policy/apply${qs}`, { method: "POST", body: JSON.stringify(doc) }, ); const changes = result.added.length + result.removed.length + (result.modeChanged ? 1 : 0) + (result.governanceChanged ? 1 : 0); if (result.modeChanged) console.log(chalk.yellow("~ policy mode")); if (result.governanceChanged) console.log(chalk.yellow("~ governance settings")); for (const a of result.added) console.log(chalk.green(`+ ${a}`)); for (const r of result.removed) console.log(chalk.red(`- ${r}`)); if (changes === 0) { console.log(chalk.gray("Policy already matches — no changes.")); } else if (opts.dryRun) { console.log( chalk.yellow(`\n${changes} change(s) would be applied (dry run).`), ); process.exit(1); // non-zero so CI fails on drift } else { console.log(chalk.green(`\nApplied ${changes} change(s).`)); } } catch (err) { console.error( chalk.red( err instanceof Error ? err.message : "Failed to apply policy", ), ); process.exit(1); } }); export const orgPolicyCommand = new Command("policy") .description("Manage the org install policy (OPEN / ALLOWLIST / BLOCKLIST)") .addCommand(getCommand) .addCommand(setModeCommand) .addCommand(addCommand) .addCommand(removeCommand) .addCommand(exportCommand) .addCommand(applyCommand);