@funkai/cli 0.1.4 → 0.3.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/.turbo/turbo-build.log +2 -2
- package/CHANGELOG.md +81 -0
- package/dist/index.mjs +939 -3484
- package/dist/index.mjs.map +1 -1
- package/package.json +6 -2
- package/src/commands/generate.ts +8 -1
- package/src/commands/prompts/create.ts +31 -2
- package/src/commands/prompts/generate.ts +91 -35
- package/src/commands/prompts/lint.ts +63 -20
- package/src/commands/prompts/setup.ts +153 -119
- package/src/commands/setup.ts +129 -4
- package/src/commands/validate.ts +8 -1
- package/src/config.ts +28 -0
- package/src/index.ts +4 -0
- package/src/lib/prompts/__tests__/flatten.test.ts +29 -27
- package/src/lib/prompts/__tests__/frontmatter.test.ts +17 -15
- package/src/lib/prompts/codegen.ts +149 -79
- package/src/lib/prompts/extract-variables.ts +20 -9
- package/src/lib/prompts/flatten.ts +62 -16
- package/src/lib/prompts/frontmatter.ts +97 -53
- package/src/lib/prompts/lint.ts +18 -23
- package/src/lib/prompts/paths.ts +90 -24
- package/src/lib/prompts/pipeline.ts +87 -11
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
-
import { resolve } from "node:path";
|
|
2
|
+
import { relative, resolve } from "node:path";
|
|
3
3
|
|
|
4
|
+
import type { PromptGroup } from "@funkai/config";
|
|
4
5
|
import { clean, PARTIALS_DIR } from "@funkai/prompts/cli";
|
|
6
|
+
import picomatch from "picomatch";
|
|
5
7
|
|
|
8
|
+
import { toFileSlug } from "./codegen.js";
|
|
6
9
|
import type { ParsedPrompt } from "./codegen.js";
|
|
7
10
|
import { extractVariables } from "./extract-variables.js";
|
|
8
11
|
import { flattenPartials } from "./flatten.js";
|
|
@@ -11,9 +14,58 @@ import { lintPrompt } from "./lint.js";
|
|
|
11
14
|
import type { LintResult } from "./lint.js";
|
|
12
15
|
import { discoverPrompts } from "./paths.js";
|
|
13
16
|
|
|
17
|
+
/**
|
|
18
|
+
* Validate that no two prompts share the same group+name combination.
|
|
19
|
+
*
|
|
20
|
+
* @param prompts - Parsed prompts with group and name fields.
|
|
21
|
+
* @throws If duplicate group+name combinations are found.
|
|
22
|
+
*
|
|
23
|
+
* @private
|
|
24
|
+
*/
|
|
25
|
+
function validateUniqueness(prompts: readonly ParsedPrompt[]): void {
|
|
26
|
+
const bySlug = Map.groupBy(prompts, (p) => toFileSlug(p.name, p.group));
|
|
27
|
+
const duplicate = [...bySlug.entries()].find(([, entries]) => entries.length > 1);
|
|
28
|
+
|
|
29
|
+
if (duplicate) {
|
|
30
|
+
const [slug, entries] = duplicate;
|
|
31
|
+
const paths = entries.map((p) => p.sourcePath).join("\n ");
|
|
32
|
+
throw new Error(`Duplicate prompt "${slug}" (group+name) found in:\n ${paths}`);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Resolve a prompt's group from config-defined group patterns.
|
|
38
|
+
*
|
|
39
|
+
* Matches the prompt's file path against each group's `includes`/`excludes`
|
|
40
|
+
* patterns. First matching group wins.
|
|
41
|
+
*
|
|
42
|
+
* @param filePath - Absolute path to the prompt file.
|
|
43
|
+
* @param groups - Config-defined group definitions.
|
|
44
|
+
* @returns The matching group name, or undefined if no match.
|
|
45
|
+
*
|
|
46
|
+
* @private
|
|
47
|
+
*/
|
|
48
|
+
function resolveGroupFromConfig(
|
|
49
|
+
filePath: string,
|
|
50
|
+
groups: readonly PromptGroup[],
|
|
51
|
+
): string | undefined {
|
|
52
|
+
const matchPath = relative(process.cwd(), filePath).replaceAll("\\", "/");
|
|
53
|
+
|
|
54
|
+
for (const group of groups) {
|
|
55
|
+
const isIncluded = picomatch(group.includes as string[]);
|
|
56
|
+
const isExcluded = picomatch((group.excludes ?? []) as string[]);
|
|
57
|
+
|
|
58
|
+
if (isIncluded(matchPath) && !isExcluded(matchPath)) {
|
|
59
|
+
return group.name;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return undefined;
|
|
63
|
+
}
|
|
64
|
+
|
|
14
65
|
/**
|
|
15
66
|
* Resolve the list of partial directories to search.
|
|
16
67
|
*
|
|
68
|
+
* @private
|
|
17
69
|
* @param customDir - Custom partials directory path.
|
|
18
70
|
* @returns Array of directories to search for partials.
|
|
19
71
|
*/
|
|
@@ -29,7 +81,8 @@ function resolvePartialsDirs(customDir: string): readonly string[] {
|
|
|
29
81
|
* Options for the prompts lint pipeline.
|
|
30
82
|
*/
|
|
31
83
|
export interface LintPipelineOptions {
|
|
32
|
-
readonly
|
|
84
|
+
readonly includes: readonly string[];
|
|
85
|
+
readonly excludes?: readonly string[];
|
|
33
86
|
readonly partials?: string;
|
|
34
87
|
}
|
|
35
88
|
|
|
@@ -48,15 +101,22 @@ export interface LintPipelineResult {
|
|
|
48
101
|
* @returns Lint results for all discovered prompts.
|
|
49
102
|
*/
|
|
50
103
|
export function runLintPipeline(options: LintPipelineOptions): LintPipelineResult {
|
|
51
|
-
|
|
104
|
+
let excludes: string[] | undefined;
|
|
105
|
+
if (options.excludes) {
|
|
106
|
+
excludes = [...options.excludes];
|
|
107
|
+
}
|
|
108
|
+
const discovered = discoverPrompts({
|
|
109
|
+
includes: [...options.includes],
|
|
110
|
+
excludes,
|
|
111
|
+
});
|
|
52
112
|
const customPartialsDir = resolve(options.partials ?? ".prompts/partials");
|
|
53
113
|
const partialsDirs = resolvePartialsDirs(customPartialsDir);
|
|
54
114
|
|
|
55
115
|
const results = discovered.map((d) => {
|
|
56
116
|
// oxlint-disable-next-line security/detect-non-literal-fs-filename -- safe: reading discovered prompt file
|
|
57
117
|
const raw = readFileSync(d.filePath, "utf8");
|
|
58
|
-
const frontmatter = parseFrontmatter(raw, d.filePath);
|
|
59
|
-
const template = flattenPartials(clean(raw), partialsDirs);
|
|
118
|
+
const frontmatter = parseFrontmatter({ content: raw, filePath: d.filePath });
|
|
119
|
+
const template = flattenPartials({ template: clean(raw), partialsDirs });
|
|
60
120
|
const templateVars = extractVariables(template);
|
|
61
121
|
return lintPrompt(frontmatter.name, d.filePath, frontmatter.schema, templateVars);
|
|
62
122
|
});
|
|
@@ -68,9 +128,11 @@ export function runLintPipeline(options: LintPipelineOptions): LintPipelineResul
|
|
|
68
128
|
* Options for the prompts generate pipeline.
|
|
69
129
|
*/
|
|
70
130
|
export interface GeneratePipelineOptions {
|
|
71
|
-
readonly
|
|
131
|
+
readonly includes: readonly string[];
|
|
132
|
+
readonly excludes?: readonly string[];
|
|
72
133
|
readonly out: string;
|
|
73
134
|
readonly partials?: string;
|
|
135
|
+
readonly groups?: readonly PromptGroup[];
|
|
74
136
|
}
|
|
75
137
|
|
|
76
138
|
/**
|
|
@@ -91,22 +153,33 @@ export interface GeneratePipelineResult {
|
|
|
91
153
|
* @returns Parsed prompts ready for code generation, along with lint results.
|
|
92
154
|
*/
|
|
93
155
|
export function runGeneratePipeline(options: GeneratePipelineOptions): GeneratePipelineResult {
|
|
94
|
-
|
|
156
|
+
let excludes: string[] | undefined;
|
|
157
|
+
if (options.excludes) {
|
|
158
|
+
excludes = [...options.excludes];
|
|
159
|
+
}
|
|
160
|
+
const discovered = discoverPrompts({
|
|
161
|
+
includes: [...options.includes],
|
|
162
|
+
excludes,
|
|
163
|
+
});
|
|
95
164
|
const customPartialsDir = resolve(options.partials ?? resolve(options.out, "../partials"));
|
|
96
165
|
const partialsDirs = resolvePartialsDirs(customPartialsDir);
|
|
166
|
+
const configGroups = options.groups ?? [];
|
|
97
167
|
|
|
98
168
|
const processed = discovered.map((d) => {
|
|
99
169
|
// oxlint-disable-next-line security/detect-non-literal-fs-filename -- safe: reading discovered prompt file
|
|
100
170
|
const raw = readFileSync(d.filePath, "utf8");
|
|
101
|
-
const frontmatter = parseFrontmatter(raw, d.filePath);
|
|
102
|
-
const template = flattenPartials(clean(raw), partialsDirs);
|
|
171
|
+
const frontmatter = parseFrontmatter({ content: raw, filePath: d.filePath });
|
|
172
|
+
const template = flattenPartials({ template: clean(raw), partialsDirs });
|
|
103
173
|
const templateVars = extractVariables(template);
|
|
104
174
|
|
|
175
|
+
// Frontmatter group wins; fall back to config-defined group patterns
|
|
176
|
+
const group = frontmatter.group ?? resolveGroupFromConfig(d.filePath, configGroups);
|
|
177
|
+
|
|
105
178
|
return {
|
|
106
179
|
lintResult: lintPrompt(frontmatter.name, d.filePath, frontmatter.schema, templateVars),
|
|
107
180
|
prompt: {
|
|
108
181
|
name: frontmatter.name,
|
|
109
|
-
group
|
|
182
|
+
group,
|
|
110
183
|
schema: frontmatter.schema,
|
|
111
184
|
template,
|
|
112
185
|
sourcePath: d.filePath,
|
|
@@ -114,9 +187,12 @@ export function runGeneratePipeline(options: GeneratePipelineOptions): GenerateP
|
|
|
114
187
|
};
|
|
115
188
|
});
|
|
116
189
|
|
|
190
|
+
const prompts = processed.map((p) => p.prompt);
|
|
191
|
+
validateUniqueness(prompts);
|
|
192
|
+
|
|
117
193
|
return {
|
|
118
194
|
discovered: discovered.length,
|
|
119
195
|
lintResults: processed.map((p) => p.lintResult),
|
|
120
|
-
prompts
|
|
196
|
+
prompts,
|
|
121
197
|
};
|
|
122
198
|
}
|