@agiflowai/aicode-toolkit 1.0.20 → 1.0.22

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/README.md CHANGED
@@ -56,6 +56,89 @@ This will:
56
56
  | `--path <path>` | Custom templates path | `./templates` |
57
57
  | `--no-download` | Skip template download | `false` |
58
58
 
59
+ ### `sync`
60
+
61
+ Generate platform config files from `.toolkit/settings.yaml` as the single source of truth.
62
+
63
+ ```bash
64
+ # Generate both .claude/settings.json and mcp-config.yaml
65
+ npx @agiflowai/aicode-toolkit sync
66
+
67
+ # Generate only .claude/settings.json (Claude Code hooks)
68
+ npx @agiflowai/aicode-toolkit sync --hooks
69
+
70
+ # Generate only mcp-config.yaml (MCP server list)
71
+ npx @agiflowai/aicode-toolkit sync --mcp
72
+ ```
73
+
74
+ **Options:**
75
+ | Option | Description |
76
+ |--------|-------------|
77
+ | `--hooks` | Write `.claude/settings.json` only |
78
+ | `--mcp` | Write `mcp-config.yaml` only |
79
+
80
+ #### Hooks → `.claude/settings.json`
81
+
82
+ Hook commands are derived automatically from `mcp-config.servers` by replacing
83
+ `mcp-serve` with `hook --type claude-code.<method>`. Configure which methods to
84
+ activate in `.toolkit/settings.yaml`:
85
+
86
+ ```yaml
87
+ scaffold-mcp:
88
+ hook:
89
+ claude-code:
90
+ preToolUse:
91
+ args: # extra CLI args appended to the generated hook command
92
+ llm-tool: gemini-cli
93
+ postToolUse: {}
94
+ stop: {}
95
+ userPromptSubmit: {}
96
+ taskCompleted: {}
97
+
98
+ architect-mcp:
99
+ hook:
100
+ claude-code:
101
+ preToolUse:
102
+ args:
103
+ llm-tool: gemini-cli
104
+ postToolUse: {}
105
+ ```
106
+
107
+ Generated hook entries fire on all tool calls (no matcher). Run `aicode sync --hooks`
108
+ to write `.claude/settings.json`.
109
+
110
+ #### `mcp-config` section → `mcp-config.yaml`
111
+
112
+ Define MCP servers in `.toolkit/settings.yaml`:
113
+
114
+ ```yaml
115
+ mcp-config:
116
+ servers:
117
+ scaffold-mcp:
118
+ command: bun
119
+ args:
120
+ - run
121
+ - packages/scaffold-mcp/src/cli.ts
122
+ - mcp-serve
123
+ - --admin-enable
124
+ - --prompt-as-skill
125
+ instruction: "Use this server for generating boilerplate code and scaffolding."
126
+ architect-mcp:
127
+ command: bun
128
+ args:
129
+ - run
130
+ - packages/architect-mcp/src/cli.ts
131
+ - mcp-serve
132
+ instruction: "Use this server for design pattern guidance and code review."
133
+ skills:
134
+ paths:
135
+ - docs/skills
136
+ ```
137
+
138
+ Run `aicode sync --mcp` to write `mcp-config.yaml` from this config.
139
+
140
+ ---
141
+
59
142
  ### `add`
60
143
 
61
144
  Add templates from GitHub repositories.
package/dist/cli.cjs CHANGED
@@ -2,16 +2,20 @@
2
2
  const require_utils = require('./utils-r_182Nor.cjs');
3
3
  const require_mcp = require('./mcp--A-5zuBI.cjs');
4
4
  let __agiflowai_aicode_utils = require("@agiflowai/aicode-utils");
5
+ let node_fs_promises = require("node:fs/promises");
5
6
  let node_path = require("node:path");
6
7
  node_path = require_utils.__toESM(node_path);
8
+ let liquidjs = require("liquidjs");
7
9
  let commander = require("commander");
8
10
  let __inquirer_prompts = require("@inquirer/prompts");
9
11
  let ora = require("ora");
10
12
  ora = require_utils.__toESM(ora);
11
13
  let xstate = require("xstate");
14
+ let js_yaml = require("js-yaml");
15
+ js_yaml = require_utils.__toESM(js_yaml);
12
16
 
13
17
  //#region package.json
14
- var version = "1.0.19";
18
+ var version = "1.0.21";
15
19
 
16
20
  //#endregion
17
21
  //#region src/commands/add.ts
@@ -75,6 +79,10 @@ const addCommand = new commander.Command("add").description("Add a template to t
75
79
  }
76
80
  });
77
81
 
82
+ //#endregion
83
+ //#region src/templates/settings.yaml.liquid?raw
84
+ var settings_yaml_default = "# .toolkit/settings.yaml\n# Toolkit configuration for aicode-toolkit\n#\n# Local overrides go in .toolkit/settings.local.yaml (add to .gitignore).\n# Local settings are shallow-merged over this file — useful for per-developer\n# LLM tool preferences or API keys without committing them.\n\n# ---------------------------------------------------------------------------\n# Project identity\n# ---------------------------------------------------------------------------\n{% if projectType %}\nprojectType: {{ projectType }}\n{% else %}\n# projectType: monolith\n{% endif %}\n{% if templatesPath %}\ntemplatesPath: {{ templatesPath }}\n{% else %}\n# templatesPath: ./templates\n{% endif %}\n{% if sourceTemplate %}\nsourceTemplate: {{ sourceTemplate }}\n{% else %}\n# sourceTemplate: react-vite\n{% endif %}\nversion: '1.0'\n\n# ---------------------------------------------------------------------------\n# scaffold-mcp — configuration for the scaffold-mcp CLI\n# CLI flags always take precedence over values set here.\n# ---------------------------------------------------------------------------\n\nscaffold-mcp:\n\n # -------------------------------------------------------------------------\n # mcp-serve — defaults for `scaffold-mcp mcp-serve`\n # -------------------------------------------------------------------------\n mcp-serve:\n # type: stdio\n # port: 3000\n # host: localhost\n # adminEnable: false\n # promptAsSkill: false\n {% if fallbackTool %}\n fallbackTool: {{ fallbackTool }}\n {% if fallbackToolConfig %}\n fallbackToolConfig:\n {% for pair in fallbackToolConfig %}\n {{ pair[0] }}: {{ pair[1] }}\n {% endfor %}\n {% else %}\n # fallbackToolConfig:\n # model: gemini-2.0-flash\n {% endif %}\n {% else %}\n # fallbackTool: gemini-cli\n # fallbackToolConfig:\n # model: gemini-2.0-flash\n {% endif %}\n # Extra CLI args merged into the mcp-serve command (key: value → --key value)\n # args:\n # review-tool: gemini-cli\n\n # -------------------------------------------------------------------------\n # hook — defaults for `scaffold-mcp hook`\n # Keyed by agent name, then by hook method.\n # Supported agents : claude-code | gemini-cli\n # Supported methods : preToolUse | postToolUse | userPromptSubmit | taskCompleted | stop\n # -------------------------------------------------------------------------\n hook:\n\n claude-code:\n preToolUse:\n {% if fallbackTool %}\n llm-tool: {{ fallbackTool }}\n {% if fallbackToolConfig %}\n tool-config:\n {% for pair in fallbackToolConfig %}\n {{ pair[0] }}: {{ pair[1] }}\n {% endfor %}\n {% endif %}\n {% else %}\n # llm-tool: gemini-cli\n # tool-config:\n # model: gemini-2.0-flash\n {% endif %}\n # args:\n # llm-tool: gemini-cli\n\n postToolUse:\n {% if fallbackTool %}\n llm-tool: {{ fallbackTool }}\n {% if fallbackToolConfig %}\n tool-config:\n {% for pair in fallbackToolConfig %}\n {{ pair[0] }}: {{ pair[1] }}\n {% endfor %}\n {% endif %}\n {% else %}\n # llm-tool: gemini-cli\n # tool-config:\n # model: gemini-2.0-flash\n {% endif %}\n # args:\n # llm-tool: gemini-cli\n\n gemini-cli:\n postToolUse:\n {% if fallbackTool %}\n llm-tool: {{ fallbackTool }}\n {% if fallbackToolConfig %}\n tool-config:\n {% for pair in fallbackToolConfig %}\n {{ pair[0] }}: {{ pair[1] }}\n {% endfor %}\n {% endif %}\n {% else %}\n # llm-tool: gemini-cli\n # tool-config:\n # model: gemini-2.0-flash\n {% endif %}\n\n# ---------------------------------------------------------------------------\n# architect-mcp — configuration for the architect-mcp CLI\n# CLI flags always take precedence over values set here.\n# ---------------------------------------------------------------------------\n\narchitect-mcp:\n\n # -------------------------------------------------------------------------\n # mcp-serve — defaults for `architect-mcp mcp-serve`\n # -------------------------------------------------------------------------\n mcp-serve:\n # type: stdio\n # port: 3000\n # host: localhost\n # adminEnable: false\n {% if fallbackTool %}\n fallbackTool: {{ fallbackTool }}\n {% if fallbackToolConfig %}\n fallbackToolConfig:\n {% for pair in fallbackToolConfig %}\n {{ pair[0] }}: {{ pair[1] }}\n {% endfor %}\n {% else %}\n # fallbackToolConfig:\n # model: gemini-2.0-flash\n {% endif %}\n {% else %}\n # fallbackTool: gemini-cli\n # fallbackToolConfig:\n # model: gemini-2.0-flash\n {% endif %}\n # designPatternTool: gemini-cli\n # designPatternToolConfig:\n # model: gemini-2.0-flash\n # reviewTool: gemini-cli\n # reviewToolConfig:\n # model: gemini-2.0-flash\n # Extra CLI args merged into the mcp-serve command (key: value → --key value)\n # args:\n # review-tool: gemini-cli\n\n # -------------------------------------------------------------------------\n # hook — defaults for `architect-mcp hook`\n # Keyed by agent name, then by hook method.\n # Supported agents : claude-code | gemini-cli\n # Supported methods : preToolUse | postToolUse\n # -------------------------------------------------------------------------\n hook:\n\n claude-code:\n preToolUse:\n {% if fallbackTool %}\n llm-tool: {{ fallbackTool }}\n {% if fallbackToolConfig %}\n tool-config:\n {% for pair in fallbackToolConfig %}\n {{ pair[0] }}: {{ pair[1] }}\n {% endfor %}\n {% endif %}\n {% else %}\n # llm-tool: gemini-cli\n # tool-config:\n # model: gemini-2.0-flash\n {% endif %}\n # args:\n # llm-tool: gemini-cli\n\n postToolUse:\n {% if fallbackTool %}\n llm-tool: {{ fallbackTool }}\n {% if fallbackToolConfig %}\n tool-config:\n {% for pair in fallbackToolConfig %}\n {{ pair[0] }}: {{ pair[1] }}\n {% endfor %}\n {% endif %}\n {% else %}\n # llm-tool: gemini-cli\n # tool-config:\n # model: gemini-2.0-flash\n {% endif %}\n # args:\n # llm-tool: gemini-cli\n\n gemini-cli:\n preToolUse:\n {% if fallbackTool %}\n llm-tool: {{ fallbackTool }}\n {% if fallbackToolConfig %}\n tool-config:\n {% for pair in fallbackToolConfig %}\n {{ pair[0] }}: {{ pair[1] }}\n {% endfor %}\n {% endif %}\n {% else %}\n # llm-tool: gemini-cli\n # tool-config:\n # model: gemini-2.0-flash\n {% endif %}\n\n postToolUse:\n {% if fallbackTool %}\n llm-tool: {{ fallbackTool }}\n {% if fallbackToolConfig %}\n tool-config:\n {% for pair in fallbackToolConfig %}\n {{ pair[0] }}: {{ pair[1] }}\n {% endfor %}\n {% endif %}\n {% else %}\n # llm-tool: gemini-cli\n # tool-config:\n # model: gemini-2.0-flash\n {% endif %}\n\n# ---------------------------------------------------------------------------\n# mcp-config — generates mcp-config.yaml via `aicode sync --mcp`\n# Servers listed here become entries in mcp-config.yaml.\n# Run `aicode sync` to write the output file.\n# ---------------------------------------------------------------------------\n\n# mcp-config:\n# servers:\n# scaffold-mcp:\n# command: bun\n# args:\n# - run\n# - packages/scaffold-mcp/src/cli.ts\n# - mcp-serve\n# - --admin-enable\n# - --prompt-as-skill\n# instruction: \"Use this server for generating boilerplate code, scaffolding new projects, and adding features to existing projects.\"\n# architect-mcp:\n# command: bun\n# args:\n# - run\n# - packages/architect-mcp/src/cli.ts\n# - mcp-serve\n# instruction: \"Use this server for design pattern guidance and code review.\"\n# skills:\n# paths:\n# - docs/skills\n";
85
+
78
86
  //#endregion
79
87
  //#region src/states/init-machine.ts
80
88
  /**
@@ -721,15 +729,21 @@ const initActors = {
721
729
  }),
722
730
  createConfig: (0, xstate.fromPromise)(async ({ input: actorInput }) => {
723
731
  if (actorInput.projectType === __agiflowai_aicode_utils.ProjectType.MONOLITH) {
724
- const toolkitConfig = {
725
- version: "1.0",
726
- templatesPath: node_path.default.relative(actorInput.workspaceRoot, actorInput.templatesPath) || "templates",
732
+ const relativeTemplatesPath = node_path.default.relative(actorInput.workspaceRoot, actorInput.templatesPath);
733
+ const content = await new liquidjs.Liquid().parseAndRender(settings_yaml_default, {
727
734
  projectType: "monolith",
735
+ templatesPath: relativeTemplatesPath || "templates",
728
736
  sourceTemplate: actorInput.selectedTemplates[0]
729
- };
730
- __agiflowai_aicode_utils.print.info("\nCreating toolkit.yaml...");
731
- await __agiflowai_aicode_utils.TemplatesManagerService.writeToolkitConfig(toolkitConfig, actorInput.workspaceRoot);
732
- __agiflowai_aicode_utils.print.success("toolkit.yaml created");
737
+ });
738
+ const toolkitDir = node_path.default.join(actorInput.workspaceRoot, ".toolkit");
739
+ const settingsPath = node_path.default.join(toolkitDir, "settings.yaml");
740
+ __agiflowai_aicode_utils.print.info("\nCreating .toolkit/settings.yaml...");
741
+ await (0, node_fs_promises.mkdir)(toolkitDir, { recursive: true });
742
+ await (0, node_fs_promises.writeFile)(settingsPath, content, {
743
+ encoding: "utf-8",
744
+ flag: "w"
745
+ });
746
+ __agiflowai_aicode_utils.print.success(".toolkit/settings.yaml created");
733
747
  }
734
748
  }),
735
749
  detectCodingAgent: (0, xstate.fromPromise)(async ({ input: actorInput }) => {
@@ -897,17 +911,176 @@ const initCommand = new commander.Command("init").description("Initialize projec
897
911
  }
898
912
  });
899
913
 
914
+ //#endregion
915
+ //#region src/commands/sync.ts
916
+ /**
917
+ * Sync Command
918
+ *
919
+ * DESIGN PATTERNS:
920
+ * - Command pattern with Commander for CLI argument parsing
921
+ * - Async/await pattern for asynchronous operations
922
+ * - Error handling pattern with try-catch and proper exit codes
923
+ *
924
+ * CODING STANDARDS:
925
+ * - Use async action handlers for asynchronous operations
926
+ * - Provide clear option descriptions and default values
927
+ * - Handle errors gracefully with process.exit()
928
+ * - Log progress and errors to console
929
+ * - Use Commander's .option() and .argument() for inputs
930
+ *
931
+ * AVOID:
932
+ * - Synchronous blocking operations in action handlers
933
+ * - Missing error handling (always use try-catch)
934
+ * - Hardcoded values (use options or environment variables)
935
+ * - Not exiting with appropriate exit codes on errors
936
+ */
937
+ const CLAUDE_SETTINGS_DIR = ".claude";
938
+ const CLAUDE_SETTINGS_FILE = "settings.json";
939
+ const MCP_CONFIG_FILE = "mcp-config.yaml";
940
+ function hasHookConfig(config) {
941
+ return !!(config["scaffold-mcp"]?.hook?.["claude-code"] || config["architect-mcp"]?.hook?.["claude-code"]);
942
+ }
943
+ function argsToFlags(args) {
944
+ const flags = [];
945
+ for (const [key, value] of Object.entries(args)) {
946
+ if (value === false) continue;
947
+ flags.push(`--${key}`);
948
+ if (value !== true) flags.push(String(value));
949
+ }
950
+ return flags;
951
+ }
952
+ function buildHookCommand(server, hookType, extraFlags) {
953
+ const serverArgs = server.args ?? [];
954
+ const scriptIdx = serverArgs.findIndex((arg) => /\.(ts|js|mjs|cjs)$/.test(arg));
955
+ if (scriptIdx >= 0) {
956
+ const prefixArgs$1 = serverArgs.slice(0, scriptIdx + 1);
957
+ return [
958
+ server.command,
959
+ ...prefixArgs$1,
960
+ "hook",
961
+ "--type",
962
+ hookType,
963
+ ...extraFlags
964
+ ].join(" ");
965
+ }
966
+ const mcpServeIdx = serverArgs.indexOf("mcp-serve");
967
+ const prefixArgs = mcpServeIdx >= 0 ? serverArgs.slice(0, mcpServeIdx) : serverArgs;
968
+ return [
969
+ server.command,
970
+ ...prefixArgs,
971
+ "hook",
972
+ "--type",
973
+ hookType,
974
+ ...extraFlags
975
+ ].join(" ");
976
+ }
977
+ function addHookEntry(output, event, command) {
978
+ if (!output[event]) output[event] = [];
979
+ output[event].push({ hooks: [{
980
+ type: "command",
981
+ command
982
+ }] });
983
+ }
984
+ function isMcpConfigYaml(value) {
985
+ return typeof value === "object" && value !== null;
986
+ }
987
+ async function readMcpConfigYaml(workspaceRoot) {
988
+ try {
989
+ const content = await (0, node_fs_promises.readFile)(node_path.default.join(workspaceRoot, MCP_CONFIG_FILE), "utf-8");
990
+ const parsed = js_yaml.default.load(content);
991
+ if (!isMcpConfigYaml(parsed)) throw new Error(`${MCP_CONFIG_FILE} has unexpected structure`);
992
+ return parsed;
993
+ } catch (err) {
994
+ if (err instanceof Error && "code" in err && err.code === "ENOENT") return {};
995
+ const message = err instanceof Error ? err.message : String(err);
996
+ throw new Error(`Failed to read ${MCP_CONFIG_FILE}: ${message}`);
997
+ }
998
+ }
999
+ const METHOD_TO_EVENT = {
1000
+ preToolUse: "PreToolUse",
1001
+ postToolUse: "PostToolUse",
1002
+ stop: "Stop",
1003
+ userPromptSubmit: "UserPromptSubmit",
1004
+ taskCompleted: "TaskCompleted"
1005
+ };
1006
+ async function writeClaudeSettings(config, workspaceRoot) {
1007
+ try {
1008
+ const mcpServers = (await readMcpConfigYaml(workspaceRoot)).mcpServers ?? {};
1009
+ const hooksOutput = {};
1010
+ let hasAny = false;
1011
+ const scaffoldAgent = config["scaffold-mcp"]?.hook?.["claude-code"];
1012
+ const scaffoldServer = mcpServers["scaffold-mcp"];
1013
+ if (scaffoldAgent && scaffoldServer) for (const [method, methodConfig] of [
1014
+ ["preToolUse", scaffoldAgent.preToolUse],
1015
+ ["postToolUse", scaffoldAgent.postToolUse],
1016
+ ["stop", scaffoldAgent.stop],
1017
+ ["userPromptSubmit", scaffoldAgent.userPromptSubmit],
1018
+ ["taskCompleted", scaffoldAgent.taskCompleted]
1019
+ ]) {
1020
+ if (methodConfig === void 0) continue;
1021
+ const extraFlags = methodConfig?.args ? argsToFlags(methodConfig?.args) : [];
1022
+ const command = buildHookCommand(scaffoldServer, `claude-code.${method}`, extraFlags);
1023
+ addHookEntry(hooksOutput, METHOD_TO_EVENT[method], command);
1024
+ hasAny = true;
1025
+ }
1026
+ const architectAgent = config["architect-mcp"]?.hook?.["claude-code"];
1027
+ const architectServer = mcpServers["architect-mcp"];
1028
+ if (architectAgent && architectServer) for (const [method, methodConfig] of [["preToolUse", architectAgent.preToolUse], ["postToolUse", architectAgent.postToolUse]]) {
1029
+ if (methodConfig === void 0) continue;
1030
+ const extraFlags = methodConfig?.args ? argsToFlags(methodConfig?.args) : [];
1031
+ const command = buildHookCommand(architectServer, `claude-code.${method}`, extraFlags);
1032
+ addHookEntry(hooksOutput, METHOD_TO_EVENT[method], command);
1033
+ hasAny = true;
1034
+ }
1035
+ if (!hasAny) {
1036
+ __agiflowai_aicode_utils.print.warning("No scaffold-mcp/architect-mcp hook.claude-code config found — skipping .claude/settings.json");
1037
+ return;
1038
+ }
1039
+ const settings = { hooks: hooksOutput };
1040
+ const claudeDir = node_path.default.join(workspaceRoot, CLAUDE_SETTINGS_DIR);
1041
+ await (0, node_fs_promises.mkdir)(claudeDir, { recursive: true });
1042
+ await (0, node_fs_promises.writeFile)(node_path.default.join(claudeDir, CLAUDE_SETTINGS_FILE), JSON.stringify(settings, null, 2), "utf-8");
1043
+ } catch (err) {
1044
+ const message = err instanceof Error ? err.message : String(err);
1045
+ throw new Error(`Failed to write ${CLAUDE_SETTINGS_DIR}/${CLAUDE_SETTINGS_FILE}: ${message}`);
1046
+ }
1047
+ }
1048
+ /**
1049
+ * Generate .claude/settings.json from .toolkit/settings.yaml and mcp-config.yaml
1050
+ */
1051
+ const syncCommand = new commander.Command("sync").description("Generate .claude/settings.json from .toolkit/settings.yaml and mcp-config.yaml").action(async () => {
1052
+ try {
1053
+ const [workspaceRoot, config] = await Promise.all([__agiflowai_aicode_utils.TemplatesManagerService.getWorkspaceRoot(), __agiflowai_aicode_utils.TemplatesManagerService.readToolkitConfig()]);
1054
+ if (!config) throw new Error("No .toolkit/settings.yaml found. Run `aicode init` first.");
1055
+ if (hasHookConfig(config)) {
1056
+ await writeClaudeSettings(config, workspaceRoot);
1057
+ __agiflowai_aicode_utils.print.success("Written .claude/settings.json");
1058
+ } else __agiflowai_aicode_utils.print.warning("No hook.claude-code config found in settings.yaml — skipping");
1059
+ } catch (error) {
1060
+ const message = error instanceof Error ? error.message : String(error);
1061
+ __agiflowai_aicode_utils.print.error(`sync failed: ${message}`);
1062
+ process.exit(1);
1063
+ }
1064
+ });
1065
+
900
1066
  //#endregion
901
1067
  //#region src/cli.ts
902
1068
  /**
903
1069
  * Main entry point
904
1070
  */
905
1071
  async function main() {
906
- const program = new commander.Command();
907
- program.name("aicode").description("AI-powered code toolkit CLI for scaffolding, architecture management, and development workflows").version(version);
908
- program.addCommand(initCommand);
909
- program.addCommand(addCommand);
910
- await program.parseAsync(process.argv);
1072
+ try {
1073
+ const program = new commander.Command();
1074
+ program.name("aicode").description("AI-powered code toolkit CLI for scaffolding, architecture management, and development workflows").version(version);
1075
+ program.addCommand(initCommand);
1076
+ program.addCommand(addCommand);
1077
+ program.addCommand(syncCommand);
1078
+ await program.parseAsync(process.argv);
1079
+ } catch (error) {
1080
+ const message = error instanceof Error ? error.message : String(error);
1081
+ console.error(`aicode: ${message}`);
1082
+ process.exit(1);
1083
+ }
911
1084
  }
912
1085
  main();
913
1086
 
package/dist/cli.mjs CHANGED
@@ -2,14 +2,17 @@
2
2
  import { r as MCP_SERVER_INFO, t as MCPServer } from "./mcp-CBcPdzNG.mjs";
3
3
  import { a as SPEC_TOOL_INFO, c as NewProjectService, i as TemplateSelectionService, l as CodingAgentService, o as SpecTool, s as SpecToolService, t as displayBanner } from "./utils-QWjfWNVp.mjs";
4
4
  import { ProjectType, TemplatesManagerService, cloneRepository, cloneSubdirectory, detectProjectType, ensureDir, findWorkspaceRoot, icons, messages, parseGitHubUrl, pathExists, print, sections } from "@agiflowai/aicode-utils";
5
+ import { mkdir as mkdir$1, readFile as readFile$1, writeFile as writeFile$1 } from "node:fs/promises";
5
6
  import path from "node:path";
7
+ import { Liquid } from "liquidjs";
6
8
  import { Command } from "commander";
7
9
  import { confirm, input, select } from "@inquirer/prompts";
8
10
  import ora from "ora";
9
11
  import { assign, createActor, createMachine, fromPromise } from "xstate";
12
+ import yaml from "js-yaml";
10
13
 
11
14
  //#region package.json
12
- var version = "1.0.19";
15
+ var version = "1.0.21";
13
16
 
14
17
  //#endregion
15
18
  //#region src/commands/add.ts
@@ -73,6 +76,10 @@ const addCommand = new Command("add").description("Add a template to templates f
73
76
  }
74
77
  });
75
78
 
79
+ //#endregion
80
+ //#region src/templates/settings.yaml.liquid?raw
81
+ var settings_yaml_default = "# .toolkit/settings.yaml\n# Toolkit configuration for aicode-toolkit\n#\n# Local overrides go in .toolkit/settings.local.yaml (add to .gitignore).\n# Local settings are shallow-merged over this file — useful for per-developer\n# LLM tool preferences or API keys without committing them.\n\n# ---------------------------------------------------------------------------\n# Project identity\n# ---------------------------------------------------------------------------\n{% if projectType %}\nprojectType: {{ projectType }}\n{% else %}\n# projectType: monolith\n{% endif %}\n{% if templatesPath %}\ntemplatesPath: {{ templatesPath }}\n{% else %}\n# templatesPath: ./templates\n{% endif %}\n{% if sourceTemplate %}\nsourceTemplate: {{ sourceTemplate }}\n{% else %}\n# sourceTemplate: react-vite\n{% endif %}\nversion: '1.0'\n\n# ---------------------------------------------------------------------------\n# scaffold-mcp — configuration for the scaffold-mcp CLI\n# CLI flags always take precedence over values set here.\n# ---------------------------------------------------------------------------\n\nscaffold-mcp:\n\n # -------------------------------------------------------------------------\n # mcp-serve — defaults for `scaffold-mcp mcp-serve`\n # -------------------------------------------------------------------------\n mcp-serve:\n # type: stdio\n # port: 3000\n # host: localhost\n # adminEnable: false\n # promptAsSkill: false\n {% if fallbackTool %}\n fallbackTool: {{ fallbackTool }}\n {% if fallbackToolConfig %}\n fallbackToolConfig:\n {% for pair in fallbackToolConfig %}\n {{ pair[0] }}: {{ pair[1] }}\n {% endfor %}\n {% else %}\n # fallbackToolConfig:\n # model: gemini-2.0-flash\n {% endif %}\n {% else %}\n # fallbackTool: gemini-cli\n # fallbackToolConfig:\n # model: gemini-2.0-flash\n {% endif %}\n # Extra CLI args merged into the mcp-serve command (key: value → --key value)\n # args:\n # review-tool: gemini-cli\n\n # -------------------------------------------------------------------------\n # hook — defaults for `scaffold-mcp hook`\n # Keyed by agent name, then by hook method.\n # Supported agents : claude-code | gemini-cli\n # Supported methods : preToolUse | postToolUse | userPromptSubmit | taskCompleted | stop\n # -------------------------------------------------------------------------\n hook:\n\n claude-code:\n preToolUse:\n {% if fallbackTool %}\n llm-tool: {{ fallbackTool }}\n {% if fallbackToolConfig %}\n tool-config:\n {% for pair in fallbackToolConfig %}\n {{ pair[0] }}: {{ pair[1] }}\n {% endfor %}\n {% endif %}\n {% else %}\n # llm-tool: gemini-cli\n # tool-config:\n # model: gemini-2.0-flash\n {% endif %}\n # args:\n # llm-tool: gemini-cli\n\n postToolUse:\n {% if fallbackTool %}\n llm-tool: {{ fallbackTool }}\n {% if fallbackToolConfig %}\n tool-config:\n {% for pair in fallbackToolConfig %}\n {{ pair[0] }}: {{ pair[1] }}\n {% endfor %}\n {% endif %}\n {% else %}\n # llm-tool: gemini-cli\n # tool-config:\n # model: gemini-2.0-flash\n {% endif %}\n # args:\n # llm-tool: gemini-cli\n\n gemini-cli:\n postToolUse:\n {% if fallbackTool %}\n llm-tool: {{ fallbackTool }}\n {% if fallbackToolConfig %}\n tool-config:\n {% for pair in fallbackToolConfig %}\n {{ pair[0] }}: {{ pair[1] }}\n {% endfor %}\n {% endif %}\n {% else %}\n # llm-tool: gemini-cli\n # tool-config:\n # model: gemini-2.0-flash\n {% endif %}\n\n# ---------------------------------------------------------------------------\n# architect-mcp — configuration for the architect-mcp CLI\n# CLI flags always take precedence over values set here.\n# ---------------------------------------------------------------------------\n\narchitect-mcp:\n\n # -------------------------------------------------------------------------\n # mcp-serve — defaults for `architect-mcp mcp-serve`\n # -------------------------------------------------------------------------\n mcp-serve:\n # type: stdio\n # port: 3000\n # host: localhost\n # adminEnable: false\n {% if fallbackTool %}\n fallbackTool: {{ fallbackTool }}\n {% if fallbackToolConfig %}\n fallbackToolConfig:\n {% for pair in fallbackToolConfig %}\n {{ pair[0] }}: {{ pair[1] }}\n {% endfor %}\n {% else %}\n # fallbackToolConfig:\n # model: gemini-2.0-flash\n {% endif %}\n {% else %}\n # fallbackTool: gemini-cli\n # fallbackToolConfig:\n # model: gemini-2.0-flash\n {% endif %}\n # designPatternTool: gemini-cli\n # designPatternToolConfig:\n # model: gemini-2.0-flash\n # reviewTool: gemini-cli\n # reviewToolConfig:\n # model: gemini-2.0-flash\n # Extra CLI args merged into the mcp-serve command (key: value → --key value)\n # args:\n # review-tool: gemini-cli\n\n # -------------------------------------------------------------------------\n # hook — defaults for `architect-mcp hook`\n # Keyed by agent name, then by hook method.\n # Supported agents : claude-code | gemini-cli\n # Supported methods : preToolUse | postToolUse\n # -------------------------------------------------------------------------\n hook:\n\n claude-code:\n preToolUse:\n {% if fallbackTool %}\n llm-tool: {{ fallbackTool }}\n {% if fallbackToolConfig %}\n tool-config:\n {% for pair in fallbackToolConfig %}\n {{ pair[0] }}: {{ pair[1] }}\n {% endfor %}\n {% endif %}\n {% else %}\n # llm-tool: gemini-cli\n # tool-config:\n # model: gemini-2.0-flash\n {% endif %}\n # args:\n # llm-tool: gemini-cli\n\n postToolUse:\n {% if fallbackTool %}\n llm-tool: {{ fallbackTool }}\n {% if fallbackToolConfig %}\n tool-config:\n {% for pair in fallbackToolConfig %}\n {{ pair[0] }}: {{ pair[1] }}\n {% endfor %}\n {% endif %}\n {% else %}\n # llm-tool: gemini-cli\n # tool-config:\n # model: gemini-2.0-flash\n {% endif %}\n # args:\n # llm-tool: gemini-cli\n\n gemini-cli:\n preToolUse:\n {% if fallbackTool %}\n llm-tool: {{ fallbackTool }}\n {% if fallbackToolConfig %}\n tool-config:\n {% for pair in fallbackToolConfig %}\n {{ pair[0] }}: {{ pair[1] }}\n {% endfor %}\n {% endif %}\n {% else %}\n # llm-tool: gemini-cli\n # tool-config:\n # model: gemini-2.0-flash\n {% endif %}\n\n postToolUse:\n {% if fallbackTool %}\n llm-tool: {{ fallbackTool }}\n {% if fallbackToolConfig %}\n tool-config:\n {% for pair in fallbackToolConfig %}\n {{ pair[0] }}: {{ pair[1] }}\n {% endfor %}\n {% endif %}\n {% else %}\n # llm-tool: gemini-cli\n # tool-config:\n # model: gemini-2.0-flash\n {% endif %}\n\n# ---------------------------------------------------------------------------\n# mcp-config — generates mcp-config.yaml via `aicode sync --mcp`\n# Servers listed here become entries in mcp-config.yaml.\n# Run `aicode sync` to write the output file.\n# ---------------------------------------------------------------------------\n\n# mcp-config:\n# servers:\n# scaffold-mcp:\n# command: bun\n# args:\n# - run\n# - packages/scaffold-mcp/src/cli.ts\n# - mcp-serve\n# - --admin-enable\n# - --prompt-as-skill\n# instruction: \"Use this server for generating boilerplate code, scaffolding new projects, and adding features to existing projects.\"\n# architect-mcp:\n# command: bun\n# args:\n# - run\n# - packages/architect-mcp/src/cli.ts\n# - mcp-serve\n# instruction: \"Use this server for design pattern guidance and code review.\"\n# skills:\n# paths:\n# - docs/skills\n";
82
+
76
83
  //#endregion
77
84
  //#region src/states/init-machine.ts
78
85
  /**
@@ -598,11 +605,11 @@ const initActors = {
598
605
  }),
599
606
  checkTemplatesFolder: fromPromise(async ({ input: actorInput }) => {
600
607
  try {
601
- const fs = await import("node:fs/promises");
608
+ const fs$1 = await import("node:fs/promises");
602
609
  const defaultTemplatesPath = path.join(actorInput.workspaceRoot, "templates");
603
610
  let templatesExists = false;
604
611
  try {
605
- await fs.access(defaultTemplatesPath);
612
+ await fs$1.access(defaultTemplatesPath);
606
613
  templatesExists = true;
607
614
  } catch {
608
615
  templatesExists = false;
@@ -630,7 +637,7 @@ const initActors = {
630
637
  print.info("");
631
638
  finalTemplatesPath = path.join(actorInput.workspaceRoot, customDir.trim());
632
639
  try {
633
- await fs.mkdir(finalTemplatesPath, { recursive: true });
640
+ await fs$1.mkdir(finalTemplatesPath, { recursive: true });
634
641
  print.success(`Created templates directory at: ${finalTemplatesPath}`);
635
642
  } catch (error) {
636
643
  throw new Error(`Failed to create templates directory at ${finalTemplatesPath}: ${error.message}`);
@@ -719,15 +726,21 @@ const initActors = {
719
726
  }),
720
727
  createConfig: fromPromise(async ({ input: actorInput }) => {
721
728
  if (actorInput.projectType === ProjectType.MONOLITH) {
722
- const toolkitConfig = {
723
- version: "1.0",
724
- templatesPath: path.relative(actorInput.workspaceRoot, actorInput.templatesPath) || "templates",
729
+ const relativeTemplatesPath = path.relative(actorInput.workspaceRoot, actorInput.templatesPath);
730
+ const content = await new Liquid().parseAndRender(settings_yaml_default, {
725
731
  projectType: "monolith",
732
+ templatesPath: relativeTemplatesPath || "templates",
726
733
  sourceTemplate: actorInput.selectedTemplates[0]
727
- };
728
- print.info("\nCreating toolkit.yaml...");
729
- await TemplatesManagerService.writeToolkitConfig(toolkitConfig, actorInput.workspaceRoot);
730
- print.success("toolkit.yaml created");
734
+ });
735
+ const toolkitDir = path.join(actorInput.workspaceRoot, ".toolkit");
736
+ const settingsPath = path.join(toolkitDir, "settings.yaml");
737
+ print.info("\nCreating .toolkit/settings.yaml...");
738
+ await mkdir$1(toolkitDir, { recursive: true });
739
+ await writeFile$1(settingsPath, content, {
740
+ encoding: "utf-8",
741
+ flag: "w"
742
+ });
743
+ print.success(".toolkit/settings.yaml created");
731
744
  }
732
745
  }),
733
746
  detectCodingAgent: fromPromise(async ({ input: actorInput }) => {
@@ -895,17 +908,176 @@ const initCommand = new Command("init").description("Initialize project with tem
895
908
  }
896
909
  });
897
910
 
911
+ //#endregion
912
+ //#region src/commands/sync.ts
913
+ /**
914
+ * Sync Command
915
+ *
916
+ * DESIGN PATTERNS:
917
+ * - Command pattern with Commander for CLI argument parsing
918
+ * - Async/await pattern for asynchronous operations
919
+ * - Error handling pattern with try-catch and proper exit codes
920
+ *
921
+ * CODING STANDARDS:
922
+ * - Use async action handlers for asynchronous operations
923
+ * - Provide clear option descriptions and default values
924
+ * - Handle errors gracefully with process.exit()
925
+ * - Log progress and errors to console
926
+ * - Use Commander's .option() and .argument() for inputs
927
+ *
928
+ * AVOID:
929
+ * - Synchronous blocking operations in action handlers
930
+ * - Missing error handling (always use try-catch)
931
+ * - Hardcoded values (use options or environment variables)
932
+ * - Not exiting with appropriate exit codes on errors
933
+ */
934
+ const CLAUDE_SETTINGS_DIR = ".claude";
935
+ const CLAUDE_SETTINGS_FILE = "settings.json";
936
+ const MCP_CONFIG_FILE = "mcp-config.yaml";
937
+ function hasHookConfig(config) {
938
+ return !!(config["scaffold-mcp"]?.hook?.["claude-code"] || config["architect-mcp"]?.hook?.["claude-code"]);
939
+ }
940
+ function argsToFlags(args) {
941
+ const flags = [];
942
+ for (const [key, value] of Object.entries(args)) {
943
+ if (value === false) continue;
944
+ flags.push(`--${key}`);
945
+ if (value !== true) flags.push(String(value));
946
+ }
947
+ return flags;
948
+ }
949
+ function buildHookCommand(server, hookType, extraFlags) {
950
+ const serverArgs = server.args ?? [];
951
+ const scriptIdx = serverArgs.findIndex((arg) => /\.(ts|js|mjs|cjs)$/.test(arg));
952
+ if (scriptIdx >= 0) {
953
+ const prefixArgs$1 = serverArgs.slice(0, scriptIdx + 1);
954
+ return [
955
+ server.command,
956
+ ...prefixArgs$1,
957
+ "hook",
958
+ "--type",
959
+ hookType,
960
+ ...extraFlags
961
+ ].join(" ");
962
+ }
963
+ const mcpServeIdx = serverArgs.indexOf("mcp-serve");
964
+ const prefixArgs = mcpServeIdx >= 0 ? serverArgs.slice(0, mcpServeIdx) : serverArgs;
965
+ return [
966
+ server.command,
967
+ ...prefixArgs,
968
+ "hook",
969
+ "--type",
970
+ hookType,
971
+ ...extraFlags
972
+ ].join(" ");
973
+ }
974
+ function addHookEntry(output, event, command) {
975
+ if (!output[event]) output[event] = [];
976
+ output[event].push({ hooks: [{
977
+ type: "command",
978
+ command
979
+ }] });
980
+ }
981
+ function isMcpConfigYaml(value) {
982
+ return typeof value === "object" && value !== null;
983
+ }
984
+ async function readMcpConfigYaml(workspaceRoot) {
985
+ try {
986
+ const content = await readFile$1(path.join(workspaceRoot, MCP_CONFIG_FILE), "utf-8");
987
+ const parsed = yaml.load(content);
988
+ if (!isMcpConfigYaml(parsed)) throw new Error(`${MCP_CONFIG_FILE} has unexpected structure`);
989
+ return parsed;
990
+ } catch (err) {
991
+ if (err instanceof Error && "code" in err && err.code === "ENOENT") return {};
992
+ const message = err instanceof Error ? err.message : String(err);
993
+ throw new Error(`Failed to read ${MCP_CONFIG_FILE}: ${message}`);
994
+ }
995
+ }
996
+ const METHOD_TO_EVENT = {
997
+ preToolUse: "PreToolUse",
998
+ postToolUse: "PostToolUse",
999
+ stop: "Stop",
1000
+ userPromptSubmit: "UserPromptSubmit",
1001
+ taskCompleted: "TaskCompleted"
1002
+ };
1003
+ async function writeClaudeSettings(config, workspaceRoot) {
1004
+ try {
1005
+ const mcpServers = (await readMcpConfigYaml(workspaceRoot)).mcpServers ?? {};
1006
+ const hooksOutput = {};
1007
+ let hasAny = false;
1008
+ const scaffoldAgent = config["scaffold-mcp"]?.hook?.["claude-code"];
1009
+ const scaffoldServer = mcpServers["scaffold-mcp"];
1010
+ if (scaffoldAgent && scaffoldServer) for (const [method, methodConfig] of [
1011
+ ["preToolUse", scaffoldAgent.preToolUse],
1012
+ ["postToolUse", scaffoldAgent.postToolUse],
1013
+ ["stop", scaffoldAgent.stop],
1014
+ ["userPromptSubmit", scaffoldAgent.userPromptSubmit],
1015
+ ["taskCompleted", scaffoldAgent.taskCompleted]
1016
+ ]) {
1017
+ if (methodConfig === void 0) continue;
1018
+ const extraFlags = methodConfig?.args ? argsToFlags(methodConfig?.args) : [];
1019
+ const command = buildHookCommand(scaffoldServer, `claude-code.${method}`, extraFlags);
1020
+ addHookEntry(hooksOutput, METHOD_TO_EVENT[method], command);
1021
+ hasAny = true;
1022
+ }
1023
+ const architectAgent = config["architect-mcp"]?.hook?.["claude-code"];
1024
+ const architectServer = mcpServers["architect-mcp"];
1025
+ if (architectAgent && architectServer) for (const [method, methodConfig] of [["preToolUse", architectAgent.preToolUse], ["postToolUse", architectAgent.postToolUse]]) {
1026
+ if (methodConfig === void 0) continue;
1027
+ const extraFlags = methodConfig?.args ? argsToFlags(methodConfig?.args) : [];
1028
+ const command = buildHookCommand(architectServer, `claude-code.${method}`, extraFlags);
1029
+ addHookEntry(hooksOutput, METHOD_TO_EVENT[method], command);
1030
+ hasAny = true;
1031
+ }
1032
+ if (!hasAny) {
1033
+ print.warning("No scaffold-mcp/architect-mcp hook.claude-code config found — skipping .claude/settings.json");
1034
+ return;
1035
+ }
1036
+ const settings = { hooks: hooksOutput };
1037
+ const claudeDir = path.join(workspaceRoot, CLAUDE_SETTINGS_DIR);
1038
+ await mkdir$1(claudeDir, { recursive: true });
1039
+ await writeFile$1(path.join(claudeDir, CLAUDE_SETTINGS_FILE), JSON.stringify(settings, null, 2), "utf-8");
1040
+ } catch (err) {
1041
+ const message = err instanceof Error ? err.message : String(err);
1042
+ throw new Error(`Failed to write ${CLAUDE_SETTINGS_DIR}/${CLAUDE_SETTINGS_FILE}: ${message}`);
1043
+ }
1044
+ }
1045
+ /**
1046
+ * Generate .claude/settings.json from .toolkit/settings.yaml and mcp-config.yaml
1047
+ */
1048
+ const syncCommand = new Command("sync").description("Generate .claude/settings.json from .toolkit/settings.yaml and mcp-config.yaml").action(async () => {
1049
+ try {
1050
+ const [workspaceRoot, config] = await Promise.all([TemplatesManagerService.getWorkspaceRoot(), TemplatesManagerService.readToolkitConfig()]);
1051
+ if (!config) throw new Error("No .toolkit/settings.yaml found. Run `aicode init` first.");
1052
+ if (hasHookConfig(config)) {
1053
+ await writeClaudeSettings(config, workspaceRoot);
1054
+ print.success("Written .claude/settings.json");
1055
+ } else print.warning("No hook.claude-code config found in settings.yaml — skipping");
1056
+ } catch (error) {
1057
+ const message = error instanceof Error ? error.message : String(error);
1058
+ print.error(`sync failed: ${message}`);
1059
+ process.exit(1);
1060
+ }
1061
+ });
1062
+
898
1063
  //#endregion
899
1064
  //#region src/cli.ts
900
1065
  /**
901
1066
  * Main entry point
902
1067
  */
903
1068
  async function main() {
904
- const program = new Command();
905
- program.name("aicode").description("AI-powered code toolkit CLI for scaffolding, architecture management, and development workflows").version(version);
906
- program.addCommand(initCommand);
907
- program.addCommand(addCommand);
908
- await program.parseAsync(process.argv);
1069
+ try {
1070
+ const program = new Command();
1071
+ program.name("aicode").description("AI-powered code toolkit CLI for scaffolding, architecture management, and development workflows").version(version);
1072
+ program.addCommand(initCommand);
1073
+ program.addCommand(addCommand);
1074
+ program.addCommand(syncCommand);
1075
+ await program.parseAsync(process.argv);
1076
+ } catch (error) {
1077
+ const message = error instanceof Error ? error.message : String(error);
1078
+ console.error(`aicode: ${message}`);
1079
+ process.exit(1);
1080
+ }
909
1081
  }
910
1082
  main();
911
1083
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@agiflowai/aicode-toolkit",
3
3
  "description": "AI-powered code toolkit CLI for scaffolding, architecture management, and development workflows",
4
- "version": "1.0.20",
4
+ "version": "1.0.22",
5
5
  "license": "AGPL-3.0",
6
6
  "author": "AgiflowIO",
7
7
  "repository": {
@@ -50,8 +50,8 @@
50
50
  "pino-pretty": "^13.1.1",
51
51
  "xstate": "^5.23.0",
52
52
  "zod": "3.25.76",
53
- "@agiflowai/aicode-utils": "1.0.14",
54
- "@agiflowai/coding-agent-bridge": "1.0.17"
53
+ "@agiflowai/aicode-utils": "1.0.16",
54
+ "@agiflowai/coding-agent-bridge": "1.0.19"
55
55
  },
56
56
  "devDependencies": {
57
57
  "@types/express": "^5.0.0",