@funkai/cli 0.1.4 → 0.2.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 +49 -0
- package/dist/index.mjs +376 -3237
- package/dist/index.mjs.map +1 -1
- package/package.json +3 -2
- package/src/commands/generate.ts +1 -1
- package/src/commands/prompts/create.ts +1 -0
- package/src/commands/prompts/generate.ts +37 -28
- package/src/commands/prompts/lint.ts +26 -17
- package/src/commands/prompts/setup.ts +53 -27
- package/src/commands/validate.ts +1 -1
- 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 +37 -37
- 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 +24 -10
- package/src/lib/prompts/pipeline.ts +5 -4
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@funkai/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "CLI for the funkai AI SDK framework",
|
|
6
6
|
"keywords": [
|
|
@@ -30,11 +30,12 @@
|
|
|
30
30
|
},
|
|
31
31
|
"dependencies": {
|
|
32
32
|
"@kidd-cli/core": "^0.10.0",
|
|
33
|
+
"es-toolkit": "^1.45.1",
|
|
33
34
|
"liquidjs": "^10.25.0",
|
|
34
35
|
"ts-pattern": "^5.9.0",
|
|
35
36
|
"yaml": "^2.8.2",
|
|
36
37
|
"zod": "^4.3.6",
|
|
37
|
-
"@funkai/prompts": "0.
|
|
38
|
+
"@funkai/prompts": "0.3.0"
|
|
38
39
|
},
|
|
39
40
|
"devDependencies": {
|
|
40
41
|
"@kidd-cli/cli": "^0.4.9",
|
package/src/commands/generate.ts
CHANGED
|
@@ -13,7 +13,7 @@ export default command({
|
|
|
13
13
|
ctx.logger.info("Running prompts code generation...");
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
-
handleGenerate(ctx.args, ctx.logger, ctx.fail);
|
|
16
|
+
handleGenerate({ args: ctx.args, logger: ctx.logger, fail: ctx.fail });
|
|
17
17
|
|
|
18
18
|
// --- Future: agents codegen ---
|
|
19
19
|
},
|
|
@@ -2,12 +2,14 @@ import { mkdirSync, writeFileSync } from "node:fs";
|
|
|
2
2
|
import { resolve } from "node:path";
|
|
3
3
|
|
|
4
4
|
import { command } from "@kidd-cli/core";
|
|
5
|
+
import { match } from "ts-pattern";
|
|
5
6
|
import { z } from "zod";
|
|
6
7
|
|
|
7
8
|
import { generatePromptModule, generateRegistry } from "@/lib/prompts/codegen.js";
|
|
8
9
|
import { hasLintErrors } from "@/lib/prompts/lint.js";
|
|
9
10
|
import { runGeneratePipeline } from "@/lib/prompts/pipeline.js";
|
|
10
11
|
|
|
12
|
+
/** Zod schema for the `prompts generate` CLI arguments. */
|
|
11
13
|
export const generateArgs = z.object({
|
|
12
14
|
out: z.string().describe("Output directory for generated files"),
|
|
13
15
|
roots: z.array(z.string()).describe("Root directories to scan for .prompt files"),
|
|
@@ -15,31 +17,35 @@ export const generateArgs = z.object({
|
|
|
15
17
|
silent: z.boolean().default(false).describe("Suppress output except errors"),
|
|
16
18
|
});
|
|
17
19
|
|
|
20
|
+
/** Inferred type of the `prompts generate` CLI arguments. */
|
|
18
21
|
export type GenerateArgs = z.infer<typeof generateArgs>;
|
|
19
22
|
|
|
20
23
|
/**
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
* @param args - Parsed CLI arguments.
|
|
24
|
-
* @param logger - Logger instance from the command context.
|
|
25
|
-
* @param fail - Failure callback from the command context.
|
|
24
|
+
* Parameters for the shared generate handler.
|
|
26
25
|
*/
|
|
27
|
-
export
|
|
28
|
-
args: {
|
|
26
|
+
export interface HandleGenerateParams {
|
|
27
|
+
readonly args: {
|
|
29
28
|
readonly out: string;
|
|
30
29
|
readonly roots: readonly string[];
|
|
31
30
|
readonly partials?: string;
|
|
32
31
|
readonly silent: boolean;
|
|
33
|
-
}
|
|
34
|
-
logger: {
|
|
32
|
+
};
|
|
33
|
+
readonly logger: {
|
|
35
34
|
info: (msg: string) => void;
|
|
36
35
|
step: (msg: string) => void;
|
|
37
36
|
error: (msg: string) => void;
|
|
38
37
|
warn: (msg: string) => void;
|
|
39
38
|
success: (msg: string) => void;
|
|
40
|
-
}
|
|
41
|
-
fail: (msg: string) => never
|
|
42
|
-
|
|
39
|
+
};
|
|
40
|
+
readonly fail: (msg: string) => never;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Shared handler for prompts code generation.
|
|
45
|
+
*
|
|
46
|
+
* @param params - Handler context with args, logger, and fail callback.
|
|
47
|
+
*/
|
|
48
|
+
export function handleGenerate({ args, logger, fail }: HandleGenerateParams): void {
|
|
43
49
|
const { out, roots, partials, silent } = args;
|
|
44
50
|
|
|
45
51
|
const { discovered, lintResults, prompts } = runGeneratePipeline({ roots, out, partials });
|
|
@@ -50,25 +56,20 @@ export function handleGenerate(
|
|
|
50
56
|
|
|
51
57
|
if (!silent) {
|
|
52
58
|
for (const prompt of prompts) {
|
|
53
|
-
|
|
54
|
-
const varList: string = (() => {
|
|
55
|
-
if (prompt.schema.length > 0) {
|
|
56
|
-
return ` (${prompt.schema.map((v) => v.name).join(", ")})`;
|
|
57
|
-
}
|
|
58
|
-
return "";
|
|
59
|
-
})();
|
|
59
|
+
const varList = formatVarList(prompt.schema);
|
|
60
60
|
logger.step(`${prompt.name}${varList}`);
|
|
61
61
|
}
|
|
62
62
|
}
|
|
63
63
|
|
|
64
|
-
for (const
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
64
|
+
for (const diag of lintResults.flatMap((result) => result.diagnostics)) {
|
|
65
|
+
match(diag.level)
|
|
66
|
+
.with("error", () => logger.error(diag.message))
|
|
67
|
+
.with("warn", () => {
|
|
68
|
+
if (!silent) {
|
|
69
|
+
logger.warn(diag.message);
|
|
70
|
+
}
|
|
71
|
+
})
|
|
72
|
+
.exhaustive();
|
|
72
73
|
}
|
|
73
74
|
|
|
74
75
|
if (hasLintErrors(lintResults)) {
|
|
@@ -94,10 +95,18 @@ export function handleGenerate(
|
|
|
94
95
|
}
|
|
95
96
|
}
|
|
96
97
|
|
|
98
|
+
/** @private */
|
|
99
|
+
function formatVarList(schema: readonly { readonly name: string }[]): string {
|
|
100
|
+
if (schema.length > 0) {
|
|
101
|
+
return ` (${schema.map((v) => v.name).join(", ")})`;
|
|
102
|
+
}
|
|
103
|
+
return "";
|
|
104
|
+
}
|
|
105
|
+
|
|
97
106
|
export default command({
|
|
98
107
|
description: "Generate TypeScript modules from .prompt files",
|
|
99
108
|
options: generateArgs,
|
|
100
109
|
handler(ctx) {
|
|
101
|
-
handleGenerate(ctx.args, ctx.logger, ctx.fail);
|
|
110
|
+
handleGenerate({ args: ctx.args, logger: ctx.logger, fail: ctx.fail });
|
|
102
111
|
},
|
|
103
112
|
});
|
|
@@ -1,33 +1,43 @@
|
|
|
1
1
|
import { command } from "@kidd-cli/core";
|
|
2
|
+
import { match } from "ts-pattern";
|
|
2
3
|
import { z } from "zod";
|
|
3
4
|
|
|
4
5
|
import { hasLintErrors } from "@/lib/prompts/lint.js";
|
|
5
6
|
import { runLintPipeline } from "@/lib/prompts/pipeline.js";
|
|
6
7
|
|
|
8
|
+
/** Zod schema for the `prompts lint` CLI arguments. */
|
|
7
9
|
export const lintArgs = z.object({
|
|
8
10
|
roots: z.array(z.string()).describe("Root directories to scan for .prompt files"),
|
|
9
11
|
partials: z.string().optional().describe("Custom partials directory"),
|
|
10
12
|
silent: z.boolean().default(false).describe("Suppress output except errors"),
|
|
11
13
|
});
|
|
12
14
|
|
|
15
|
+
/** Inferred type of the `prompts lint` CLI arguments. */
|
|
13
16
|
export type LintArgs = z.infer<typeof lintArgs>;
|
|
14
17
|
|
|
15
18
|
/**
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
* @param args - Parsed CLI arguments.
|
|
19
|
-
* @param logger - Logger instance from the command context.
|
|
20
|
-
* @param fail - Failure callback from the command context.
|
|
19
|
+
* Parameters for the shared lint handler.
|
|
21
20
|
*/
|
|
22
|
-
export
|
|
23
|
-
args: {
|
|
24
|
-
|
|
21
|
+
export interface HandleLintParams {
|
|
22
|
+
readonly args: {
|
|
23
|
+
readonly roots: readonly string[];
|
|
24
|
+
readonly partials?: string;
|
|
25
|
+
readonly silent: boolean;
|
|
26
|
+
};
|
|
27
|
+
readonly logger: {
|
|
25
28
|
info: (msg: string) => void;
|
|
26
29
|
error: (msg: string) => void;
|
|
27
30
|
warn: (msg: string) => void;
|
|
28
|
-
}
|
|
29
|
-
fail: (msg: string) => never
|
|
30
|
-
|
|
31
|
+
};
|
|
32
|
+
readonly fail: (msg: string) => never;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Shared handler for prompts lint/validation.
|
|
37
|
+
*
|
|
38
|
+
* @param params - Handler context with args, logger, and fail callback.
|
|
39
|
+
*/
|
|
40
|
+
export function handleLint({ args, logger, fail }: HandleLintParams): void {
|
|
31
41
|
const { roots, partials, silent } = args;
|
|
32
42
|
|
|
33
43
|
const { discovered, results } = runLintPipeline({ roots, partials });
|
|
@@ -39,11 +49,10 @@ export function handleLint(
|
|
|
39
49
|
const diagnostics = results.flatMap((result) => result.diagnostics);
|
|
40
50
|
|
|
41
51
|
for (const diag of diagnostics) {
|
|
42
|
-
|
|
43
|
-
logger.error(diag.message)
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
}
|
|
52
|
+
match(diag.level)
|
|
53
|
+
.with("error", () => logger.error(diag.message))
|
|
54
|
+
.with("warn", () => logger.warn(diag.message))
|
|
55
|
+
.exhaustive();
|
|
47
56
|
}
|
|
48
57
|
|
|
49
58
|
const errorCount = diagnostics.filter((d) => d.level === "error").length;
|
|
@@ -71,6 +80,6 @@ export default command({
|
|
|
71
80
|
description: "Validate .prompt files for schema/template mismatches",
|
|
72
81
|
options: lintArgs,
|
|
73
82
|
handler(ctx) {
|
|
74
|
-
handleLint(ctx.args, ctx.logger, ctx.fail);
|
|
83
|
+
handleLint({ args: ctx.args, logger: ctx.logger, fail: ctx.fail });
|
|
75
84
|
},
|
|
76
85
|
});
|
|
@@ -57,13 +57,7 @@ export default command({
|
|
|
57
57
|
const currentRecs = (extensions.recommendations ?? []) as string[];
|
|
58
58
|
const extensionId = "sissel.shopify-liquid";
|
|
59
59
|
|
|
60
|
-
|
|
61
|
-
const recommendations: string[] = (() => {
|
|
62
|
-
if (currentRecs.includes(extensionId)) {
|
|
63
|
-
return currentRecs;
|
|
64
|
-
}
|
|
65
|
-
return [...currentRecs, extensionId];
|
|
66
|
-
})();
|
|
60
|
+
const recommendations = ensureRecommendation(currentRecs, extensionId);
|
|
67
61
|
const updatedExtensions = {
|
|
68
62
|
...extensions,
|
|
69
63
|
recommendations,
|
|
@@ -80,24 +74,12 @@ export default command({
|
|
|
80
74
|
|
|
81
75
|
if (shouldGitignore) {
|
|
82
76
|
const gitignorePath = resolve(GITIGNORE_FILE);
|
|
83
|
-
|
|
84
|
-
const existing: string = (() => {
|
|
85
|
-
if (existsSync(gitignorePath)) {
|
|
86
|
-
return readFileSync(gitignorePath, "utf8");
|
|
87
|
-
}
|
|
88
|
-
return "";
|
|
89
|
-
})();
|
|
77
|
+
const existing = readFileOrEmpty(gitignorePath);
|
|
90
78
|
|
|
91
79
|
if (existing.includes(GITIGNORE_ENTRY)) {
|
|
92
80
|
ctx.logger.info(`${GITIGNORE_ENTRY} already in ${gitignorePath}`);
|
|
93
81
|
} else {
|
|
94
|
-
|
|
95
|
-
const separator: string = (() => {
|
|
96
|
-
if (existing.length > 0 && !existing.endsWith("\n")) {
|
|
97
|
-
return "\n";
|
|
98
|
-
}
|
|
99
|
-
return "";
|
|
100
|
-
})();
|
|
82
|
+
const separator = trailingSeparator(existing);
|
|
101
83
|
const block = `${separator}\n# Generated prompt client (created by \`funkai prompts generate\`)\n${GITIGNORE_ENTRY}\n`;
|
|
102
84
|
writeFileSync(gitignorePath, `${existing}${block}`, "utf8");
|
|
103
85
|
ctx.logger.success(`Added ${GITIGNORE_ENTRY} to ${gitignorePath}`);
|
|
@@ -141,9 +123,49 @@ export default command({
|
|
|
141
123
|
},
|
|
142
124
|
});
|
|
143
125
|
|
|
126
|
+
// ---------------------------------------------------------------------------
|
|
127
|
+
// Private
|
|
128
|
+
// ---------------------------------------------------------------------------
|
|
129
|
+
|
|
130
|
+
/** @private */
|
|
131
|
+
function errorMessage(error: unknown): string {
|
|
132
|
+
if (error instanceof Error) {
|
|
133
|
+
return error.message;
|
|
134
|
+
}
|
|
135
|
+
return String(error);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/** @private */
|
|
139
|
+
function ensureRecommendation(current: readonly string[], id: string): string[] {
|
|
140
|
+
if (current.includes(id)) {
|
|
141
|
+
return [...current];
|
|
142
|
+
}
|
|
143
|
+
return [...current, id];
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/** @private */
|
|
147
|
+
function readFileOrEmpty(filePath: string): string {
|
|
148
|
+
// oxlint-disable-next-line security/detect-non-literal-fs-filename -- safe: reading file from CLI discovery
|
|
149
|
+
if (existsSync(filePath)) {
|
|
150
|
+
return readFileSync(filePath, "utf8");
|
|
151
|
+
}
|
|
152
|
+
return "";
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/** @private */
|
|
156
|
+
function trailingSeparator(content: string): string {
|
|
157
|
+
if (content.length > 0 && !content.endsWith("\n")) {
|
|
158
|
+
return "\n";
|
|
159
|
+
}
|
|
160
|
+
return "";
|
|
161
|
+
}
|
|
162
|
+
|
|
144
163
|
/**
|
|
145
|
-
* Read a JSON file, returning an empty object if it doesn't exist
|
|
146
|
-
*
|
|
164
|
+
* Read a JSON file, returning an empty object if it doesn't exist.
|
|
165
|
+
* Throws if the file exists but contains invalid JSON, preventing
|
|
166
|
+
* silent data loss from overwriting malformed config files.
|
|
167
|
+
*
|
|
168
|
+
* @private
|
|
147
169
|
*/
|
|
148
170
|
function readJsonFile(filePath: string): Record<string, unknown> {
|
|
149
171
|
// oxlint-disable-next-line security/detect-non-literal-fs-filename -- safe: tsconfig path from CLI discovery
|
|
@@ -151,11 +173,15 @@ function readJsonFile(filePath: string): Record<string, unknown> {
|
|
|
151
173
|
return {};
|
|
152
174
|
}
|
|
153
175
|
|
|
176
|
+
// oxlint-disable-next-line security/detect-non-literal-fs-filename -- safe: reading tsconfig file
|
|
177
|
+
const content = readFileSync(filePath, "utf8");
|
|
154
178
|
try {
|
|
155
|
-
// oxlint-disable-next-line security/detect-non-literal-fs-filename -- safe: reading tsconfig file
|
|
156
|
-
const content = readFileSync(filePath, "utf8");
|
|
157
179
|
return JSON.parse(content) as Record<string, unknown>;
|
|
158
|
-
} catch {
|
|
159
|
-
|
|
180
|
+
} catch (error) {
|
|
181
|
+
throw new Error(
|
|
182
|
+
`Failed to parse ${filePath}: ${errorMessage(error)}. ` +
|
|
183
|
+
"Fix the JSON syntax or remove the file before running setup.",
|
|
184
|
+
{ cause: error },
|
|
185
|
+
);
|
|
160
186
|
}
|
|
161
187
|
}
|
package/src/commands/validate.ts
CHANGED
|
@@ -10,7 +10,7 @@ describe(flattenPartials, () => {
|
|
|
10
10
|
describe("param parsing", () => {
|
|
11
11
|
it("resolves a single literal param", () => {
|
|
12
12
|
const template = "{% render 'identity', role: 'Bot' %}";
|
|
13
|
-
const result = flattenPartials(template, [PARTIALS_DIR]);
|
|
13
|
+
const result = flattenPartials({ template, partialsDirs: [PARTIALS_DIR] });
|
|
14
14
|
|
|
15
15
|
expect(result).toContain("<identity>");
|
|
16
16
|
expect(result).toContain("You are Bot, .");
|
|
@@ -19,14 +19,14 @@ describe(flattenPartials, () => {
|
|
|
19
19
|
|
|
20
20
|
it("resolves multiple literal params", () => {
|
|
21
21
|
const template = "{% render 'identity', role: 'TestBot', desc: 'a test agent' %}";
|
|
22
|
-
const result = flattenPartials(template, [PARTIALS_DIR]);
|
|
22
|
+
const result = flattenPartials({ template, partialsDirs: [PARTIALS_DIR] });
|
|
23
23
|
|
|
24
24
|
expect(result).toContain("You are TestBot, a test agent.");
|
|
25
25
|
});
|
|
26
26
|
|
|
27
27
|
it("accepts an empty string as a valid literal param value", () => {
|
|
28
28
|
const template = "{% render 'identity', role: '', desc: 'helper' %}";
|
|
29
|
-
const result = flattenPartials(template, [PARTIALS_DIR]);
|
|
29
|
+
const result = flattenPartials({ template, partialsDirs: [PARTIALS_DIR] });
|
|
30
30
|
|
|
31
31
|
expect(result).toContain("You are , helper.");
|
|
32
32
|
expect(result).not.toContain("{% render");
|
|
@@ -35,7 +35,7 @@ describe(flattenPartials, () => {
|
|
|
35
35
|
it("throws when the first param uses a variable reference", () => {
|
|
36
36
|
const template = "{% render 'identity', role: agentRole, desc: 'helper' %}";
|
|
37
37
|
|
|
38
|
-
expect(() => flattenPartials(template, [PARTIALS_DIR])).toThrow(
|
|
38
|
+
expect(() => flattenPartials({ template, partialsDirs: [PARTIALS_DIR] })).toThrow(
|
|
39
39
|
'parameter "role" uses a variable reference',
|
|
40
40
|
);
|
|
41
41
|
});
|
|
@@ -43,7 +43,7 @@ describe(flattenPartials, () => {
|
|
|
43
43
|
it("throws when a non-first param uses a variable reference", () => {
|
|
44
44
|
const template = "{% render 'identity', role: 'Bot', desc: myDesc %}";
|
|
45
45
|
|
|
46
|
-
expect(() => flattenPartials(template, [PARTIALS_DIR])).toThrow(
|
|
46
|
+
expect(() => flattenPartials({ template, partialsDirs: [PARTIALS_DIR] })).toThrow(
|
|
47
47
|
'parameter "desc" uses a variable reference',
|
|
48
48
|
);
|
|
49
49
|
});
|
|
@@ -51,19 +51,21 @@ describe(flattenPartials, () => {
|
|
|
51
51
|
it("throws when all params are variable references", () => {
|
|
52
52
|
const template = "{% render 'identity', role: agentRole, desc: agentDesc %}";
|
|
53
53
|
|
|
54
|
-
expect(() => flattenPartials(template, [PARTIALS_DIR])).toThrow(
|
|
54
|
+
expect(() => flattenPartials({ template, partialsDirs: [PARTIALS_DIR] })).toThrow(
|
|
55
|
+
"uses a variable reference",
|
|
56
|
+
);
|
|
55
57
|
});
|
|
56
58
|
|
|
57
59
|
it("handles extra whitespace around colons in params", () => {
|
|
58
60
|
const template = "{% render 'identity', role : 'Bot', desc : 'helper' %}";
|
|
59
|
-
const result = flattenPartials(template, [PARTIALS_DIR]);
|
|
61
|
+
const result = flattenPartials({ template, partialsDirs: [PARTIALS_DIR] });
|
|
60
62
|
|
|
61
63
|
expect(result).toContain("You are Bot, helper.");
|
|
62
64
|
});
|
|
63
65
|
|
|
64
66
|
it("handles param values containing spaces", () => {
|
|
65
67
|
const template = "{% render 'identity', role: 'Test Bot', desc: 'a helpful assistant' %}";
|
|
66
|
-
const result = flattenPartials(template, [PARTIALS_DIR]);
|
|
68
|
+
const result = flattenPartials({ template, partialsDirs: [PARTIALS_DIR] });
|
|
67
69
|
|
|
68
70
|
expect(result).toContain("You are Test Bot, a helpful assistant.");
|
|
69
71
|
});
|
|
@@ -72,12 +74,12 @@ describe(flattenPartials, () => {
|
|
|
72
74
|
describe("render tag parsing", () => {
|
|
73
75
|
it("returns template unchanged when no render tags exist", () => {
|
|
74
76
|
const template = "<identity>\nYou are a bot.\n</identity>";
|
|
75
|
-
expect(flattenPartials(template, [PARTIALS_DIR])).toBe(template);
|
|
77
|
+
expect(flattenPartials({ template, partialsDirs: [PARTIALS_DIR] })).toBe(template);
|
|
76
78
|
});
|
|
77
79
|
|
|
78
80
|
it("parses a render tag with no params", () => {
|
|
79
81
|
const template = "{% render 'identity' %}\n\nDone.";
|
|
80
|
-
const result = flattenPartials(template, [PARTIALS_DIR]);
|
|
82
|
+
const result = flattenPartials({ template, partialsDirs: [PARTIALS_DIR] });
|
|
81
83
|
|
|
82
84
|
expect(result).toContain("<identity>");
|
|
83
85
|
expect(result).toContain("</identity>");
|
|
@@ -86,7 +88,7 @@ describe(flattenPartials, () => {
|
|
|
86
88
|
|
|
87
89
|
it("parses left-only whitespace trim {%-", () => {
|
|
88
90
|
const template = "{%- render 'identity', role: 'Bot', desc: 'helper' %}\nDone.";
|
|
89
|
-
const result = flattenPartials(template, [PARTIALS_DIR]);
|
|
91
|
+
const result = flattenPartials({ template, partialsDirs: [PARTIALS_DIR] });
|
|
90
92
|
|
|
91
93
|
expect(result).toContain("You are Bot, helper.");
|
|
92
94
|
expect(result).not.toContain("{%-");
|
|
@@ -94,7 +96,7 @@ describe(flattenPartials, () => {
|
|
|
94
96
|
|
|
95
97
|
it("parses right-only whitespace trim -%}", () => {
|
|
96
98
|
const template = "{% render 'identity', role: 'Bot', desc: 'helper' -%}\nDone.";
|
|
97
|
-
const result = flattenPartials(template, [PARTIALS_DIR]);
|
|
99
|
+
const result = flattenPartials({ template, partialsDirs: [PARTIALS_DIR] });
|
|
98
100
|
|
|
99
101
|
expect(result).toContain("You are Bot, helper.");
|
|
100
102
|
expect(result).not.toContain("-%}");
|
|
@@ -102,7 +104,7 @@ describe(flattenPartials, () => {
|
|
|
102
104
|
|
|
103
105
|
it("parses both-side whitespace trim {%- -%}", () => {
|
|
104
106
|
const template = "{%- render 'identity', role: 'Bot', desc: 'helper' -%}\nDone.";
|
|
105
|
-
const result = flattenPartials(template, [PARTIALS_DIR]);
|
|
107
|
+
const result = flattenPartials({ template, partialsDirs: [PARTIALS_DIR] });
|
|
106
108
|
|
|
107
109
|
expect(result).toContain("You are Bot, helper.");
|
|
108
110
|
expect(result).not.toContain("{%");
|
|
@@ -110,19 +112,19 @@ describe(flattenPartials, () => {
|
|
|
110
112
|
|
|
111
113
|
it("handles extra whitespace between {% and render keyword", () => {
|
|
112
114
|
const template = "{% render 'identity', role: 'Bot', desc: 'helper' %}";
|
|
113
|
-
const result = flattenPartials(template, [PARTIALS_DIR]);
|
|
115
|
+
const result = flattenPartials({ template, partialsDirs: [PARTIALS_DIR] });
|
|
114
116
|
|
|
115
117
|
expect(result).toContain("You are Bot, helper.");
|
|
116
118
|
});
|
|
117
119
|
|
|
118
120
|
it("does not match render tags with double quotes", () => {
|
|
119
121
|
const template = '{% render "identity" %}';
|
|
120
|
-
expect(flattenPartials(template, [PARTIALS_DIR])).toBe(template);
|
|
122
|
+
expect(flattenPartials({ template, partialsDirs: [PARTIALS_DIR] })).toBe(template);
|
|
121
123
|
});
|
|
122
124
|
|
|
123
125
|
it("does not match malformed render tags without closing %}", () => {
|
|
124
126
|
const template = "{% render 'identity'";
|
|
125
|
-
expect(flattenPartials(template, [PARTIALS_DIR])).toBe(template);
|
|
127
|
+
expect(flattenPartials({ template, partialsDirs: [PARTIALS_DIR] })).toBe(template);
|
|
126
128
|
});
|
|
127
129
|
});
|
|
128
130
|
|
|
@@ -130,7 +132,7 @@ describe(flattenPartials, () => {
|
|
|
130
132
|
it("flattens identity partial with literal params", () => {
|
|
131
133
|
const template =
|
|
132
134
|
"{% render 'identity', role: 'TestBot', desc: 'a test agent' %}\n\nFollow instructions.";
|
|
133
|
-
const result = flattenPartials(template, [PARTIALS_DIR]);
|
|
135
|
+
const result = flattenPartials({ template, partialsDirs: [PARTIALS_DIR] });
|
|
134
136
|
|
|
135
137
|
expect(result).toContain("<identity>");
|
|
136
138
|
expect(result).toContain("You are TestBot, a test agent.");
|
|
@@ -141,7 +143,7 @@ describe(flattenPartials, () => {
|
|
|
141
143
|
|
|
142
144
|
it("flattens constraints partial with no bindings", () => {
|
|
143
145
|
const template = "{% render 'constraints' %}";
|
|
144
|
-
const result = flattenPartials(template, [PARTIALS_DIR]);
|
|
146
|
+
const result = flattenPartials({ template, partialsDirs: [PARTIALS_DIR] });
|
|
145
147
|
|
|
146
148
|
expect(result).toContain("<constraints>");
|
|
147
149
|
expect(result).toContain("</constraints>");
|
|
@@ -153,7 +155,7 @@ describe(flattenPartials, () => {
|
|
|
153
155
|
|
|
154
156
|
it("flattens tools partial with no bindings (else branch)", () => {
|
|
155
157
|
const template = "{% render 'tools' %}";
|
|
156
|
-
const result = flattenPartials(template, [PARTIALS_DIR]);
|
|
158
|
+
const result = flattenPartials({ template, partialsDirs: [PARTIALS_DIR] });
|
|
157
159
|
|
|
158
160
|
expect(result).toContain("<tools>");
|
|
159
161
|
expect(result).toContain("</tools>");
|
|
@@ -168,7 +170,7 @@ describe(flattenPartials, () => {
|
|
|
168
170
|
"{% render 'identity', role: 'Agent', desc: 'analyzer' %}",
|
|
169
171
|
].join("\n");
|
|
170
172
|
|
|
171
|
-
const result = flattenPartials(template, [PARTIALS_DIR]);
|
|
173
|
+
const result = flattenPartials({ template, partialsDirs: [PARTIALS_DIR] });
|
|
172
174
|
|
|
173
175
|
expect(result).toContain("You are Bot, helper.");
|
|
174
176
|
expect(result).toContain("You are Agent, analyzer.");
|
|
@@ -178,7 +180,7 @@ describe(flattenPartials, () => {
|
|
|
178
180
|
it("preserves surrounding markdown content", () => {
|
|
179
181
|
const template =
|
|
180
182
|
"# System Prompt\n\n{% render 'identity', role: 'Bot', desc: 'helper' %}\n\n## Instructions\n\nDo the thing.";
|
|
181
|
-
const result = flattenPartials(template, [PARTIALS_DIR]);
|
|
183
|
+
const result = flattenPartials({ template, partialsDirs: [PARTIALS_DIR] });
|
|
182
184
|
|
|
183
185
|
expect(result).toMatch(/^# System Prompt/);
|
|
184
186
|
expect(result).toContain("You are Bot, helper.");
|
|
@@ -188,13 +190,13 @@ describe(flattenPartials, () => {
|
|
|
188
190
|
it("throws when partial file does not exist", () => {
|
|
189
191
|
const template = "{% render 'nonexistent' %}";
|
|
190
192
|
|
|
191
|
-
expect(() => flattenPartials(template, [PARTIALS_DIR])).toThrow();
|
|
193
|
+
expect(() => flattenPartials({ template, partialsDirs: [PARTIALS_DIR] })).toThrow();
|
|
192
194
|
});
|
|
193
195
|
|
|
194
196
|
it("searches multiple partialsDirs in order", () => {
|
|
195
197
|
const emptyDir = resolve(import.meta.dirname);
|
|
196
198
|
const template = "{% render 'identity', role: 'Bot', desc: 'test' %}";
|
|
197
|
-
const result = flattenPartials(template, [emptyDir, PARTIALS_DIR]);
|
|
199
|
+
const result = flattenPartials({ template, partialsDirs: [emptyDir, PARTIALS_DIR] });
|
|
198
200
|
|
|
199
201
|
expect(result).toContain("You are Bot, test.");
|
|
200
202
|
});
|
|
@@ -203,13 +205,13 @@ describe(flattenPartials, () => {
|
|
|
203
205
|
describe("template preservation", () => {
|
|
204
206
|
it("preserves {{ var }} and {% if %} expressions", () => {
|
|
205
207
|
const template = "Hello {{ name }}.\n{% if context %}{{ context }}{% endif %}";
|
|
206
|
-
expect(flattenPartials(template, [PARTIALS_DIR])).toBe(template);
|
|
208
|
+
expect(flattenPartials({ template, partialsDirs: [PARTIALS_DIR] })).toBe(template);
|
|
207
209
|
});
|
|
208
210
|
|
|
209
211
|
it("flattens render tag while preserving surrounding Liquid blocks", () => {
|
|
210
212
|
const template =
|
|
211
213
|
"{% if show_identity %}\n{% render 'identity', role: 'Bot', desc: 'helper' %}\n{% endif %}\n\n{{ instructions }}";
|
|
212
|
-
const result = flattenPartials(template, [PARTIALS_DIR]);
|
|
214
|
+
const result = flattenPartials({ template, partialsDirs: [PARTIALS_DIR] });
|
|
213
215
|
|
|
214
216
|
expect(result).toContain("{% if show_identity %}");
|
|
215
217
|
expect(result).toContain("{% endif %}");
|
|
@@ -219,12 +221,12 @@ describe(flattenPartials, () => {
|
|
|
219
221
|
});
|
|
220
222
|
|
|
221
223
|
it("returns empty string unchanged", () => {
|
|
222
|
-
expect(flattenPartials("", [PARTIALS_DIR])).toBe("");
|
|
224
|
+
expect(flattenPartials({ template: "", partialsDirs: [PARTIALS_DIR] })).toBe("");
|
|
223
225
|
});
|
|
224
226
|
|
|
225
227
|
it("returns whitespace-only template unchanged", () => {
|
|
226
228
|
const ws = " \n\n ";
|
|
227
|
-
expect(flattenPartials(ws, [PARTIALS_DIR])).toBe(ws);
|
|
229
|
+
expect(flattenPartials({ template: ws, partialsDirs: [PARTIALS_DIR] })).toBe(ws);
|
|
228
230
|
});
|
|
229
231
|
});
|
|
230
232
|
});
|