@funkai/cli 0.2.0 → 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 +32 -0
- package/dist/index.mjs +538 -222
- package/dist/index.mjs.map +1 -1
- package/package.json +5 -2
- package/src/commands/generate.ts +8 -1
- package/src/commands/prompts/create.ts +30 -2
- package/src/commands/prompts/generate.ts +58 -11
- package/src/commands/prompts/lint.ts +41 -7
- package/src/commands/prompts/setup.ts +103 -95
- 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/codegen.ts +113 -43
- package/src/lib/prompts/paths.ts +67 -15
- package/src/lib/prompts/pipeline.ts +82 -7
|
@@ -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,6 +14,54 @@ 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
|
*
|
|
@@ -30,7 +81,8 @@ function resolvePartialsDirs(customDir: string): readonly string[] {
|
|
|
30
81
|
* Options for the prompts lint pipeline.
|
|
31
82
|
*/
|
|
32
83
|
export interface LintPipelineOptions {
|
|
33
|
-
readonly
|
|
84
|
+
readonly includes: readonly string[];
|
|
85
|
+
readonly excludes?: readonly string[];
|
|
34
86
|
readonly partials?: string;
|
|
35
87
|
}
|
|
36
88
|
|
|
@@ -49,7 +101,14 @@ export interface LintPipelineResult {
|
|
|
49
101
|
* @returns Lint results for all discovered prompts.
|
|
50
102
|
*/
|
|
51
103
|
export function runLintPipeline(options: LintPipelineOptions): LintPipelineResult {
|
|
52
|
-
|
|
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
|
+
});
|
|
53
112
|
const customPartialsDir = resolve(options.partials ?? ".prompts/partials");
|
|
54
113
|
const partialsDirs = resolvePartialsDirs(customPartialsDir);
|
|
55
114
|
|
|
@@ -69,9 +128,11 @@ export function runLintPipeline(options: LintPipelineOptions): LintPipelineResul
|
|
|
69
128
|
* Options for the prompts generate pipeline.
|
|
70
129
|
*/
|
|
71
130
|
export interface GeneratePipelineOptions {
|
|
72
|
-
readonly
|
|
131
|
+
readonly includes: readonly string[];
|
|
132
|
+
readonly excludes?: readonly string[];
|
|
73
133
|
readonly out: string;
|
|
74
134
|
readonly partials?: string;
|
|
135
|
+
readonly groups?: readonly PromptGroup[];
|
|
75
136
|
}
|
|
76
137
|
|
|
77
138
|
/**
|
|
@@ -92,9 +153,17 @@ export interface GeneratePipelineResult {
|
|
|
92
153
|
* @returns Parsed prompts ready for code generation, along with lint results.
|
|
93
154
|
*/
|
|
94
155
|
export function runGeneratePipeline(options: GeneratePipelineOptions): GeneratePipelineResult {
|
|
95
|
-
|
|
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
|
+
});
|
|
96
164
|
const customPartialsDir = resolve(options.partials ?? resolve(options.out, "../partials"));
|
|
97
165
|
const partialsDirs = resolvePartialsDirs(customPartialsDir);
|
|
166
|
+
const configGroups = options.groups ?? [];
|
|
98
167
|
|
|
99
168
|
const processed = discovered.map((d) => {
|
|
100
169
|
// oxlint-disable-next-line security/detect-non-literal-fs-filename -- safe: reading discovered prompt file
|
|
@@ -103,11 +172,14 @@ export function runGeneratePipeline(options: GeneratePipelineOptions): GenerateP
|
|
|
103
172
|
const template = flattenPartials({ template: clean(raw), partialsDirs });
|
|
104
173
|
const templateVars = extractVariables(template);
|
|
105
174
|
|
|
175
|
+
// Frontmatter group wins; fall back to config-defined group patterns
|
|
176
|
+
const group = frontmatter.group ?? resolveGroupFromConfig(d.filePath, configGroups);
|
|
177
|
+
|
|
106
178
|
return {
|
|
107
179
|
lintResult: lintPrompt(frontmatter.name, d.filePath, frontmatter.schema, templateVars),
|
|
108
180
|
prompt: {
|
|
109
181
|
name: frontmatter.name,
|
|
110
|
-
group
|
|
182
|
+
group,
|
|
111
183
|
schema: frontmatter.schema,
|
|
112
184
|
template,
|
|
113
185
|
sourcePath: d.filePath,
|
|
@@ -115,9 +187,12 @@ export function runGeneratePipeline(options: GeneratePipelineOptions): GenerateP
|
|
|
115
187
|
};
|
|
116
188
|
});
|
|
117
189
|
|
|
190
|
+
const prompts = processed.map((p) => p.prompt);
|
|
191
|
+
validateUniqueness(prompts);
|
|
192
|
+
|
|
118
193
|
return {
|
|
119
194
|
discovered: discovered.length,
|
|
120
195
|
lintResults: processed.map((p) => p.lintResult),
|
|
121
|
-
prompts
|
|
196
|
+
prompts,
|
|
122
197
|
};
|
|
123
198
|
}
|