@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.
@@ -1,35 +1,45 @@
1
1
  import { existsSync, lstatSync, readdirSync, readFileSync } from "node:fs";
2
2
  import { basename, extname, join, resolve } from "node:path";
3
3
 
4
+ import { parse as parseYaml } from "yaml";
5
+
4
6
  import { FRONTMATTER_RE, NAME_RE } from "./frontmatter.js";
5
7
 
6
8
  const MAX_DEPTH = 5;
7
9
  const PROMPT_EXT = ".prompt";
8
10
 
11
+ /** A `.prompt` file discovered during directory scanning. */
9
12
  export interface DiscoveredPrompt {
10
13
  readonly name: string;
11
14
  readonly filePath: string;
12
15
  }
13
16
 
17
+ // ---------------------------------------------------------------------------
18
+ // Private
19
+ // ---------------------------------------------------------------------------
20
+
14
21
  /**
15
22
  * Extract the `name` field from YAML frontmatter.
16
23
  *
17
- * This is a lightweight extraction that avoids pulling in a full YAML parser.
18
- * It looks for `name: <value>` in the frontmatter block.
24
+ * Uses a proper YAML parser to handle quoted values and edge cases.
25
+ *
26
+ * @private
19
27
  */
20
28
  function extractName(content: string): string | undefined {
21
- const match = content.match(FRONTMATTER_RE);
22
- if (!match) {
29
+ const fmMatch = content.match(FRONTMATTER_RE);
30
+ if (!fmMatch) {
23
31
  return undefined;
24
32
  }
25
33
 
26
- const [, frontmatter] = match;
27
- const nameLine = frontmatter.split("\n").find((line) => line.startsWith("name:"));
28
- if (!nameLine) {
34
+ try {
35
+ const parsed = parseYaml(fmMatch[1]) as Record<string, unknown> | null;
36
+ if (parsed !== null && parsed !== undefined && typeof parsed.name === "string") {
37
+ return parsed.name;
38
+ }
39
+ return undefined;
40
+ } catch {
29
41
  return undefined;
30
42
  }
31
-
32
- return nameLine.slice("name:".length).trim();
33
43
  }
34
44
 
35
45
  /**
@@ -37,6 +47,8 @@ function extractName(content: string): string | undefined {
37
47
  *
38
48
  * If the file is named `prompt.prompt`, uses the parent directory name.
39
49
  * Otherwise uses the file stem (e.g. `my-agent.prompt` -> `my-agent`).
50
+ *
51
+ * @private
40
52
  */
41
53
  function deriveNameFromPath(filePath: string): string {
42
54
  const stem = basename(filePath, PROMPT_EXT);
@@ -48,6 +60,8 @@ function deriveNameFromPath(filePath: string): string {
48
60
 
49
61
  /**
50
62
  * Recursively scan a directory for `.prompt` files.
63
+ *
64
+ * @private
51
65
  */
52
66
  function scanDirectory(dir: string, depth: number): DiscoveredPrompt[] {
53
67
  if (depth > MAX_DEPTH) {
@@ -102,7 +116,7 @@ function scanDirectory(dir: string, depth: number): DiscoveredPrompt[] {
102
116
  * @returns Sorted, deduplicated list of discovered prompts.
103
117
  * @throws If duplicate prompt names are found across roots.
104
118
  */
105
- export function discoverPrompts(roots: readonly string[]): DiscoveredPrompt[] {
119
+ export function discoverPrompts(roots: readonly string[]): readonly DiscoveredPrompt[] {
106
120
  const all = roots.flatMap((root) => scanDirectory(resolve(root), 0));
107
121
 
108
122
  const byName = Map.groupBy(all, (prompt) => prompt.name);
@@ -14,6 +14,7 @@ import { discoverPrompts } from "./paths.js";
14
14
  /**
15
15
  * Resolve the list of partial directories to search.
16
16
  *
17
+ * @private
17
18
  * @param customDir - Custom partials directory path.
18
19
  * @returns Array of directories to search for partials.
19
20
  */
@@ -55,8 +56,8 @@ export function runLintPipeline(options: LintPipelineOptions): LintPipelineResul
55
56
  const results = discovered.map((d) => {
56
57
  // oxlint-disable-next-line security/detect-non-literal-fs-filename -- safe: reading discovered prompt file
57
58
  const raw = readFileSync(d.filePath, "utf8");
58
- const frontmatter = parseFrontmatter(raw, d.filePath);
59
- const template = flattenPartials(clean(raw), partialsDirs);
59
+ const frontmatter = parseFrontmatter({ content: raw, filePath: d.filePath });
60
+ const template = flattenPartials({ template: clean(raw), partialsDirs });
60
61
  const templateVars = extractVariables(template);
61
62
  return lintPrompt(frontmatter.name, d.filePath, frontmatter.schema, templateVars);
62
63
  });
@@ -98,8 +99,8 @@ export function runGeneratePipeline(options: GeneratePipelineOptions): GenerateP
98
99
  const processed = discovered.map((d) => {
99
100
  // oxlint-disable-next-line security/detect-non-literal-fs-filename -- safe: reading discovered prompt file
100
101
  const raw = readFileSync(d.filePath, "utf8");
101
- const frontmatter = parseFrontmatter(raw, d.filePath);
102
- const template = flattenPartials(clean(raw), partialsDirs);
102
+ const frontmatter = parseFrontmatter({ content: raw, filePath: d.filePath });
103
+ const template = flattenPartials({ template: clean(raw), partialsDirs });
103
104
  const templateVars = extractVariables(template);
104
105
 
105
106
  return {