@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.
@@ -1,6 +1,7 @@
1
1
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
2
2
  import { resolve } from "node:path";
3
3
 
4
+ import type { Context } from "@kidd-cli/core";
4
5
  import { command } from "@kidd-cli/core";
5
6
 
6
7
  const VSCODE_DIR = ".vscode";
@@ -12,138 +13,167 @@ const GITIGNORE_ENTRY = ".prompts/client/";
12
13
  const PROMPTS_ALIAS = "~prompts";
13
14
  const PROMPTS_ALIAS_PATH = "./.prompts/client/index.ts";
14
15
 
15
- export default command({
16
- description: "Configure VSCode IDE settings for .prompt files",
17
- async handler(ctx) {
18
- ctx.logger.intro("Prompt SDK Project Setup");
16
+ /**
17
+ * Shared prompts setup logic used by both `funkai prompts setup` and `funkai setup`.
18
+ *
19
+ * @param ctx - The CLI context with prompts and logger.
20
+ */
21
+ export async function setupPrompts(ctx: Pick<Context, "prompts" | "logger">): Promise<void> {
22
+ const shouldConfigure = await ctx.prompts.confirm({
23
+ message: "Configure VSCode to treat .prompt files as Markdown with Liquid syntax?",
24
+ initialValue: true,
25
+ });
26
+
27
+ if (shouldConfigure) {
28
+ const vscodeDir = resolve(VSCODE_DIR);
29
+ mkdirSync(vscodeDir, { recursive: true });
30
+
31
+ const settingsPath = resolve(vscodeDir, SETTINGS_FILE);
32
+ const settings = readJsonFile(settingsPath);
33
+
34
+ const updatedSettings = {
35
+ ...settings,
36
+ "files.associations": {
37
+ ...((settings["files.associations"] ?? {}) as Record<string, string>),
38
+ "*.prompt": "markdown",
39
+ },
40
+ "liquid.engine": "standard",
41
+ };
42
+
43
+ writeFileSync(settingsPath, `${JSON.stringify(updatedSettings, null, 2)}\n`, "utf8");
44
+ ctx.logger.success(`Updated ${settingsPath}`);
45
+ }
19
46
 
20
- const shouldConfigure = await ctx.prompts.confirm({
21
- message: "Configure VSCode to treat .prompt files as Markdown with Liquid syntax?",
22
- initialValue: true,
23
- });
47
+ const shouldRecommend = await ctx.prompts.confirm({
48
+ message: "Add Shopify Liquid extension to VSCode recommendations?",
49
+ initialValue: true,
50
+ });
24
51
 
25
- if (shouldConfigure) {
26
- const vscodeDir = resolve(VSCODE_DIR);
27
- mkdirSync(vscodeDir, { recursive: true });
52
+ if (shouldRecommend) {
53
+ const vscodeDir = resolve(VSCODE_DIR);
54
+ mkdirSync(vscodeDir, { recursive: true });
28
55
 
29
- const settingsPath = resolve(vscodeDir, SETTINGS_FILE);
30
- const settings = readJsonFile(settingsPath);
56
+ const extensionsPath = resolve(vscodeDir, EXTENSIONS_FILE);
57
+ const extensions = readJsonFile(extensionsPath);
31
58
 
32
- const updatedSettings = {
33
- ...settings,
34
- "files.associations": {
35
- ...((settings["files.associations"] ?? {}) as Record<string, string>),
36
- "*.prompt": "markdown",
37
- },
38
- "liquid.engine": "standard",
39
- };
59
+ const currentRecs = (extensions.recommendations ?? []) as string[];
60
+ const extensionId = "sissel.shopify-liquid";
40
61
 
41
- writeFileSync(settingsPath, `${JSON.stringify(updatedSettings, null, 2)}\n`, "utf8");
42
- ctx.logger.success(`Updated ${settingsPath}`);
43
- }
62
+ const recommendations = ensureRecommendation(currentRecs, extensionId);
63
+ const updatedExtensions = {
64
+ ...extensions,
65
+ recommendations,
66
+ };
44
67
 
45
- const shouldRecommend = await ctx.prompts.confirm({
46
- message: "Add Shopify Liquid extension to VSCode recommendations?",
47
- initialValue: true,
48
- });
49
-
50
- if (shouldRecommend) {
51
- const vscodeDir = resolve(VSCODE_DIR);
52
- mkdirSync(vscodeDir, { recursive: true });
53
-
54
- const extensionsPath = resolve(vscodeDir, EXTENSIONS_FILE);
55
- const extensions = readJsonFile(extensionsPath);
56
-
57
- const currentRecs = (extensions.recommendations ?? []) as string[];
58
- const extensionId = "sissel.shopify-liquid";
59
-
60
- // oxlint-disable-next-line unicorn/prefer-ternary -- no-ternary rule forbids ternaries
61
- const recommendations: string[] = (() => {
62
- if (currentRecs.includes(extensionId)) {
63
- return currentRecs;
64
- }
65
- return [...currentRecs, extensionId];
66
- })();
67
- const updatedExtensions = {
68
- ...extensions,
69
- recommendations,
70
- };
71
-
72
- writeFileSync(extensionsPath, `${JSON.stringify(updatedExtensions, null, 2)}\n`, "utf8");
73
- ctx.logger.success(`Updated ${extensionsPath}`);
74
- }
68
+ writeFileSync(extensionsPath, `${JSON.stringify(updatedExtensions, null, 2)}\n`, "utf8");
69
+ ctx.logger.success(`Updated ${extensionsPath}`);
70
+ }
75
71
 
76
- const shouldGitignore = await ctx.prompts.confirm({
77
- message: "Add .prompts/client/ to .gitignore? (generated client should not be committed)",
78
- initialValue: true,
79
- });
80
-
81
- if (shouldGitignore) {
82
- const gitignorePath = resolve(GITIGNORE_FILE);
83
- // oxlint-disable-next-line unicorn/prefer-ternary -- no-ternary rule forbids ternaries
84
- const existing: string = (() => {
85
- if (existsSync(gitignorePath)) {
86
- return readFileSync(gitignorePath, "utf8");
87
- }
88
- return "";
89
- })();
90
-
91
- if (existing.includes(GITIGNORE_ENTRY)) {
92
- ctx.logger.info(`${GITIGNORE_ENTRY} already in ${gitignorePath}`);
93
- } else {
94
- // oxlint-disable-next-line unicorn/prefer-ternary -- no-ternary rule forbids ternaries
95
- const separator: string = (() => {
96
- if (existing.length > 0 && !existing.endsWith("\n")) {
97
- return "\n";
98
- }
99
- return "";
100
- })();
101
- const block = `${separator}\n# Generated prompt client (created by \`funkai prompts generate\`)\n${GITIGNORE_ENTRY}\n`;
102
- writeFileSync(gitignorePath, `${existing}${block}`, "utf8");
103
- ctx.logger.success(`Added ${GITIGNORE_ENTRY} to ${gitignorePath}`);
104
- }
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 = readFileOrEmpty(gitignorePath);
80
+
81
+ if (existing.includes(GITIGNORE_ENTRY)) {
82
+ ctx.logger.info(`${GITIGNORE_ENTRY} already in ${gitignorePath}`);
83
+ } else {
84
+ const separator = trailingSeparator(existing);
85
+ const block = `${separator}\n# Generated prompt client (created by \`funkai prompts generate\`)\n${GITIGNORE_ENTRY}\n`;
86
+ writeFileSync(gitignorePath, `${existing}${block}`, "utf8");
87
+ ctx.logger.success(`Added ${GITIGNORE_ENTRY} to ${gitignorePath}`);
105
88
  }
89
+ }
106
90
 
107
- const shouldTsconfig = await ctx.prompts.confirm({
108
- message: "Add ~prompts path alias to tsconfig.json?",
109
- initialValue: true,
110
- });
111
-
112
- if (shouldTsconfig) {
113
- const tsconfigPath = resolve(TSCONFIG_FILE);
114
- const tsconfig = readJsonFile(tsconfigPath);
115
-
116
- const compilerOptions = (tsconfig.compilerOptions ?? {}) as Record<string, unknown>;
117
- const existingPaths = (compilerOptions.paths ?? {}) as Record<string, string[]>;
118
-
119
- // oxlint-disable-next-line security/detect-object-injection -- safe: PROMPTS_ALIAS is a known constant string
120
- if (existingPaths[PROMPTS_ALIAS]) {
121
- ctx.logger.info(`${PROMPTS_ALIAS} alias already in ${tsconfigPath}`);
122
- } else {
123
- const updatedTsconfig = {
124
- ...tsconfig,
125
- compilerOptions: {
126
- ...compilerOptions,
127
- paths: {
128
- ...existingPaths,
129
- // oxlint-disable-next-line security/detect-object-injection -- safe: PROMPTS_ALIAS is a known constant string
130
- [PROMPTS_ALIAS]: [PROMPTS_ALIAS_PATH],
131
- },
91
+ const shouldTsconfig = await ctx.prompts.confirm({
92
+ message: "Add ~prompts path alias to tsconfig.json?",
93
+ initialValue: true,
94
+ });
95
+
96
+ if (shouldTsconfig) {
97
+ const tsconfigPath = resolve(TSCONFIG_FILE);
98
+ const tsconfig = readJsonFile(tsconfigPath);
99
+
100
+ const compilerOptions = (tsconfig.compilerOptions ?? {}) as Record<string, unknown>;
101
+ const existingPaths = (compilerOptions.paths ?? {}) as Record<string, string[]>;
102
+
103
+ // oxlint-disable-next-line security/detect-object-injection -- safe: PROMPTS_ALIAS is a known constant string
104
+ if (existingPaths[PROMPTS_ALIAS]) {
105
+ ctx.logger.info(`${PROMPTS_ALIAS} alias already in ${tsconfigPath}`);
106
+ } else {
107
+ const updatedTsconfig = {
108
+ ...tsconfig,
109
+ compilerOptions: {
110
+ ...compilerOptions,
111
+ paths: {
112
+ ...existingPaths,
113
+ // oxlint-disable-next-line security/detect-object-injection -- safe: PROMPTS_ALIAS is a known constant string
114
+ [PROMPTS_ALIAS]: [PROMPTS_ALIAS_PATH],
132
115
  },
133
- };
116
+ },
117
+ };
134
118
 
135
- writeFileSync(tsconfigPath, `${JSON.stringify(updatedTsconfig, null, 2)}\n`, "utf8");
136
- ctx.logger.success(`Added ${PROMPTS_ALIAS} alias to ${tsconfigPath}`);
137
- }
119
+ writeFileSync(tsconfigPath, `${JSON.stringify(updatedTsconfig, null, 2)}\n`, "utf8");
120
+ ctx.logger.success(`Added ${PROMPTS_ALIAS} alias to ${tsconfigPath}`);
138
121
  }
122
+ }
123
+ }
139
124
 
140
- ctx.logger.outro("Project setup complete.");
125
+ export default command({
126
+ description: "Configure VSCode IDE settings for .prompt files",
127
+ async handler(ctx) {
128
+ ctx.logger.intro("Prompt SDK — Project Setup");
129
+ await setupPrompts(ctx);
130
+ ctx.logger.outro("Prompts setup complete.");
141
131
  },
142
132
  });
143
133
 
134
+ // ---------------------------------------------------------------------------
135
+ // Private
136
+ // ---------------------------------------------------------------------------
137
+
138
+ /** @private */
139
+ function errorMessage(error: unknown): string {
140
+ if (error instanceof Error) {
141
+ return error.message;
142
+ }
143
+ return String(error);
144
+ }
145
+
146
+ /** @private */
147
+ function ensureRecommendation(current: readonly string[], id: string): string[] {
148
+ if (current.includes(id)) {
149
+ return [...current];
150
+ }
151
+ return [...current, id];
152
+ }
153
+
154
+ /** @private */
155
+ function readFileOrEmpty(filePath: string): string {
156
+ // oxlint-disable-next-line security/detect-non-literal-fs-filename -- safe: reading file from CLI discovery
157
+ if (existsSync(filePath)) {
158
+ return readFileSync(filePath, "utf8");
159
+ }
160
+ return "";
161
+ }
162
+
163
+ /** @private */
164
+ function trailingSeparator(content: string): string {
165
+ if (content.length > 0 && !content.endsWith("\n")) {
166
+ return "\n";
167
+ }
168
+ return "";
169
+ }
170
+
144
171
  /**
145
- * Read a JSON file, returning an empty object if it doesn't exist
146
- * or contains invalid JSON.
172
+ * Read a JSON file, returning an empty object if it doesn't exist.
173
+ * Throws if the file exists but contains invalid JSON, preventing
174
+ * silent data loss from overwriting malformed config files.
175
+ *
176
+ * @private
147
177
  */
148
178
  function readJsonFile(filePath: string): Record<string, unknown> {
149
179
  // oxlint-disable-next-line security/detect-non-literal-fs-filename -- safe: tsconfig path from CLI discovery
@@ -151,11 +181,15 @@ function readJsonFile(filePath: string): Record<string, unknown> {
151
181
  return {};
152
182
  }
153
183
 
184
+ // oxlint-disable-next-line security/detect-non-literal-fs-filename -- safe: reading tsconfig file
185
+ const content = readFileSync(filePath, "utf8");
154
186
  try {
155
- // oxlint-disable-next-line security/detect-non-literal-fs-filename -- safe: reading tsconfig file
156
- const content = readFileSync(filePath, "utf8");
157
187
  return JSON.parse(content) as Record<string, unknown>;
158
- } catch {
159
- return {};
188
+ } catch (error) {
189
+ throw new Error(
190
+ `Failed to parse ${filePath}: ${errorMessage(error)}. ` +
191
+ "Fix the JSON syntax or remove the file before running setup.",
192
+ { cause: error },
193
+ );
160
194
  }
161
195
  }
@@ -1,12 +1,137 @@
1
+ import { writeFileSync } from "node:fs";
2
+ import { resolve } from "node:path";
3
+
1
4
  import { command } from "@kidd-cli/core";
5
+ import { match } from "ts-pattern";
6
+
7
+ import { setupPrompts } from "@/commands/prompts/setup.js";
8
+
9
+ /** @private */
10
+ const CONFIG_TEMPLATE_AGENTS_ONLY = `import { defineConfig } from "@funkai/config";
11
+
12
+ export default defineConfig({
13
+ agents: {},
14
+ });
15
+ `;
2
16
 
3
17
  export default command({
4
18
  description: "Set up your project for the funkai SDK",
5
19
  async handler(ctx) {
6
20
  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.");
21
+
22
+ // --- Domain selection ---
23
+ const domains = await ctx.prompts.multiselect({
24
+ message: "Which domains do you want to set up?",
25
+ options: [
26
+ {
27
+ value: "prompts" as const,
28
+ label: "Prompts",
29
+ hint: "LiquidJS templating, codegen, IDE integration",
30
+ },
31
+ { value: "agents" as const, label: "Agents", hint: "Agent scaffolding and configuration" },
32
+ ],
33
+ initialValues: ["prompts" as const],
34
+ required: true,
35
+ });
36
+
37
+ const hasPrompts = domains.includes("prompts");
38
+ const hasAgents = domains.includes("agents");
39
+
40
+ // --- Create funkai.config.ts ---
41
+ const shouldCreateConfig = await ctx.prompts.confirm({
42
+ message: "Create funkai.config.ts?",
43
+ initialValue: true,
44
+ });
45
+
46
+ if (shouldCreateConfig) {
47
+ let includes = ["src/prompts/**"];
48
+ let out = ".prompts/client";
49
+
50
+ if (hasPrompts) {
51
+ const includesInput = await ctx.prompts.text({
52
+ message: "Prompt include patterns (comma-separated)",
53
+ defaultValue: "src/prompts/**",
54
+ placeholder: "src/prompts/**",
55
+ });
56
+ includes = includesInput.split(",").map((r) => r.trim());
57
+
58
+ out = await ctx.prompts.text({
59
+ message: "Output directory for generated prompt modules",
60
+ defaultValue: ".prompts/client",
61
+ placeholder: ".prompts/client",
62
+ });
63
+ }
64
+
65
+ const template = buildConfigTemplate({ hasPrompts, hasAgents, includes, out });
66
+ const configPath = resolve("funkai.config.ts");
67
+ // oxlint-disable-next-line security/detect-non-literal-fs-filename -- safe: writing config file to project root
68
+ writeFileSync(configPath, template, "utf8");
69
+ ctx.logger.success(`Created ${configPath}`);
70
+ }
71
+
72
+ // --- Run domain-specific setup ---
73
+ if (hasPrompts) {
74
+ ctx.logger.info("");
75
+ ctx.logger.info("Configuring Prompts...");
76
+ await setupPrompts(ctx);
77
+ }
78
+
79
+ if (hasAgents) {
80
+ ctx.logger.info("");
81
+ ctx.logger.info("Agents configuration is not yet available.");
82
+ ctx.logger.info("The agents section has been added to your config for future use.");
83
+ }
84
+
85
+ ctx.logger.outro("Project setup complete.");
11
86
  },
12
87
  });
88
+
89
+ // ---------------------------------------------------------------------------
90
+ // Private
91
+ // ---------------------------------------------------------------------------
92
+
93
+ /** @private */
94
+ interface ConfigTemplateOptions {
95
+ readonly hasPrompts: boolean;
96
+ readonly hasAgents: boolean;
97
+ readonly includes: readonly string[];
98
+ readonly out: string;
99
+ }
100
+
101
+ /** @private */
102
+ function buildConfigTemplate({
103
+ hasPrompts,
104
+ hasAgents,
105
+ includes,
106
+ out,
107
+ }: ConfigTemplateOptions): string {
108
+ if (hasPrompts && hasAgents) {
109
+ return buildCustomTemplate(includes, out, true);
110
+ }
111
+ if (hasPrompts) {
112
+ return buildCustomTemplate(includes, out, false);
113
+ }
114
+ return CONFIG_TEMPLATE_AGENTS_ONLY;
115
+ }
116
+
117
+ /** @private */
118
+ function buildCustomTemplate(
119
+ includes: readonly string[],
120
+ out: string,
121
+ includeAgents: boolean,
122
+ ): string {
123
+ const includesStr = includes.map((r) => `"${r}"`).join(", ");
124
+ const agentsBlock = match(includeAgents)
125
+ .with(true, () => "\n agents: {},\n")
126
+ .with(false, () => "\n")
127
+ .exhaustive();
128
+
129
+ return `import { defineConfig } from "@funkai/config";
130
+
131
+ export default defineConfig({
132
+ prompts: {
133
+ includes: [${includesStr}],
134
+ out: "${out}",
135
+ },${agentsBlock}});
136
+ `;
137
+ }
@@ -1,19 +1,26 @@
1
1
  import { command } from "@kidd-cli/core";
2
2
 
3
3
  import { handleLint, lintArgs } from "./prompts/lint.js";
4
+ import { getConfig } from "@/config.js";
4
5
 
5
6
  export default command({
6
7
  description: "Run all validations across the funkai SDK",
7
8
  options: lintArgs,
8
9
  handler(ctx) {
9
10
  const { silent } = ctx.args;
11
+ const config = getConfig(ctx);
10
12
 
11
13
  // --- Prompts validation ---
12
14
  if (!silent) {
13
15
  ctx.logger.info("Running prompts validation...");
14
16
  }
15
17
 
16
- handleLint(ctx.args, ctx.logger, ctx.fail);
18
+ handleLint({
19
+ args: ctx.args,
20
+ config: config.prompts,
21
+ logger: ctx.logger,
22
+ fail: ctx.fail,
23
+ });
17
24
 
18
25
  // --- Future: agents validation ---
19
26
 
package/src/config.ts ADDED
@@ -0,0 +1,28 @@
1
+ import type { FunkaiConfig } from "@funkai/config";
2
+ import { configSchema } from "@funkai/config";
3
+ import type { ConfigType, Context } from "@kidd-cli/core";
4
+
5
+ export { configSchema };
6
+
7
+ /**
8
+ * Extract the typed funkai config from a command context.
9
+ *
10
+ * kidd-cli's `Merge<CliConfig, TConfig>` erases augmented keys when
11
+ * `TConfig` defaults to `Record<string, unknown>`, so we cast here.
12
+ *
13
+ * @param ctx - The CLI context.
14
+ * @returns The typed funkai configuration.
15
+ *
16
+ * @example
17
+ * ```ts
18
+ * const config = getConfig(ctx);
19
+ * const promptsConfig = config.prompts;
20
+ * ```
21
+ */
22
+ export function getConfig(ctx: Pick<Context, "config">): Readonly<FunkaiConfig> {
23
+ return ctx.config as unknown as Readonly<FunkaiConfig>;
24
+ }
25
+
26
+ declare module "@kidd-cli/core" {
27
+ interface CliConfig extends ConfigType<typeof configSchema> {}
28
+ }
package/src/index.ts CHANGED
@@ -10,6 +10,7 @@ import promptsLint from "@/commands/prompts/lint.js";
10
10
  import promptsSetup from "@/commands/prompts/setup.js";
11
11
  import setup from "@/commands/setup.js";
12
12
  import validate from "@/commands/validate.js";
13
+ import { configSchema } from "@/config.js";
13
14
 
14
15
  const require = createRequire(import.meta.url);
15
16
  const packageJson = require("../package.json") as { readonly version: string };
@@ -18,6 +19,9 @@ await cli({
18
19
  description: "CLI for the funkai AI SDK framework",
19
20
  name: "funkai",
20
21
  version: packageJson.version,
22
+ config: {
23
+ schema: configSchema,
24
+ },
21
25
  commands: {
22
26
  generate,
23
27
  setup,