ai-policy-pack-cli 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,87 @@
1
+ # AI Policy Pack CLI
2
+
3
+ `ai-policy-pack-cli` is a multi-IDE policy generator that renders instruction files for:
4
+
5
+ - Cursor (`.cursor/rules/*.mdc`)
6
+ - GitHub Copilot (`.github/copilot-instructions.md`, `.github/instructions/*.instructions.md`)
7
+ - Antigravity (`AGENTS.md`, optional `GEMINI.md`)
8
+ - Claude-compatible tools (`CLAUDE.md`)
9
+
10
+ The CLI uses one canonical policy preset and maps it into each target format.
11
+
12
+ ## Quick Start
13
+
14
+ ```bash
15
+ npx ai-policy-pack-cli install
16
+ ```
17
+
18
+ Non-interactive mode:
19
+
20
+ ```bash
21
+ npx ai-policy-pack-cli install --targets cursor,copilot,antigravity,claude --yes
22
+ ```
23
+
24
+ ## Commands
25
+
26
+ ### Install
27
+
28
+ ```bash
29
+ npx ai-policy-pack-cli install [--targets cursor,copilot,antigravity,claude] [--preset karpathy] [--yes] [--dry-run]
30
+ ```
31
+
32
+ - Generates target files from preset
33
+ - Writes `.ai-policy/manifest.json` for tracking
34
+
35
+ ### Verify
36
+
37
+ ```bash
38
+ npx ai-policy-pack-cli verify
39
+ ```
40
+
41
+ - Re-renders expected output from manifest
42
+ - Fails if generated files drifted
43
+ - Suitable for CI checks
44
+
45
+ ### Update
46
+
47
+ ```bash
48
+ npx ai-policy-pack-cli update [--preset karpathy] [--yes] [--dry-run]
49
+ ```
50
+
51
+ - Re-applies current targets from manifest
52
+ - Refreshes files and manifest hashes
53
+
54
+ ## Presets
55
+
56
+ Current default preset:
57
+
58
+ - `presets/karpathy/policy.yml`
59
+
60
+ Preset layout:
61
+
62
+ - `globalRules`: shared guardrails
63
+ - `fileRules`: per-pattern rules
64
+ - `targetOverrides`: optional target-specific instructions
65
+
66
+ ## CI Integration
67
+
68
+ Example GitHub Actions step:
69
+
70
+ ```yaml
71
+ - name: Verify AI policy files
72
+ run: npx ai-policy-pack-cli verify
73
+ ```
74
+
75
+ ## Development
76
+
77
+ ```bash
78
+ npm install
79
+ npm run build
80
+ npm test
81
+ ```
82
+
83
+ ## Release Guidance
84
+
85
+ - Use semantic versioning for policy and renderer changes
86
+ - Treat generated-file shape changes as minor or major depending on compatibility impact
87
+ - Keep changelog entries grouped by target (`cursor`, `copilot`, `antigravity`, `claude`)
@@ -0,0 +1,41 @@
1
+ export class AntigravityAdapter {
2
+ render(context) {
3
+ const { policy } = context;
4
+ const files = [];
5
+ const override = policy.targetOverrides?.antigravity;
6
+ const agentsContent = [
7
+ "# AGENTS",
8
+ "",
9
+ "Shared rules for all compatible AI coding tools.",
10
+ "",
11
+ "## Core Rules",
12
+ ...policy.globalRules.map((rule) => `- ${rule}`),
13
+ "",
14
+ "## Language-Specific Rules",
15
+ ...policy.fileRules.flatMap((rule) => [
16
+ `### ${rule.name}`,
17
+ `Applies to: ${rule.patterns.join(", ")}`,
18
+ ...rule.rules.map((item) => `- ${item}`),
19
+ ""
20
+ ])
21
+ ].join("\n");
22
+ files.push({
23
+ path: "AGENTS.md",
24
+ content: `${agentsContent}\n`
25
+ });
26
+ if (override?.geminiRules && override.geminiRules.length > 0) {
27
+ const geminiContent = [
28
+ "# GEMINI",
29
+ "",
30
+ "Antigravity-specific overrides.",
31
+ "",
32
+ ...override.geminiRules.map((rule) => `- ${rule}`)
33
+ ].join("\n");
34
+ files.push({
35
+ path: "GEMINI.md",
36
+ content: `${geminiContent}\n`
37
+ });
38
+ }
39
+ return files;
40
+ }
41
+ }
@@ -0,0 +1,27 @@
1
+ export class ClaudeAdapter {
2
+ render(context) {
3
+ const { policy } = context;
4
+ const content = [
5
+ "# CLAUDE",
6
+ "",
7
+ "Project instructions for Claude-compatible agents.",
8
+ "",
9
+ "## Core Rules",
10
+ ...policy.globalRules.map((rule) => `- ${rule}`),
11
+ "",
12
+ "## File-Specific Rules",
13
+ ...policy.fileRules.flatMap((rule) => [
14
+ `### ${rule.name}`,
15
+ `Applies to: ${rule.patterns.join(", ")}`,
16
+ ...rule.rules.map((item) => `- ${item}`),
17
+ ""
18
+ ])
19
+ ].join("\n");
20
+ return [
21
+ {
22
+ path: "CLAUDE.md",
23
+ content: `${content}\n`
24
+ }
25
+ ];
26
+ }
27
+ }
@@ -0,0 +1,35 @@
1
+ export class CopilotAdapter {
2
+ render(context) {
3
+ const files = [];
4
+ const { policy } = context;
5
+ const override = policy.targetOverrides?.copilot;
6
+ const repoWide = [
7
+ "# Copilot Instructions",
8
+ "",
9
+ ...(override?.repoIntro ?? []),
10
+ ...(override?.repoIntro?.length ? [""] : []),
11
+ "## Core Rules",
12
+ ...policy.globalRules.map((rule) => `- ${rule}`)
13
+ ].join("\n");
14
+ files.push({
15
+ path: ".github/copilot-instructions.md",
16
+ content: `${repoWide}\n`
17
+ });
18
+ for (const fileRule of policy.fileRules) {
19
+ const scoped = [
20
+ "---",
21
+ `applyTo: "${fileRule.patterns.join(",")}"`,
22
+ "---",
23
+ "",
24
+ `# ${fileRule.name}`,
25
+ "",
26
+ ...fileRule.rules.map((rule) => `- ${rule}`)
27
+ ].join("\n");
28
+ files.push({
29
+ path: `.github/instructions/${fileRule.name}.instructions.md`,
30
+ content: `${scoped}\n`
31
+ });
32
+ }
33
+ return files;
34
+ }
35
+ }
@@ -0,0 +1,44 @@
1
+ export class CursorAdapter {
2
+ render(context) {
3
+ const files = [];
4
+ const { policy } = context;
5
+ const globalContent = [
6
+ "---",
7
+ "description: Global team rules generated by ai-policy-pack",
8
+ "alwaysApply: true",
9
+ "---",
10
+ "",
11
+ "# Team Global Rules",
12
+ "",
13
+ ...policy.globalRules.map((rule) => `- ${rule}`)
14
+ ].join("\n");
15
+ files.push({
16
+ path: ".cursor/rules/global-policy.mdc",
17
+ content: `${globalContent}\n`
18
+ });
19
+ for (const fileRule of policy.fileRules) {
20
+ const content = [
21
+ "---",
22
+ `description: ${fileRule.name} generated by ai-policy-pack`,
23
+ `globs: ${fileRule.patterns.join(",")}`,
24
+ "alwaysApply: false",
25
+ "---",
26
+ "",
27
+ `# ${toTitleCase(fileRule.name)}`,
28
+ "",
29
+ ...fileRule.rules.map((rule) => `- ${rule}`)
30
+ ].join("\n");
31
+ files.push({
32
+ path: `.cursor/rules/${fileRule.name}.mdc`,
33
+ content: `${content}\n`
34
+ });
35
+ }
36
+ return files;
37
+ }
38
+ }
39
+ function toTitleCase(value) {
40
+ return value
41
+ .split("-")
42
+ .map((part) => part.charAt(0).toUpperCase() + part.slice(1))
43
+ .join(" ");
44
+ }
@@ -0,0 +1,21 @@
1
+ import { AntigravityAdapter } from "./antigravity.js";
2
+ import { ClaudeAdapter } from "./claude.js";
3
+ import { CopilotAdapter } from "./copilot.js";
4
+ import { CursorAdapter } from "./cursor.js";
5
+ export function getAdapter(target) {
6
+ switch (target) {
7
+ case "cursor":
8
+ return new CursorAdapter();
9
+ case "copilot":
10
+ return new CopilotAdapter();
11
+ case "antigravity":
12
+ return new AntigravityAdapter();
13
+ case "claude":
14
+ return new ClaudeAdapter();
15
+ default:
16
+ return assertNever(target);
17
+ }
18
+ }
19
+ function assertNever(value) {
20
+ throw new Error(`Unhandled target: ${String(value)}`);
21
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,40 @@
1
+ #!/usr/bin/env node
2
+ import { promises as fs } from "node:fs";
3
+ import path from "node:path";
4
+ import { parseArgs } from "./core/args.js";
5
+ import { logger } from "./core/logger.js";
6
+ import { CliError } from "./core/errors.js";
7
+ import { runInstallCommand } from "./commands/install.js";
8
+ import { runVerifyCommand } from "./commands/verify.js";
9
+ import { runUpdateCommand } from "./commands/update.js";
10
+ async function main() {
11
+ const cwd = process.cwd();
12
+ const packageVersion = await readPackageVersion(cwd);
13
+ const args = parseArgs(process.argv.slice(2));
14
+ switch (args.command) {
15
+ case "install":
16
+ await runInstallCommand(args, { cwd, packageVersion, logger });
17
+ return;
18
+ case "verify":
19
+ await runVerifyCommand(args, { cwd, logger });
20
+ return;
21
+ case "update":
22
+ await runUpdateCommand(args, { cwd, packageVersion, logger });
23
+ return;
24
+ default:
25
+ throw new CliError(`Unsupported command: ${String(args.command)}`);
26
+ }
27
+ }
28
+ async function readPackageVersion(cwd) {
29
+ const packagePath = path.join(cwd, "package.json");
30
+ const parsed = JSON.parse(await fs.readFile(packagePath, "utf8"));
31
+ return parsed.version ?? "0.0.0";
32
+ }
33
+ main().catch((error) => {
34
+ if (error instanceof CliError) {
35
+ logger.error(error.message);
36
+ process.exit(error.exitCode);
37
+ }
38
+ logger.error(error.message);
39
+ process.exit(1);
40
+ });
@@ -0,0 +1,48 @@
1
+ import { readFileIfExists, writeGeneratedFiles } from "../core/write-files.js";
2
+ import { confirmAction, selectTargetsInteractive } from "../core/prompt.js";
3
+ import { hashContent, writeManifest } from "../core/manifest.js";
4
+ import { loadPolicyPreset } from "../core/load-policy.js";
5
+ import { renderForTargets } from "../core/render.js";
6
+ import { CliError } from "../core/errors.js";
7
+ export async function runInstallCommand(args, deps) {
8
+ const targets = await resolveTargets(args);
9
+ const presetName = args.preset ?? "karpathy";
10
+ const policy = await loadPolicyPreset(deps.cwd, presetName);
11
+ const files = renderForTargets(policy, targets);
12
+ deps.logger.info(`Preset: ${policy.name}`);
13
+ deps.logger.info(`Targets: ${targets.join(", ")}`);
14
+ for (const file of files) {
15
+ const current = await readFileIfExists(deps.cwd, file.path);
16
+ const status = current === undefined ? "create" : current === file.content ? "unchanged" : "update";
17
+ deps.logger.info(`[${status}] ${file.path}`);
18
+ }
19
+ if (args.dryRun) {
20
+ deps.logger.info("Dry run complete. No files written.");
21
+ return;
22
+ }
23
+ if (!args.yes) {
24
+ const ok = await confirmAction("Apply generated files?");
25
+ if (!ok) {
26
+ throw new CliError("Aborted by user", 1);
27
+ }
28
+ }
29
+ await writeGeneratedFiles(deps.cwd, files);
30
+ await writeManifest(deps.cwd, {
31
+ schemaVersion: 1,
32
+ packageVersion: deps.packageVersion,
33
+ preset: presetName,
34
+ targets,
35
+ files: files.map((file) => ({ path: file.path, hash: hashContent(file.content) })),
36
+ generatedAt: new Date().toISOString()
37
+ });
38
+ deps.logger.info("Install complete.");
39
+ }
40
+ async function resolveTargets(args) {
41
+ if (args.targets && args.targets.length > 0) {
42
+ return args.targets;
43
+ }
44
+ if (args.yes) {
45
+ throw new CliError("Missing --targets in non-interactive mode. Example: --targets cursor,copilot");
46
+ }
47
+ return selectTargetsInteractive();
48
+ }
@@ -0,0 +1,39 @@
1
+ import { readManifest, writeManifest, hashContent } from "../core/manifest.js";
2
+ import { loadPolicyPreset } from "../core/load-policy.js";
3
+ import { renderForTargets } from "../core/render.js";
4
+ import { writeGeneratedFiles } from "../core/write-files.js";
5
+ import { confirmAction } from "../core/prompt.js";
6
+ import { CliError } from "../core/errors.js";
7
+ export async function runUpdateCommand(args, deps) {
8
+ const manifest = await readManifest(deps.cwd);
9
+ if (!manifest) {
10
+ throw new CliError("Manifest not found. Run install first.");
11
+ }
12
+ const presetName = args.preset || manifest.preset;
13
+ const preset = await loadPolicyPreset(deps.cwd, presetName);
14
+ const files = renderForTargets(preset, manifest.targets);
15
+ deps.logger.info(`Updating preset: ${preset.name}`);
16
+ for (const file of files) {
17
+ deps.logger.info(`[write] ${file.path}`);
18
+ }
19
+ if (args.dryRun) {
20
+ deps.logger.info("Dry run complete. No files written.");
21
+ return;
22
+ }
23
+ if (!args.yes) {
24
+ const ok = await confirmAction("Apply update?");
25
+ if (!ok) {
26
+ throw new CliError("Aborted by user", 1);
27
+ }
28
+ }
29
+ await writeGeneratedFiles(deps.cwd, files);
30
+ await writeManifest(deps.cwd, {
31
+ schemaVersion: 1,
32
+ packageVersion: deps.packageVersion,
33
+ preset: presetName,
34
+ targets: manifest.targets,
35
+ files: files.map((file) => ({ path: file.path, hash: hashContent(file.content) })),
36
+ generatedAt: new Date().toISOString()
37
+ });
38
+ deps.logger.info("Update complete.");
39
+ }
@@ -0,0 +1,46 @@
1
+ import path from "node:path";
2
+ import { promises as fs } from "node:fs";
3
+ import { readManifest, hashContent } from "../core/manifest.js";
4
+ import { loadPolicyPreset } from "../core/load-policy.js";
5
+ import { renderForTargets } from "../core/render.js";
6
+ import { CliError } from "../core/errors.js";
7
+ export async function runVerifyCommand(args, deps) {
8
+ const manifest = await readManifest(deps.cwd);
9
+ if (!manifest) {
10
+ throw new CliError("Manifest not found. Run install first.");
11
+ }
12
+ const preset = await loadPolicyPreset(deps.cwd, manifest.preset);
13
+ const rendered = renderForTargets(preset, manifest.targets);
14
+ const renderedMap = new Map(rendered.map((file) => [file.path, file.content]));
15
+ const mismatches = [];
16
+ for (const entry of manifest.files) {
17
+ const expectedContent = renderedMap.get(entry.path);
18
+ if (!expectedContent) {
19
+ mismatches.push(`${entry.path} (missing from rendered output)`);
20
+ continue;
21
+ }
22
+ const filePath = path.join(deps.cwd, entry.path);
23
+ let actual = "";
24
+ try {
25
+ actual = await fs.readFile(filePath, "utf8");
26
+ }
27
+ catch (error) {
28
+ if (error.code === "ENOENT") {
29
+ mismatches.push(`${entry.path} (file missing)`);
30
+ continue;
31
+ }
32
+ throw error;
33
+ }
34
+ if (hashContent(actual) !== hashContent(expectedContent) || hashContent(expectedContent) !== entry.hash) {
35
+ mismatches.push(`${entry.path} (content drift)`);
36
+ }
37
+ }
38
+ if (mismatches.length > 0) {
39
+ for (const item of mismatches) {
40
+ deps.logger.error(item);
41
+ }
42
+ throw new CliError(`Verify failed with ${mismatches.length} mismatch(es)`, 2);
43
+ }
44
+ deps.logger.info("Verify passed.");
45
+ void args;
46
+ }
@@ -0,0 +1,74 @@
1
+ import { CliError } from "./errors.js";
2
+ const VALID_TARGETS = ["cursor", "copilot", "antigravity", "claude"];
3
+ export function parseArgs(argv) {
4
+ const [commandRaw, ...flags] = argv;
5
+ if (!commandRaw) {
6
+ throw new CliError("Missing command. Use: install | verify | update");
7
+ }
8
+ if (!isCommand(commandRaw)) {
9
+ throw new CliError(`Unknown command: ${commandRaw}`);
10
+ }
11
+ let preset;
12
+ let targets;
13
+ let yes = false;
14
+ let dryRun = false;
15
+ let force = false;
16
+ for (let index = 0; index < flags.length; index += 1) {
17
+ const flag = flags[index];
18
+ if (flag === "--yes") {
19
+ yes = true;
20
+ continue;
21
+ }
22
+ if (flag === "--dry-run") {
23
+ dryRun = true;
24
+ continue;
25
+ }
26
+ if (flag === "--force") {
27
+ force = true;
28
+ continue;
29
+ }
30
+ if (flag === "--preset") {
31
+ const next = flags[index + 1];
32
+ if (!next) {
33
+ throw new CliError("--preset requires a value");
34
+ }
35
+ preset = next;
36
+ index += 1;
37
+ continue;
38
+ }
39
+ if (flag === "--targets") {
40
+ const next = flags[index + 1];
41
+ if (!next) {
42
+ throw new CliError("--targets requires a comma-separated value");
43
+ }
44
+ targets = parseTargets(next);
45
+ index += 1;
46
+ continue;
47
+ }
48
+ throw new CliError(`Unknown flag: ${flag}`);
49
+ }
50
+ return {
51
+ command: commandRaw,
52
+ preset,
53
+ targets,
54
+ yes,
55
+ dryRun,
56
+ force
57
+ };
58
+ }
59
+ function parseTargets(value) {
60
+ const raw = value.split(",").map((item) => item.trim()).filter(Boolean);
61
+ if (raw.length === 0) {
62
+ throw new CliError("--targets must not be empty");
63
+ }
64
+ const deduped = [...new Set(raw)];
65
+ for (const target of deduped) {
66
+ if (!VALID_TARGETS.includes(target)) {
67
+ throw new CliError(`Unsupported target: ${target}`);
68
+ }
69
+ }
70
+ return deduped;
71
+ }
72
+ function isCommand(value) {
73
+ return value === "install" || value === "verify" || value === "update";
74
+ }
@@ -0,0 +1,8 @@
1
+ export class CliError extends Error {
2
+ exitCode;
3
+ constructor(message, exitCode = 1) {
4
+ super(message);
5
+ this.name = "CliError";
6
+ this.exitCode = exitCode;
7
+ }
8
+ }
@@ -0,0 +1,10 @@
1
+ import path from "node:path";
2
+ import { promises as fs } from "node:fs";
3
+ import yaml from "js-yaml";
4
+ import { validatePolicyPreset } from "./policy-schema.js";
5
+ export async function loadPolicyPreset(cwd, presetName) {
6
+ const presetPath = path.join(cwd, "presets", presetName, "policy.yml");
7
+ const raw = await fs.readFile(presetPath, "utf8");
8
+ const parsed = yaml.load(raw);
9
+ return validatePolicyPreset(parsed);
10
+ }
@@ -0,0 +1,11 @@
1
+ export const logger = {
2
+ info(message) {
3
+ process.stdout.write(`${message}\n`);
4
+ },
5
+ warn(message) {
6
+ process.stderr.write(`WARN: ${message}\n`);
7
+ },
8
+ error(message) {
9
+ process.stderr.write(`ERROR: ${message}\n`);
10
+ }
11
+ };
@@ -0,0 +1,29 @@
1
+ import { createHash } from "node:crypto";
2
+ import path from "node:path";
3
+ import { promises as fs } from "node:fs";
4
+ export const MANIFEST_PATH = ".ai-policy/manifest.json";
5
+ export function hashContent(content) {
6
+ return createHash("sha256").update(content).digest("hex");
7
+ }
8
+ export async function readManifest(cwd) {
9
+ const manifestPath = path.join(cwd, MANIFEST_PATH);
10
+ try {
11
+ const raw = await fs.readFile(manifestPath, "utf8");
12
+ const parsed = JSON.parse(raw);
13
+ if (parsed.schemaVersion !== 1) {
14
+ throw new Error(`Unsupported manifest schemaVersion: ${String(parsed.schemaVersion)}`);
15
+ }
16
+ return parsed;
17
+ }
18
+ catch (error) {
19
+ if (error.code === "ENOENT") {
20
+ return undefined;
21
+ }
22
+ throw error;
23
+ }
24
+ }
25
+ export async function writeManifest(cwd, manifest) {
26
+ const manifestPath = path.join(cwd, MANIFEST_PATH);
27
+ await fs.mkdir(path.dirname(manifestPath), { recursive: true });
28
+ await fs.writeFile(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`, "utf8");
29
+ }
@@ -0,0 +1,87 @@
1
+ export function validatePolicyPreset(input) {
2
+ if (!isObject(input)) {
3
+ throw new Error("Policy preset must be an object");
4
+ }
5
+ const name = expectString(input.name, "name");
6
+ const description = optionalString(input.description, "description");
7
+ const globalRules = expectStringArray(input.globalRules, "globalRules");
8
+ const fileRules = expectFileRules(input.fileRules);
9
+ const targetOverrides = expectTargetOverrides(input.targetOverrides);
10
+ return {
11
+ name,
12
+ description,
13
+ globalRules,
14
+ fileRules,
15
+ targetOverrides
16
+ };
17
+ }
18
+ function expectFileRules(value) {
19
+ if (!Array.isArray(value)) {
20
+ throw new Error("fileRules must be an array");
21
+ }
22
+ return value.map((item, index) => {
23
+ if (!isObject(item)) {
24
+ throw new Error(`fileRules[${index}] must be an object`);
25
+ }
26
+ return {
27
+ name: expectString(item.name, `fileRules[${index}].name`),
28
+ patterns: expectStringArray(item.patterns, `fileRules[${index}].patterns`),
29
+ rules: expectStringArray(item.rules, `fileRules[${index}].rules`)
30
+ };
31
+ });
32
+ }
33
+ function expectTargetOverrides(value) {
34
+ if (value === undefined) {
35
+ return undefined;
36
+ }
37
+ if (!isObject(value)) {
38
+ throw new Error("targetOverrides must be an object");
39
+ }
40
+ const targets = ["cursor", "copilot", "antigravity", "claude"];
41
+ const result = {};
42
+ for (const target of targets) {
43
+ const rawOverride = value[target];
44
+ if (rawOverride === undefined) {
45
+ continue;
46
+ }
47
+ if (!isObject(rawOverride)) {
48
+ throw new Error(`targetOverrides.${target} must be an object`);
49
+ }
50
+ result[target] = {
51
+ repoIntro: optionalStringArray(rawOverride.repoIntro, `targetOverrides.${target}.repoIntro`),
52
+ geminiRules: optionalStringArray(rawOverride.geminiRules, `targetOverrides.${target}.geminiRules`)
53
+ };
54
+ }
55
+ return result;
56
+ }
57
+ function expectString(value, key) {
58
+ if (typeof value !== "string" || !value.trim()) {
59
+ throw new Error(`${key} must be a non-empty string`);
60
+ }
61
+ return value.trim();
62
+ }
63
+ function optionalString(value, key) {
64
+ if (value === undefined) {
65
+ return undefined;
66
+ }
67
+ return expectString(value, key);
68
+ }
69
+ function expectStringArray(value, key) {
70
+ if (!Array.isArray(value)) {
71
+ throw new Error(`${key} must be an array`);
72
+ }
73
+ const normalized = value.map((item, index) => expectString(item, `${key}[${index}]`));
74
+ if (normalized.length === 0) {
75
+ throw new Error(`${key} must not be empty`);
76
+ }
77
+ return normalized;
78
+ }
79
+ function optionalStringArray(value, key) {
80
+ if (value === undefined) {
81
+ return undefined;
82
+ }
83
+ return expectStringArray(value, key);
84
+ }
85
+ function isObject(value) {
86
+ return typeof value === "object" && value !== null;
87
+ }
@@ -0,0 +1,30 @@
1
+ import { CliError } from "./errors.js";
2
+ const ALL_TARGETS = ["cursor", "copilot", "antigravity", "claude"];
3
+ export async function selectTargetsInteractive() {
4
+ process.stdout.write("Select targets (comma-separated numbers):\n1) cursor\n2) copilot\n3) antigravity\n4) claude\n> ");
5
+ const input = await readLine();
6
+ const tokens = input.split(",").map((part) => part.trim()).filter(Boolean);
7
+ if (tokens.length === 0) {
8
+ throw new CliError("No targets selected");
9
+ }
10
+ const indices = [...new Set(tokens.map((token) => Number(token)))];
11
+ if (indices.some((value) => Number.isNaN(value) || value < 1 || value > ALL_TARGETS.length)) {
12
+ throw new CliError("Invalid selection. Expected numbers 1-4");
13
+ }
14
+ return indices.map((index) => ALL_TARGETS[index - 1]);
15
+ }
16
+ export async function confirmAction(message) {
17
+ process.stdout.write(`${message} [y/N]: `);
18
+ const input = (await readLine()).trim().toLowerCase();
19
+ return input === "y" || input === "yes";
20
+ }
21
+ function readLine() {
22
+ return new Promise((resolve) => {
23
+ process.stdin.resume();
24
+ process.stdin.setEncoding("utf8");
25
+ process.stdin.once("data", (data) => {
26
+ process.stdin.pause();
27
+ resolve(data.toString().trim());
28
+ });
29
+ });
30
+ }
@@ -0,0 +1,19 @@
1
+ import { getAdapter } from "../adapters/index.js";
2
+ export function renderForTargets(policy, targets) {
3
+ const files = [];
4
+ for (const target of targets) {
5
+ const adapter = getAdapter(target);
6
+ files.push(...adapter.render({ policy }));
7
+ }
8
+ return dedupeGeneratedFiles(files);
9
+ }
10
+ function dedupeGeneratedFiles(files) {
11
+ const map = new Map();
12
+ for (const file of files) {
13
+ map.set(file.path, file.content);
14
+ }
15
+ return [...map.entries()].map(([filePath, content]) => ({
16
+ path: filePath,
17
+ content
18
+ }));
19
+ }
@@ -0,0 +1,21 @@
1
+ import path from "node:path";
2
+ import { promises as fs } from "node:fs";
3
+ export async function writeGeneratedFiles(cwd, files) {
4
+ for (const file of files) {
5
+ const targetPath = path.join(cwd, file.path);
6
+ await fs.mkdir(path.dirname(targetPath), { recursive: true });
7
+ await fs.writeFile(targetPath, file.content, "utf8");
8
+ }
9
+ }
10
+ export async function readFileIfExists(cwd, relativePath) {
11
+ const targetPath = path.join(cwd, relativePath);
12
+ try {
13
+ return await fs.readFile(targetPath, "utf8");
14
+ }
15
+ catch (error) {
16
+ if (error.code === "ENOENT") {
17
+ return undefined;
18
+ }
19
+ throw error;
20
+ }
21
+ }
@@ -0,0 +1,15 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { loadPolicyPreset } from "../src/core/load-policy.js";
3
+ import { renderForTargets } from "../src/core/render.js";
4
+ const CWD = "d:/ATS/Project/AI/rulecusor";
5
+ describe("adapters render", () => {
6
+ it("renders target-specific files", async () => {
7
+ const policy = await loadPolicyPreset(CWD, "karpathy");
8
+ const files = renderForTargets(policy, ["cursor", "copilot", "antigravity", "claude"]);
9
+ const paths = files.map((item) => item.path).sort();
10
+ expect(paths).toContain(".cursor/rules/global-policy.mdc");
11
+ expect(paths).toContain(".github/copilot-instructions.md");
12
+ expect(paths).toContain("AGENTS.md");
13
+ expect(paths).toContain("CLAUDE.md");
14
+ });
15
+ });
@@ -0,0 +1,28 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { parseArgs } from "../src/core/args.js";
3
+ import { CliError } from "../src/core/errors.js";
4
+ describe("parseArgs", () => {
5
+ it("parses install args with all flags", () => {
6
+ const parsed = parseArgs([
7
+ "install",
8
+ "--targets",
9
+ "cursor,copilot",
10
+ "--preset",
11
+ "karpathy",
12
+ "--yes",
13
+ "--dry-run",
14
+ "--force"
15
+ ]);
16
+ expect(parsed).toEqual({
17
+ command: "install",
18
+ targets: ["cursor", "copilot"],
19
+ preset: "karpathy",
20
+ yes: true,
21
+ dryRun: true,
22
+ force: true
23
+ });
24
+ });
25
+ it("throws on unsupported target", () => {
26
+ expect(() => parseArgs(["install", "--targets", "foo"])).toThrow(CliError);
27
+ });
28
+ });
@@ -0,0 +1,26 @@
1
+ import path from "node:path";
2
+ import os from "node:os";
3
+ import { promises as fs } from "node:fs";
4
+ export async function createTempWorkspace(prefix) {
5
+ const dir = await fs.mkdtemp(path.join(os.tmpdir(), `${prefix}-`));
6
+ return dir;
7
+ }
8
+ export async function copyFixtureSourceToWorkspace(sourceRoot, workspace) {
9
+ await copyDir(path.join(sourceRoot, "src"), path.join(workspace, "src"));
10
+ await copyDir(path.join(sourceRoot, "presets"), path.join(workspace, "presets"));
11
+ await fs.writeFile(path.join(workspace, "package.json"), JSON.stringify({ name: "fixture", version: "0.1.0" }, null, 2), "utf8");
12
+ }
13
+ async function copyDir(src, dest) {
14
+ await fs.mkdir(dest, { recursive: true });
15
+ const entries = await fs.readdir(src, { withFileTypes: true });
16
+ for (const entry of entries) {
17
+ const srcPath = path.join(src, entry.name);
18
+ const destPath = path.join(dest, entry.name);
19
+ if (entry.isDirectory()) {
20
+ await copyDir(srcPath, destPath);
21
+ }
22
+ else {
23
+ await fs.copyFile(srcPath, destPath);
24
+ }
25
+ }
26
+ }
@@ -0,0 +1,43 @@
1
+ import path from "node:path";
2
+ import { promises as fs } from "node:fs";
3
+ import { describe, expect, it } from "vitest";
4
+ import { parseArgs } from "../src/core/args.js";
5
+ import { runInstallCommand } from "../src/commands/install.js";
6
+ import { runVerifyCommand } from "../src/commands/verify.js";
7
+ import { runUpdateCommand } from "../src/commands/update.js";
8
+ import { createTempWorkspace, copyFixtureSourceToWorkspace } from "./helpers.js";
9
+ const silentLogger = {
10
+ info: (_message) => undefined,
11
+ warn: (_message) => undefined,
12
+ error: (_message) => undefined
13
+ };
14
+ describe("install -> verify -> drift -> update flow", () => {
15
+ it("handles end-to-end lifecycle", async () => {
16
+ const root = "d:/ATS/Project/AI/rulecusor";
17
+ const workspace = await createTempWorkspace("ai-policy-pack");
18
+ await copyFixtureSourceToWorkspace(root, workspace);
19
+ await runInstallCommand(parseArgs(["install", "--targets", "cursor,copilot,antigravity,claude", "--yes"]), {
20
+ cwd: workspace,
21
+ packageVersion: "0.1.0",
22
+ logger: silentLogger
23
+ });
24
+ await runVerifyCommand(parseArgs(["verify"]), {
25
+ cwd: workspace,
26
+ logger: silentLogger
27
+ });
28
+ await fs.appendFile(path.join(workspace, "AGENTS.md"), "\nmanual drift\n", "utf8");
29
+ await expect(runVerifyCommand(parseArgs(["verify"]), {
30
+ cwd: workspace,
31
+ logger: silentLogger
32
+ })).rejects.toThrow();
33
+ await runUpdateCommand(parseArgs(["update", "--yes"]), {
34
+ cwd: workspace,
35
+ packageVersion: "0.1.1",
36
+ logger: silentLogger
37
+ });
38
+ await runVerifyCommand(parseArgs(["verify"]), {
39
+ cwd: workspace,
40
+ logger: silentLogger
41
+ });
42
+ });
43
+ });
@@ -0,0 +1,39 @@
1
+ # Migration and Rollout
2
+
3
+ This guide helps teams adopt `ai-policy-pack-cli` across multiple repositories.
4
+
5
+ ## Initial Rollout
6
+
7
+ 1. Install and generate files:
8
+ ```bash
9
+ npx ai-policy-pack-cli install --targets cursor,copilot,antigravity,claude --yes
10
+ ```
11
+ 2. Commit generated files and `.ai-policy/manifest.json`.
12
+ 3. Add `npx ai-policy-pack-cli verify` to CI.
13
+
14
+ ## Updating Existing Repositories
15
+
16
+ 1. Upgrade package version in your toolchain.
17
+ 2. Run:
18
+ ```bash
19
+ npx ai-policy-pack-cli update --yes
20
+ ```
21
+ 3. Review generated diff.
22
+ 4. Run:
23
+ ```bash
24
+ npx ai-policy-pack-cli verify
25
+ ```
26
+
27
+ ## Drift Policy
28
+
29
+ If users edit generated files manually:
30
+
31
+ - CI `verify` will fail with mismatch details.
32
+ - Run `update` to restore generated state.
33
+
34
+ ## Target Mapping Reference
35
+
36
+ - Cursor: `.cursor/rules/*.mdc`
37
+ - Copilot: `.github/copilot-instructions.md`, `.github/instructions/*.instructions.md`
38
+ - Antigravity: `AGENTS.md`, `GEMINI.md` (if configured by override)
39
+ - Claude: `CLAUDE.md`
package/package.json ADDED
@@ -0,0 +1,38 @@
1
+ {
2
+ "name": "ai-policy-pack-cli",
3
+ "version": "0.1.0",
4
+ "description": "Multi-IDE AI policy pack generator for Cursor, Copilot, Antigravity, and Claude",
5
+ "type": "module",
6
+ "bin": {
7
+ "ai-policy-pack": "dist/src/cli.js"
8
+ },
9
+ "scripts": {
10
+ "build": "tsc -p tsconfig.json",
11
+ "test": "vitest run",
12
+ "test:watch": "vitest",
13
+ "lint": "tsc -p tsconfig.json --noEmit"
14
+ },
15
+ "files": [
16
+ "dist",
17
+ "presets",
18
+ "README.md",
19
+ "docs"
20
+ ],
21
+ "keywords": [
22
+ "cursor",
23
+ "copilot",
24
+ "antigravity",
25
+ "claude",
26
+ "ai-rules"
27
+ ],
28
+ "license": "MIT",
29
+ "dependencies": {
30
+ "js-yaml": "^4.1.0"
31
+ },
32
+ "devDependencies": {
33
+ "@types/js-yaml": "^4.0.9",
34
+ "@types/node": "^22.15.18",
35
+ "typescript": "^5.8.3",
36
+ "vitest": "^2.1.9"
37
+ }
38
+ }
@@ -0,0 +1,30 @@
1
+ name: karpathy
2
+ description: Karpathy-inspired guardrails adapted for multi-IDE agents
3
+ globalRules:
4
+ - State assumptions and ask clarifying questions before coding when requirements are ambiguous.
5
+ - Prefer the simplest implementation that solves the user's request; avoid speculative abstractions.
6
+ - Keep changes surgical and scoped to the requested task; avoid unrelated refactors.
7
+ - Define explicit success criteria and verify changes with tests or commands before claiming completion.
8
+ - Remove dead code created by your own change but do not clean unrelated legacy code unless asked.
9
+ fileRules:
10
+ - name: typescript-standards
11
+ patterns:
12
+ - "**/*.ts"
13
+ - "**/*.tsx"
14
+ rules:
15
+ - Favor strict typing and avoid any unless there is a documented reason.
16
+ - Keep functions focused and small; split logic when a function mixes responsibilities.
17
+ - Add concise comments only when code intent is not obvious.
18
+ - name: python-standards
19
+ patterns:
20
+ - "**/*.py"
21
+ rules:
22
+ - Prefer explicit names over abbreviations and keep functions single-purpose.
23
+ - Handle expected exceptions explicitly and include actionable error messages.
24
+ targetOverrides:
25
+ antigravity:
26
+ geminiRules:
27
+ - Use GEMINI.md for tool-specific overrides and AGENTS.md for shared team policy.
28
+ copilot:
29
+ repoIntro:
30
+ - Follow repository build, test, and lint commands from project docs before submitting changes.