@funkai/cli 0.1.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.
Files changed (55) hide show
  1. package/.turbo/turbo-build.log +16 -0
  2. package/.turbo/turbo-test$colon$coverage.log +36 -0
  3. package/.turbo/turbo-test.log +26 -0
  4. package/.turbo/turbo-typecheck.log +4 -0
  5. package/CHANGELOG.md +12 -0
  6. package/LICENSE +21 -0
  7. package/README.md +85 -0
  8. package/bin/funkai.mjs +2 -0
  9. package/coverage/lcov-report/base.css +224 -0
  10. package/coverage/lcov-report/block-navigation.js +87 -0
  11. package/coverage/lcov-report/commands/create.ts.html +208 -0
  12. package/coverage/lcov-report/commands/generate.ts.html +388 -0
  13. package/coverage/lcov-report/commands/index.html +161 -0
  14. package/coverage/lcov-report/commands/lint.ts.html +331 -0
  15. package/coverage/lcov-report/commands/setup.ts.html +493 -0
  16. package/coverage/lcov-report/favicon.png +0 -0
  17. package/coverage/lcov-report/index.html +131 -0
  18. package/coverage/lcov-report/lib/codegen.ts.html +805 -0
  19. package/coverage/lcov-report/lib/extract-variables.ts.html +181 -0
  20. package/coverage/lcov-report/lib/flatten.ts.html +385 -0
  21. package/coverage/lcov-report/lib/frontmatter.ts.html +487 -0
  22. package/coverage/lcov-report/lib/index.html +191 -0
  23. package/coverage/lcov-report/lib/lint.ts.html +307 -0
  24. package/coverage/lcov-report/lib/paths.ts.html +487 -0
  25. package/coverage/lcov-report/prettify.css +1 -0
  26. package/coverage/lcov-report/prettify.js +2 -0
  27. package/coverage/lcov-report/sort-arrow-sprite.png +0 -0
  28. package/coverage/lcov-report/sorter.js +210 -0
  29. package/coverage/lcov.info +749 -0
  30. package/dist/index.mjs +19297 -0
  31. package/dist/index.mjs.map +1 -0
  32. package/kidd.config.ts +7 -0
  33. package/package.json +54 -0
  34. package/src/commands/agents/validate.ts +9 -0
  35. package/src/commands/generate.ts +78 -0
  36. package/src/commands/prompts/create.ts +41 -0
  37. package/src/commands/prompts/generate.ts +71 -0
  38. package/src/commands/prompts/lint.ts +57 -0
  39. package/src/commands/prompts/setup.ts +149 -0
  40. package/src/commands/setup.ts +12 -0
  41. package/src/commands/validate.ts +47 -0
  42. package/src/index.ts +7 -0
  43. package/src/lib/prompts/__tests__/extract-variables.test.ts +47 -0
  44. package/src/lib/prompts/__tests__/flatten.test.ts +230 -0
  45. package/src/lib/prompts/__tests__/frontmatter.test.ts +89 -0
  46. package/src/lib/prompts/__tests__/lint.test.ts +80 -0
  47. package/src/lib/prompts/codegen.ts +240 -0
  48. package/src/lib/prompts/extract-variables.ts +32 -0
  49. package/src/lib/prompts/flatten.ts +91 -0
  50. package/src/lib/prompts/frontmatter.ts +143 -0
  51. package/src/lib/prompts/lint.ts +74 -0
  52. package/src/lib/prompts/paths.ts +118 -0
  53. package/src/lib/prompts/pipeline.ts +114 -0
  54. package/tsconfig.json +25 -0
  55. package/vitest.config.ts +21 -0
package/kidd.config.ts ADDED
@@ -0,0 +1,7 @@
1
+ import { defineConfig } from "@kidd-cli/core";
2
+
3
+ export default defineConfig({
4
+ build: { out: "./dist" },
5
+ commands: "./src/commands",
6
+ entry: "./src/index.ts",
7
+ });
package/package.json ADDED
@@ -0,0 +1,54 @@
1
+ {
2
+ "name": "@funkai/cli",
3
+ "version": "0.1.0",
4
+ "private": false,
5
+ "description": "CLI for the funkai AI SDK framework",
6
+ "keywords": [
7
+ "ai",
8
+ "cli",
9
+ "codegen",
10
+ "funkai",
11
+ "prompts",
12
+ "typescript"
13
+ ],
14
+ "homepage": "https://github.com/joggrdocs/funkai/tree/main/packages/cli#readme",
15
+ "bugs": {
16
+ "url": "https://github.com/joggrdocs/funkai/issues"
17
+ },
18
+ "license": "MIT",
19
+ "repository": {
20
+ "type": "git",
21
+ "url": "https://github.com/joggrdocs/funkai.git",
22
+ "directory": "packages/cli"
23
+ },
24
+ "bin": {
25
+ "funkai": "./bin/funkai.mjs"
26
+ },
27
+ "type": "module",
28
+ "dependencies": {
29
+ "@kidd-cli/core": "^0.4.0",
30
+ "liquidjs": "^10.25.0",
31
+ "ts-pattern": "^5.9.0",
32
+ "yaml": "^2.8.2",
33
+ "zod": "^4.3.6",
34
+ "@funkai/prompts": "0.1.0"
35
+ },
36
+ "devDependencies": {
37
+ "@kidd-cli/cli": "^0.2.0",
38
+ "@types/node": "^25.5.0",
39
+ "@vitest/coverage-v8": "^4.1.0",
40
+ "tsdown": "^0.21.2",
41
+ "typescript": "^5.9.3",
42
+ "vitest": "^4.1.0"
43
+ },
44
+ "engines": {
45
+ "node": ">=24.0.0"
46
+ },
47
+ "scripts": {
48
+ "dev": "kidd dev",
49
+ "build": "kidd build",
50
+ "typecheck": "tsc --noEmit",
51
+ "test": "vitest run",
52
+ "test:coverage": "vitest run --coverage"
53
+ }
54
+ }
@@ -0,0 +1,9 @@
1
+ import { command } from "@kidd-cli/core";
2
+
3
+ export default command({
4
+ description: "Validate agent configurations",
5
+ handler(ctx) {
6
+ ctx.logger.info("Agent validation is not yet available.");
7
+ ctx.logger.info("This command will validate agent configurations and tool schemas.");
8
+ },
9
+ });
@@ -0,0 +1,78 @@
1
+ import { mkdirSync, writeFileSync } from "node:fs";
2
+ import { resolve } from "node:path";
3
+
4
+ import { command } from "@kidd-cli/core";
5
+ import { match } from "ts-pattern";
6
+ import { z } from "zod";
7
+
8
+ import { generatePromptModule, generateRegistry } from "@/lib/prompts/codegen.js";
9
+ import { hasLintErrors } from "@/lib/prompts/lint.js";
10
+ import { runGeneratePipeline } from "@/lib/prompts/pipeline.js";
11
+
12
+ export default command({
13
+ description: "Run all code generation across the funkai SDK",
14
+ args: z.object({
15
+ out: z.string().describe("Output directory for generated files"),
16
+ roots: z.array(z.string()).describe("Root directories to scan for .prompt files"),
17
+ partials: z.string().optional().describe("Custom partials directory"),
18
+ silent: z.boolean().default(false).describe("Suppress output except errors"),
19
+ }),
20
+ handler(ctx) {
21
+ const { out, roots, partials, silent } = ctx.args;
22
+
23
+ // --- Prompts codegen ---
24
+ if (!silent) {
25
+ ctx.logger.info("Running prompts code generation...");
26
+ }
27
+
28
+ const { discovered, lintResults, prompts } = runGeneratePipeline({ roots, out, partials });
29
+
30
+ if (!silent) {
31
+ ctx.logger.info(`Found ${discovered} prompt(s)`);
32
+ }
33
+
34
+ for (const prompt of prompts) {
35
+ if (!silent) {
36
+ const varCount = prompt.schema.length;
37
+ const varList = match(varCount > 0)
38
+ .with(true, () => ` (${prompt.schema.map((v) => v.name).join(", ")})`)
39
+ .otherwise(() => "");
40
+ ctx.logger.step(`${prompt.name}${varList}`);
41
+ }
42
+ }
43
+
44
+ for (const result of lintResults) {
45
+ for (const diag of result.diagnostics) {
46
+ if (diag.level === "error") {
47
+ ctx.logger.error(diag.message);
48
+ } else {
49
+ ctx.logger.warn(diag.message);
50
+ }
51
+ }
52
+ }
53
+
54
+ if (hasLintErrors([...lintResults])) {
55
+ ctx.fail("Lint errors found. Fix them before generating.");
56
+ }
57
+
58
+ const outDir = resolve(out);
59
+ // oxlint-disable-next-line security/detect-non-literal-fs-filename -- safe: output directory from CLI config
60
+ mkdirSync(outDir, { recursive: true });
61
+
62
+ for (const prompt of prompts) {
63
+ const content = generatePromptModule(prompt);
64
+ // oxlint-disable-next-line security/detect-non-literal-fs-filename -- safe: writing generated module to output directory
65
+ writeFileSync(resolve(outDir, `${prompt.name}.ts`), content, "utf-8");
66
+ }
67
+
68
+ const registryContent = generateRegistry([...prompts]);
69
+ // oxlint-disable-next-line security/detect-non-literal-fs-filename -- safe: writing generated registry to output directory
70
+ writeFileSync(resolve(outDir, "index.ts"), registryContent, "utf-8");
71
+
72
+ // --- Future: agents codegen ---
73
+
74
+ if (!silent) {
75
+ ctx.logger.success(`Generated ${prompts.length} prompt module(s) + registry → ${outDir}`);
76
+ }
77
+ },
78
+ });
@@ -0,0 +1,41 @@
1
+ import { existsSync, mkdirSync, writeFileSync } from "node:fs";
2
+ import { resolve } from "node:path";
3
+
4
+ import { command } from "@kidd-cli/core";
5
+ import { match, P } from "ts-pattern";
6
+ import { z } from "zod";
7
+
8
+ const TEMPLATE = (name: string) => `---
9
+ name: ${name}
10
+ ---
11
+
12
+ `;
13
+
14
+ export default command({
15
+ description: "Create a new .prompt file",
16
+ args: z.object({
17
+ name: z.string().describe("Prompt name (kebab-case)"),
18
+ out: z.string().optional().describe("Output directory (defaults to cwd)"),
19
+ partial: z.boolean().default(false).describe("Create as a partial in .prompts/partials/"),
20
+ }),
21
+ handler(ctx) {
22
+ const { name, out, partial } = ctx.args;
23
+ const dir = match({ partial, out })
24
+ .with({ partial: true }, () => resolve(".prompts/partials"))
25
+ .with({ out: P.string }, ({ out: outDir }) => resolve(outDir))
26
+ .otherwise(() => process.cwd());
27
+ const filePath = resolve(dir, `${name}.prompt`);
28
+
29
+ // oxlint-disable-next-line security/detect-non-literal-fs-filename -- safe: user-provided CLI argument for prompt file creation
30
+ if (existsSync(filePath)) {
31
+ ctx.fail(`File already exists: ${filePath}`);
32
+ }
33
+
34
+ // oxlint-disable-next-line security/detect-non-literal-fs-filename -- safe: directory derived from user CLI path argument
35
+ mkdirSync(dir, { recursive: true });
36
+ // oxlint-disable-next-line security/detect-non-literal-fs-filename -- safe: file path derived from user CLI path argument
37
+ writeFileSync(filePath, TEMPLATE(name), "utf-8");
38
+
39
+ ctx.logger.success(`Created ${filePath}`);
40
+ },
41
+ });
@@ -0,0 +1,71 @@
1
+ import { mkdirSync, writeFileSync } from "node:fs";
2
+ import { resolve } from "node:path";
3
+
4
+ import { command } from "@kidd-cli/core";
5
+ import { match } from "ts-pattern";
6
+ import { z } from "zod";
7
+
8
+ import { generatePromptModule, generateRegistry } from "@/lib/prompts/codegen.js";
9
+ import { hasLintErrors } from "@/lib/prompts/lint.js";
10
+ import { runGeneratePipeline } from "@/lib/prompts/pipeline.js";
11
+
12
+ export default command({
13
+ description: "Generate TypeScript modules from .prompt files",
14
+ args: z.object({
15
+ out: z.string().describe("Output directory for generated files"),
16
+ roots: z.array(z.string()).describe("Root directories to scan for .prompt files"),
17
+ partials: z.string().optional().describe("Custom partials directory"),
18
+ silent: z.boolean().default(false).describe("Suppress output except errors"),
19
+ }),
20
+ handler(ctx) {
21
+ const { out, roots, partials, silent } = ctx.args;
22
+
23
+ const { discovered, lintResults, prompts } = runGeneratePipeline({ roots, out, partials });
24
+
25
+ if (!silent) {
26
+ ctx.logger.info(`Found ${discovered} prompt(s)`);
27
+ }
28
+
29
+ if (!silent) {
30
+ prompts.forEach((prompt) => {
31
+ const varCount = prompt.schema.length;
32
+ const varList = match(varCount > 0)
33
+ .with(true, () => ` (${prompt.schema.map((v) => v.name).join(", ")})`)
34
+ .otherwise(() => "");
35
+ ctx.logger.step(`${prompt.name}${varList}`);
36
+ });
37
+ }
38
+
39
+ lintResults
40
+ .flatMap((result) => result.diagnostics)
41
+ .forEach((diag) => {
42
+ if (diag.level === "error") {
43
+ ctx.logger.error(diag.message);
44
+ } else {
45
+ ctx.logger.warn(diag.message);
46
+ }
47
+ });
48
+
49
+ if (hasLintErrors([...lintResults])) {
50
+ ctx.fail("Lint errors found. Fix them before generating.");
51
+ }
52
+
53
+ const outDir = resolve(out);
54
+ // oxlint-disable-next-line security/detect-non-literal-fs-filename -- safe: output directory from CLI config
55
+ mkdirSync(outDir, { recursive: true });
56
+
57
+ prompts.forEach((prompt) => {
58
+ const content = generatePromptModule(prompt);
59
+ // oxlint-disable-next-line security/detect-non-literal-fs-filename -- safe: writing generated module to output directory
60
+ writeFileSync(resolve(outDir, `${prompt.name}.ts`), content, "utf-8");
61
+ });
62
+
63
+ const registryContent = generateRegistry([...prompts]);
64
+ // oxlint-disable-next-line security/detect-non-literal-fs-filename -- safe: writing generated registry to output directory
65
+ writeFileSync(resolve(outDir, "index.ts"), registryContent, "utf-8");
66
+
67
+ if (!silent) {
68
+ ctx.logger.success(`Generated ${prompts.length} prompt module(s) + registry → ${outDir}`);
69
+ }
70
+ },
71
+ });
@@ -0,0 +1,57 @@
1
+ import { command } from "@kidd-cli/core";
2
+ import { match } from "ts-pattern";
3
+ import { z } from "zod";
4
+
5
+ import { hasLintErrors } from "@/lib/prompts/lint.js";
6
+ import { runLintPipeline } from "@/lib/prompts/pipeline.js";
7
+
8
+ export default command({
9
+ description: "Validate .prompt files for schema/template mismatches",
10
+ args: z.object({
11
+ roots: z.array(z.string()).describe("Root directories to scan for .prompt files"),
12
+ partials: z.string().optional().describe("Custom partials directory"),
13
+ silent: z.boolean().default(false).describe("Suppress output except errors"),
14
+ }),
15
+ handler(ctx) {
16
+ const { roots, partials, silent } = ctx.args;
17
+
18
+ const { discovered, results } = runLintPipeline({ roots, partials });
19
+
20
+ if (!silent) {
21
+ ctx.logger.info(`Linting ${discovered} prompt(s)...`);
22
+ }
23
+
24
+ const diagnostics = results.flatMap((result) => result.diagnostics);
25
+
26
+ diagnostics.forEach((diag) => {
27
+ if (diag.level === "error") {
28
+ ctx.logger.error(diag.message);
29
+ } else {
30
+ ctx.logger.warn(diag.message);
31
+ }
32
+ });
33
+
34
+ const errorCount = diagnostics.filter((d) => d.level === "error").length;
35
+ const warnCount = diagnostics.filter((d) => d.level !== "error").length;
36
+
37
+ if (!silent) {
38
+ const summary = [
39
+ `${discovered} prompt(s) linted`,
40
+ match(errorCount > 0)
41
+ .with(true, () => `${errorCount} error(s)`)
42
+ .otherwise(() => undefined),
43
+ match(warnCount > 0)
44
+ .with(true, () => `${warnCount} warning(s)`)
45
+ .otherwise(() => undefined),
46
+ ]
47
+ .filter(Boolean)
48
+ .join(", ");
49
+
50
+ ctx.logger.info(summary);
51
+ }
52
+
53
+ if (hasLintErrors([...results])) {
54
+ ctx.fail("Lint errors found.");
55
+ }
56
+ },
57
+ });
@@ -0,0 +1,149 @@
1
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
2
+ import { resolve } from "node:path";
3
+
4
+ import { command } from "@kidd-cli/core";
5
+ import { match } from "ts-pattern";
6
+
7
+ const VSCODE_DIR = ".vscode";
8
+ const SETTINGS_FILE = "settings.json";
9
+ const EXTENSIONS_FILE = "extensions.json";
10
+ const GITIGNORE_FILE = ".gitignore";
11
+ const TSCONFIG_FILE = "tsconfig.json";
12
+ const GITIGNORE_ENTRY = ".prompts/client/";
13
+ const PROMPTS_ALIAS = "~prompts";
14
+ const PROMPTS_ALIAS_PATH = "./.prompts/client/index.ts";
15
+
16
+ export default command({
17
+ description: "Configure VSCode IDE settings for .prompt files",
18
+ async handler(ctx) {
19
+ ctx.logger.intro("Prompt SDK — Project Setup");
20
+
21
+ const shouldConfigure = await ctx.prompts.confirm({
22
+ message: "Configure VSCode to treat .prompt files as Markdown with Liquid syntax?",
23
+ initialValue: true,
24
+ });
25
+
26
+ if (shouldConfigure) {
27
+ const vscodeDir = resolve(VSCODE_DIR);
28
+ mkdirSync(vscodeDir, { recursive: true });
29
+
30
+ const settingsPath = resolve(vscodeDir, SETTINGS_FILE);
31
+ const settings = readJsonFile(settingsPath);
32
+
33
+ const updatedSettings = {
34
+ ...settings,
35
+ "files.associations": {
36
+ ...((settings["files.associations"] ?? {}) as Record<string, string>),
37
+ "*.prompt": "markdown",
38
+ },
39
+ "liquid.engine": "standard",
40
+ };
41
+
42
+ writeFileSync(settingsPath, JSON.stringify(updatedSettings, null, 2) + "\n", "utf-8");
43
+ ctx.logger.success(`Updated ${settingsPath}`);
44
+ }
45
+
46
+ const shouldRecommend = await ctx.prompts.confirm({
47
+ message: "Add Shopify Liquid extension to VSCode recommendations?",
48
+ initialValue: true,
49
+ });
50
+
51
+ if (shouldRecommend) {
52
+ const vscodeDir = resolve(VSCODE_DIR);
53
+ mkdirSync(vscodeDir, { recursive: true });
54
+
55
+ const extensionsPath = resolve(vscodeDir, EXTENSIONS_FILE);
56
+ const extensions = readJsonFile(extensionsPath);
57
+
58
+ const currentRecs = (extensions.recommendations ?? []) as string[];
59
+ const extensionId = "sissel.shopify-liquid";
60
+
61
+ const updatedExtensions = {
62
+ ...extensions,
63
+ recommendations: currentRecs.includes(extensionId)
64
+ ? currentRecs
65
+ : [...currentRecs, extensionId],
66
+ };
67
+
68
+ writeFileSync(extensionsPath, JSON.stringify(updatedExtensions, null, 2) + "\n", "utf-8");
69
+ ctx.logger.success(`Updated ${extensionsPath}`);
70
+ }
71
+
72
+ const shouldGitignore = await ctx.prompts.confirm({
73
+ message: "Add .prompts/client/ to .gitignore? (generated client should not be committed)",
74
+ initialValue: true,
75
+ });
76
+
77
+ if (shouldGitignore) {
78
+ const gitignorePath = resolve(GITIGNORE_FILE);
79
+ const existing = match(existsSync(gitignorePath))
80
+ .with(true, () => readFileSync(gitignorePath, "utf-8"))
81
+ .otherwise(() => "");
82
+
83
+ if (!existing.includes(GITIGNORE_ENTRY)) {
84
+ const separator = match(existing.length > 0 && !existing.endsWith("\n"))
85
+ .with(true, () => "\n")
86
+ .otherwise(() => "");
87
+ const block = `${separator}\n# Generated prompt client (created by \`funkai prompts generate\`)\n${GITIGNORE_ENTRY}\n`;
88
+ writeFileSync(gitignorePath, existing + block, "utf-8");
89
+ ctx.logger.success(`Added ${GITIGNORE_ENTRY} to ${gitignorePath}`);
90
+ } else {
91
+ ctx.logger.info(`${GITIGNORE_ENTRY} already in ${gitignorePath}`);
92
+ }
93
+ }
94
+
95
+ const shouldTsconfig = await ctx.prompts.confirm({
96
+ message: "Add ~prompts path alias to tsconfig.json?",
97
+ initialValue: true,
98
+ });
99
+
100
+ if (shouldTsconfig) {
101
+ const tsconfigPath = resolve(TSCONFIG_FILE);
102
+ const tsconfig = readJsonFile(tsconfigPath);
103
+
104
+ const compilerOptions = (tsconfig.compilerOptions ?? {}) as Record<string, unknown>;
105
+ const existingPaths = (compilerOptions.paths ?? {}) as Record<string, string[]>;
106
+
107
+ // oxlint-disable-next-line security/detect-object-injection -- safe: PROMPTS_ALIAS is a known constant string
108
+ if (!existingPaths[PROMPTS_ALIAS]) {
109
+ const updatedTsconfig = {
110
+ ...tsconfig,
111
+ compilerOptions: {
112
+ ...compilerOptions,
113
+ paths: {
114
+ ...existingPaths,
115
+ // oxlint-disable-next-line security/detect-object-injection -- safe: PROMPTS_ALIAS is a known constant string
116
+ [PROMPTS_ALIAS]: [PROMPTS_ALIAS_PATH],
117
+ },
118
+ },
119
+ };
120
+
121
+ writeFileSync(tsconfigPath, JSON.stringify(updatedTsconfig, null, 2) + "\n", "utf-8");
122
+ ctx.logger.success(`Added ${PROMPTS_ALIAS} alias to ${tsconfigPath}`);
123
+ } else {
124
+ ctx.logger.info(`${PROMPTS_ALIAS} alias already in ${tsconfigPath}`);
125
+ }
126
+ }
127
+
128
+ ctx.logger.outro("Project setup complete.");
129
+ },
130
+ });
131
+
132
+ /**
133
+ * Read a JSON file, returning an empty object if it doesn't exist
134
+ * or contains invalid JSON.
135
+ */
136
+ function readJsonFile(filePath: string): Record<string, unknown> {
137
+ // oxlint-disable-next-line security/detect-non-literal-fs-filename -- safe: tsconfig path from CLI discovery
138
+ if (!existsSync(filePath)) {
139
+ return {};
140
+ }
141
+
142
+ try {
143
+ // oxlint-disable-next-line security/detect-non-literal-fs-filename -- safe: reading tsconfig file
144
+ const content = readFileSync(filePath, "utf-8");
145
+ return JSON.parse(content) as Record<string, unknown>;
146
+ } catch {
147
+ return {};
148
+ }
149
+ }
@@ -0,0 +1,12 @@
1
+ import { command } from "@kidd-cli/core";
2
+
3
+ export default command({
4
+ description: "Set up your project for the funkai SDK",
5
+ async handler(ctx) {
6
+ ctx.logger.intro("funkai — Project Setup");
7
+ ctx.logger.info("Run domain-specific setup commands:");
8
+ ctx.logger.step("funkai prompts setup — Configure IDE and project for .prompt files");
9
+ ctx.logger.step("funkai agents setup — (coming soon)");
10
+ ctx.logger.outro("Choose the setup command for your domain.");
11
+ },
12
+ });
@@ -0,0 +1,47 @@
1
+ import { command } from "@kidd-cli/core";
2
+ import { match } from "ts-pattern";
3
+ import { z } from "zod";
4
+
5
+ import { hasLintErrors } from "@/lib/prompts/lint.js";
6
+ import { runLintPipeline } from "@/lib/prompts/pipeline.js";
7
+
8
+ export default command({
9
+ description: "Run all validations across the funkai SDK",
10
+ args: z.object({
11
+ roots: z.array(z.string()).describe("Root directories to scan for .prompt files"),
12
+ partials: z.string().optional().describe("Custom partials directory"),
13
+ silent: z.boolean().default(false).describe("Suppress output except errors"),
14
+ }),
15
+ handler(ctx) {
16
+ const { roots, partials, silent } = ctx.args;
17
+
18
+ // --- Prompts validation ---
19
+ if (!silent) {
20
+ ctx.logger.info("Running prompts validation...");
21
+ }
22
+
23
+ const { discovered, results } = runLintPipeline({ roots, partials });
24
+
25
+ if (!silent) {
26
+ ctx.logger.info(`Found ${discovered} prompt(s)`);
27
+ }
28
+
29
+ const diagnostics = results.flatMap((result) => result.diagnostics);
30
+
31
+ for (const diag of diagnostics) {
32
+ match(diag.level)
33
+ .with("error", () => ctx.logger.error(diag.message))
34
+ .otherwise(() => ctx.logger.warn(diag.message));
35
+ }
36
+
37
+ // --- Future: agents validation ---
38
+
39
+ if (hasLintErrors([...results])) {
40
+ ctx.fail("Validation errors found.");
41
+ }
42
+
43
+ if (!silent) {
44
+ ctx.logger.success("All validations passed.");
45
+ }
46
+ },
47
+ });
package/src/index.ts ADDED
@@ -0,0 +1,7 @@
1
+ import { cli } from "@kidd-cli/core";
2
+
3
+ await cli({
4
+ description: "CLI for the funkai AI SDK framework",
5
+ name: "funkai",
6
+ version: "0.4.0",
7
+ });
@@ -0,0 +1,47 @@
1
+ import { describe, expect, it } from "vitest";
2
+
3
+ import { extractVariables } from "@/lib/prompts/extract-variables.js";
4
+
5
+ describe("extractVariables", () => {
6
+ it("extracts simple variables", () => {
7
+ expect(extractVariables("{{ name }}")).toEqual(["name"]);
8
+ });
9
+
10
+ it("extracts multiple variables sorted alphabetically", () => {
11
+ expect(extractVariables("{{ b }} {{ a }}")).toEqual(["a", "b"]);
12
+ });
13
+
14
+ it("extracts the root name from filtered variables", () => {
15
+ const result = extractVariables("{{ name | upcase }}");
16
+ expect(result).toEqual(["name"]);
17
+ });
18
+
19
+ it("extracts the root name from nested property access", () => {
20
+ const result = extractVariables("{{ user.name }}");
21
+ expect(result).toEqual(["user"]);
22
+ });
23
+
24
+ it("deduplicates repeated variables", () => {
25
+ expect(extractVariables("{{ x }} {{ x }}")).toEqual(["x"]);
26
+ });
27
+
28
+ it("extracts variables from for loops", () => {
29
+ const result = extractVariables("{% for item in items %}{{ item }}{% endfor %}");
30
+ expect(result).toContain("items");
31
+ });
32
+
33
+ it("throws on dangerous variable names", () => {
34
+ expect(() => extractVariables("{{ __proto__ }}")).toThrow("Dangerous variable name");
35
+ expect(() => extractVariables("{{ constructor }}")).toThrow("Dangerous variable name");
36
+ expect(() => extractVariables("{{ prototype }}")).toThrow("Dangerous variable name");
37
+ });
38
+
39
+ it("returns empty array for templates with no variables", () => {
40
+ expect(extractVariables("Hello world")).toEqual([]);
41
+ });
42
+
43
+ it("extracts variables from conditional blocks", () => {
44
+ const result = extractVariables("{% if show_header %}Header{% endif %}");
45
+ expect(result).toContain("show_header");
46
+ });
47
+ });