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 +87 -0
- package/dist/src/adapters/antigravity.js +41 -0
- package/dist/src/adapters/claude.js +27 -0
- package/dist/src/adapters/copilot.js +35 -0
- package/dist/src/adapters/cursor.js +44 -0
- package/dist/src/adapters/index.js +21 -0
- package/dist/src/adapters/types.js +1 -0
- package/dist/src/cli.js +40 -0
- package/dist/src/commands/install.js +48 -0
- package/dist/src/commands/update.js +39 -0
- package/dist/src/commands/verify.js +46 -0
- package/dist/src/core/args.js +74 -0
- package/dist/src/core/errors.js +8 -0
- package/dist/src/core/load-policy.js +10 -0
- package/dist/src/core/logger.js +11 -0
- package/dist/src/core/manifest.js +29 -0
- package/dist/src/core/policy-schema.js +87 -0
- package/dist/src/core/prompt.js +30 -0
- package/dist/src/core/render.js +19 -0
- package/dist/src/core/write-files.js +21 -0
- package/dist/test/adapters.spec.js +15 -0
- package/dist/test/args.spec.js +28 -0
- package/dist/test/helpers.js +26 -0
- package/dist/test/install-verify-update.spec.js +43 -0
- package/docs/migration.md +39 -0
- package/package.json +38 -0
- package/presets/karpathy/policy.yml +30 -0
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 {};
|
package/dist/src/cli.js
ADDED
|
@@ -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,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,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.
|