@funkai/cli 0.3.0 → 0.3.1
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 +7 -0
- package/README.md +11 -11
- package/dist/index.mjs +153 -96
- package/dist/index.mjs.map +1 -1
- package/package.json +4 -4
- package/src/commands/generate.ts +16 -1
- package/src/commands/prompts/create.ts +3 -0
- package/src/commands/prompts/generate.ts +48 -10
- package/src/commands/prompts/lint.ts +31 -3
- package/src/commands/prompts/setup.ts +3 -3
- package/src/commands/setup.ts +39 -17
- package/src/commands/validate.ts +13 -2
- package/src/lib/prompts/__tests__/lint.test.ts +36 -24
- package/src/lib/prompts/codegen.ts +3 -4
- package/src/lib/prompts/flatten.ts +10 -5
- package/src/lib/prompts/frontmatter.ts +24 -8
- package/src/lib/prompts/lint.ts +31 -10
- package/src/lib/prompts/paths.ts +12 -11
- package/src/lib/prompts/pipeline.ts +50 -27
- package/tsconfig.json +11 -3
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@funkai/cli",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.1",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "CLI for the funkai AI SDK framework",
|
|
6
6
|
"keywords": [
|
|
@@ -31,13 +31,13 @@
|
|
|
31
31
|
"dependencies": {
|
|
32
32
|
"@kidd-cli/core": "^0.10.0",
|
|
33
33
|
"es-toolkit": "^1.45.1",
|
|
34
|
-
"liquidjs": "^10.25.
|
|
34
|
+
"liquidjs": "^10.25.1",
|
|
35
35
|
"picomatch": "^4.0.3",
|
|
36
36
|
"ts-pattern": "^5.9.0",
|
|
37
|
-
"yaml": "^2.8.
|
|
37
|
+
"yaml": "^2.8.3",
|
|
38
38
|
"zod": "^4.3.6",
|
|
39
39
|
"@funkai/config": "0.2.0",
|
|
40
|
-
"@funkai/prompts": "0.4.
|
|
40
|
+
"@funkai/prompts": "0.4.1"
|
|
41
41
|
},
|
|
42
42
|
"devDependencies": {
|
|
43
43
|
"@kidd-cli/cli": "^0.4.9",
|
package/src/commands/generate.ts
CHANGED
|
@@ -15,8 +15,23 @@ export default command({
|
|
|
15
15
|
ctx.logger.info("Running prompts code generation...");
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
+
const generateHandleArgs: {
|
|
19
|
+
silent: boolean;
|
|
20
|
+
out?: string;
|
|
21
|
+
includes?: readonly string[];
|
|
22
|
+
partials?: string;
|
|
23
|
+
} = { silent: ctx.args.silent };
|
|
24
|
+
if (ctx.args.out !== undefined) {
|
|
25
|
+
generateHandleArgs.out = ctx.args.out;
|
|
26
|
+
}
|
|
27
|
+
if (ctx.args.includes !== undefined) {
|
|
28
|
+
generateHandleArgs.includes = ctx.args.includes;
|
|
29
|
+
}
|
|
30
|
+
if (ctx.args.partials !== undefined) {
|
|
31
|
+
generateHandleArgs.partials = ctx.args.partials;
|
|
32
|
+
}
|
|
18
33
|
handleGenerate({
|
|
19
|
-
args:
|
|
34
|
+
args: generateHandleArgs,
|
|
20
35
|
config: config.prompts,
|
|
21
36
|
logger: ctx.logger,
|
|
22
37
|
fail: ctx.fail,
|
|
@@ -33,6 +33,9 @@ export default command({
|
|
|
33
33
|
if (includes.length > 0) {
|
|
34
34
|
// Extract the static base directory from the first include pattern
|
|
35
35
|
const [pattern] = includes;
|
|
36
|
+
if (pattern === undefined) {
|
|
37
|
+
return undefined;
|
|
38
|
+
}
|
|
36
39
|
const parts = pattern.split("/");
|
|
37
40
|
const staticParts = parts.filter((p) => !p.includes("*") && !p.includes("?"));
|
|
38
41
|
if (staticParts.length > 0) {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { mkdirSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { resolve } from "node:path";
|
|
3
3
|
|
|
4
|
-
import type { FunkaiConfig } from "@funkai/config";
|
|
4
|
+
import type { FunkaiConfig, PromptGroup } from "@funkai/config";
|
|
5
5
|
import { command } from "@kidd-cli/core";
|
|
6
6
|
import { match } from "ts-pattern";
|
|
7
7
|
import { z } from "zod";
|
|
@@ -71,7 +71,22 @@ function resolveGenerateArgs(
|
|
|
71
71
|
fail("Missing --out flag. Provide it via CLI or set prompts.out in funkai.config.ts.");
|
|
72
72
|
}
|
|
73
73
|
|
|
74
|
-
|
|
74
|
+
const resolved: {
|
|
75
|
+
out: string;
|
|
76
|
+
includes: readonly string[];
|
|
77
|
+
excludes: readonly string[];
|
|
78
|
+
silent: boolean;
|
|
79
|
+
partials?: string;
|
|
80
|
+
} = {
|
|
81
|
+
out,
|
|
82
|
+
includes,
|
|
83
|
+
excludes,
|
|
84
|
+
silent: args.silent,
|
|
85
|
+
};
|
|
86
|
+
if (partials !== undefined) {
|
|
87
|
+
resolved.partials = partials;
|
|
88
|
+
}
|
|
89
|
+
return resolved;
|
|
75
90
|
}
|
|
76
91
|
|
|
77
92
|
/**
|
|
@@ -82,13 +97,21 @@ function resolveGenerateArgs(
|
|
|
82
97
|
export function handleGenerate({ args, config, logger, fail }: HandleGenerateParams): void {
|
|
83
98
|
const { out, includes, excludes, partials, silent } = resolveGenerateArgs(args, config, fail);
|
|
84
99
|
|
|
85
|
-
const
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
100
|
+
const configGroups = config && config.groups;
|
|
101
|
+
const pipelineOptions: {
|
|
102
|
+
includes: readonly string[];
|
|
103
|
+
excludes: readonly string[];
|
|
104
|
+
out: string;
|
|
105
|
+
partials?: string;
|
|
106
|
+
groups?: readonly PromptGroup[];
|
|
107
|
+
} = { includes, excludes, out };
|
|
108
|
+
if (partials !== undefined) {
|
|
109
|
+
pipelineOptions.partials = partials;
|
|
110
|
+
}
|
|
111
|
+
if (configGroups !== undefined) {
|
|
112
|
+
pipelineOptions.groups = configGroups;
|
|
113
|
+
}
|
|
114
|
+
const { discovered, lintResults, prompts } = runGeneratePipeline(pipelineOptions);
|
|
92
115
|
|
|
93
116
|
if (!silent) {
|
|
94
117
|
logger.info(`Found ${discovered} prompt(s)`);
|
|
@@ -149,8 +172,23 @@ export default command({
|
|
|
149
172
|
options: generateArgs,
|
|
150
173
|
handler(ctx) {
|
|
151
174
|
const config = getConfig(ctx);
|
|
175
|
+
const generateArgs2: {
|
|
176
|
+
silent: boolean;
|
|
177
|
+
out?: string;
|
|
178
|
+
includes?: readonly string[];
|
|
179
|
+
partials?: string;
|
|
180
|
+
} = { silent: ctx.args.silent };
|
|
181
|
+
if (ctx.args.out !== undefined) {
|
|
182
|
+
generateArgs2.out = ctx.args.out;
|
|
183
|
+
}
|
|
184
|
+
if (ctx.args.includes !== undefined) {
|
|
185
|
+
generateArgs2.includes = ctx.args.includes;
|
|
186
|
+
}
|
|
187
|
+
if (ctx.args.partials !== undefined) {
|
|
188
|
+
generateArgs2.partials = ctx.args.partials;
|
|
189
|
+
}
|
|
152
190
|
handleGenerate({
|
|
153
|
-
args:
|
|
191
|
+
args: generateArgs2,
|
|
154
192
|
config: config.prompts,
|
|
155
193
|
logger: ctx.logger,
|
|
156
194
|
fail: ctx.fail,
|
|
@@ -57,7 +57,16 @@ function resolveLintArgs(
|
|
|
57
57
|
const excludes = (config && config.excludes) ?? [];
|
|
58
58
|
const partials = args.partials ?? (config && config.partials);
|
|
59
59
|
|
|
60
|
-
|
|
60
|
+
const resolved: {
|
|
61
|
+
includes: readonly string[];
|
|
62
|
+
excludes: readonly string[];
|
|
63
|
+
silent: boolean;
|
|
64
|
+
partials?: string;
|
|
65
|
+
} = { includes, excludes, silent: args.silent };
|
|
66
|
+
if (partials !== undefined) {
|
|
67
|
+
resolved.partials = partials;
|
|
68
|
+
}
|
|
69
|
+
return resolved;
|
|
61
70
|
}
|
|
62
71
|
|
|
63
72
|
/**
|
|
@@ -68,7 +77,15 @@ function resolveLintArgs(
|
|
|
68
77
|
export function handleLint({ args, config, logger, fail }: HandleLintParams): void {
|
|
69
78
|
const { includes, excludes, partials, silent } = resolveLintArgs(args, config, fail);
|
|
70
79
|
|
|
71
|
-
const
|
|
80
|
+
const lintPipelineOptions: {
|
|
81
|
+
includes: readonly string[];
|
|
82
|
+
excludes: readonly string[];
|
|
83
|
+
partials?: string;
|
|
84
|
+
} = { includes, excludes };
|
|
85
|
+
if (partials !== undefined) {
|
|
86
|
+
lintPipelineOptions.partials = partials;
|
|
87
|
+
}
|
|
88
|
+
const { discovered, results } = runLintPipeline(lintPipelineOptions);
|
|
72
89
|
|
|
73
90
|
if (!silent) {
|
|
74
91
|
logger.info(`Linting ${discovered} prompt(s)...`);
|
|
@@ -109,8 +126,19 @@ export default command({
|
|
|
109
126
|
options: lintArgs,
|
|
110
127
|
handler(ctx) {
|
|
111
128
|
const config = getConfig(ctx);
|
|
129
|
+
const lintHandleArgs: {
|
|
130
|
+
silent: boolean;
|
|
131
|
+
includes?: readonly string[];
|
|
132
|
+
partials?: string;
|
|
133
|
+
} = { silent: ctx.args.silent };
|
|
134
|
+
if (ctx.args.includes !== undefined) {
|
|
135
|
+
lintHandleArgs.includes = ctx.args.includes;
|
|
136
|
+
}
|
|
137
|
+
if (ctx.args.partials !== undefined) {
|
|
138
|
+
lintHandleArgs.partials = ctx.args.partials;
|
|
139
|
+
}
|
|
112
140
|
handleLint({
|
|
113
|
-
args:
|
|
141
|
+
args: lintHandleArgs,
|
|
114
142
|
config: config.prompts,
|
|
115
143
|
logger: ctx.logger,
|
|
116
144
|
fail: ctx.fail,
|
|
@@ -56,7 +56,7 @@ export async function setupPrompts(ctx: Pick<Context, "prompts" | "logger">): Pr
|
|
|
56
56
|
const extensionsPath = resolve(vscodeDir, EXTENSIONS_FILE);
|
|
57
57
|
const extensions = readJsonFile(extensionsPath);
|
|
58
58
|
|
|
59
|
-
const currentRecs = (extensions
|
|
59
|
+
const currentRecs = (extensions["recommendations"] ?? []) as string[];
|
|
60
60
|
const extensionId = "sissel.shopify-liquid";
|
|
61
61
|
|
|
62
62
|
const recommendations = ensureRecommendation(currentRecs, extensionId);
|
|
@@ -97,8 +97,8 @@ export async function setupPrompts(ctx: Pick<Context, "prompts" | "logger">): Pr
|
|
|
97
97
|
const tsconfigPath = resolve(TSCONFIG_FILE);
|
|
98
98
|
const tsconfig = readJsonFile(tsconfigPath);
|
|
99
99
|
|
|
100
|
-
const compilerOptions = (tsconfig
|
|
101
|
-
const existingPaths = (compilerOptions
|
|
100
|
+
const compilerOptions = (tsconfig["compilerOptions"] ?? {}) as Record<string, unknown>;
|
|
101
|
+
const existingPaths = (compilerOptions["paths"] ?? {}) as Record<string, string[]>;
|
|
102
102
|
|
|
103
103
|
// oxlint-disable-next-line security/detect-object-injection -- safe: PROMPTS_ALIAS is a known constant string
|
|
104
104
|
if (existingPaths[PROMPTS_ALIAS]) {
|
package/src/commands/setup.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { writeFileSync } from "node:fs";
|
|
2
2
|
import { resolve } from "node:path";
|
|
3
3
|
|
|
4
|
+
import type { Context } from "@kidd-cli/core";
|
|
4
5
|
import { command } from "@kidd-cli/core";
|
|
5
6
|
import { match } from "ts-pattern";
|
|
6
7
|
|
|
@@ -44,23 +45,7 @@ export default command({
|
|
|
44
45
|
});
|
|
45
46
|
|
|
46
47
|
if (shouldCreateConfig) {
|
|
47
|
-
|
|
48
|
-
let out = ".prompts/client";
|
|
49
|
-
|
|
50
|
-
if (hasPrompts) {
|
|
51
|
-
const includesInput = await ctx.prompts.text({
|
|
52
|
-
message: "Prompt include patterns (comma-separated)",
|
|
53
|
-
defaultValue: "src/prompts/**",
|
|
54
|
-
placeholder: "src/prompts/**",
|
|
55
|
-
});
|
|
56
|
-
includes = includesInput.split(",").map((r) => r.trim());
|
|
57
|
-
|
|
58
|
-
out = await ctx.prompts.text({
|
|
59
|
-
message: "Output directory for generated prompt modules",
|
|
60
|
-
defaultValue: ".prompts/client",
|
|
61
|
-
placeholder: ".prompts/client",
|
|
62
|
-
});
|
|
63
|
-
}
|
|
48
|
+
const { includes, out } = await resolvePromptSettings(ctx, hasPrompts);
|
|
64
49
|
|
|
65
50
|
const template = buildConfigTemplate({ hasPrompts, hasAgents, includes, out });
|
|
66
51
|
const configPath = resolve("funkai.config.ts");
|
|
@@ -90,6 +75,43 @@ export default command({
|
|
|
90
75
|
// Private
|
|
91
76
|
// ---------------------------------------------------------------------------
|
|
92
77
|
|
|
78
|
+
/** @private */
|
|
79
|
+
interface PromptSettings {
|
|
80
|
+
readonly includes: readonly string[];
|
|
81
|
+
readonly out: string;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Gather prompt include patterns and output directory from the user.
|
|
86
|
+
*
|
|
87
|
+
* @private
|
|
88
|
+
* @param ctx - CLI context for prompts.
|
|
89
|
+
* @param hasPrompts - Whether the prompts domain is selected.
|
|
90
|
+
* @returns The resolved prompt settings.
|
|
91
|
+
*/
|
|
92
|
+
async function resolvePromptSettings(ctx: Context, hasPrompts: boolean): Promise<PromptSettings> {
|
|
93
|
+
if (!hasPrompts) {
|
|
94
|
+
return { includes: ["src/prompts/**"], out: ".prompts/client" };
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const includesInput = await ctx.prompts.text({
|
|
98
|
+
message: "Prompt include patterns (comma-separated)",
|
|
99
|
+
defaultValue: "src/prompts/**",
|
|
100
|
+
placeholder: "src/prompts/**",
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
const out = await ctx.prompts.text({
|
|
104
|
+
message: "Output directory for generated prompt modules",
|
|
105
|
+
defaultValue: ".prompts/client",
|
|
106
|
+
placeholder: ".prompts/client",
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
return {
|
|
110
|
+
includes: includesInput.split(",").map((r) => r.trim()),
|
|
111
|
+
out,
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
|
|
93
115
|
/** @private */
|
|
94
116
|
interface ConfigTemplateOptions {
|
|
95
117
|
readonly hasPrompts: boolean;
|
package/src/commands/validate.ts
CHANGED
|
@@ -15,8 +15,19 @@ export default command({
|
|
|
15
15
|
ctx.logger.info("Running prompts validation...");
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
+
const lintHandleArgs: {
|
|
19
|
+
silent: boolean;
|
|
20
|
+
includes?: readonly string[];
|
|
21
|
+
partials?: string;
|
|
22
|
+
} = { silent: ctx.args.silent };
|
|
23
|
+
if (ctx.args.includes !== undefined) {
|
|
24
|
+
lintHandleArgs.includes = ctx.args.includes;
|
|
25
|
+
}
|
|
26
|
+
if (ctx.args.partials !== undefined) {
|
|
27
|
+
lintHandleArgs.partials = ctx.args.partials;
|
|
28
|
+
}
|
|
18
29
|
handleLint({
|
|
19
|
-
args:
|
|
30
|
+
args: lintHandleArgs,
|
|
20
31
|
config: config.prompts,
|
|
21
32
|
logger: ctx.logger,
|
|
22
33
|
fail: ctx.fail,
|
|
@@ -25,7 +36,7 @@ export default command({
|
|
|
25
36
|
// --- Future: agents validation ---
|
|
26
37
|
|
|
27
38
|
if (!silent) {
|
|
28
|
-
ctx.logger.success("
|
|
39
|
+
ctx.logger.success("No errors found.");
|
|
29
40
|
}
|
|
30
41
|
},
|
|
31
42
|
});
|
|
@@ -4,48 +4,60 @@ import { hasLintErrors, lintPrompt } from "@/lib/prompts/lint.js";
|
|
|
4
4
|
|
|
5
5
|
describe(lintPrompt, () => {
|
|
6
6
|
it("returns no diagnostics when vars match schema", () => {
|
|
7
|
-
const result = lintPrompt(
|
|
8
|
-
"test",
|
|
9
|
-
"test.prompt",
|
|
10
|
-
[{ name: "scope", type: "string", required: true }],
|
|
11
|
-
["scope"],
|
|
12
|
-
);
|
|
7
|
+
const result = lintPrompt({
|
|
8
|
+
name: "test",
|
|
9
|
+
filePath: "test.prompt",
|
|
10
|
+
schemaVars: [{ name: "scope", type: "string", required: true }],
|
|
11
|
+
templateVars: ["scope"],
|
|
12
|
+
});
|
|
13
13
|
expect(result.diagnostics).toEqual([]);
|
|
14
14
|
});
|
|
15
15
|
|
|
16
16
|
it("errors on undefined template variable", () => {
|
|
17
|
-
const result = lintPrompt(
|
|
17
|
+
const result = lintPrompt({
|
|
18
|
+
name: "test",
|
|
19
|
+
filePath: "test.prompt",
|
|
20
|
+
schemaVars: [],
|
|
21
|
+
templateVars: ["scope"],
|
|
22
|
+
});
|
|
18
23
|
expect(result.diagnostics).toHaveLength(1);
|
|
19
|
-
|
|
20
|
-
expect(
|
|
24
|
+
const [diag0] = result.diagnostics;
|
|
25
|
+
expect(diag0?.level).toBe("error");
|
|
26
|
+
expect(diag0?.message).toContain('Undefined variable "scope"');
|
|
21
27
|
});
|
|
22
28
|
|
|
23
29
|
it("warns on unused schema variable", () => {
|
|
24
|
-
const result = lintPrompt(
|
|
25
|
-
"test",
|
|
26
|
-
"test.prompt",
|
|
27
|
-
[{ name: "scope", type: "string", required: true }],
|
|
28
|
-
[],
|
|
29
|
-
);
|
|
30
|
+
const result = lintPrompt({
|
|
31
|
+
name: "test",
|
|
32
|
+
filePath: "test.prompt",
|
|
33
|
+
schemaVars: [{ name: "scope", type: "string", required: true }],
|
|
34
|
+
templateVars: [],
|
|
35
|
+
});
|
|
30
36
|
expect(result.diagnostics).toHaveLength(1);
|
|
31
|
-
|
|
32
|
-
expect(
|
|
37
|
+
const [diag0] = result.diagnostics;
|
|
38
|
+
expect(diag0?.level).toBe("warn");
|
|
39
|
+
expect(diag0?.message).toContain('Unused variable "scope"');
|
|
33
40
|
});
|
|
34
41
|
|
|
35
42
|
it("reports both errors and warnings", () => {
|
|
36
|
-
const result = lintPrompt(
|
|
37
|
-
"test",
|
|
38
|
-
"test.prompt",
|
|
39
|
-
[{ name: "declared", type: "string", required: true }],
|
|
40
|
-
["undeclared"],
|
|
41
|
-
);
|
|
43
|
+
const result = lintPrompt({
|
|
44
|
+
name: "test",
|
|
45
|
+
filePath: "test.prompt",
|
|
46
|
+
schemaVars: [{ name: "declared", type: "string", required: true }],
|
|
47
|
+
templateVars: ["undeclared"],
|
|
48
|
+
});
|
|
42
49
|
expect(result.diagnostics).toHaveLength(2);
|
|
43
50
|
const levels = result.diagnostics.map((d) => d.level).toSorted();
|
|
44
51
|
expect(levels).toEqual(["error", "warn"]);
|
|
45
52
|
});
|
|
46
53
|
|
|
47
54
|
it("returns no diagnostics for prompts with no schema and no vars", () => {
|
|
48
|
-
const result = lintPrompt(
|
|
55
|
+
const result = lintPrompt({
|
|
56
|
+
name: "test",
|
|
57
|
+
filePath: "test.prompt",
|
|
58
|
+
schemaVars: [],
|
|
59
|
+
templateVars: [],
|
|
60
|
+
});
|
|
49
61
|
expect(result.diagnostics).toEqual([]);
|
|
50
62
|
});
|
|
51
63
|
});
|
|
@@ -108,10 +108,9 @@ function generateSchemaExpression(vars: readonly SchemaVariable[]): string {
|
|
|
108
108
|
|
|
109
109
|
/** @private */
|
|
110
110
|
function formatHeader(sourcePath?: string): string {
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
}
|
|
111
|
+
const sourceLine = match(sourcePath)
|
|
112
|
+
.with(undefined, () => "")
|
|
113
|
+
.otherwise((p) => `// Source: ${p}\n`);
|
|
115
114
|
return [
|
|
116
115
|
"// ─── AUTO-GENERATED ────────────────────────────────────────",
|
|
117
116
|
`${sourceLine}// Regenerate: funkai prompts generate`,
|
|
@@ -32,18 +32,19 @@ function parseParamsOrEmpty(raw: string, partialName: string): Record<string, st
|
|
|
32
32
|
*/
|
|
33
33
|
function parseParams(raw: string, partialName: string): Record<string, string> {
|
|
34
34
|
const literalMatches = [...raw.matchAll(LITERAL_PARAM_RE)];
|
|
35
|
-
const allParamNames = [...raw.matchAll(/(\w+)\s*:/g)].map((
|
|
35
|
+
const allParamNames = [...raw.matchAll(/(\w+)\s*:/g)].map(([, m1]) => m1);
|
|
36
36
|
|
|
37
37
|
return Object.fromEntries(
|
|
38
38
|
allParamNames.map((name) => {
|
|
39
|
-
const literal = literalMatches.find((
|
|
39
|
+
const literal = literalMatches.find(([, m1]) => m1 === name);
|
|
40
40
|
if (!literal) {
|
|
41
41
|
throw new Error(
|
|
42
42
|
`Cannot flatten {% render '${partialName}' %}: parameter "${name}" uses a variable reference. ` +
|
|
43
43
|
"Only literal string values are supported at codegen time.",
|
|
44
44
|
);
|
|
45
45
|
}
|
|
46
|
-
|
|
46
|
+
const { 2: literalValue } = literal;
|
|
47
|
+
return [name, literalValue];
|
|
47
48
|
}),
|
|
48
49
|
);
|
|
49
50
|
}
|
|
@@ -81,10 +82,14 @@ function renderPartial(engine: Liquid, tag: RenderTag): string {
|
|
|
81
82
|
*/
|
|
82
83
|
function parseRenderTags(template: string): RenderTag[] {
|
|
83
84
|
return [...template.matchAll(RENDER_TAG_RE)].map((m) => {
|
|
85
|
+
const [, partialName] = m;
|
|
86
|
+
if (partialName === undefined) {
|
|
87
|
+
throw new Error("Malformed render tag: missing partial name");
|
|
88
|
+
}
|
|
84
89
|
const rawParams: string = (m[2] ?? "").trim();
|
|
85
|
-
const params = parseParamsOrEmpty(rawParams,
|
|
90
|
+
const params = parseParamsOrEmpty(rawParams, partialName);
|
|
86
91
|
|
|
87
|
-
return { fullMatch: m[0], partialName
|
|
92
|
+
return { fullMatch: m[0], partialName, params };
|
|
88
93
|
});
|
|
89
94
|
}
|
|
90
95
|
|
|
@@ -76,7 +76,11 @@ export function parseFrontmatter({ content, filePath }: ParseFrontmatterParams):
|
|
|
76
76
|
throw new Error(`No frontmatter found in ${filePath}`);
|
|
77
77
|
}
|
|
78
78
|
|
|
79
|
-
const
|
|
79
|
+
const [, fmContent] = fmMatch;
|
|
80
|
+
if (fmContent === undefined) {
|
|
81
|
+
throw new Error(`No frontmatter content found in ${filePath}`);
|
|
82
|
+
}
|
|
83
|
+
const parsed = parseYamlContent(fmContent, filePath);
|
|
80
84
|
|
|
81
85
|
if (!parsed || typeof parsed !== "object") {
|
|
82
86
|
throw new Error(`Frontmatter is not a valid object in ${filePath}`);
|
|
@@ -94,12 +98,24 @@ export function parseFrontmatter({ content, filePath }: ParseFrontmatterParams):
|
|
|
94
98
|
);
|
|
95
99
|
}
|
|
96
100
|
|
|
97
|
-
const group = parseGroup(parsed
|
|
98
|
-
const version = parseVersion(parsed
|
|
101
|
+
const group = parseGroup(parsed["group"], filePath);
|
|
102
|
+
const version = parseVersion(parsed["version"]);
|
|
99
103
|
|
|
100
|
-
const schema = parseSchemaBlock(parsed
|
|
104
|
+
const schema = parseSchemaBlock(parsed["schema"], filePath);
|
|
101
105
|
|
|
102
|
-
|
|
106
|
+
const result: {
|
|
107
|
+
name: string;
|
|
108
|
+
schema: readonly SchemaVariable[];
|
|
109
|
+
group?: string;
|
|
110
|
+
version?: string;
|
|
111
|
+
} = { name, schema };
|
|
112
|
+
if (group !== undefined) {
|
|
113
|
+
result.group = group;
|
|
114
|
+
}
|
|
115
|
+
if (version !== undefined) {
|
|
116
|
+
result.version = version;
|
|
117
|
+
}
|
|
118
|
+
return result;
|
|
103
119
|
}
|
|
104
120
|
|
|
105
121
|
// ---------------------------------------------------------------------------
|
|
@@ -180,9 +196,9 @@ function parseSchemaBlock(raw: unknown, filePath: string): readonly SchemaVariab
|
|
|
180
196
|
typeof v === "object" && v !== null && !Array.isArray(v),
|
|
181
197
|
),
|
|
182
198
|
(def) => {
|
|
183
|
-
const type = stringOrDefault(def
|
|
184
|
-
const required = def
|
|
185
|
-
const description = stringOrUndefined(def
|
|
199
|
+
const type = stringOrDefault(def["type"], "string");
|
|
200
|
+
const required = def["required"] !== false;
|
|
201
|
+
const description = stringOrUndefined(def["description"]);
|
|
186
202
|
return { name: varName, type, required, description };
|
|
187
203
|
},
|
|
188
204
|
)
|
package/src/lib/prompts/lint.ts
CHANGED
|
@@ -17,6 +17,20 @@ export interface LintResult {
|
|
|
17
17
|
readonly diagnostics: readonly LintDiagnostic[];
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
+
/**
|
|
21
|
+
* Parameters for linting a single prompt.
|
|
22
|
+
*/
|
|
23
|
+
export interface LintPromptParams {
|
|
24
|
+
/** Prompt name (for error messages). */
|
|
25
|
+
readonly name: string;
|
|
26
|
+
/** Source file path (for error messages). */
|
|
27
|
+
readonly filePath: string;
|
|
28
|
+
/** Variables declared in frontmatter schema. */
|
|
29
|
+
readonly schemaVars: readonly SchemaVariable[];
|
|
30
|
+
/** Variables extracted from the template body. */
|
|
31
|
+
readonly templateVars: readonly string[];
|
|
32
|
+
}
|
|
33
|
+
|
|
20
34
|
/**
|
|
21
35
|
* Lint a prompt by comparing declared schema variables against
|
|
22
36
|
* variables actually used in the template body.
|
|
@@ -24,18 +38,25 @@ export interface LintResult {
|
|
|
24
38
|
* - **Error**: template uses a variable NOT declared in the schema (undefined var).
|
|
25
39
|
* - **Warn**: schema declares a variable NOT used in the template (unused var).
|
|
26
40
|
*
|
|
27
|
-
* @param
|
|
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.
|
|
41
|
+
* @param params - Lint prompt parameters.
|
|
31
42
|
* @returns Lint result with diagnostics.
|
|
43
|
+
*
|
|
44
|
+
* @example
|
|
45
|
+
* ```ts
|
|
46
|
+
* const result = lintPrompt({
|
|
47
|
+
* name: 'greeting',
|
|
48
|
+
* filePath: 'prompts/greeting.prompt',
|
|
49
|
+
* schemaVars: [{ name: 'name', type: 'string', required: true }],
|
|
50
|
+
* templateVars: ['name'],
|
|
51
|
+
* });
|
|
52
|
+
* ```
|
|
32
53
|
*/
|
|
33
|
-
export function lintPrompt(
|
|
34
|
-
name
|
|
35
|
-
filePath
|
|
36
|
-
schemaVars
|
|
37
|
-
templateVars
|
|
38
|
-
): LintResult {
|
|
54
|
+
export function lintPrompt({
|
|
55
|
+
name,
|
|
56
|
+
filePath,
|
|
57
|
+
schemaVars,
|
|
58
|
+
templateVars,
|
|
59
|
+
}: LintPromptParams): LintResult {
|
|
39
60
|
const declared = new Set(schemaVars.map((v) => v.name));
|
|
40
61
|
const used = new Set(templateVars);
|
|
41
62
|
|
package/src/lib/prompts/paths.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { existsSync, lstatSync, readdirSync, readFileSync } from "node:fs";
|
|
|
2
2
|
import { basename, extname, join, relative, resolve } from "node:path";
|
|
3
3
|
|
|
4
4
|
import picomatch from "picomatch";
|
|
5
|
+
import { match } from "ts-pattern";
|
|
5
6
|
import { parse as parseYaml } from "yaml";
|
|
6
7
|
|
|
7
8
|
import { FRONTMATTER_RE, NAME_RE } from "./frontmatter.js";
|
|
@@ -43,9 +44,13 @@ function extractName(content: string): string | undefined {
|
|
|
43
44
|
}
|
|
44
45
|
|
|
45
46
|
try {
|
|
46
|
-
const
|
|
47
|
-
if (
|
|
48
|
-
return
|
|
47
|
+
const [, fmContent] = fmMatch;
|
|
48
|
+
if (fmContent === undefined) {
|
|
49
|
+
return undefined;
|
|
50
|
+
}
|
|
51
|
+
const parsed = parseYaml(fmContent) as Record<string, unknown> | null;
|
|
52
|
+
if (parsed !== null && parsed !== undefined && typeof parsed["name"] === "string") {
|
|
53
|
+
return parsed["name"];
|
|
49
54
|
}
|
|
50
55
|
return undefined;
|
|
51
56
|
} catch {
|
|
@@ -81,14 +86,10 @@ function deriveNameFromPath(filePath: string): string {
|
|
|
81
86
|
function extractBaseDir(pattern: string): string {
|
|
82
87
|
const globChars = new Set(["*", "?", "{", "["]);
|
|
83
88
|
const parts = pattern.split("/");
|
|
84
|
-
const
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
break;
|
|
89
|
-
}
|
|
90
|
-
staticParts.push(part);
|
|
91
|
-
}
|
|
89
|
+
const firstGlobIndex = parts.findIndex((part) => [...part].some((ch) => globChars.has(ch)));
|
|
90
|
+
const staticParts = match(firstGlobIndex)
|
|
91
|
+
.with(-1, () => parts)
|
|
92
|
+
.otherwise(() => parts.slice(0, firstGlobIndex));
|
|
92
93
|
|
|
93
94
|
if (staticParts.length === 0) {
|
|
94
95
|
return ".";
|