Skip to content

Commit eba8c5c

Browse files
committed
refactor catalogs
1 parent d4ae282 commit eba8c5c

14 files changed

Lines changed: 388 additions & 112 deletions

File tree

.github/workflows/ci.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ on:
88
paths-ignore:
99
- '**.md'
1010
- 'docs/**'
11+
- 'examples/**'
1112
- 'LICENSE'
1213
- '.gitignore'
1314
pull_request:
@@ -16,6 +17,7 @@ on:
1617
paths-ignore:
1718
- '**.md'
1819
- 'docs/**'
20+
- 'examples/**'
1921
- 'LICENSE'
2022
- '.gitignore'
2123

src/catalog.ts

Lines changed: 0 additions & 84 deletions
This file was deleted.

src/catalogs/base.ts

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
import { z } from "zod";
2+
3+
// ---------------------------------------------------------------------------
4+
// Catalog Module Interface
5+
// ---------------------------------------------------------------------------
6+
export interface CatalogModule {
7+
name: string;
8+
commands: readonly string[] | ["any"];
9+
detectors: readonly string[];
10+
typeEnum: readonly string[];
11+
buildPrompt(): string;
12+
}
13+
14+
// ---------------------------------------------------------------------------
15+
// Dynamic Zod Schemas
16+
// ---------------------------------------------------------------------------
17+
export function createStepSchema(allTypes: readonly string[]) {
18+
return z.object({
19+
id: z.number(),
20+
type: z.enum(allTypes as [string, ...string[]]),
21+
command: z.string(),
22+
args: z.array(z.string()).default([]),
23+
description: z.string(),
24+
cwd: z.string().nullable().optional(),
25+
});
26+
}
27+
28+
export function createPlanSchema(
29+
stepSchema: ReturnType<typeof createStepSchema>,
30+
) {
31+
return z.object({
32+
goal: z.string(),
33+
steps: z.array(stepSchema).min(1).max(10),
34+
});
35+
}
36+
37+
// ---------------------------------------------------------------------------
38+
// Types inferred from schemas (will be set dynamically)
39+
// ---------------------------------------------------------------------------
40+
export type Step = {
41+
id: number;
42+
type: string;
43+
command: string;
44+
args: string[];
45+
description: string;
46+
cwd?: string | null;
47+
};
48+
49+
export type Plan = {
50+
goal: string;
51+
steps: Step[];
52+
};
53+
54+
// ---------------------------------------------------------------------------
55+
// Validation
56+
// ---------------------------------------------------------------------------
57+
export function validateStep(
58+
step: Step,
59+
catalogMap: Map<string, readonly string[] | ["any"]>,
60+
): { valid: boolean; reason?: string } {
61+
const allowed = catalogMap.get(step.type);
62+
63+
if (!allowed) {
64+
return {
65+
valid: false,
66+
reason: `Unknown type "${step.type}"`,
67+
};
68+
}
69+
70+
if (allowed[0] === "any") {
71+
return { valid: true };
72+
}
73+
74+
if (!(allowed as readonly string[]).includes(step.command)) {
75+
return {
76+
valid: false,
77+
reason: `"${step.command}" is not an allowed command for type "${step.type}". Allowed: ${allowed.join(", ")}`,
78+
};
79+
}
80+
81+
return { valid: true };
82+
}

src/catalogs/docker.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import type { CatalogModule } from "./base.js";
2+
3+
export const dockerCatalog: CatalogModule = {
4+
name: "docker",
5+
commands: [
6+
"build",
7+
"run",
8+
"compose",
9+
"push",
10+
"pull",
11+
"exec",
12+
"logs",
13+
"ps",
14+
"stop",
15+
"start",
16+
"rm",
17+
"rmi",
18+
],
19+
detectors: ["Dockerfile", "docker-compose.yml", "docker-compose.yaml"],
20+
typeEnum: ["docker"],
21+
buildPrompt() {
22+
return ` - docker: [${this.commands.join(", ")}]`;
23+
},
24+
};

src/catalogs/fs.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import type { CatalogModule } from "./base.js";
2+
3+
export const fsCatalog: CatalogModule = {
4+
name: "fs",
5+
commands: ["mkdir", "rm", "cp", "mv", "touch", "cat", "ls"],
6+
detectors: [],
7+
typeEnum: ["fs"],
8+
buildPrompt() {
9+
return ` - fs: [${this.commands.join(", ")}]`;
10+
},
11+
};

src/catalogs/git.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import type { CatalogModule } from "./base.js";
2+
3+
export const gitCatalog: CatalogModule = {
4+
name: "git",
5+
commands: [
6+
"init",
7+
"add",
8+
"commit",
9+
"push",
10+
"pull",
11+
"clone",
12+
"status",
13+
"log",
14+
"branch",
15+
"checkout",
16+
"merge",
17+
"stash",
18+
],
19+
detectors: [".git"],
20+
typeEnum: ["git"],
21+
buildPrompt() {
22+
return ` - git: [${this.commands.join(", ")}]`;
23+
},
24+
};

src/catalogs/index.ts

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
import { existsSync } from "fs";
2+
import { resolve } from "path";
3+
import type { CatalogModule } from "./base.js";
4+
import { packageCatalog } from "./package.js";
5+
import { gitCatalog } from "./git.js";
6+
import { dockerCatalog } from "./docker.js";
7+
import { fsCatalog } from "./fs.js";
8+
import { shellCatalog } from "./shell.js";
9+
10+
// ---------------------------------------------------------------------------
11+
// All available catalogs
12+
// ---------------------------------------------------------------------------
13+
const ALL_CATALOGS: CatalogModule[] = [
14+
packageCatalog,
15+
gitCatalog,
16+
dockerCatalog,
17+
fsCatalog,
18+
shellCatalog,
19+
];
20+
21+
const CATALOG_MAP = new Map<string, CatalogModule>(
22+
ALL_CATALOGS.map((c) => [c.name, c]),
23+
);
24+
25+
// ---------------------------------------------------------------------------
26+
// Auto-detect active catalogs based on project structure
27+
// ---------------------------------------------------------------------------
28+
export function detectCatalogs(
29+
cwd: string = process.cwd(),
30+
forcedCatalogs?: string[],
31+
): CatalogModule[] {
32+
// If forced catalogs specified, validate and use only those
33+
if (forcedCatalogs && forcedCatalogs.length > 0) {
34+
const active: CatalogModule[] = [];
35+
for (const name of forcedCatalogs) {
36+
const catalog = CATALOG_MAP.get(name);
37+
if (!catalog) {
38+
const valid = Array.from(CATALOG_MAP.keys()).join(", ");
39+
throw new Error(`Unknown catalog "${name}". Valid: ${valid}`);
40+
}
41+
active.push(catalog);
42+
}
43+
return active;
44+
}
45+
46+
// Auto-detect mode
47+
const active: CatalogModule[] = [];
48+
49+
for (const catalog of ALL_CATALOGS) {
50+
// Always include catalogs with no detectors (fs, shell)
51+
if (catalog.detectors.length === 0) {
52+
active.push(catalog);
53+
continue;
54+
}
55+
56+
// Check if any detector exists
57+
const detected = catalog.detectors.some((detector) => {
58+
const path = resolve(cwd, detector);
59+
return existsSync(path);
60+
});
61+
62+
if (detected) {
63+
active.push(catalog);
64+
}
65+
}
66+
67+
return active;
68+
}
69+
70+
// ---------------------------------------------------------------------------
71+
// Build prompt from active catalogs
72+
// ---------------------------------------------------------------------------
73+
export function buildCatalogPrompt(
74+
cwd: string = process.cwd(),
75+
forcedCatalogs?: string[],
76+
): string {
77+
const active = detectCatalogs(cwd, forcedCatalogs);
78+
const lines = active.map((catalog) => catalog.buildPrompt());
79+
return `Allowed command types and commands:\n${lines.join("\n")}`;
80+
}
81+
82+
// ---------------------------------------------------------------------------
83+
// Get all type enum values from active catalogs
84+
// ---------------------------------------------------------------------------
85+
export function getAllTypeEnums(
86+
cwd: string = process.cwd(),
87+
forcedCatalogs?: string[],
88+
): string[] {
89+
const active = detectCatalogs(cwd, forcedCatalogs);
90+
const types: string[] = [];
91+
for (const catalog of active) {
92+
types.push(...catalog.typeEnum);
93+
}
94+
return [...new Set(types)]; // dedupe
95+
}
96+
97+
// ---------------------------------------------------------------------------
98+
// Build command map for validation
99+
// ---------------------------------------------------------------------------
100+
export function buildCommandMap(
101+
cwd: string = process.cwd(),
102+
forcedCatalogs?: string[],
103+
): Map<string, readonly string[] | ["any"]> {
104+
const active = detectCatalogs(cwd, forcedCatalogs);
105+
const map = new Map<string, readonly string[] | ["any"]>();
106+
107+
for (const catalog of active) {
108+
for (const type of catalog.typeEnum) {
109+
map.set(type, catalog.commands);
110+
}
111+
}
112+
113+
return map;
114+
}
115+
116+
// ---------------------------------------------------------------------------
117+
// Re-export base types and functions
118+
// ---------------------------------------------------------------------------
119+
export {
120+
createStepSchema,
121+
createPlanSchema,
122+
validateStep,
123+
type CatalogModule,
124+
type Step,
125+
type Plan,
126+
} from "./base.js";
127+
128+
// Export individual catalogs for advanced use
129+
export { packageCatalog, gitCatalog, dockerCatalog, fsCatalog, shellCatalog };

src/catalogs/package.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import type { CatalogModule } from "./base.js";
2+
3+
export const packageCatalog: CatalogModule = {
4+
name: "package",
5+
commands: ["install", "run", "build", "test", "publish", "ci", "add", "remove"],
6+
detectors: ["package.json"],
7+
typeEnum: ["npm", "pnpm", "yarn", "bun"],
8+
buildPrompt() {
9+
return ` - npm: [${this.commands.join(", ")}]
10+
- pnpm: [${this.commands.join(", ")}]
11+
- yarn: [${this.commands.join(", ")}]
12+
- bun: [${this.commands.join(", ")}]`;
13+
},
14+
};

0 commit comments

Comments
 (0)