@funkai/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.
Files changed (55) hide show
  1. package/.turbo/turbo-build.log +16 -0
  2. package/.turbo/turbo-test$colon$coverage.log +36 -0
  3. package/.turbo/turbo-test.log +26 -0
  4. package/.turbo/turbo-typecheck.log +4 -0
  5. package/CHANGELOG.md +12 -0
  6. package/LICENSE +21 -0
  7. package/README.md +85 -0
  8. package/bin/funkai.mjs +2 -0
  9. package/coverage/lcov-report/base.css +224 -0
  10. package/coverage/lcov-report/block-navigation.js +87 -0
  11. package/coverage/lcov-report/commands/create.ts.html +208 -0
  12. package/coverage/lcov-report/commands/generate.ts.html +388 -0
  13. package/coverage/lcov-report/commands/index.html +161 -0
  14. package/coverage/lcov-report/commands/lint.ts.html +331 -0
  15. package/coverage/lcov-report/commands/setup.ts.html +493 -0
  16. package/coverage/lcov-report/favicon.png +0 -0
  17. package/coverage/lcov-report/index.html +131 -0
  18. package/coverage/lcov-report/lib/codegen.ts.html +805 -0
  19. package/coverage/lcov-report/lib/extract-variables.ts.html +181 -0
  20. package/coverage/lcov-report/lib/flatten.ts.html +385 -0
  21. package/coverage/lcov-report/lib/frontmatter.ts.html +487 -0
  22. package/coverage/lcov-report/lib/index.html +191 -0
  23. package/coverage/lcov-report/lib/lint.ts.html +307 -0
  24. package/coverage/lcov-report/lib/paths.ts.html +487 -0
  25. package/coverage/lcov-report/prettify.css +1 -0
  26. package/coverage/lcov-report/prettify.js +2 -0
  27. package/coverage/lcov-report/sort-arrow-sprite.png +0 -0
  28. package/coverage/lcov-report/sorter.js +210 -0
  29. package/coverage/lcov.info +749 -0
  30. package/dist/index.mjs +19297 -0
  31. package/dist/index.mjs.map +1 -0
  32. package/kidd.config.ts +7 -0
  33. package/package.json +54 -0
  34. package/src/commands/agents/validate.ts +9 -0
  35. package/src/commands/generate.ts +78 -0
  36. package/src/commands/prompts/create.ts +41 -0
  37. package/src/commands/prompts/generate.ts +71 -0
  38. package/src/commands/prompts/lint.ts +57 -0
  39. package/src/commands/prompts/setup.ts +149 -0
  40. package/src/commands/setup.ts +12 -0
  41. package/src/commands/validate.ts +47 -0
  42. package/src/index.ts +7 -0
  43. package/src/lib/prompts/__tests__/extract-variables.test.ts +47 -0
  44. package/src/lib/prompts/__tests__/flatten.test.ts +230 -0
  45. package/src/lib/prompts/__tests__/frontmatter.test.ts +89 -0
  46. package/src/lib/prompts/__tests__/lint.test.ts +80 -0
  47. package/src/lib/prompts/codegen.ts +240 -0
  48. package/src/lib/prompts/extract-variables.ts +32 -0
  49. package/src/lib/prompts/flatten.ts +91 -0
  50. package/src/lib/prompts/frontmatter.ts +143 -0
  51. package/src/lib/prompts/lint.ts +74 -0
  52. package/src/lib/prompts/paths.ts +118 -0
  53. package/src/lib/prompts/pipeline.ts +114 -0
  54. package/tsconfig.json +25 -0
  55. package/vitest.config.ts +21 -0
@@ -0,0 +1,91 @@
1
+ import { Liquid } from "liquidjs";
2
+ import { match } from "ts-pattern";
3
+
4
+ // oxlint-disable-next-line security/detect-unsafe-regex -- template parsing, not adversarial input
5
+ const RENDER_TAG_RE = /\{%-?\s*render\s+'([^']+)'(?:\s*,\s*(.*?))?\s*-?%\}/g;
6
+ const LITERAL_PARAM_RE = /(\w+)\s*:\s*'([^']*)'/g;
7
+
8
+ interface RenderTag {
9
+ fullMatch: string;
10
+ partialName: string;
11
+ params: Record<string, string>;
12
+ }
13
+
14
+ /**
15
+ * Parse literal string parameters from a render tag's param string.
16
+ *
17
+ * Only supports literal string values (e.g. `role: 'Bot'`).
18
+ * Throws if a parameter value is a variable reference.
19
+ */
20
+ function parseParams(raw: string, partialName: string): Record<string, string> {
21
+ const literalMatches = [...raw.matchAll(LITERAL_PARAM_RE)];
22
+ const allParamNames = [...raw.matchAll(/(\w+)\s*:/g)].map((m) => m[1]);
23
+
24
+ return Object.fromEntries(
25
+ allParamNames.map((name) => {
26
+ const literal = literalMatches.find((m) => m[1] === name);
27
+ if (!literal) {
28
+ throw new Error(
29
+ `Cannot flatten {% render '${partialName}' %}: parameter "${name}" uses a variable reference. ` +
30
+ "Only literal string values are supported at codegen time.",
31
+ );
32
+ }
33
+ return [name, literal[2]];
34
+ }),
35
+ );
36
+ }
37
+
38
+ /**
39
+ * Find all `{% render %}` tags in a template string.
40
+ */
41
+ function parseRenderTags(template: string): RenderTag[] {
42
+ return [...template.matchAll(RENDER_TAG_RE)].map((m) => {
43
+ const rawParams = match(m[2] != null)
44
+ .with(true, () => m[2].trim())
45
+ .otherwise(() => "");
46
+ const params = match(rawParams.length > 0)
47
+ .with(true, () => parseParams(rawParams, m[1]))
48
+ .otherwise(() => ({}));
49
+
50
+ return { fullMatch: m[0], partialName: m[1], params };
51
+ });
52
+ }
53
+
54
+ /**
55
+ * Flatten `{% render %}` partial tags in a template at codegen time.
56
+ *
57
+ * Finds all `{% render 'name', key: 'value' %}` tags, reads the
58
+ * corresponding partial file, renders it with the literal parameters,
59
+ * and replaces the tag with the rendered output.
60
+ *
61
+ * All other Liquid expressions (`{{ var }}`, `{% if %}`, `{% for %}`)
62
+ * are preserved for runtime rendering.
63
+ *
64
+ * @param template - Template string (frontmatter already stripped).
65
+ * @param partialsDirs - Directories to search for partial `.prompt` files.
66
+ * @returns Flattened template with all render tags resolved.
67
+ */
68
+ export function flattenPartials(template: string, partialsDirs: string[]): string {
69
+ const tags = parseRenderTags(template);
70
+ if (tags.length === 0) {
71
+ return template;
72
+ }
73
+
74
+ const engine = new Liquid({
75
+ root: partialsDirs,
76
+ partials: partialsDirs,
77
+ extname: ".prompt",
78
+ });
79
+
80
+ const result = tags.reduce((acc, tag) => {
81
+ const rendered = engine.parseAndRenderSync(
82
+ `{% render '${tag.partialName}' ${Object.entries(tag.params)
83
+ .map(([k, v]) => `${k}: '${v}'`)
84
+ .join(", ")} %}`,
85
+ );
86
+
87
+ return acc.replace(tag.fullMatch, rendered);
88
+ }, template);
89
+
90
+ return result;
91
+ }
@@ -0,0 +1,143 @@
1
+ import { match } from "ts-pattern";
2
+ import { parse as parseYaml } from "yaml";
3
+
4
+ const FRONTMATTER_RE = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?/;
5
+ const NAME_RE = /^[a-z0-9-]+$/;
6
+
7
+ /**
8
+ * Parse raw YAML content into a record, wrapping parse errors
9
+ * with file path context.
10
+ *
11
+ * @param yaml - Raw YAML string to parse.
12
+ * @param filePath - File path for error messages.
13
+ * @returns The parsed YAML as a record.
14
+ *
15
+ * @private
16
+ */
17
+ function parseYamlContent(yaml: string, filePath: string): Record<string, unknown> {
18
+ try {
19
+ return parseYaml(yaml) as Record<string, unknown>;
20
+ } catch (err) {
21
+ throw new Error(`Failed to parse YAML frontmatter in ${filePath}: ${err}`, { cause: err });
22
+ }
23
+ }
24
+
25
+ /**
26
+ * A variable declared in the frontmatter `schema` block.
27
+ */
28
+ export interface SchemaVariable {
29
+ name: string;
30
+ type: string;
31
+ required: boolean;
32
+ description?: string;
33
+ }
34
+
35
+ /**
36
+ * Parsed frontmatter from a `.prompt` file.
37
+ */
38
+ export interface ParsedFrontmatter {
39
+ name: string;
40
+ group?: string;
41
+ version?: string;
42
+ schema: SchemaVariable[];
43
+ }
44
+
45
+ /**
46
+ * Parse YAML frontmatter from a `.prompt` file's raw content.
47
+ *
48
+ * Extracts `name`, `group`, `version`, and `schema` fields.
49
+ * The `schema` field maps variable names to their type definitions.
50
+ *
51
+ * @param content - Raw file content (including frontmatter fences).
52
+ * @param filePath - File path for error messages.
53
+ * @returns Parsed frontmatter with schema variables.
54
+ * @throws If frontmatter is missing, malformed, or has an invalid name.
55
+ */
56
+ export function parseFrontmatter(content: string, filePath: string): ParsedFrontmatter {
57
+ const fmMatch = content.match(FRONTMATTER_RE);
58
+ if (!fmMatch) {
59
+ throw new Error(`No frontmatter found in ${filePath}`);
60
+ }
61
+
62
+ const parsed = parseYamlContent(fmMatch[1], filePath);
63
+
64
+ if (!parsed || typeof parsed !== "object") {
65
+ throw new Error(`Frontmatter is not a valid object in ${filePath}`);
66
+ }
67
+
68
+ const name = parsed.name;
69
+ if (typeof name !== "string" || name.length === 0) {
70
+ throw new Error(`Missing or empty "name" in frontmatter: ${filePath}`);
71
+ }
72
+
73
+ if (!NAME_RE.test(name)) {
74
+ throw new Error(
75
+ `Invalid prompt name "${name}" in ${filePath}. ` +
76
+ "Names must be lowercase alphanumeric with hyphens only.",
77
+ );
78
+ }
79
+
80
+ const group = match(typeof parsed.group === "string")
81
+ .with(true, () => {
82
+ const g = parsed.group as string;
83
+ const invalidSegment = g.split("/").find((segment) => !NAME_RE.test(segment));
84
+ if (invalidSegment !== undefined) {
85
+ throw new Error(
86
+ `Invalid group segment "${invalidSegment}" in ${filePath}. ` +
87
+ "Group segments must be lowercase alphanumeric with hyphens only.",
88
+ );
89
+ }
90
+ return g;
91
+ })
92
+ .otherwise(() => undefined);
93
+ const version = match(parsed.version != null)
94
+ .with(true, () => String(parsed.version))
95
+ .otherwise(() => undefined);
96
+
97
+ const schema = parseSchemaBlock(parsed.schema, filePath);
98
+
99
+ return { name, group, version, schema };
100
+ }
101
+
102
+ /**
103
+ * Parse the `schema` block from frontmatter into an array of variable definitions.
104
+ *
105
+ * @private
106
+ */
107
+ function parseSchemaBlock(raw: unknown, filePath: string): SchemaVariable[] {
108
+ if (raw == null) {
109
+ return [];
110
+ }
111
+
112
+ if (typeof raw !== "object" || Array.isArray(raw)) {
113
+ throw new Error(
114
+ `Invalid "schema" in ${filePath}: expected an object mapping variable names to definitions`,
115
+ );
116
+ }
117
+
118
+ const schema = raw as Record<string, unknown>;
119
+
120
+ return Object.entries(schema).map(([varName, value]): SchemaVariable => {
121
+ if (typeof value === "string") {
122
+ return { name: varName, type: value, required: true };
123
+ }
124
+
125
+ if (typeof value === "object" && value !== null && !Array.isArray(value)) {
126
+ const def = value as Record<string, unknown>;
127
+ const type = match(typeof def.type === "string")
128
+ .with(true, () => def.type as string)
129
+ .otherwise(() => "string");
130
+ const required = def.required !== false;
131
+ const description = match(typeof def.description === "string")
132
+ .with(true, () => def.description as string)
133
+ .otherwise(() => undefined);
134
+
135
+ return { name: varName, type, required, description };
136
+ }
137
+
138
+ throw new Error(
139
+ `Invalid schema definition for "${varName}" in ${filePath}. ` +
140
+ "Expected a type string or an object with { type, required?, description? }.",
141
+ );
142
+ });
143
+ }
@@ -0,0 +1,74 @@
1
+ import type { SchemaVariable } from "./frontmatter.js";
2
+
3
+ /**
4
+ * A single lint diagnostic.
5
+ */
6
+ export interface LintDiagnostic {
7
+ level: "error" | "warn";
8
+ message: string;
9
+ }
10
+
11
+ /**
12
+ * Result of linting a single prompt file.
13
+ */
14
+ export interface LintResult {
15
+ name: string;
16
+ filePath: string;
17
+ diagnostics: LintDiagnostic[];
18
+ }
19
+
20
+ /**
21
+ * Lint a prompt by comparing declared schema variables against
22
+ * variables actually used in the template body.
23
+ *
24
+ * - **Error**: template uses a variable NOT declared in the schema (undefined var).
25
+ * - **Warn**: schema declares a variable NOT used in the template (unused var).
26
+ *
27
+ * @param name - Prompt name (for error messages).
28
+ * @param filePath - Source file path (for error messages).
29
+ * @param schemaVars - Variables declared in frontmatter schema.
30
+ * @param templateVars - Variables extracted from the template body.
31
+ * @returns Lint result with diagnostics.
32
+ */
33
+ export function lintPrompt(
34
+ name: string,
35
+ filePath: string,
36
+ schemaVars: SchemaVariable[],
37
+ templateVars: string[],
38
+ ): LintResult {
39
+ const diagnostics: LintDiagnostic[] = [];
40
+ const declared = new Set(schemaVars.map((v) => v.name));
41
+ const used = new Set(templateVars);
42
+
43
+ for (const varName of used) {
44
+ if (!declared.has(varName)) {
45
+ diagnostics.push({
46
+ level: "error",
47
+ message:
48
+ `Undefined variable "${varName}" in ${name}.prompt\n` +
49
+ ` Variable "${varName}" is used in the template but not declared in frontmatter schema.\n` +
50
+ " Add it to the schema section in the frontmatter.",
51
+ });
52
+ }
53
+ }
54
+
55
+ for (const varName of declared) {
56
+ if (!used.has(varName)) {
57
+ diagnostics.push({
58
+ level: "warn",
59
+ message:
60
+ `Unused variable "${varName}" in ${name}.prompt\n` +
61
+ ` Variable "${varName}" is declared in the schema but never used in the template.`,
62
+ });
63
+ }
64
+ }
65
+
66
+ return { name, filePath, diagnostics };
67
+ }
68
+
69
+ /**
70
+ * Check whether any lint results contain errors.
71
+ */
72
+ export function hasLintErrors(results: LintResult[]): boolean {
73
+ return results.some((r) => r.diagnostics.some((d) => d.level === "error"));
74
+ }
@@ -0,0 +1,118 @@
1
+ import { existsSync, lstatSync, readdirSync, readFileSync } from "node:fs";
2
+ import { basename, extname, join, resolve } from "node:path";
3
+
4
+ const MAX_DEPTH = 5;
5
+ const PROMPT_EXT = ".prompt";
6
+ const FRONTMATTER_RE = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?/;
7
+ const NAME_RE = /^[a-z0-9-]+$/;
8
+
9
+ export interface DiscoveredPrompt {
10
+ name: string;
11
+ filePath: string;
12
+ }
13
+
14
+ /**
15
+ * Extract the `name` field from YAML frontmatter.
16
+ *
17
+ * This is a lightweight extraction that avoids pulling in a full YAML parser.
18
+ * It looks for `name: <value>` in the frontmatter block.
19
+ */
20
+ function extractName(content: string): string | undefined {
21
+ const match = content.match(FRONTMATTER_RE);
22
+ if (!match) {
23
+ return undefined;
24
+ }
25
+
26
+ const frontmatter = match[1];
27
+ const nameLine = frontmatter.split("\n").find((line) => line.startsWith("name:"));
28
+ if (!nameLine) {
29
+ return undefined;
30
+ }
31
+
32
+ return nameLine.slice("name:".length).trim();
33
+ }
34
+
35
+ /**
36
+ * Derive a prompt name from a file path when no frontmatter name is present.
37
+ *
38
+ * If the file is named `prompt.prompt`, uses the parent directory name.
39
+ * Otherwise uses the file stem (e.g. `my-agent.prompt` -> `my-agent`).
40
+ */
41
+ function deriveNameFromPath(filePath: string): string {
42
+ const stem = basename(filePath, PROMPT_EXT);
43
+ if (stem === "prompt") {
44
+ return basename(resolve(filePath, ".."));
45
+ }
46
+ return stem;
47
+ }
48
+
49
+ /**
50
+ * Recursively scan a directory for `.prompt` files.
51
+ */
52
+ function scanDirectory(dir: string, depth: number): DiscoveredPrompt[] {
53
+ if (depth > MAX_DEPTH) {
54
+ return [];
55
+ }
56
+ // oxlint-disable-next-line security/detect-non-literal-fs-filename -- safe: directory traversal for prompt discovery
57
+ if (!existsSync(dir)) {
58
+ return [];
59
+ }
60
+
61
+ // oxlint-disable-next-line security/detect-non-literal-fs-filename -- safe: stat check on traversed directory
62
+ const stat = lstatSync(dir);
63
+ if (!stat.isDirectory() || stat.isSymbolicLink()) {
64
+ return [];
65
+ }
66
+
67
+ // oxlint-disable-next-line security/detect-non-literal-fs-filename -- safe: reading entries from traversed directory
68
+ const entries = readdirSync(dir, { withFileTypes: true });
69
+
70
+ return entries
71
+ .filter((entry) => !entry.isSymbolicLink())
72
+ .flatMap((entry): DiscoveredPrompt[] => {
73
+ const fullPath = join(dir, entry.name);
74
+
75
+ if (entry.isFile() && extname(entry.name) === PROMPT_EXT) {
76
+ // oxlint-disable-next-line security/detect-non-literal-fs-filename -- safe: reading prompt file content for name extraction
77
+ const content = readFileSync(fullPath, "utf-8");
78
+ const name = extractName(content) ?? deriveNameFromPath(fullPath);
79
+
80
+ if (!NAME_RE.test(name)) {
81
+ throw new Error(
82
+ `Invalid prompt name "${name}" from ${fullPath}. ` +
83
+ "Names must be lowercase alphanumeric with hyphens only.",
84
+ );
85
+ }
86
+
87
+ return [{ name, filePath: fullPath }];
88
+ }
89
+
90
+ if (entry.isDirectory()) {
91
+ return scanDirectory(fullPath, depth + 1);
92
+ }
93
+
94
+ return [];
95
+ });
96
+ }
97
+
98
+ /**
99
+ * Discover all `.prompt` files from the given root directories.
100
+ *
101
+ * @param roots - Directories to scan recursively.
102
+ * @returns Sorted, deduplicated list of discovered prompts.
103
+ * @throws If duplicate prompt names are found across roots.
104
+ */
105
+ export function discoverPrompts(roots: string[]): DiscoveredPrompt[] {
106
+ const all = roots.flatMap((root) => scanDirectory(resolve(root), 0));
107
+
108
+ const byName = Map.groupBy(all, (prompt) => prompt.name);
109
+
110
+ const duplicate = [...byName.entries()].find(([, prompts]) => prompts.length > 1);
111
+ if (duplicate) {
112
+ const [name, prompts] = duplicate;
113
+ const paths = prompts.map((p) => p.filePath).join("\n ");
114
+ throw new Error(`Duplicate prompt name "${name}" found in:\n ${paths}`);
115
+ }
116
+
117
+ return all.toSorted((a, b) => a.name.localeCompare(b.name));
118
+ }
@@ -0,0 +1,114 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import { resolve } from "node:path";
3
+
4
+ import { clean, PARTIALS_DIR } from "@funkai/prompts";
5
+ import { match } from "ts-pattern";
6
+
7
+ import { type ParsedPrompt } from "./codegen.js";
8
+ import { extractVariables } from "./extract-variables.js";
9
+ import { flattenPartials } from "./flatten.js";
10
+ import { parseFrontmatter } from "./frontmatter.js";
11
+ import { lintPrompt, type LintResult } from "./lint.js";
12
+ import { discoverPrompts } from "./paths.js";
13
+
14
+ /**
15
+ * Options for the prompts lint pipeline.
16
+ */
17
+ export interface LintPipelineOptions {
18
+ readonly roots: readonly string[];
19
+ readonly partials?: string;
20
+ }
21
+
22
+ /**
23
+ * Result of running the prompts lint pipeline.
24
+ */
25
+ export interface LintPipelineResult {
26
+ readonly discovered: number;
27
+ readonly results: readonly LintResult[];
28
+ }
29
+
30
+ /**
31
+ * Run the prompts lint pipeline: discover, parse, and validate .prompt files.
32
+ *
33
+ * @param options - Pipeline configuration.
34
+ * @returns Lint results for all discovered prompts.
35
+ */
36
+ export function runLintPipeline(options: LintPipelineOptions): LintPipelineResult {
37
+ const discovered = discoverPrompts([...options.roots]);
38
+ const customPartialsDir = resolve(options.partials ?? ".prompts/partials");
39
+ // oxlint-disable-next-line security/detect-non-literal-fs-filename -- safe: checking custom partials directory from CLI config
40
+ const partialsDirs = match(existsSync(customPartialsDir))
41
+ .with(true, () => [customPartialsDir, PARTIALS_DIR])
42
+ .otherwise(() => [PARTIALS_DIR]);
43
+
44
+ const results = discovered.map((d) => {
45
+ // oxlint-disable-next-line security/detect-non-literal-fs-filename -- safe: reading discovered prompt file
46
+ const raw = readFileSync(d.filePath, "utf-8");
47
+ const frontmatter = parseFrontmatter(raw, d.filePath);
48
+ const template = flattenPartials(clean(raw), partialsDirs);
49
+ const templateVars = extractVariables(template);
50
+ return lintPrompt(frontmatter.name, d.filePath, frontmatter.schema, templateVars);
51
+ });
52
+
53
+ return { discovered: discovered.length, results };
54
+ }
55
+
56
+ /**
57
+ * Options for the prompts generate pipeline.
58
+ */
59
+ export interface GeneratePipelineOptions {
60
+ readonly roots: readonly string[];
61
+ readonly out: string;
62
+ readonly partials?: string;
63
+ }
64
+
65
+ /**
66
+ * Result of running the prompts generate pipeline.
67
+ */
68
+ export interface GeneratePipelineResult {
69
+ readonly discovered: number;
70
+ readonly lintResults: readonly LintResult[];
71
+ readonly prompts: readonly ParsedPrompt[];
72
+ }
73
+
74
+ /**
75
+ * Run the prompts generate pipeline: discover, parse, lint, and prepare prompts for codegen.
76
+ *
77
+ * Does NOT write files — returns the prepared data for the caller to write.
78
+ *
79
+ * @param options - Pipeline configuration.
80
+ * @returns Parsed prompts ready for code generation, along with lint results.
81
+ */
82
+ export function runGeneratePipeline(options: GeneratePipelineOptions): GeneratePipelineResult {
83
+ const discovered = discoverPrompts([...options.roots]);
84
+ const customPartialsDir = resolve(options.partials ?? resolve(options.out, "../partials"));
85
+ // oxlint-disable-next-line security/detect-non-literal-fs-filename -- safe: checking custom partials directory from CLI config
86
+ const partialsDirs = match(existsSync(customPartialsDir))
87
+ .with(true, () => [customPartialsDir, PARTIALS_DIR])
88
+ .otherwise(() => [PARTIALS_DIR]);
89
+
90
+ const processed = discovered.map((d) => {
91
+ // oxlint-disable-next-line security/detect-non-literal-fs-filename -- safe: reading discovered prompt file
92
+ const raw = readFileSync(d.filePath, "utf-8");
93
+ const frontmatter = parseFrontmatter(raw, d.filePath);
94
+ const template = flattenPartials(clean(raw), partialsDirs);
95
+ const templateVars = extractVariables(template);
96
+
97
+ return {
98
+ lintResult: lintPrompt(frontmatter.name, d.filePath, frontmatter.schema, templateVars),
99
+ prompt: {
100
+ name: frontmatter.name,
101
+ group: frontmatter.group,
102
+ schema: frontmatter.schema,
103
+ template,
104
+ sourcePath: d.filePath,
105
+ } satisfies ParsedPrompt,
106
+ };
107
+ });
108
+
109
+ return {
110
+ discovered: discovered.length,
111
+ lintResults: processed.map((p) => p.lintResult),
112
+ prompts: processed.map((p) => p.prompt),
113
+ };
114
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,25 @@
1
+ {
2
+ "$schema": "https://json.schemastore.org/tsconfig",
3
+ "compilerOptions": {
4
+ "target": "ES2024",
5
+ "module": "NodeNext",
6
+ "moduleResolution": "NodeNext",
7
+ "lib": ["ES2024"],
8
+ "strict": true,
9
+ "esModuleInterop": true,
10
+ "skipLibCheck": true,
11
+ "forceConsistentCasingInFileNames": true,
12
+ "resolveJsonModule": true,
13
+ "isolatedModules": true,
14
+ "declaration": true,
15
+ "declarationMap": true,
16
+ "sourceMap": true,
17
+ "outDir": "./dist",
18
+ "rootDir": ".",
19
+ "paths": {
20
+ "@/*": ["./src/*"]
21
+ }
22
+ },
23
+ "include": ["src"],
24
+ "exclude": ["node_modules", "dist"]
25
+ }
@@ -0,0 +1,21 @@
1
+ import { resolve } from "node:path";
2
+
3
+ import { defineConfig } from "vitest/config";
4
+
5
+ export default defineConfig({
6
+ test: {
7
+ include: ["src/**/*.test.{ts,tsx}"],
8
+ passWithNoTests: true,
9
+ coverage: {
10
+ provider: "v8",
11
+ include: ["src/**/*.ts"],
12
+ exclude: ["src/**/*.test.ts", "src/**/*.test-d.ts", "src/**/index.ts"],
13
+ reporter: ["text", "lcov"],
14
+ },
15
+ },
16
+ resolve: {
17
+ alias: {
18
+ "@": resolve(__dirname, "./src"),
19
+ },
20
+ },
21
+ });