@funkai/cli 0.1.1 → 0.1.3

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,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 = match(typeof parsed.group === "string")
81
- .with(true, () => {
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
- .otherwise(() => undefined);
93
- const version = match(parsed.version != null)
94
- .with(true, () => String(parsed.version))
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 = match(typeof def.type === "string")
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 = match(typeof def.description === "string")
132
- .with(true, () => def.description as string)
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
  }
@@ -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
  }
@@ -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);
@@ -1,8 +1,7 @@
1
1
  import { existsSync, readFileSync } from "node:fs";
2
2
  import { resolve } from "node:path";
3
3
 
4
- import { clean, PARTIALS_DIR } from "@funkai/prompts";
5
- import { match } from "ts-pattern";
4
+ import { clean, PARTIALS_DIR } from "@funkai/prompts/cli";
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 = match(existsSync(customPartialsDir))
41
- .with(true, () => [customPartialsDir, PARTIALS_DIR])
42
- .otherwise(() => [PARTIALS_DIR]);
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 = match(existsSync(customPartialsDir))
87
- .with(true, () => [customPartialsDir, PARTIALS_DIR])
88
- .otherwise(() => [PARTIALS_DIR]);
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