@funkai/cli 0.1.4 → 0.3.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@funkai/cli",
3
- "version": "0.1.4",
3
+ "version": "0.3.0",
4
4
  "private": false,
5
5
  "description": "CLI for the funkai AI SDK framework",
6
6
  "keywords": [
@@ -30,15 +30,19 @@
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",
35
+ "picomatch": "^4.0.3",
34
36
  "ts-pattern": "^5.9.0",
35
37
  "yaml": "^2.8.2",
36
38
  "zod": "^4.3.6",
37
- "@funkai/prompts": "0.2.0"
39
+ "@funkai/config": "0.2.0",
40
+ "@funkai/prompts": "0.4.0"
38
41
  },
39
42
  "devDependencies": {
40
43
  "@kidd-cli/cli": "^0.4.9",
41
44
  "@types/node": "^25.5.0",
45
+ "@types/picomatch": "^4.0.2",
42
46
  "@vitest/coverage-v8": "^4.1.0",
43
47
  "tsdown": "^0.21.4",
44
48
  "typescript": "^5.9.3",
@@ -1,19 +1,26 @@
1
1
  import { command } from "@kidd-cli/core";
2
2
 
3
3
  import { generateArgs, handleGenerate } from "./prompts/generate.js";
4
+ import { getConfig } from "@/config.js";
4
5
 
5
6
  export default command({
6
7
  description: "Run all code generation across the funkai SDK",
7
8
  options: generateArgs,
8
9
  handler(ctx) {
9
10
  const { silent } = ctx.args;
11
+ const config = getConfig(ctx);
10
12
 
11
13
  // --- Prompts codegen ---
12
14
  if (!silent) {
13
15
  ctx.logger.info("Running prompts code generation...");
14
16
  }
15
17
 
16
- handleGenerate(ctx.args, ctx.logger, ctx.fail);
18
+ handleGenerate({
19
+ args: ctx.args,
20
+ config: config.prompts,
21
+ logger: ctx.logger,
22
+ fail: ctx.fail,
23
+ });
17
24
 
18
25
  // --- Future: agents codegen ---
19
26
  },
@@ -5,6 +5,9 @@ import { command } from "@kidd-cli/core";
5
5
  import { match, P } from "ts-pattern";
6
6
  import { z } from "zod";
7
7
 
8
+ import { getConfig } from "@/config.js";
9
+
10
+ /** @private */
8
11
  const createTemplate = (name: string) => `---
9
12
  name: ${name}
10
13
  ---
@@ -15,15 +18,41 @@ export default command({
15
18
  description: "Create a new .prompt file",
16
19
  options: z.object({
17
20
  name: z.string().describe("Prompt name (kebab-case)"),
18
- out: z.string().optional().describe("Output directory (defaults to cwd)"),
21
+ out: z
22
+ .string()
23
+ .optional()
24
+ .describe("Output directory (defaults to first root in config or cwd)"),
19
25
  partial: z.boolean().default(false).describe("Create as a partial in .prompts/partials/"),
20
26
  }),
21
27
  handler(ctx) {
22
28
  const { name, out, partial } = ctx.args;
29
+ const config = getConfig(ctx);
30
+ const promptsConfig = config.prompts;
31
+ const firstInclude = match(promptsConfig)
32
+ .with({ includes: P.array(P.string).select() }, (includes) => {
33
+ if (includes.length > 0) {
34
+ // Extract the static base directory from the first include pattern
35
+ const [pattern] = includes;
36
+ const parts = pattern.split("/");
37
+ const staticParts = parts.filter((p) => !p.includes("*") && !p.includes("?"));
38
+ if (staticParts.length > 0) {
39
+ return staticParts.join("/");
40
+ }
41
+ return undefined;
42
+ }
43
+ return undefined;
44
+ })
45
+ .otherwise(() => undefined);
46
+
23
47
  const dir = match({ partial, out })
24
48
  .with({ partial: true }, () => resolve(".prompts/partials"))
25
49
  .with({ out: P.string }, ({ out: outDir }) => resolve(outDir))
26
- .otherwise(() => process.cwd());
50
+ .otherwise(() => {
51
+ if (firstInclude) {
52
+ return resolve(firstInclude);
53
+ }
54
+ return process.cwd();
55
+ });
27
56
  const filePath = resolve(dir, `${name}.prompt`);
28
57
 
29
58
  // oxlint-disable-next-line security/detect-non-literal-fs-filename -- safe: user-provided CLI argument for prompt file creation
@@ -1,48 +1,94 @@
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
5
  import { command } from "@kidd-cli/core";
6
+ import { match } from "ts-pattern";
5
7
  import { z } from "zod";
6
8
 
7
- import { generatePromptModule, generateRegistry } from "@/lib/prompts/codegen.js";
9
+ import { getConfig } from "@/config.js";
10
+ import { generatePromptModule, generateRegistry, toFileSlug } from "@/lib/prompts/codegen.js";
8
11
  import { hasLintErrors } from "@/lib/prompts/lint.js";
9
12
  import { runGeneratePipeline } from "@/lib/prompts/pipeline.js";
10
13
 
14
+ /** Zod schema for the `prompts generate` CLI arguments. */
11
15
  export const generateArgs = z.object({
12
- out: z.string().describe("Output directory for generated files"),
13
- roots: z.array(z.string()).describe("Root directories to scan for .prompt files"),
16
+ out: z.string().optional().describe("Output directory for generated files"),
17
+ includes: z.array(z.string()).optional().describe("Glob patterns to scan for .prompt files"),
14
18
  partials: z.string().optional().describe("Custom partials directory"),
15
19
  silent: z.boolean().default(false).describe("Suppress output except errors"),
16
20
  });
17
21
 
22
+ /** Inferred type of the `prompts generate` CLI arguments. */
18
23
  export type GenerateArgs = z.infer<typeof generateArgs>;
19
24
 
20
25
  /**
21
- * Shared handler for prompts code generation.
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.
26
+ * Parameters for the shared generate handler.
26
27
  */
27
- export function handleGenerate(
28
- args: {
29
- readonly out: string;
30
- readonly roots: readonly string[];
28
+ export interface HandleGenerateParams {
29
+ readonly args: {
30
+ readonly out?: string;
31
+ readonly includes?: readonly string[];
31
32
  readonly partials?: string;
32
33
  readonly silent: boolean;
33
- },
34
- logger: {
34
+ };
35
+ readonly config?: FunkaiConfig["prompts"];
36
+ readonly logger: {
35
37
  info: (msg: string) => void;
36
38
  step: (msg: string) => void;
37
39
  error: (msg: string) => void;
38
40
  warn: (msg: string) => void;
39
41
  success: (msg: string) => void;
40
- },
42
+ };
43
+ readonly fail: (msg: string) => never;
44
+ }
45
+
46
+ /**
47
+ * Resolve generate args by merging CLI flags with config defaults.
48
+ *
49
+ * @param args - CLI arguments (take precedence).
50
+ * @param config - Prompts config from funkai.config.ts (fallback).
51
+ * @param fail - Error handler for missing required values.
52
+ * @returns Resolved args with required fields guaranteed.
53
+ */
54
+ function resolveGenerateArgs(
55
+ args: HandleGenerateParams["args"],
56
+ config: FunkaiConfig["prompts"],
41
57
  fail: (msg: string) => never,
42
- ): void {
43
- const { out, roots, partials, silent } = args;
58
+ ): {
59
+ readonly out: string;
60
+ readonly includes: readonly string[];
61
+ readonly excludes: readonly string[];
62
+ readonly partials?: string;
63
+ readonly silent: boolean;
64
+ } {
65
+ const out = args.out ?? (config && config.out);
66
+ const includes = args.includes ?? (config && config.includes) ?? ["./**"];
67
+ const excludes = (config && config.excludes) ?? [];
68
+ const partials = args.partials ?? (config && config.partials);
69
+
70
+ if (!out) {
71
+ fail("Missing --out flag. Provide it via CLI or set prompts.out in funkai.config.ts.");
72
+ }
73
+
74
+ return { out, includes, excludes, partials, silent: args.silent };
75
+ }
76
+
77
+ /**
78
+ * Shared handler for prompts code generation.
79
+ *
80
+ * @param params - Handler context with args, config, logger, and fail callback.
81
+ */
82
+ export function handleGenerate({ args, config, logger, fail }: HandleGenerateParams): void {
83
+ const { out, includes, excludes, partials, silent } = resolveGenerateArgs(args, config, fail);
44
84
 
45
- const { discovered, lintResults, prompts } = runGeneratePipeline({ roots, out, partials });
85
+ const { discovered, lintResults, prompts } = runGeneratePipeline({
86
+ includes,
87
+ excludes,
88
+ out,
89
+ partials,
90
+ groups: config && config.groups,
91
+ });
46
92
 
47
93
  if (!silent) {
48
94
  logger.info(`Found ${discovered} prompt(s)`);
@@ -50,25 +96,20 @@ export function handleGenerate(
50
96
 
51
97
  if (!silent) {
52
98
  for (const prompt of prompts) {
53
- // oxlint-disable-next-line unicorn/prefer-ternary -- no-ternary rule forbids ternaries
54
- const varList: string = (() => {
55
- if (prompt.schema.length > 0) {
56
- return ` (${prompt.schema.map((v) => v.name).join(", ")})`;
57
- }
58
- return "";
59
- })();
99
+ const varList = formatVarList(prompt.schema);
60
100
  logger.step(`${prompt.name}${varList}`);
61
101
  }
62
102
  }
63
103
 
64
- for (const result of lintResults) {
65
- for (const diag of result.diagnostics) {
66
- if (diag.level === "error") {
67
- logger.error(diag.message);
68
- } else {
69
- logger.warn(diag.message);
70
- }
71
- }
104
+ for (const diag of lintResults.flatMap((result) => result.diagnostics)) {
105
+ match(diag.level)
106
+ .with("error", () => logger.error(diag.message))
107
+ .with("warn", () => {
108
+ if (!silent) {
109
+ logger.warn(diag.message);
110
+ }
111
+ })
112
+ .exhaustive();
72
113
  }
73
114
 
74
115
  if (hasLintErrors(lintResults)) {
@@ -81,8 +122,9 @@ export function handleGenerate(
81
122
 
82
123
  for (const prompt of prompts) {
83
124
  const content = generatePromptModule(prompt);
125
+ const fileSlug = toFileSlug(prompt.name, prompt.group);
84
126
  // oxlint-disable-next-line security/detect-non-literal-fs-filename -- safe: writing generated module to output directory
85
- writeFileSync(resolve(outDir, `${prompt.name}.ts`), content, "utf8");
127
+ writeFileSync(resolve(outDir, `${fileSlug}.ts`), content, "utf8");
86
128
  }
87
129
 
88
130
  const registryContent = generateRegistry(prompts);
@@ -94,10 +136,24 @@ export function handleGenerate(
94
136
  }
95
137
  }
96
138
 
139
+ /** @private */
140
+ function formatVarList(schema: readonly { readonly name: string }[]): string {
141
+ if (schema.length > 0) {
142
+ return ` (${schema.map((v) => v.name).join(", ")})`;
143
+ }
144
+ return "";
145
+ }
146
+
97
147
  export default command({
98
148
  description: "Generate TypeScript modules from .prompt files",
99
149
  options: generateArgs,
100
150
  handler(ctx) {
101
- handleGenerate(ctx.args, ctx.logger, ctx.fail);
151
+ const config = getConfig(ctx);
152
+ handleGenerate({
153
+ args: ctx.args,
154
+ config: config.prompts,
155
+ logger: ctx.logger,
156
+ fail: ctx.fail,
157
+ });
102
158
  },
103
159
  });
@@ -1,36 +1,74 @@
1
+ import type { FunkaiConfig } from "@funkai/config";
1
2
  import { command } from "@kidd-cli/core";
3
+ import { match } from "ts-pattern";
2
4
  import { z } from "zod";
3
5
 
6
+ import { getConfig } from "@/config.js";
4
7
  import { hasLintErrors } from "@/lib/prompts/lint.js";
5
8
  import { runLintPipeline } from "@/lib/prompts/pipeline.js";
6
9
 
10
+ /** Zod schema for the `prompts lint` CLI arguments. */
7
11
  export const lintArgs = z.object({
8
- roots: z.array(z.string()).describe("Root directories to scan for .prompt files"),
12
+ includes: z.array(z.string()).optional().describe("Glob patterns to scan for .prompt files"),
9
13
  partials: z.string().optional().describe("Custom partials directory"),
10
14
  silent: z.boolean().default(false).describe("Suppress output except errors"),
11
15
  });
12
16
 
17
+ /** Inferred type of the `prompts lint` CLI arguments. */
13
18
  export type LintArgs = z.infer<typeof lintArgs>;
14
19
 
15
20
  /**
16
- * Shared handler for prompts lint/validation.
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.
21
+ * Parameters for the shared lint handler.
21
22
  */
22
- export function handleLint(
23
- args: { readonly roots: readonly string[]; readonly partials?: string; readonly silent: boolean },
24
- logger: {
23
+ export interface HandleLintParams {
24
+ readonly args: {
25
+ readonly includes?: readonly string[];
26
+ readonly partials?: string;
27
+ readonly silent: boolean;
28
+ };
29
+ readonly config?: FunkaiConfig["prompts"];
30
+ readonly logger: {
25
31
  info: (msg: string) => void;
26
32
  error: (msg: string) => void;
27
33
  warn: (msg: string) => void;
28
- },
29
- fail: (msg: string) => never,
30
- ): void {
31
- const { roots, partials, silent } = args;
34
+ };
35
+ readonly fail: (msg: string) => never;
36
+ }
37
+
38
+ /**
39
+ * Resolve lint args by merging CLI flags with config defaults.
40
+ *
41
+ * @param args - CLI arguments (take precedence).
42
+ * @param config - Prompts config from funkai.config.ts (fallback).
43
+ * @param fail - Error handler for missing required values.
44
+ * @returns Resolved args with required fields guaranteed.
45
+ */
46
+ function resolveLintArgs(
47
+ args: HandleLintParams["args"],
48
+ config: FunkaiConfig["prompts"],
49
+ _fail: (msg: string) => never,
50
+ ): {
51
+ readonly includes: readonly string[];
52
+ readonly excludes: readonly string[];
53
+ readonly partials?: string;
54
+ readonly silent: boolean;
55
+ } {
56
+ const includes = args.includes ?? (config && config.includes) ?? ["./**"];
57
+ const excludes = (config && config.excludes) ?? [];
58
+ const partials = args.partials ?? (config && config.partials);
32
59
 
33
- const { discovered, results } = runLintPipeline({ roots, partials });
60
+ return { includes, excludes, partials, silent: args.silent };
61
+ }
62
+
63
+ /**
64
+ * Shared handler for prompts lint/validation.
65
+ *
66
+ * @param params - Handler context with args, config, logger, and fail callback.
67
+ */
68
+ export function handleLint({ args, config, logger, fail }: HandleLintParams): void {
69
+ const { includes, excludes, partials, silent } = resolveLintArgs(args, config, fail);
70
+
71
+ const { discovered, results } = runLintPipeline({ includes, excludes, partials });
34
72
 
35
73
  if (!silent) {
36
74
  logger.info(`Linting ${discovered} prompt(s)...`);
@@ -39,11 +77,10 @@ export function handleLint(
39
77
  const diagnostics = results.flatMap((result) => result.diagnostics);
40
78
 
41
79
  for (const diag of diagnostics) {
42
- if (diag.level === "error") {
43
- logger.error(diag.message);
44
- } else {
45
- logger.warn(diag.message);
46
- }
80
+ match(diag.level)
81
+ .with("error", () => logger.error(diag.message))
82
+ .with("warn", () => logger.warn(diag.message))
83
+ .exhaustive();
47
84
  }
48
85
 
49
86
  const errorCount = diagnostics.filter((d) => d.level === "error").length;
@@ -71,6 +108,12 @@ export default command({
71
108
  description: "Validate .prompt files for schema/template mismatches",
72
109
  options: lintArgs,
73
110
  handler(ctx) {
74
- handleLint(ctx.args, ctx.logger, ctx.fail);
111
+ const config = getConfig(ctx);
112
+ handleLint({
113
+ args: ctx.args,
114
+ config: config.prompts,
115
+ logger: ctx.logger,
116
+ fail: ctx.fail,
117
+ });
75
118
  },
76
119
  });