@funkai/cli 0.1.1 → 0.1.2
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/dist/index.mjs +5018 -298
- package/dist/index.mjs.map +1 -1
- package/package.json +5 -5
- package/src/commands/generate.ts +4 -62
- package/src/commands/prompts/generate.ts +75 -48
- package/src/commands/prompts/lint.ts +62 -44
- package/src/commands/prompts/setup.ts +2 -7
- package/src/commands/validate.ts +4 -27
- package/src/index.ts +6 -1
- package/src/lib/prompts/codegen.ts +12 -16
- package/src/lib/prompts/flatten.ts +2 -7
- package/src/lib/prompts/frontmatter.ts +28 -33
- package/src/lib/prompts/lint.ts +8 -8
- package/src/lib/prompts/paths.ts +5 -5
- package/src/lib/prompts/pipeline.ts +6 -7
|
@@ -1,8 +1,7 @@
|
|
|
1
|
-
import { match } from "ts-pattern";
|
|
2
1
|
import { parse as parseYaml } from "yaml";
|
|
3
2
|
|
|
4
|
-
const FRONTMATTER_RE = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?/;
|
|
5
|
-
const NAME_RE = /^[a-z0-9-]+$/;
|
|
3
|
+
export const FRONTMATTER_RE = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?/;
|
|
4
|
+
export const NAME_RE = /^[a-z0-9-]+$/;
|
|
6
5
|
|
|
7
6
|
/**
|
|
8
7
|
* Parse raw YAML content into a record, wrapping parse errors
|
|
@@ -26,20 +25,20 @@ function parseYamlContent(yaml: string, filePath: string): Record<string, unknow
|
|
|
26
25
|
* A variable declared in the frontmatter `schema` block.
|
|
27
26
|
*/
|
|
28
27
|
export interface SchemaVariable {
|
|
29
|
-
name: string;
|
|
30
|
-
type: string;
|
|
31
|
-
required: boolean;
|
|
32
|
-
description?: string;
|
|
28
|
+
readonly name: string;
|
|
29
|
+
readonly type: string;
|
|
30
|
+
readonly required: boolean;
|
|
31
|
+
readonly description?: string;
|
|
33
32
|
}
|
|
34
33
|
|
|
35
34
|
/**
|
|
36
35
|
* Parsed frontmatter from a `.prompt` file.
|
|
37
36
|
*/
|
|
38
37
|
export interface ParsedFrontmatter {
|
|
39
|
-
name: string;
|
|
40
|
-
group?: string;
|
|
41
|
-
version?: string;
|
|
42
|
-
schema: SchemaVariable[];
|
|
38
|
+
readonly name: string;
|
|
39
|
+
readonly group?: string;
|
|
40
|
+
readonly version?: string;
|
|
41
|
+
readonly schema: readonly SchemaVariable[];
|
|
43
42
|
}
|
|
44
43
|
|
|
45
44
|
/**
|
|
@@ -77,22 +76,21 @@ export function parseFrontmatter(content: string, filePath: string): ParsedFront
|
|
|
77
76
|
);
|
|
78
77
|
}
|
|
79
78
|
|
|
80
|
-
const group =
|
|
81
|
-
.
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
.otherwise(() => undefined);
|
|
79
|
+
const group =
|
|
80
|
+
typeof parsed.group === "string"
|
|
81
|
+
? (() => {
|
|
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
|
+
: undefined;
|
|
93
|
+
const version = parsed.version != null ? String(parsed.version) : undefined;
|
|
96
94
|
|
|
97
95
|
const schema = parseSchemaBlock(parsed.schema, filePath);
|
|
98
96
|
|
|
@@ -124,13 +122,10 @@ function parseSchemaBlock(raw: unknown, filePath: string): SchemaVariable[] {
|
|
|
124
122
|
|
|
125
123
|
if (typeof value === "object" && value !== null && !Array.isArray(value)) {
|
|
126
124
|
const def = value as Record<string, unknown>;
|
|
127
|
-
const type =
|
|
128
|
-
.with(true, () => def.type as string)
|
|
129
|
-
.otherwise(() => "string");
|
|
125
|
+
const type = typeof def.type === "string" ? (def.type as string) : "string";
|
|
130
126
|
const required = def.required !== false;
|
|
131
|
-
const description =
|
|
132
|
-
.
|
|
133
|
-
.otherwise(() => undefined);
|
|
127
|
+
const description =
|
|
128
|
+
typeof def.description === "string" ? (def.description as string) : undefined;
|
|
134
129
|
|
|
135
130
|
return { name: varName, type, required, description };
|
|
136
131
|
}
|
package/src/lib/prompts/lint.ts
CHANGED
|
@@ -4,17 +4,17 @@ import type { SchemaVariable } from "./frontmatter.js";
|
|
|
4
4
|
* A single lint diagnostic.
|
|
5
5
|
*/
|
|
6
6
|
export interface LintDiagnostic {
|
|
7
|
-
level: "error" | "warn";
|
|
8
|
-
message: string;
|
|
7
|
+
readonly level: "error" | "warn";
|
|
8
|
+
readonly message: string;
|
|
9
9
|
}
|
|
10
10
|
|
|
11
11
|
/**
|
|
12
12
|
* Result of linting a single prompt file.
|
|
13
13
|
*/
|
|
14
14
|
export interface LintResult {
|
|
15
|
-
name: string;
|
|
16
|
-
filePath: string;
|
|
17
|
-
diagnostics: LintDiagnostic[];
|
|
15
|
+
readonly name: string;
|
|
16
|
+
readonly filePath: string;
|
|
17
|
+
readonly diagnostics: readonly LintDiagnostic[];
|
|
18
18
|
}
|
|
19
19
|
|
|
20
20
|
/**
|
|
@@ -33,8 +33,8 @@ export interface LintResult {
|
|
|
33
33
|
export function lintPrompt(
|
|
34
34
|
name: string,
|
|
35
35
|
filePath: string,
|
|
36
|
-
schemaVars: SchemaVariable[],
|
|
37
|
-
templateVars: string[],
|
|
36
|
+
schemaVars: readonly SchemaVariable[],
|
|
37
|
+
templateVars: readonly string[],
|
|
38
38
|
): LintResult {
|
|
39
39
|
const diagnostics: LintDiagnostic[] = [];
|
|
40
40
|
const declared = new Set(schemaVars.map((v) => v.name));
|
|
@@ -69,6 +69,6 @@ export function lintPrompt(
|
|
|
69
69
|
/**
|
|
70
70
|
* Check whether any lint results contain errors.
|
|
71
71
|
*/
|
|
72
|
-
export function hasLintErrors(results: LintResult[]): boolean {
|
|
72
|
+
export function hasLintErrors(results: readonly LintResult[]): boolean {
|
|
73
73
|
return results.some((r) => r.diagnostics.some((d) => d.level === "error"));
|
|
74
74
|
}
|
package/src/lib/prompts/paths.ts
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
import { existsSync, lstatSync, readdirSync, readFileSync } from "node:fs";
|
|
2
2
|
import { basename, extname, join, resolve } from "node:path";
|
|
3
3
|
|
|
4
|
+
import { FRONTMATTER_RE, NAME_RE } from "./frontmatter.js";
|
|
5
|
+
|
|
4
6
|
const MAX_DEPTH = 5;
|
|
5
7
|
const PROMPT_EXT = ".prompt";
|
|
6
|
-
const FRONTMATTER_RE = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?/;
|
|
7
|
-
const NAME_RE = /^[a-z0-9-]+$/;
|
|
8
8
|
|
|
9
9
|
export interface DiscoveredPrompt {
|
|
10
|
-
name: string;
|
|
11
|
-
filePath: string;
|
|
10
|
+
readonly name: string;
|
|
11
|
+
readonly filePath: string;
|
|
12
12
|
}
|
|
13
13
|
|
|
14
14
|
/**
|
|
@@ -102,7 +102,7 @@ function scanDirectory(dir: string, depth: number): DiscoveredPrompt[] {
|
|
|
102
102
|
* @returns Sorted, deduplicated list of discovered prompts.
|
|
103
103
|
* @throws If duplicate prompt names are found across roots.
|
|
104
104
|
*/
|
|
105
|
-
export function discoverPrompts(roots: string[]): DiscoveredPrompt[] {
|
|
105
|
+
export function discoverPrompts(roots: readonly string[]): DiscoveredPrompt[] {
|
|
106
106
|
const all = roots.flatMap((root) => scanDirectory(resolve(root), 0));
|
|
107
107
|
|
|
108
108
|
const byName = Map.groupBy(all, (prompt) => prompt.name);
|
|
@@ -2,7 +2,6 @@ import { existsSync, readFileSync } from "node:fs";
|
|
|
2
2
|
import { resolve } from "node:path";
|
|
3
3
|
|
|
4
4
|
import { clean, PARTIALS_DIR } from "@funkai/prompts";
|
|
5
|
-
import { match } from "ts-pattern";
|
|
6
5
|
|
|
7
6
|
import { type ParsedPrompt } from "./codegen.js";
|
|
8
7
|
import { extractVariables } from "./extract-variables.js";
|
|
@@ -37,9 +36,9 @@ export function runLintPipeline(options: LintPipelineOptions): LintPipelineResul
|
|
|
37
36
|
const discovered = discoverPrompts([...options.roots]);
|
|
38
37
|
const customPartialsDir = resolve(options.partials ?? ".prompts/partials");
|
|
39
38
|
// oxlint-disable-next-line security/detect-non-literal-fs-filename -- safe: checking custom partials directory from CLI config
|
|
40
|
-
const partialsDirs =
|
|
41
|
-
|
|
42
|
-
|
|
39
|
+
const partialsDirs = existsSync(customPartialsDir)
|
|
40
|
+
? [customPartialsDir, PARTIALS_DIR]
|
|
41
|
+
: [PARTIALS_DIR];
|
|
43
42
|
|
|
44
43
|
const results = discovered.map((d) => {
|
|
45
44
|
// oxlint-disable-next-line security/detect-non-literal-fs-filename -- safe: reading discovered prompt file
|
|
@@ -83,9 +82,9 @@ export function runGeneratePipeline(options: GeneratePipelineOptions): GenerateP
|
|
|
83
82
|
const discovered = discoverPrompts([...options.roots]);
|
|
84
83
|
const customPartialsDir = resolve(options.partials ?? resolve(options.out, "../partials"));
|
|
85
84
|
// oxlint-disable-next-line security/detect-non-literal-fs-filename -- safe: checking custom partials directory from CLI config
|
|
86
|
-
const partialsDirs =
|
|
87
|
-
|
|
88
|
-
|
|
85
|
+
const partialsDirs = existsSync(customPartialsDir)
|
|
86
|
+
? [customPartialsDir, PARTIALS_DIR]
|
|
87
|
+
: [PARTIALS_DIR];
|
|
89
88
|
|
|
90
89
|
const processed = discovered.map((d) => {
|
|
91
90
|
// oxlint-disable-next-line security/detect-non-literal-fs-filename -- safe: reading discovered prompt file
|