@agiflowai/aicode-utils 1.0.14 → 1.0.16

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/dist/index.cjs CHANGED
@@ -191,16 +191,41 @@ const log = {
191
191
  var TemplatesManagerService = class TemplatesManagerService {
192
192
  static SCAFFOLD_CONFIG_FILE = "scaffold.yaml";
193
193
  static TEMPLATES_FOLDER = "templates";
194
+ static TOOLKIT_FOLDER = ".toolkit";
195
+ static SETTINGS_FILE = "settings.yaml";
196
+ static SETTINGS_LOCAL_FILE = "settings.local.yaml";
194
197
  static TOOLKIT_CONFIG_FILE = "toolkit.yaml";
195
198
  /**
199
+ * Recursively merge two plain objects. Primitive and array values in `local`
200
+ * replace those in `base`; plain-object values are merged recursively.
201
+ */
202
+ static deepMerge(base, local) {
203
+ const result = { ...base };
204
+ for (const [key, localValue] of Object.entries(local)) {
205
+ const baseValue = result[key];
206
+ if (localValue !== null && typeof localValue === "object" && !Array.isArray(localValue) && baseValue !== null && typeof baseValue === "object" && !Array.isArray(baseValue)) result[key] = TemplatesManagerService.deepMerge(baseValue, localValue);
207
+ else result[key] = localValue;
208
+ }
209
+ return result;
210
+ }
211
+ /**
212
+ * Deep-merge two ToolkitConfig objects. Plain-object values are merged
213
+ * recursively; primitives and arrays in `local` replace those in `base`.
214
+ * This allows settings.local.yaml to override a single leaf key (e.g.
215
+ * scaffold-mcp.mcp-serve.fallbackTool) without wiping sibling keys.
216
+ */
217
+ static mergeToolkitConfigs(base, local) {
218
+ return TemplatesManagerService.deepMerge(base, local);
219
+ }
220
+ /**
196
221
  * Find the templates directory by searching upwards from the starting path.
197
222
  *
198
223
  * Algorithm:
199
224
  * 1. Start from the provided path (default: current working directory)
200
225
  * 2. Search upwards to find the workspace root (where .git exists or filesystem root)
201
- * 3. Check if toolkit.yaml exists at workspace root
202
- * - If yes, read templatesPath from toolkit.yaml
203
- * - If no, default to 'templates' folder in workspace root
226
+ * 3. Read toolkit config (checks .toolkit/settings.yaml, then toolkit.yaml)
227
+ * - If config has templatesPath, use it
228
+ * - If no config, default to 'templates' folder in workspace root
204
229
  * 4. Verify the templates directory exists
205
230
  *
206
231
  * @param startPath - The path to start searching from (defaults to process.cwd())
@@ -208,16 +233,11 @@ var TemplatesManagerService = class TemplatesManagerService {
208
233
  */
209
234
  static async findTemplatesPath(startPath = process.cwd()) {
210
235
  const workspaceRoot = await TemplatesManagerService.findWorkspaceRoot(startPath);
211
- const toolkitConfigPath = node_path.default.join(workspaceRoot, TemplatesManagerService.TOOLKIT_CONFIG_FILE);
212
- if (await pathExists(toolkitConfigPath)) {
213
- const yaml = await import("js-yaml");
214
- const content = await node_fs_promises.readFile(toolkitConfigPath, "utf-8");
215
- const config = yaml.load(content);
216
- if (config?.templatesPath) {
217
- const templatesPath$1 = node_path.default.isAbsolute(config.templatesPath) ? config.templatesPath : node_path.default.join(workspaceRoot, config.templatesPath);
218
- if (await pathExists(templatesPath$1)) return templatesPath$1;
219
- else return null;
220
- }
236
+ const config = await TemplatesManagerService.readToolkitConfig(startPath);
237
+ if (config?.templatesPath) {
238
+ const templatesPath$1 = node_path.default.isAbsolute(config.templatesPath) ? config.templatesPath : node_path.default.join(workspaceRoot, config.templatesPath);
239
+ if (await pathExists(templatesPath$1)) return templatesPath$1;
240
+ return null;
221
241
  }
222
242
  const templatesPath = node_path.default.join(workspaceRoot, TemplatesManagerService.TEMPLATES_FOLDER);
223
243
  if (await pathExists(templatesPath)) return templatesPath;
@@ -244,16 +264,11 @@ var TemplatesManagerService = class TemplatesManagerService {
244
264
  */
245
265
  static findTemplatesPathSync(startPath = process.cwd()) {
246
266
  const workspaceRoot = TemplatesManagerService.findWorkspaceRootSync(startPath);
247
- const toolkitConfigPath = node_path.default.join(workspaceRoot, TemplatesManagerService.TOOLKIT_CONFIG_FILE);
248
- if (pathExistsSync(toolkitConfigPath)) {
249
- const yaml = require("js-yaml");
250
- const content = (0, node_fs.readFileSync)(toolkitConfigPath, "utf-8");
251
- const config = yaml.load(content);
252
- if (config?.templatesPath) {
253
- const templatesPath$1 = node_path.default.isAbsolute(config.templatesPath) ? config.templatesPath : node_path.default.join(workspaceRoot, config.templatesPath);
254
- if (pathExistsSync(templatesPath$1)) return templatesPath$1;
255
- else return null;
256
- }
267
+ const config = TemplatesManagerService.readToolkitConfigSync(startPath);
268
+ if (config?.templatesPath) {
269
+ const templatesPath$1 = node_path.default.isAbsolute(config.templatesPath) ? config.templatesPath : node_path.default.join(workspaceRoot, config.templatesPath);
270
+ if (pathExistsSync(templatesPath$1)) return templatesPath$1;
271
+ return null;
257
272
  }
258
273
  const templatesPath = node_path.default.join(workspaceRoot, TemplatesManagerService.TEMPLATES_FOLDER);
259
274
  if (pathExistsSync(templatesPath)) return templatesPath;
@@ -294,44 +309,80 @@ var TemplatesManagerService = class TemplatesManagerService {
294
309
  return TemplatesManagerService.TEMPLATES_FOLDER;
295
310
  }
296
311
  /**
297
- * Read toolkit.yaml configuration from workspace root
312
+ * Read toolkit configuration from workspace root.
313
+ *
314
+ * Priority order:
315
+ * 1. .toolkit/settings.yaml (new location)
316
+ * 2. Shallow-merge .toolkit/settings.local.yaml over settings.yaml if present
317
+ * 3. Fallback to root toolkit.yaml (deprecated, backward-compat)
298
318
  *
299
319
  * @param startPath - The path to start searching from (defaults to process.cwd())
300
320
  * @returns The toolkit configuration object or null if not found
301
321
  */
302
322
  static async readToolkitConfig(startPath = process.cwd()) {
303
323
  const workspaceRoot = await TemplatesManagerService.findWorkspaceRoot(startPath);
304
- const toolkitConfigPath = node_path.default.join(workspaceRoot, TemplatesManagerService.TOOLKIT_CONFIG_FILE);
305
- if (!await pathExists(toolkitConfigPath)) return null;
306
324
  const yaml = await import("js-yaml");
307
- const content = await node_fs_promises.readFile(toolkitConfigPath, "utf-8");
325
+ const toolkitFolder = node_path.default.join(workspaceRoot, TemplatesManagerService.TOOLKIT_FOLDER);
326
+ const settingsPath = node_path.default.join(toolkitFolder, TemplatesManagerService.SETTINGS_FILE);
327
+ const settingsLocalPath = node_path.default.join(toolkitFolder, TemplatesManagerService.SETTINGS_LOCAL_FILE);
328
+ if (await pathExists(settingsPath)) {
329
+ const baseContent = await node_fs_promises.readFile(settingsPath, "utf-8");
330
+ const base = yaml.load(baseContent);
331
+ if (await pathExists(settingsLocalPath)) {
332
+ const localContent = await node_fs_promises.readFile(settingsLocalPath, "utf-8");
333
+ const local = yaml.load(localContent);
334
+ return TemplatesManagerService.mergeToolkitConfigs(base, local);
335
+ }
336
+ return base;
337
+ }
338
+ const legacyConfigPath = node_path.default.join(workspaceRoot, TemplatesManagerService.TOOLKIT_CONFIG_FILE);
339
+ if (!await pathExists(legacyConfigPath)) return null;
340
+ const content = await node_fs_promises.readFile(legacyConfigPath, "utf-8");
308
341
  return yaml.load(content);
309
342
  }
310
343
  /**
311
- * Read toolkit.yaml configuration from workspace root (sync)
344
+ * Read toolkit configuration from workspace root (sync).
345
+ *
346
+ * Priority order:
347
+ * 1. .toolkit/settings.yaml (new location)
348
+ * 2. Shallow-merge .toolkit/settings.local.yaml over settings.yaml if present
349
+ * 3. Fallback to root toolkit.yaml (deprecated, backward-compat)
312
350
  *
313
351
  * @param startPath - The path to start searching from (defaults to process.cwd())
314
352
  * @returns The toolkit configuration object or null if not found
315
353
  */
316
354
  static readToolkitConfigSync(startPath = process.cwd()) {
317
355
  const workspaceRoot = TemplatesManagerService.findWorkspaceRootSync(startPath);
318
- const toolkitConfigPath = node_path.default.join(workspaceRoot, TemplatesManagerService.TOOLKIT_CONFIG_FILE);
319
- if (!pathExistsSync(toolkitConfigPath)) return null;
320
356
  const yaml = require("js-yaml");
321
- const content = (0, node_fs.readFileSync)(toolkitConfigPath, "utf-8");
322
- return yaml.load(content);
357
+ const toolkitFolder = node_path.default.join(workspaceRoot, TemplatesManagerService.TOOLKIT_FOLDER);
358
+ const settingsPath = node_path.default.join(toolkitFolder, TemplatesManagerService.SETTINGS_FILE);
359
+ const settingsLocalPath = node_path.default.join(toolkitFolder, TemplatesManagerService.SETTINGS_LOCAL_FILE);
360
+ if (pathExistsSync(settingsPath)) {
361
+ const base = yaml.load((0, node_fs.readFileSync)(settingsPath, "utf-8"));
362
+ if (pathExistsSync(settingsLocalPath)) {
363
+ const local = yaml.load((0, node_fs.readFileSync)(settingsLocalPath, "utf-8"));
364
+ return TemplatesManagerService.mergeToolkitConfigs(base, local);
365
+ }
366
+ return base;
367
+ }
368
+ const legacyConfigPath = node_path.default.join(workspaceRoot, TemplatesManagerService.TOOLKIT_CONFIG_FILE);
369
+ if (!pathExistsSync(legacyConfigPath)) return null;
370
+ return yaml.load((0, node_fs.readFileSync)(legacyConfigPath, "utf-8"));
323
371
  }
324
372
  /**
325
- * Write toolkit.yaml configuration to workspace root
373
+ * Write toolkit configuration to .toolkit/settings.yaml.
374
+ * Creates the .toolkit directory if it does not exist.
326
375
  *
327
376
  * @param config - The toolkit configuration to write
328
377
  * @param startPath - The path to start searching from (defaults to process.cwd())
329
378
  */
330
379
  static async writeToolkitConfig(config, startPath = process.cwd()) {
331
380
  const workspaceRoot = await TemplatesManagerService.findWorkspaceRoot(startPath);
332
- const toolkitConfigPath = node_path.default.join(workspaceRoot, TemplatesManagerService.TOOLKIT_CONFIG_FILE);
381
+ const toolkitFolder = node_path.default.join(workspaceRoot, TemplatesManagerService.TOOLKIT_FOLDER);
382
+ const settingsPath = node_path.default.join(toolkitFolder, TemplatesManagerService.SETTINGS_FILE);
383
+ await node_fs_promises.mkdir(toolkitFolder, { recursive: true });
333
384
  const content = (await import("js-yaml")).dump(config, { indent: 2 });
334
- await node_fs_promises.writeFile(toolkitConfigPath, content, "utf-8");
385
+ await node_fs_promises.writeFile(settingsPath, content, "utf-8");
335
386
  }
336
387
  /**
337
388
  * Get the workspace root directory
package/dist/index.d.cts CHANGED
@@ -47,82 +47,332 @@ interface NxProjectJson {
47
47
  //#region src/types/index.d.ts
48
48
 
49
49
  /**
50
- * Toolkit configuration from toolkit.yaml
50
+ * Configuration for the scaffold-mcp mcp-serve command.
51
+ * Keys map 1-to-1 with CLI flags (camelCase).
52
+ */
53
+ interface McpServeConfig {
54
+ /** Transport type. Default: stdio. */
55
+ type?: 'stdio' | 'http' | 'sse';
56
+ /** Port for http/sse transport. Default: 3000. */
57
+ port?: number;
58
+ /** Host to bind for http/sse transport. Default: localhost. */
59
+ host?: string;
60
+ /** Enable admin tools such as generate-boilerplate. Default: false. */
61
+ adminEnable?: boolean;
62
+ /** Render prompts with skill front matter for Claude Code. Default: false. */
63
+ promptAsSkill?: boolean;
64
+ /** Fallback LLM tool used when scaffold-mcp needs AI assistance. */
65
+ fallbackTool?: string;
66
+ /** Config passed to the fallback LLM tool. */
67
+ fallbackToolConfig?: Record<string, unknown>;
68
+ /** Extra CLI args merged into the mcp-serve command (key → --key value). */
69
+ args?: Record<string, string | boolean | number>;
70
+ }
71
+ /**
72
+ * Configuration for a single hook method invocation.
73
+ * Keys use kebab-case matching the adapter/CLI convention.
74
+ */
75
+ interface HookMethodConfig {
76
+ /** LLM tool to invoke for this hook method (e.g. claude-code, gemini-cli). */
77
+ 'llm-tool'?: string;
78
+ /** Config object forwarded to the LLM tool (e.g. { model: 'gemini-2.0-flash' }). */
79
+ 'tool-config'?: Record<string, unknown>;
80
+ /** Fallback LLM tool used when the primary tool is unavailable. */
81
+ 'fallback-tool'?: string;
82
+ /** Config object forwarded to the fallback LLM tool. */
83
+ 'fallback-tool-config'?: Record<string, unknown>;
84
+ /** Extra CLI args appended to the generated hook command (key → --key value). */
85
+ args?: Record<string, string | boolean | number>;
86
+ }
87
+ /**
88
+ * Per-method configuration for a specific agent.
89
+ */
90
+ interface HookAgentConfig {
91
+ /** Config applied when the agent invokes the PreToolUse hook. */
92
+ preToolUse?: HookMethodConfig;
93
+ /** Config applied when the agent invokes the PostToolUse hook. */
94
+ postToolUse?: HookMethodConfig;
95
+ /** Config applied when the agent invokes the Stop hook. */
96
+ stop?: HookMethodConfig;
97
+ /** Config applied when the agent invokes the UserPromptSubmit hook. */
98
+ userPromptSubmit?: HookMethodConfig;
99
+ /** Config applied when the agent invokes the TaskCompleted hook. */
100
+ taskCompleted?: HookMethodConfig;
101
+ }
102
+ /**
103
+ * Hook configuration keyed by agent name.
104
+ */
105
+ interface HookConfig {
106
+ /** Hook config for Claude Code agent. */
107
+ 'claude-code'?: HookAgentConfig;
108
+ /** Hook config for Gemini CLI agent. */
109
+ 'gemini-cli'?: HookAgentConfig;
110
+ }
111
+ /**
112
+ * Top-level scaffold-mcp configuration block.
113
+ */
114
+ interface ScaffoldMcpConfig {
115
+ /** Defaults for the `scaffold-mcp mcp-serve` command. */
116
+ 'mcp-serve'?: McpServeConfig;
117
+ /** Hook method defaults keyed by agent name. */
118
+ hook?: HookConfig;
119
+ }
120
+ /**
121
+ * Configuration for the architect-mcp mcp-serve command.
122
+ * Keys map 1-to-1 with CLI flags (camelCase).
123
+ */
124
+ interface ArchitectMcpServeConfig {
125
+ /** Transport type. Default: stdio. */
126
+ type?: 'stdio' | 'http' | 'sse';
127
+ /** Port for http/sse transport. Default: 3000. */
128
+ port?: number;
129
+ /** Host to bind for http/sse transport. Default: localhost. */
130
+ host?: string;
131
+ /** Enable admin tools such as add-design-pattern and add-rule. Default: false. */
132
+ adminEnable?: boolean;
133
+ /** Fallback LLM tool used for both design-pattern and review operations. */
134
+ fallbackTool?: string;
135
+ /** Config passed to the fallback LLM tool. */
136
+ fallbackToolConfig?: Record<string, unknown>;
137
+ /** LLM tool used specifically for get-file-design-pattern analysis. */
138
+ designPatternTool?: string;
139
+ /** Config passed to the design-pattern LLM tool. */
140
+ designPatternToolConfig?: Record<string, unknown>;
141
+ /** LLM tool used specifically for review-code-change analysis. */
142
+ reviewTool?: string;
143
+ /** Config passed to the review LLM tool. */
144
+ reviewToolConfig?: Record<string, unknown>;
145
+ /** Extra CLI args merged into the mcp-serve command (key → --key value). */
146
+ args?: Record<string, string | boolean | number>;
147
+ }
148
+ /**
149
+ * Configuration for a single architect-mcp hook method invocation.
150
+ * Keys use kebab-case matching the adapter/CLI convention.
151
+ */
152
+ interface ArchitectHookMethodConfig {
153
+ /** LLM tool to invoke for this hook method. */
154
+ 'llm-tool'?: string;
155
+ /** Config object forwarded to the LLM tool. */
156
+ 'tool-config'?: Record<string, unknown>;
157
+ /** Extra CLI args appended to the generated hook command (key → --key value). */
158
+ args?: Record<string, string | boolean | number>;
159
+ }
160
+ /**
161
+ * Per-method configuration for a specific agent (architect-mcp).
162
+ * Only preToolUse and postToolUse are supported.
163
+ */
164
+ interface ArchitectHookAgentConfig {
165
+ /** Config applied when the agent invokes the PreToolUse hook. */
166
+ preToolUse?: ArchitectHookMethodConfig;
167
+ /** Config applied when the agent invokes the PostToolUse hook. */
168
+ postToolUse?: ArchitectHookMethodConfig;
169
+ }
170
+ /**
171
+ * Hook configuration keyed by agent name (architect-mcp).
172
+ */
173
+ interface ArchitectHookConfig {
174
+ /** Hook config for Claude Code agent. */
175
+ 'claude-code'?: ArchitectHookAgentConfig;
176
+ /** Hook config for Gemini CLI agent. */
177
+ 'gemini-cli'?: ArchitectHookAgentConfig;
178
+ }
179
+ /**
180
+ * Top-level architect-mcp configuration block.
181
+ */
182
+ interface ArchitectMcpConfig {
183
+ /** Defaults for the `architect-mcp mcp-serve` command. */
184
+ 'mcp-serve'?: ArchitectMcpServeConfig;
185
+ /** Hook method defaults keyed by agent name. */
186
+ hook?: ArchitectHookConfig;
187
+ }
188
+ /**
189
+ * Toolkit configuration from .toolkit/settings.yaml (or legacy toolkit.yaml)
51
190
  */
52
191
  interface ToolkitConfig {
192
+ /** Config schema version (e.g. '1.0'). */
53
193
  version?: string;
194
+ /** Path to the scaffold templates directory, relative to workspace root. */
54
195
  templatesPath?: string;
196
+ /** Project structure type: monolith (single app) or monorepo (multiple packages). */
55
197
  projectType?: 'monolith' | 'monorepo';
198
+ /** Active template name (monolith only — monorepo reads from project.json). */
56
199
  sourceTemplate?: string;
200
+ /** scaffold-mcp server and hook configuration. */
201
+ 'scaffold-mcp'?: ScaffoldMcpConfig;
202
+ /** architect-mcp server and hook configuration. */
203
+ 'architect-mcp'?: ArchitectMcpConfig;
57
204
  }
58
205
  /**
59
206
  * Project configuration from project.json
60
207
  */
61
208
  interface ProjectConfig {
209
+ /** Package/project name as declared in project.json. */
62
210
  name: string;
211
+ /** Root directory of the project, relative to workspace root. */
63
212
  root: string;
213
+ /** Template this project was scaffolded from. */
64
214
  sourceTemplate?: string;
215
+ /** Project type as declared in project.json (e.g. 'application' or 'library'). */
65
216
  projectType?: string;
66
217
  }
67
218
  /**
68
219
  * Scaffold template include configuration
69
220
  */
70
221
  interface ParsedInclude {
222
+ /** Absolute path of the source template file. */
71
223
  sourcePath: string;
224
+ /** Absolute path of the destination file in the target directory. */
72
225
  targetPath: string;
226
+ /** Conditions that must be satisfied for this include to be applied. */
73
227
  conditions?: Record<string, string>;
74
228
  }
75
229
  /**
76
230
  * Result of a scaffold operation
77
231
  */
78
232
  interface ScaffoldResult {
233
+ /** Whether the scaffold operation completed without errors. */
79
234
  success: boolean;
235
+ /** Human-readable summary of the operation outcome. */
80
236
  message: string;
237
+ /** Non-fatal warnings collected during the operation. */
81
238
  warnings?: string[];
239
+ /** Paths of files that were newly created. */
82
240
  createdFiles?: string[];
241
+ /** Paths of files that already existed and were not overwritten. */
83
242
  existingFiles?: string[];
84
243
  }
244
+ /**
245
+ * Minimal stat result returned by IFileSystemService.stat.
246
+ */
247
+ interface FileStat {
248
+ /**
249
+ * @returns True when the path is a directory.
250
+ */
251
+ isDirectory(): boolean;
252
+ /**
253
+ * @returns True when the path is a regular file.
254
+ */
255
+ isFile(): boolean;
256
+ }
85
257
  /**
86
258
  * Abstract interface for file system operations
87
259
  */
88
260
  interface IFileSystemService {
261
+ /**
262
+ * Check whether a path exists on disk.
263
+ * @param path - Absolute path to check.
264
+ * @returns True when the path exists.
265
+ */
89
266
  pathExists(path: string): Promise<boolean>;
267
+ /**
268
+ * Read a file as text.
269
+ * @param path - Absolute path of the file.
270
+ * @param encoding - Character encoding (default: utf-8).
271
+ * @returns File contents as a string.
272
+ */
90
273
  readFile(path: string, encoding?: BufferEncoding): Promise<string>;
91
- readJson(path: string): Promise<any>;
274
+ /**
275
+ * Read and parse a JSON file. Returns unknown — callers must narrow the type.
276
+ * @param path - Absolute path of the JSON file.
277
+ * @returns Parsed value with type unknown.
278
+ */
279
+ readJson(path: string): Promise<unknown>;
280
+ /**
281
+ * Write text content to a file.
282
+ * @param path - Absolute path of the target file.
283
+ * @param content - Text to write.
284
+ * @param encoding - Character encoding (default: utf-8).
285
+ * @returns Promise that resolves when the file is written.
286
+ */
92
287
  writeFile(path: string, content: string, encoding?: BufferEncoding): Promise<void>;
288
+ /**
289
+ * Create a directory and all parent directories.
290
+ * @param path - Absolute path of the directory to create.
291
+ * @returns Promise that resolves when the directory exists.
292
+ */
93
293
  ensureDir(path: string): Promise<void>;
294
+ /**
295
+ * Copy a file or directory from src to dest.
296
+ * @param src - Absolute source path.
297
+ * @param dest - Absolute destination path.
298
+ * @returns Promise that resolves when the copy is complete.
299
+ */
94
300
  copy(src: string, dest: string): Promise<void>;
301
+ /**
302
+ * List the entries of a directory.
303
+ * @param path - Absolute path of the directory.
304
+ * @returns Array of entry names (not full paths).
305
+ */
95
306
  readdir(path: string): Promise<string[]>;
96
- stat(path: string): Promise<{
97
- isDirectory(): boolean;
98
- isFile(): boolean;
99
- }>;
307
+ /**
308
+ * Return stat info for a path.
309
+ * @param path - Absolute path to stat.
310
+ * @returns FileStat with isDirectory and isFile helpers.
311
+ */
312
+ stat(path: string): Promise<FileStat>;
100
313
  }
101
314
  /**
102
315
  * Abstract interface for variable replacement in templates
103
316
  */
104
317
  interface IVariableReplacementService {
105
- processFilesForVariableReplacement(dirPath: string, variables: Record<string, any>): Promise<void>;
106
- replaceVariablesInFile(filePath: string, variables: Record<string, any>): Promise<void>;
318
+ /**
319
+ * Walk dirPath and apply variable substitution to every non-binary file.
320
+ * @param dirPath - Directory to process recursively.
321
+ * @param variables - Key/value pairs used for substitution.
322
+ * @returns Promise that resolves when all files have been processed.
323
+ */
324
+ processFilesForVariableReplacement(dirPath: string, variables: Record<string, unknown>): Promise<void>;
325
+ /**
326
+ * Apply variable substitution to a single file.
327
+ * @param filePath - File to process.
328
+ * @param variables - Key/value pairs used for substitution.
329
+ * @returns Promise that resolves when the file has been processed.
330
+ */
331
+ replaceVariablesInFile(filePath: string, variables: Record<string, unknown>): Promise<void>;
332
+ /**
333
+ * Checks if a file should be treated as a binary (non-text) file.
334
+ * @param filePath - Path to check.
335
+ * @returns True if the file is binary.
336
+ */
107
337
  isBinaryFile(filePath: string): boolean;
108
338
  }
109
339
  /**
110
- * Context object passed to generator functions
340
+ * Context object passed to generator functions.
341
+ * Bundles all dependencies needed to produce scaffold output.
111
342
  */
112
343
  interface GeneratorContext {
113
- variables: Record<string, any>;
114
- config: any;
344
+ /** Template variables resolved for the current scaffold operation. */
345
+ variables: Record<string, unknown>;
346
+ /** Raw scaffold configuration loaded from scaffold.yaml. */
347
+ config: unknown;
348
+ /** Absolute path of the directory where output files will be written. */
115
349
  targetPath: string;
350
+ /** Absolute path of the source template directory. */
116
351
  templatePath: string;
352
+ /** File-system abstraction used for all I/O inside generators. */
117
353
  fileSystem: IFileSystemService;
118
- scaffoldConfigLoader: any;
354
+ /** Loader for scaffold config files — typed as unknown to avoid circular deps. */
355
+ scaffoldConfigLoader: unknown;
356
+ /** Variable-replacement service injected to avoid circular imports. */
119
357
  variableReplacer: IVariableReplacementService;
120
- ScaffoldProcessingService: any;
358
+ /** ScaffoldProcessingService constructor — passed to avoid circular imports. */
359
+ ScaffoldProcessingService: new (...args: unknown[]) => unknown;
360
+ /**
361
+ * Return the workspace root path.
362
+ * @returns Absolute path of the workspace root.
363
+ */
121
364
  getRootPath: () => string;
365
+ /**
366
+ * Return the absolute path of a project relative to the workspace root.
367
+ * @param projectPath - Project path relative to the workspace root.
368
+ * @returns Absolute path of the project.
369
+ */
122
370
  getProjectPath: (projectPath: string) => string;
123
371
  }
124
372
  /**
125
- * Type definition for generator functions
373
+ * Type definition for generator functions.
374
+ * @param context - The generator context bundling all scaffold dependencies.
375
+ * @returns A promise resolving to the scaffold result.
126
376
  */
127
377
  type GeneratorFunction = (context: GeneratorContext) => Promise<ScaffoldResult>;
128
378
  //#endregion
@@ -254,16 +504,31 @@ declare class ScaffoldProcessingService {
254
504
  declare class TemplatesManagerService {
255
505
  private static SCAFFOLD_CONFIG_FILE;
256
506
  private static TEMPLATES_FOLDER;
507
+ private static TOOLKIT_FOLDER;
508
+ private static SETTINGS_FILE;
509
+ private static SETTINGS_LOCAL_FILE;
257
510
  private static TOOLKIT_CONFIG_FILE;
511
+ /**
512
+ * Recursively merge two plain objects. Primitive and array values in `local`
513
+ * replace those in `base`; plain-object values are merged recursively.
514
+ */
515
+ private static deepMerge;
516
+ /**
517
+ * Deep-merge two ToolkitConfig objects. Plain-object values are merged
518
+ * recursively; primitives and arrays in `local` replace those in `base`.
519
+ * This allows settings.local.yaml to override a single leaf key (e.g.
520
+ * scaffold-mcp.mcp-serve.fallbackTool) without wiping sibling keys.
521
+ */
522
+ private static mergeToolkitConfigs;
258
523
  /**
259
524
  * Find the templates directory by searching upwards from the starting path.
260
525
  *
261
526
  * Algorithm:
262
527
  * 1. Start from the provided path (default: current working directory)
263
528
  * 2. Search upwards to find the workspace root (where .git exists or filesystem root)
264
- * 3. Check if toolkit.yaml exists at workspace root
265
- * - If yes, read templatesPath from toolkit.yaml
266
- * - If no, default to 'templates' folder in workspace root
529
+ * 3. Read toolkit config (checks .toolkit/settings.yaml, then toolkit.yaml)
530
+ * - If config has templatesPath, use it
531
+ * - If no config, default to 'templates' folder in workspace root
267
532
  * 4. Verify the templates directory exists
268
533
  *
269
534
  * @param startPath - The path to start searching from (defaults to process.cwd())
@@ -302,21 +567,32 @@ declare class TemplatesManagerService {
302
567
  */
303
568
  static getTemplatesFolderName(): string;
304
569
  /**
305
- * Read toolkit.yaml configuration from workspace root
570
+ * Read toolkit configuration from workspace root.
571
+ *
572
+ * Priority order:
573
+ * 1. .toolkit/settings.yaml (new location)
574
+ * 2. Shallow-merge .toolkit/settings.local.yaml over settings.yaml if present
575
+ * 3. Fallback to root toolkit.yaml (deprecated, backward-compat)
306
576
  *
307
577
  * @param startPath - The path to start searching from (defaults to process.cwd())
308
578
  * @returns The toolkit configuration object or null if not found
309
579
  */
310
580
  static readToolkitConfig(startPath?: string): Promise<ToolkitConfig | null>;
311
581
  /**
312
- * Read toolkit.yaml configuration from workspace root (sync)
582
+ * Read toolkit configuration from workspace root (sync).
583
+ *
584
+ * Priority order:
585
+ * 1. .toolkit/settings.yaml (new location)
586
+ * 2. Shallow-merge .toolkit/settings.local.yaml over settings.yaml if present
587
+ * 3. Fallback to root toolkit.yaml (deprecated, backward-compat)
313
588
  *
314
589
  * @param startPath - The path to start searching from (defaults to process.cwd())
315
590
  * @returns The toolkit configuration object or null if not found
316
591
  */
317
592
  static readToolkitConfigSync(startPath?: string): ToolkitConfig | null;
318
593
  /**
319
- * Write toolkit.yaml configuration to workspace root
594
+ * Write toolkit configuration to .toolkit/settings.yaml.
595
+ * Creates the .toolkit directory if it does not exist.
320
596
  *
321
597
  * @param config - The toolkit configuration to write
322
598
  * @param startPath - The path to start searching from (defaults to process.cwd())
@@ -680,4 +956,4 @@ interface ProjectTypeDetectionResult {
680
956
  */
681
957
  declare function detectProjectType(workspaceRoot: string): Promise<ProjectTypeDetectionResult>;
682
958
  //#endregion
683
- export { ConfigSource, GeneratorContext, GeneratorFunction, GitHubDirectoryEntry, IFileSystemService, IVariableReplacementService, NxProjectJson, ParsedGitHubUrl, ParsedInclude, ProjectConfig, ProjectConfigResolver, ProjectConfigResult, ProjectFinderService, ProjectType, ScaffoldProcessingService, ScaffoldResult, TemplatesManagerService, ToolkitConfig, accessSync, cloneRepository, cloneSubdirectory, copy, detectProjectType, ensureDir, fetchGitHubDirectoryContents, findWorkspaceRoot, generateStableId, gitInit, icons, log, logger, messages, mkdir, mkdirSync, move, parseGitHubUrl, pathExists, pathExistsSync, print, readFile, readFileSync, readJson, readJsonSync, readdir, remove, sections, stat, statSync, writeFile, writeFileSync };
959
+ export { ArchitectHookAgentConfig, ArchitectHookConfig, ArchitectHookMethodConfig, ArchitectMcpConfig, ArchitectMcpServeConfig, ConfigSource, FileStat, GeneratorContext, GeneratorFunction, GitHubDirectoryEntry, HookAgentConfig, HookConfig, HookMethodConfig, IFileSystemService, IVariableReplacementService, McpServeConfig, type NxProjectJson, ParsedGitHubUrl, ParsedInclude, ProjectConfig, ProjectConfigResolver, type ProjectConfigResult, ProjectFinderService, ProjectType, ScaffoldMcpConfig, ScaffoldProcessingService, ScaffoldResult, TemplatesManagerService, ToolkitConfig, accessSync, cloneRepository, cloneSubdirectory, copy, detectProjectType, ensureDir, fetchGitHubDirectoryContents, findWorkspaceRoot, generateStableId, gitInit, icons, log, logger, messages, mkdir, mkdirSync, move, parseGitHubUrl, pathExists, pathExistsSync, print, readFile, readFileSync, readJson, readJsonSync, readdir, remove, sections, stat, statSync, writeFile, writeFileSync };
package/dist/index.d.mts CHANGED
@@ -47,82 +47,332 @@ interface NxProjectJson {
47
47
  //#region src/types/index.d.ts
48
48
 
49
49
  /**
50
- * Toolkit configuration from toolkit.yaml
50
+ * Configuration for the scaffold-mcp mcp-serve command.
51
+ * Keys map 1-to-1 with CLI flags (camelCase).
52
+ */
53
+ interface McpServeConfig {
54
+ /** Transport type. Default: stdio. */
55
+ type?: 'stdio' | 'http' | 'sse';
56
+ /** Port for http/sse transport. Default: 3000. */
57
+ port?: number;
58
+ /** Host to bind for http/sse transport. Default: localhost. */
59
+ host?: string;
60
+ /** Enable admin tools such as generate-boilerplate. Default: false. */
61
+ adminEnable?: boolean;
62
+ /** Render prompts with skill front matter for Claude Code. Default: false. */
63
+ promptAsSkill?: boolean;
64
+ /** Fallback LLM tool used when scaffold-mcp needs AI assistance. */
65
+ fallbackTool?: string;
66
+ /** Config passed to the fallback LLM tool. */
67
+ fallbackToolConfig?: Record<string, unknown>;
68
+ /** Extra CLI args merged into the mcp-serve command (key → --key value). */
69
+ args?: Record<string, string | boolean | number>;
70
+ }
71
+ /**
72
+ * Configuration for a single hook method invocation.
73
+ * Keys use kebab-case matching the adapter/CLI convention.
74
+ */
75
+ interface HookMethodConfig {
76
+ /** LLM tool to invoke for this hook method (e.g. claude-code, gemini-cli). */
77
+ 'llm-tool'?: string;
78
+ /** Config object forwarded to the LLM tool (e.g. { model: 'gemini-2.0-flash' }). */
79
+ 'tool-config'?: Record<string, unknown>;
80
+ /** Fallback LLM tool used when the primary tool is unavailable. */
81
+ 'fallback-tool'?: string;
82
+ /** Config object forwarded to the fallback LLM tool. */
83
+ 'fallback-tool-config'?: Record<string, unknown>;
84
+ /** Extra CLI args appended to the generated hook command (key → --key value). */
85
+ args?: Record<string, string | boolean | number>;
86
+ }
87
+ /**
88
+ * Per-method configuration for a specific agent.
89
+ */
90
+ interface HookAgentConfig {
91
+ /** Config applied when the agent invokes the PreToolUse hook. */
92
+ preToolUse?: HookMethodConfig;
93
+ /** Config applied when the agent invokes the PostToolUse hook. */
94
+ postToolUse?: HookMethodConfig;
95
+ /** Config applied when the agent invokes the Stop hook. */
96
+ stop?: HookMethodConfig;
97
+ /** Config applied when the agent invokes the UserPromptSubmit hook. */
98
+ userPromptSubmit?: HookMethodConfig;
99
+ /** Config applied when the agent invokes the TaskCompleted hook. */
100
+ taskCompleted?: HookMethodConfig;
101
+ }
102
+ /**
103
+ * Hook configuration keyed by agent name.
104
+ */
105
+ interface HookConfig {
106
+ /** Hook config for Claude Code agent. */
107
+ 'claude-code'?: HookAgentConfig;
108
+ /** Hook config for Gemini CLI agent. */
109
+ 'gemini-cli'?: HookAgentConfig;
110
+ }
111
+ /**
112
+ * Top-level scaffold-mcp configuration block.
113
+ */
114
+ interface ScaffoldMcpConfig {
115
+ /** Defaults for the `scaffold-mcp mcp-serve` command. */
116
+ 'mcp-serve'?: McpServeConfig;
117
+ /** Hook method defaults keyed by agent name. */
118
+ hook?: HookConfig;
119
+ }
120
+ /**
121
+ * Configuration for the architect-mcp mcp-serve command.
122
+ * Keys map 1-to-1 with CLI flags (camelCase).
123
+ */
124
+ interface ArchitectMcpServeConfig {
125
+ /** Transport type. Default: stdio. */
126
+ type?: 'stdio' | 'http' | 'sse';
127
+ /** Port for http/sse transport. Default: 3000. */
128
+ port?: number;
129
+ /** Host to bind for http/sse transport. Default: localhost. */
130
+ host?: string;
131
+ /** Enable admin tools such as add-design-pattern and add-rule. Default: false. */
132
+ adminEnable?: boolean;
133
+ /** Fallback LLM tool used for both design-pattern and review operations. */
134
+ fallbackTool?: string;
135
+ /** Config passed to the fallback LLM tool. */
136
+ fallbackToolConfig?: Record<string, unknown>;
137
+ /** LLM tool used specifically for get-file-design-pattern analysis. */
138
+ designPatternTool?: string;
139
+ /** Config passed to the design-pattern LLM tool. */
140
+ designPatternToolConfig?: Record<string, unknown>;
141
+ /** LLM tool used specifically for review-code-change analysis. */
142
+ reviewTool?: string;
143
+ /** Config passed to the review LLM tool. */
144
+ reviewToolConfig?: Record<string, unknown>;
145
+ /** Extra CLI args merged into the mcp-serve command (key → --key value). */
146
+ args?: Record<string, string | boolean | number>;
147
+ }
148
+ /**
149
+ * Configuration for a single architect-mcp hook method invocation.
150
+ * Keys use kebab-case matching the adapter/CLI convention.
151
+ */
152
+ interface ArchitectHookMethodConfig {
153
+ /** LLM tool to invoke for this hook method. */
154
+ 'llm-tool'?: string;
155
+ /** Config object forwarded to the LLM tool. */
156
+ 'tool-config'?: Record<string, unknown>;
157
+ /** Extra CLI args appended to the generated hook command (key → --key value). */
158
+ args?: Record<string, string | boolean | number>;
159
+ }
160
+ /**
161
+ * Per-method configuration for a specific agent (architect-mcp).
162
+ * Only preToolUse and postToolUse are supported.
163
+ */
164
+ interface ArchitectHookAgentConfig {
165
+ /** Config applied when the agent invokes the PreToolUse hook. */
166
+ preToolUse?: ArchitectHookMethodConfig;
167
+ /** Config applied when the agent invokes the PostToolUse hook. */
168
+ postToolUse?: ArchitectHookMethodConfig;
169
+ }
170
+ /**
171
+ * Hook configuration keyed by agent name (architect-mcp).
172
+ */
173
+ interface ArchitectHookConfig {
174
+ /** Hook config for Claude Code agent. */
175
+ 'claude-code'?: ArchitectHookAgentConfig;
176
+ /** Hook config for Gemini CLI agent. */
177
+ 'gemini-cli'?: ArchitectHookAgentConfig;
178
+ }
179
+ /**
180
+ * Top-level architect-mcp configuration block.
181
+ */
182
+ interface ArchitectMcpConfig {
183
+ /** Defaults for the `architect-mcp mcp-serve` command. */
184
+ 'mcp-serve'?: ArchitectMcpServeConfig;
185
+ /** Hook method defaults keyed by agent name. */
186
+ hook?: ArchitectHookConfig;
187
+ }
188
+ /**
189
+ * Toolkit configuration from .toolkit/settings.yaml (or legacy toolkit.yaml)
51
190
  */
52
191
  interface ToolkitConfig {
192
+ /** Config schema version (e.g. '1.0'). */
53
193
  version?: string;
194
+ /** Path to the scaffold templates directory, relative to workspace root. */
54
195
  templatesPath?: string;
196
+ /** Project structure type: monolith (single app) or monorepo (multiple packages). */
55
197
  projectType?: 'monolith' | 'monorepo';
198
+ /** Active template name (monolith only — monorepo reads from project.json). */
56
199
  sourceTemplate?: string;
200
+ /** scaffold-mcp server and hook configuration. */
201
+ 'scaffold-mcp'?: ScaffoldMcpConfig;
202
+ /** architect-mcp server and hook configuration. */
203
+ 'architect-mcp'?: ArchitectMcpConfig;
57
204
  }
58
205
  /**
59
206
  * Project configuration from project.json
60
207
  */
61
208
  interface ProjectConfig {
209
+ /** Package/project name as declared in project.json. */
62
210
  name: string;
211
+ /** Root directory of the project, relative to workspace root. */
63
212
  root: string;
213
+ /** Template this project was scaffolded from. */
64
214
  sourceTemplate?: string;
215
+ /** Project type as declared in project.json (e.g. 'application' or 'library'). */
65
216
  projectType?: string;
66
217
  }
67
218
  /**
68
219
  * Scaffold template include configuration
69
220
  */
70
221
  interface ParsedInclude {
222
+ /** Absolute path of the source template file. */
71
223
  sourcePath: string;
224
+ /** Absolute path of the destination file in the target directory. */
72
225
  targetPath: string;
226
+ /** Conditions that must be satisfied for this include to be applied. */
73
227
  conditions?: Record<string, string>;
74
228
  }
75
229
  /**
76
230
  * Result of a scaffold operation
77
231
  */
78
232
  interface ScaffoldResult {
233
+ /** Whether the scaffold operation completed without errors. */
79
234
  success: boolean;
235
+ /** Human-readable summary of the operation outcome. */
80
236
  message: string;
237
+ /** Non-fatal warnings collected during the operation. */
81
238
  warnings?: string[];
239
+ /** Paths of files that were newly created. */
82
240
  createdFiles?: string[];
241
+ /** Paths of files that already existed and were not overwritten. */
83
242
  existingFiles?: string[];
84
243
  }
244
+ /**
245
+ * Minimal stat result returned by IFileSystemService.stat.
246
+ */
247
+ interface FileStat {
248
+ /**
249
+ * @returns True when the path is a directory.
250
+ */
251
+ isDirectory(): boolean;
252
+ /**
253
+ * @returns True when the path is a regular file.
254
+ */
255
+ isFile(): boolean;
256
+ }
85
257
  /**
86
258
  * Abstract interface for file system operations
87
259
  */
88
260
  interface IFileSystemService {
261
+ /**
262
+ * Check whether a path exists on disk.
263
+ * @param path - Absolute path to check.
264
+ * @returns True when the path exists.
265
+ */
89
266
  pathExists(path: string): Promise<boolean>;
267
+ /**
268
+ * Read a file as text.
269
+ * @param path - Absolute path of the file.
270
+ * @param encoding - Character encoding (default: utf-8).
271
+ * @returns File contents as a string.
272
+ */
90
273
  readFile(path: string, encoding?: BufferEncoding): Promise<string>;
91
- readJson(path: string): Promise<any>;
274
+ /**
275
+ * Read and parse a JSON file. Returns unknown — callers must narrow the type.
276
+ * @param path - Absolute path of the JSON file.
277
+ * @returns Parsed value with type unknown.
278
+ */
279
+ readJson(path: string): Promise<unknown>;
280
+ /**
281
+ * Write text content to a file.
282
+ * @param path - Absolute path of the target file.
283
+ * @param content - Text to write.
284
+ * @param encoding - Character encoding (default: utf-8).
285
+ * @returns Promise that resolves when the file is written.
286
+ */
92
287
  writeFile(path: string, content: string, encoding?: BufferEncoding): Promise<void>;
288
+ /**
289
+ * Create a directory and all parent directories.
290
+ * @param path - Absolute path of the directory to create.
291
+ * @returns Promise that resolves when the directory exists.
292
+ */
93
293
  ensureDir(path: string): Promise<void>;
294
+ /**
295
+ * Copy a file or directory from src to dest.
296
+ * @param src - Absolute source path.
297
+ * @param dest - Absolute destination path.
298
+ * @returns Promise that resolves when the copy is complete.
299
+ */
94
300
  copy(src: string, dest: string): Promise<void>;
301
+ /**
302
+ * List the entries of a directory.
303
+ * @param path - Absolute path of the directory.
304
+ * @returns Array of entry names (not full paths).
305
+ */
95
306
  readdir(path: string): Promise<string[]>;
96
- stat(path: string): Promise<{
97
- isDirectory(): boolean;
98
- isFile(): boolean;
99
- }>;
307
+ /**
308
+ * Return stat info for a path.
309
+ * @param path - Absolute path to stat.
310
+ * @returns FileStat with isDirectory and isFile helpers.
311
+ */
312
+ stat(path: string): Promise<FileStat>;
100
313
  }
101
314
  /**
102
315
  * Abstract interface for variable replacement in templates
103
316
  */
104
317
  interface IVariableReplacementService {
105
- processFilesForVariableReplacement(dirPath: string, variables: Record<string, any>): Promise<void>;
106
- replaceVariablesInFile(filePath: string, variables: Record<string, any>): Promise<void>;
318
+ /**
319
+ * Walk dirPath and apply variable substitution to every non-binary file.
320
+ * @param dirPath - Directory to process recursively.
321
+ * @param variables - Key/value pairs used for substitution.
322
+ * @returns Promise that resolves when all files have been processed.
323
+ */
324
+ processFilesForVariableReplacement(dirPath: string, variables: Record<string, unknown>): Promise<void>;
325
+ /**
326
+ * Apply variable substitution to a single file.
327
+ * @param filePath - File to process.
328
+ * @param variables - Key/value pairs used for substitution.
329
+ * @returns Promise that resolves when the file has been processed.
330
+ */
331
+ replaceVariablesInFile(filePath: string, variables: Record<string, unknown>): Promise<void>;
332
+ /**
333
+ * Checks if a file should be treated as a binary (non-text) file.
334
+ * @param filePath - Path to check.
335
+ * @returns True if the file is binary.
336
+ */
107
337
  isBinaryFile(filePath: string): boolean;
108
338
  }
109
339
  /**
110
- * Context object passed to generator functions
340
+ * Context object passed to generator functions.
341
+ * Bundles all dependencies needed to produce scaffold output.
111
342
  */
112
343
  interface GeneratorContext {
113
- variables: Record<string, any>;
114
- config: any;
344
+ /** Template variables resolved for the current scaffold operation. */
345
+ variables: Record<string, unknown>;
346
+ /** Raw scaffold configuration loaded from scaffold.yaml. */
347
+ config: unknown;
348
+ /** Absolute path of the directory where output files will be written. */
115
349
  targetPath: string;
350
+ /** Absolute path of the source template directory. */
116
351
  templatePath: string;
352
+ /** File-system abstraction used for all I/O inside generators. */
117
353
  fileSystem: IFileSystemService;
118
- scaffoldConfigLoader: any;
354
+ /** Loader for scaffold config files — typed as unknown to avoid circular deps. */
355
+ scaffoldConfigLoader: unknown;
356
+ /** Variable-replacement service injected to avoid circular imports. */
119
357
  variableReplacer: IVariableReplacementService;
120
- ScaffoldProcessingService: any;
358
+ /** ScaffoldProcessingService constructor — passed to avoid circular imports. */
359
+ ScaffoldProcessingService: new (...args: unknown[]) => unknown;
360
+ /**
361
+ * Return the workspace root path.
362
+ * @returns Absolute path of the workspace root.
363
+ */
121
364
  getRootPath: () => string;
365
+ /**
366
+ * Return the absolute path of a project relative to the workspace root.
367
+ * @param projectPath - Project path relative to the workspace root.
368
+ * @returns Absolute path of the project.
369
+ */
122
370
  getProjectPath: (projectPath: string) => string;
123
371
  }
124
372
  /**
125
- * Type definition for generator functions
373
+ * Type definition for generator functions.
374
+ * @param context - The generator context bundling all scaffold dependencies.
375
+ * @returns A promise resolving to the scaffold result.
126
376
  */
127
377
  type GeneratorFunction = (context: GeneratorContext) => Promise<ScaffoldResult>;
128
378
  //#endregion
@@ -254,16 +504,31 @@ declare class ScaffoldProcessingService {
254
504
  declare class TemplatesManagerService {
255
505
  private static SCAFFOLD_CONFIG_FILE;
256
506
  private static TEMPLATES_FOLDER;
507
+ private static TOOLKIT_FOLDER;
508
+ private static SETTINGS_FILE;
509
+ private static SETTINGS_LOCAL_FILE;
257
510
  private static TOOLKIT_CONFIG_FILE;
511
+ /**
512
+ * Recursively merge two plain objects. Primitive and array values in `local`
513
+ * replace those in `base`; plain-object values are merged recursively.
514
+ */
515
+ private static deepMerge;
516
+ /**
517
+ * Deep-merge two ToolkitConfig objects. Plain-object values are merged
518
+ * recursively; primitives and arrays in `local` replace those in `base`.
519
+ * This allows settings.local.yaml to override a single leaf key (e.g.
520
+ * scaffold-mcp.mcp-serve.fallbackTool) without wiping sibling keys.
521
+ */
522
+ private static mergeToolkitConfigs;
258
523
  /**
259
524
  * Find the templates directory by searching upwards from the starting path.
260
525
  *
261
526
  * Algorithm:
262
527
  * 1. Start from the provided path (default: current working directory)
263
528
  * 2. Search upwards to find the workspace root (where .git exists or filesystem root)
264
- * 3. Check if toolkit.yaml exists at workspace root
265
- * - If yes, read templatesPath from toolkit.yaml
266
- * - If no, default to 'templates' folder in workspace root
529
+ * 3. Read toolkit config (checks .toolkit/settings.yaml, then toolkit.yaml)
530
+ * - If config has templatesPath, use it
531
+ * - If no config, default to 'templates' folder in workspace root
267
532
  * 4. Verify the templates directory exists
268
533
  *
269
534
  * @param startPath - The path to start searching from (defaults to process.cwd())
@@ -302,21 +567,32 @@ declare class TemplatesManagerService {
302
567
  */
303
568
  static getTemplatesFolderName(): string;
304
569
  /**
305
- * Read toolkit.yaml configuration from workspace root
570
+ * Read toolkit configuration from workspace root.
571
+ *
572
+ * Priority order:
573
+ * 1. .toolkit/settings.yaml (new location)
574
+ * 2. Shallow-merge .toolkit/settings.local.yaml over settings.yaml if present
575
+ * 3. Fallback to root toolkit.yaml (deprecated, backward-compat)
306
576
  *
307
577
  * @param startPath - The path to start searching from (defaults to process.cwd())
308
578
  * @returns The toolkit configuration object or null if not found
309
579
  */
310
580
  static readToolkitConfig(startPath?: string): Promise<ToolkitConfig | null>;
311
581
  /**
312
- * Read toolkit.yaml configuration from workspace root (sync)
582
+ * Read toolkit configuration from workspace root (sync).
583
+ *
584
+ * Priority order:
585
+ * 1. .toolkit/settings.yaml (new location)
586
+ * 2. Shallow-merge .toolkit/settings.local.yaml over settings.yaml if present
587
+ * 3. Fallback to root toolkit.yaml (deprecated, backward-compat)
313
588
  *
314
589
  * @param startPath - The path to start searching from (defaults to process.cwd())
315
590
  * @returns The toolkit configuration object or null if not found
316
591
  */
317
592
  static readToolkitConfigSync(startPath?: string): ToolkitConfig | null;
318
593
  /**
319
- * Write toolkit.yaml configuration to workspace root
594
+ * Write toolkit configuration to .toolkit/settings.yaml.
595
+ * Creates the .toolkit directory if it does not exist.
320
596
  *
321
597
  * @param config - The toolkit configuration to write
322
598
  * @param startPath - The path to start searching from (defaults to process.cwd())
@@ -680,4 +956,4 @@ interface ProjectTypeDetectionResult {
680
956
  */
681
957
  declare function detectProjectType(workspaceRoot: string): Promise<ProjectTypeDetectionResult>;
682
958
  //#endregion
683
- export { ConfigSource, GeneratorContext, GeneratorFunction, type GitHubDirectoryEntry, IFileSystemService, IVariableReplacementService, NxProjectJson, type ParsedGitHubUrl, ParsedInclude, ProjectConfig, ProjectConfigResolver, ProjectConfigResult, ProjectFinderService, ProjectType, ScaffoldProcessingService, ScaffoldResult, TemplatesManagerService, ToolkitConfig, accessSync, cloneRepository, cloneSubdirectory, copy, detectProjectType, ensureDir, fetchGitHubDirectoryContents, findWorkspaceRoot, generateStableId, gitInit, icons, log, logger, messages, mkdir, mkdirSync, move, parseGitHubUrl, pathExists, pathExistsSync, print, readFile, readFileSync, readJson, readJsonSync, readdir, remove, sections, stat, statSync, writeFile, writeFileSync };
959
+ export { ArchitectHookAgentConfig, ArchitectHookConfig, ArchitectHookMethodConfig, ArchitectMcpConfig, ArchitectMcpServeConfig, ConfigSource, FileStat, GeneratorContext, GeneratorFunction, type GitHubDirectoryEntry, HookAgentConfig, HookConfig, HookMethodConfig, IFileSystemService, IVariableReplacementService, McpServeConfig, type NxProjectJson, type ParsedGitHubUrl, ParsedInclude, ProjectConfig, ProjectConfigResolver, type ProjectConfigResult, ProjectFinderService, ProjectType, ScaffoldMcpConfig, ScaffoldProcessingService, ScaffoldResult, TemplatesManagerService, ToolkitConfig, accessSync, cloneRepository, cloneSubdirectory, copy, detectProjectType, ensureDir, fetchGitHubDirectoryContents, findWorkspaceRoot, generateStableId, gitInit, icons, log, logger, messages, mkdir, mkdirSync, move, parseGitHubUrl, pathExists, pathExistsSync, print, readFile, readFileSync, readJson, readJsonSync, readdir, remove, sections, stat, statSync, writeFile, writeFileSync };
package/dist/index.mjs CHANGED
@@ -164,16 +164,41 @@ const log = {
164
164
  var TemplatesManagerService = class TemplatesManagerService {
165
165
  static SCAFFOLD_CONFIG_FILE = "scaffold.yaml";
166
166
  static TEMPLATES_FOLDER = "templates";
167
+ static TOOLKIT_FOLDER = ".toolkit";
168
+ static SETTINGS_FILE = "settings.yaml";
169
+ static SETTINGS_LOCAL_FILE = "settings.local.yaml";
167
170
  static TOOLKIT_CONFIG_FILE = "toolkit.yaml";
168
171
  /**
172
+ * Recursively merge two plain objects. Primitive and array values in `local`
173
+ * replace those in `base`; plain-object values are merged recursively.
174
+ */
175
+ static deepMerge(base, local) {
176
+ const result = { ...base };
177
+ for (const [key, localValue] of Object.entries(local)) {
178
+ const baseValue = result[key];
179
+ if (localValue !== null && typeof localValue === "object" && !Array.isArray(localValue) && baseValue !== null && typeof baseValue === "object" && !Array.isArray(baseValue)) result[key] = TemplatesManagerService.deepMerge(baseValue, localValue);
180
+ else result[key] = localValue;
181
+ }
182
+ return result;
183
+ }
184
+ /**
185
+ * Deep-merge two ToolkitConfig objects. Plain-object values are merged
186
+ * recursively; primitives and arrays in `local` replace those in `base`.
187
+ * This allows settings.local.yaml to override a single leaf key (e.g.
188
+ * scaffold-mcp.mcp-serve.fallbackTool) without wiping sibling keys.
189
+ */
190
+ static mergeToolkitConfigs(base, local) {
191
+ return TemplatesManagerService.deepMerge(base, local);
192
+ }
193
+ /**
169
194
  * Find the templates directory by searching upwards from the starting path.
170
195
  *
171
196
  * Algorithm:
172
197
  * 1. Start from the provided path (default: current working directory)
173
198
  * 2. Search upwards to find the workspace root (where .git exists or filesystem root)
174
- * 3. Check if toolkit.yaml exists at workspace root
175
- * - If yes, read templatesPath from toolkit.yaml
176
- * - If no, default to 'templates' folder in workspace root
199
+ * 3. Read toolkit config (checks .toolkit/settings.yaml, then toolkit.yaml)
200
+ * - If config has templatesPath, use it
201
+ * - If no config, default to 'templates' folder in workspace root
177
202
  * 4. Verify the templates directory exists
178
203
  *
179
204
  * @param startPath - The path to start searching from (defaults to process.cwd())
@@ -181,16 +206,11 @@ var TemplatesManagerService = class TemplatesManagerService {
181
206
  */
182
207
  static async findTemplatesPath(startPath = process.cwd()) {
183
208
  const workspaceRoot = await TemplatesManagerService.findWorkspaceRoot(startPath);
184
- const toolkitConfigPath = path.join(workspaceRoot, TemplatesManagerService.TOOLKIT_CONFIG_FILE);
185
- if (await pathExists(toolkitConfigPath)) {
186
- const yaml$1 = await import("js-yaml");
187
- const content = await fs.readFile(toolkitConfigPath, "utf-8");
188
- const config = yaml$1.load(content);
189
- if (config?.templatesPath) {
190
- const templatesPath$1 = path.isAbsolute(config.templatesPath) ? config.templatesPath : path.join(workspaceRoot, config.templatesPath);
191
- if (await pathExists(templatesPath$1)) return templatesPath$1;
192
- else return null;
193
- }
209
+ const config = await TemplatesManagerService.readToolkitConfig(startPath);
210
+ if (config?.templatesPath) {
211
+ const templatesPath$1 = path.isAbsolute(config.templatesPath) ? config.templatesPath : path.join(workspaceRoot, config.templatesPath);
212
+ if (await pathExists(templatesPath$1)) return templatesPath$1;
213
+ return null;
194
214
  }
195
215
  const templatesPath = path.join(workspaceRoot, TemplatesManagerService.TEMPLATES_FOLDER);
196
216
  if (await pathExists(templatesPath)) return templatesPath;
@@ -217,16 +237,11 @@ var TemplatesManagerService = class TemplatesManagerService {
217
237
  */
218
238
  static findTemplatesPathSync(startPath = process.cwd()) {
219
239
  const workspaceRoot = TemplatesManagerService.findWorkspaceRootSync(startPath);
220
- const toolkitConfigPath = path.join(workspaceRoot, TemplatesManagerService.TOOLKIT_CONFIG_FILE);
221
- if (pathExistsSync(toolkitConfigPath)) {
222
- const yaml$1 = __require("js-yaml");
223
- const content = readFileSync$1(toolkitConfigPath, "utf-8");
224
- const config = yaml$1.load(content);
225
- if (config?.templatesPath) {
226
- const templatesPath$1 = path.isAbsolute(config.templatesPath) ? config.templatesPath : path.join(workspaceRoot, config.templatesPath);
227
- if (pathExistsSync(templatesPath$1)) return templatesPath$1;
228
- else return null;
229
- }
240
+ const config = TemplatesManagerService.readToolkitConfigSync(startPath);
241
+ if (config?.templatesPath) {
242
+ const templatesPath$1 = path.isAbsolute(config.templatesPath) ? config.templatesPath : path.join(workspaceRoot, config.templatesPath);
243
+ if (pathExistsSync(templatesPath$1)) return templatesPath$1;
244
+ return null;
230
245
  }
231
246
  const templatesPath = path.join(workspaceRoot, TemplatesManagerService.TEMPLATES_FOLDER);
232
247
  if (pathExistsSync(templatesPath)) return templatesPath;
@@ -267,44 +282,80 @@ var TemplatesManagerService = class TemplatesManagerService {
267
282
  return TemplatesManagerService.TEMPLATES_FOLDER;
268
283
  }
269
284
  /**
270
- * Read toolkit.yaml configuration from workspace root
285
+ * Read toolkit configuration from workspace root.
286
+ *
287
+ * Priority order:
288
+ * 1. .toolkit/settings.yaml (new location)
289
+ * 2. Shallow-merge .toolkit/settings.local.yaml over settings.yaml if present
290
+ * 3. Fallback to root toolkit.yaml (deprecated, backward-compat)
271
291
  *
272
292
  * @param startPath - The path to start searching from (defaults to process.cwd())
273
293
  * @returns The toolkit configuration object or null if not found
274
294
  */
275
295
  static async readToolkitConfig(startPath = process.cwd()) {
276
296
  const workspaceRoot = await TemplatesManagerService.findWorkspaceRoot(startPath);
277
- const toolkitConfigPath = path.join(workspaceRoot, TemplatesManagerService.TOOLKIT_CONFIG_FILE);
278
- if (!await pathExists(toolkitConfigPath)) return null;
279
297
  const yaml$1 = await import("js-yaml");
280
- const content = await fs.readFile(toolkitConfigPath, "utf-8");
298
+ const toolkitFolder = path.join(workspaceRoot, TemplatesManagerService.TOOLKIT_FOLDER);
299
+ const settingsPath = path.join(toolkitFolder, TemplatesManagerService.SETTINGS_FILE);
300
+ const settingsLocalPath = path.join(toolkitFolder, TemplatesManagerService.SETTINGS_LOCAL_FILE);
301
+ if (await pathExists(settingsPath)) {
302
+ const baseContent = await fs.readFile(settingsPath, "utf-8");
303
+ const base = yaml$1.load(baseContent);
304
+ if (await pathExists(settingsLocalPath)) {
305
+ const localContent = await fs.readFile(settingsLocalPath, "utf-8");
306
+ const local = yaml$1.load(localContent);
307
+ return TemplatesManagerService.mergeToolkitConfigs(base, local);
308
+ }
309
+ return base;
310
+ }
311
+ const legacyConfigPath = path.join(workspaceRoot, TemplatesManagerService.TOOLKIT_CONFIG_FILE);
312
+ if (!await pathExists(legacyConfigPath)) return null;
313
+ const content = await fs.readFile(legacyConfigPath, "utf-8");
281
314
  return yaml$1.load(content);
282
315
  }
283
316
  /**
284
- * Read toolkit.yaml configuration from workspace root (sync)
317
+ * Read toolkit configuration from workspace root (sync).
318
+ *
319
+ * Priority order:
320
+ * 1. .toolkit/settings.yaml (new location)
321
+ * 2. Shallow-merge .toolkit/settings.local.yaml over settings.yaml if present
322
+ * 3. Fallback to root toolkit.yaml (deprecated, backward-compat)
285
323
  *
286
324
  * @param startPath - The path to start searching from (defaults to process.cwd())
287
325
  * @returns The toolkit configuration object or null if not found
288
326
  */
289
327
  static readToolkitConfigSync(startPath = process.cwd()) {
290
328
  const workspaceRoot = TemplatesManagerService.findWorkspaceRootSync(startPath);
291
- const toolkitConfigPath = path.join(workspaceRoot, TemplatesManagerService.TOOLKIT_CONFIG_FILE);
292
- if (!pathExistsSync(toolkitConfigPath)) return null;
293
329
  const yaml$1 = __require("js-yaml");
294
- const content = readFileSync$1(toolkitConfigPath, "utf-8");
295
- return yaml$1.load(content);
330
+ const toolkitFolder = path.join(workspaceRoot, TemplatesManagerService.TOOLKIT_FOLDER);
331
+ const settingsPath = path.join(toolkitFolder, TemplatesManagerService.SETTINGS_FILE);
332
+ const settingsLocalPath = path.join(toolkitFolder, TemplatesManagerService.SETTINGS_LOCAL_FILE);
333
+ if (pathExistsSync(settingsPath)) {
334
+ const base = yaml$1.load(readFileSync$1(settingsPath, "utf-8"));
335
+ if (pathExistsSync(settingsLocalPath)) {
336
+ const local = yaml$1.load(readFileSync$1(settingsLocalPath, "utf-8"));
337
+ return TemplatesManagerService.mergeToolkitConfigs(base, local);
338
+ }
339
+ return base;
340
+ }
341
+ const legacyConfigPath = path.join(workspaceRoot, TemplatesManagerService.TOOLKIT_CONFIG_FILE);
342
+ if (!pathExistsSync(legacyConfigPath)) return null;
343
+ return yaml$1.load(readFileSync$1(legacyConfigPath, "utf-8"));
296
344
  }
297
345
  /**
298
- * Write toolkit.yaml configuration to workspace root
346
+ * Write toolkit configuration to .toolkit/settings.yaml.
347
+ * Creates the .toolkit directory if it does not exist.
299
348
  *
300
349
  * @param config - The toolkit configuration to write
301
350
  * @param startPath - The path to start searching from (defaults to process.cwd())
302
351
  */
303
352
  static async writeToolkitConfig(config, startPath = process.cwd()) {
304
353
  const workspaceRoot = await TemplatesManagerService.findWorkspaceRoot(startPath);
305
- const toolkitConfigPath = path.join(workspaceRoot, TemplatesManagerService.TOOLKIT_CONFIG_FILE);
354
+ const toolkitFolder = path.join(workspaceRoot, TemplatesManagerService.TOOLKIT_FOLDER);
355
+ const settingsPath = path.join(toolkitFolder, TemplatesManagerService.SETTINGS_FILE);
356
+ await fs.mkdir(toolkitFolder, { recursive: true });
306
357
  const content = (await import("js-yaml")).dump(config, { indent: 2 });
307
- await fs.writeFile(toolkitConfigPath, content, "utf-8");
358
+ await fs.writeFile(settingsPath, content, "utf-8");
308
359
  }
309
360
  /**
310
361
  * Get the workspace root directory
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@agiflowai/aicode-utils",
3
3
  "description": "Shared utilities and types for AI-powered code generation, scaffolding, and analysis",
4
- "version": "1.0.14",
4
+ "version": "1.0.16",
5
5
  "license": "AGPL-3.0",
6
6
  "author": "AgiflowIO",
7
7
  "repository": {