@agiflowai/scaffold-mcp 1.1.0 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.mjs CHANGED
@@ -1,12 +1,53 @@
1
1
  #!/usr/bin/env node
2
- import { a as createServer, i as StdioTransportHandler, n as HttpTransportHandler, o as version, r as SseTransportHandler, t as TransportMode } from "./src-BEwgHhCT.mjs";
3
- import { n as ScaffoldingMethodsService, s as FileSystemService } from "./ListScaffoldingMethodsTool-DxdrPlcb.mjs";
4
- import { l as BoilerplateService } from "./tools-Cl06aoBi.mjs";
5
- import { ProjectConfigResolver, TemplatesManagerService, icons, messages, print, sections } from "@agiflowai/aicode-utils";
2
+ import { a as createServer, i as StdioTransportHandler, n as HttpTransportHandler, o as version, r as SseTransportHandler, t as TransportMode } from "./src-CUQR93cQ.mjs";
3
+ import { n as ScaffoldingMethodsService, s as FileSystemService } from "./ListScaffoldingMethodsTool-dHaM4QXT.mjs";
4
+ import { c as GenerateBoilerplateFileTool, o as GenerateFeatureScaffoldTool, r as writePendingScaffoldLog, s as GenerateBoilerplateTool, t as WriteToFileTool, u as BoilerplateService } from "./tools-BR1vQd_Y.mjs";
5
+ import { ProjectConfigResolver, TemplatesManagerService, generateStableId, icons, messages, print, readFile, sections } from "@agiflowai/aicode-utils";
6
6
  import path from "node:path";
7
7
  import { Command } from "commander";
8
- import { CLAUDE_CODE, GEMINI_CLI, SUPPORTED_LLM_TOOLS, isValidLlmTool } from "@agiflowai/coding-agent-bridge";
9
- import { ClaudeCodeAdapter, GeminiCliAdapter, parseHookType } from "@agiflowai/hooks-adapter";
8
+ import { CLAUDE_CODE, CODEX, GEMINI_CLI, SUPPORTED_LLM_TOOLS, isValidLlmTool } from "@agiflowai/coding-agent-bridge";
9
+ import { ClaudeCodeAdapter, CodexAdapter, GeminiCliAdapter, parseHookType } from "@agiflowai/hooks-adapter";
10
+ //#region src/commands/utils.ts
11
+ function parseJsonOption(value, flagName) {
12
+ if (!value) throw new Error(`${flagName} is required`);
13
+ try {
14
+ return JSON.parse(value);
15
+ } catch (error) {
16
+ throw new Error(`Invalid JSON for ${flagName}: ${error instanceof Error ? error.message : String(error)}`);
17
+ }
18
+ }
19
+ async function resolveTemplatesPath() {
20
+ const templatesDir = await TemplatesManagerService.findTemplatesPath();
21
+ if (!templatesDir) throw new Error("Templates folder not found. Create a templates folder or specify templatesPath in toolkit.yaml");
22
+ return templatesDir;
23
+ }
24
+ async function detectMonolithMode() {
25
+ try {
26
+ return (await ProjectConfigResolver.resolveProjectConfig(process.cwd())).type === "monolith";
27
+ } catch {
28
+ return false;
29
+ }
30
+ }
31
+ async function loadTextOption(config) {
32
+ const { value, filePath, valueFlag, fileFlag, required = false } = config;
33
+ if (value !== void 0 && filePath !== void 0) throw new Error(`Use only one of ${valueFlag} or ${fileFlag}`);
34
+ if (filePath !== void 0) return readFile(filePath, "utf-8");
35
+ if (value !== void 0) return value;
36
+ if (required) throw new Error(`${valueFlag} or ${fileFlag} is required`);
37
+ }
38
+ function collectOption(value, previous = []) {
39
+ return [...previous, value];
40
+ }
41
+ function firstTextContent(result) {
42
+ const firstContent = result.content[0];
43
+ return firstContent?.type === "text" ? firstContent.text : "";
44
+ }
45
+ function assertToolSuccess(result) {
46
+ const text = firstTextContent(result);
47
+ if (result.isError) throw new Error(text || "Tool execution failed");
48
+ return text;
49
+ }
50
+ //#endregion
10
51
  //#region src/commands/boilerplate.ts
11
52
  /**
12
53
  * Boilerplate CLI command
@@ -43,7 +84,7 @@ boilerplateCommand.command("list").description("List all available boilerplate t
43
84
  process.exit(1);
44
85
  }
45
86
  });
46
- boilerplateCommand.command("create <boilerplateName>").description("Create a new project from a boilerplate template").option("-v, --vars <json>", "JSON string containing variables for the boilerplate").option("-m, --monolith", "Create as monolith project at workspace root with toolkit.yaml (default: false, creates as monorepo with project.json)").option("-t, --target-folder <path>", "Override target folder (defaults to boilerplate targetFolder for monorepo, workspace root for monolith)").option("--verbose", "Enable verbose logging").action(async (boilerplateName, options) => {
87
+ boilerplateCommand.command("create <boilerplateName>").description("Create a new project from a boilerplate template").option("-v, --vars <json>", "JSON string containing variables for the boilerplate").option("-m, --monolith", "Create as monolith project at workspace root with toolkit.yaml (default: false, creates as monorepo with project.json)").option("-t, --target-folder <path>", "Override target folder (defaults to boilerplate targetFolder for monorepo, workspace root for monolith)").option("--marker <tag>", "Custom scaffold marker tag to inject into generated code files").option("--verbose", "Enable verbose logging").action(async (boilerplateName, options) => {
47
88
  try {
48
89
  const templatesDir = await TemplatesManagerService.findTemplatesPath();
49
90
  if (!templatesDir) {
@@ -93,7 +134,8 @@ boilerplateCommand.command("create <boilerplateName>").description("Create a new
93
134
  boilerplateName,
94
135
  variables,
95
136
  monolith: options.monolith,
96
- targetFolderOverride: options.targetFolder
137
+ targetFolderOverride: options.targetFolder,
138
+ marker: options.marker
97
139
  });
98
140
  if (result.success) {
99
141
  messages.success("Project created successfully!");
@@ -120,6 +162,42 @@ boilerplateCommand.command("create <boilerplateName>").description("Create a new
120
162
  process.exit(1);
121
163
  }
122
164
  });
165
+ boilerplateCommand.command("generate <boilerplateName>").description("Create a new boilerplate configuration in a template's scaffold.yaml").option("-t, --template <name>", "Template name (optional in monolith mode)").option("--description <text>", "Boilerplate description").option("--description-file <path>", "Read boilerplate description from a file").option("--instruction <text>", "Detailed boilerplate instructions").option("--instruction-file <path>", "Read detailed boilerplate instructions from a file").option("--target-folder <path>", "Target folder for generated projects").option("--variables <json>", "JSON array of variable definitions: [{\"name\":\"appName\",\"description\":\"App name\",\"type\":\"string\",\"required\":true}]").option("-i, --include <path>", "Template include path (repeatable)", collectOption, []).action(async (boilerplateName, options) => {
166
+ try {
167
+ const templatesPath = await resolveTemplatesPath();
168
+ const isMonolith = await detectMonolithMode();
169
+ const description = await loadTextOption({
170
+ value: options.description,
171
+ filePath: options.descriptionFile,
172
+ valueFlag: "--description",
173
+ fileFlag: "--description-file",
174
+ required: true
175
+ });
176
+ const instruction = await loadTextOption({
177
+ value: options.instruction,
178
+ filePath: options.instructionFile,
179
+ valueFlag: "--instruction",
180
+ fileFlag: "--instruction-file"
181
+ });
182
+ const variables = parseJsonOption(options.variables, "--variables");
183
+ if (!Array.isArray(variables)) throw new Error("--variables must be a JSON array");
184
+ const targetFolder = options.targetFolder ?? (isMonolith ? "." : void 0);
185
+ if (!targetFolder) throw new Error("--target-folder is required outside monolith mode");
186
+ const result = await new GenerateBoilerplateTool(templatesPath, isMonolith).execute({
187
+ templateName: options.template,
188
+ boilerplateName,
189
+ description: description ?? "",
190
+ instruction,
191
+ targetFolder,
192
+ variables,
193
+ includes: options.include ?? []
194
+ });
195
+ print.info(assertToolSuccess(result));
196
+ } catch (error) {
197
+ print.error("Error generating boilerplate:", error instanceof Error ? error.message : String(error));
198
+ process.exit(1);
199
+ }
200
+ });
123
201
  boilerplateCommand.command("info <boilerplateName>").description("Show detailed information about a boilerplate template").action(async (boilerplateName) => {
124
202
  try {
125
203
  const templatesDir = await TemplatesManagerService.findTemplatesPath();
@@ -145,6 +223,28 @@ boilerplateCommand.command("info <boilerplateName>").description("Show detailed
145
223
  }
146
224
  });
147
225
  //#endregion
226
+ //#region src/commands/file.ts
227
+ const fileCommand$1 = new Command("file").description("Workspace file utilities");
228
+ fileCommand$1.command("write <filePath>").description("Write content to a file inside the current workspace").option("--content <text>", "Content to write").option("--content-file <path>", "Read content to write from a file").action(async (filePath, options) => {
229
+ try {
230
+ const content = await loadTextOption({
231
+ value: options.content,
232
+ filePath: options.contentFile,
233
+ valueFlag: "--content",
234
+ fileFlag: "--content-file",
235
+ required: true
236
+ });
237
+ const result = await new WriteToFileTool().execute({
238
+ file_path: filePath,
239
+ content
240
+ });
241
+ print.info(assertToolSuccess(result));
242
+ } catch (error) {
243
+ print.error("Error writing file:", error instanceof Error ? error.message : String(error));
244
+ process.exit(1);
245
+ }
246
+ });
247
+ //#endregion
148
248
  //#region src/commands/mcp-serve.ts
149
249
  /**
150
250
  * MCP Serve Command
@@ -298,13 +398,6 @@ const mcpServeCommand = new Command("mcp-serve").description("Start MCP server w
298
398
  const scaffoldCommand = new Command("scaffold").description("Add features to existing projects");
299
399
  scaffoldCommand.command("list [projectPath]").description("List available scaffolding methods for a project or template").option("-t, --template <name>", "Template name (e.g., nextjs-15, typescript-mcp-package)").option("-c, --cursor <cursor>", "Pagination cursor for next page").action(async (projectPath, options) => {
300
400
  try {
301
- if (!projectPath && !options.template) {
302
- messages.error("Either projectPath or --template option must be provided");
303
- messages.hint("Examples:");
304
- print.debug(" scaffold-mcp scaffold list ./my-project");
305
- print.debug(" scaffold-mcp scaffold list --template nextjs-15");
306
- process.exit(1);
307
- }
308
401
  const templatesDir = await TemplatesManagerService.findTemplatesPath();
309
402
  if (!templatesDir) {
310
403
  messages.error("Templates folder not found. Create a templates folder or specify templatesPath in toolkit.yaml");
@@ -313,15 +406,16 @@ scaffoldCommand.command("list [projectPath]").description("List available scaffo
313
406
  const scaffoldingMethodsService = new ScaffoldingMethodsService(new FileSystemService(), templatesDir);
314
407
  let result;
315
408
  let displayName;
316
- if (projectPath) {
317
- const absolutePath = path.resolve(projectPath);
409
+ if (projectPath || !options.template) {
410
+ const inputProjectPath = projectPath ?? process.cwd();
411
+ const absolutePath = path.resolve(inputProjectPath);
318
412
  if (!await ProjectConfigResolver.hasConfiguration(absolutePath)) {
319
413
  messages.error(`No project configuration found in ${absolutePath}`);
320
414
  messages.hint("For monorepo: ensure project.json exists with sourceTemplate field\nFor monolith: ensure toolkit.yaml exists at workspace root\nOr use --template option to list methods for a specific template");
321
415
  process.exit(1);
322
416
  }
323
417
  result = await scaffoldingMethodsService.listScaffoldingMethods(absolutePath, options.cursor);
324
- displayName = projectPath;
418
+ displayName = projectPath ?? absolutePath;
325
419
  } else {
326
420
  result = await scaffoldingMethodsService.listScaffoldingMethodsByTemplate(options.template, options.cursor);
327
421
  displayName = `template: ${options.template}`;
@@ -350,7 +444,7 @@ scaffoldCommand.command("list [projectPath]").description("List available scaffo
350
444
  process.exit(1);
351
445
  }
352
446
  });
353
- scaffoldCommand.command("add <featureName>").description("Add a feature to an existing project").option("-p, --project <path>", "Project path", process.cwd()).option("-v, --vars <json>", "JSON string containing variables for the feature").option("--verbose", "Enable verbose logging").action(async (featureName, options) => {
447
+ scaffoldCommand.command("add <featureName>").description("Add a feature to an existing project").option("-p, --project <path>", "Project path", process.cwd()).option("-v, --vars <json>", "JSON string containing variables for the feature").option("--marker <tag>", "Custom scaffold marker tag to inject into generated code files").option("--verbose", "Enable verbose logging").action(async (featureName, options) => {
354
448
  try {
355
449
  const projectPath = path.resolve(options.project);
356
450
  if (!await ProjectConfigResolver.hasConfiguration(projectPath)) {
@@ -407,9 +501,17 @@ scaffoldCommand.command("add <featureName>").description("Add a feature to an ex
407
501
  const result = await scaffoldingMethodsService.useScaffoldMethod({
408
502
  projectPath,
409
503
  scaffold_feature_name: featureName,
410
- variables
504
+ variables,
505
+ marker: options.marker
411
506
  });
412
507
  if (result.success) {
508
+ const scaffoldId = generateStableId(6);
509
+ if (result.createdFiles && result.createdFiles.length > 0) await writePendingScaffoldLog({
510
+ scaffoldId,
511
+ projectPath,
512
+ featureName,
513
+ generatedFiles: result.createdFiles
514
+ });
413
515
  messages.success("✅ Feature added successfully!");
414
516
  if (result.createdFiles && result.createdFiles.length > 0) {
415
517
  print.header("\n📁 Created files:");
@@ -423,6 +525,7 @@ scaffoldCommand.command("add <featureName>").description("Add a feature to an ex
423
525
  print.debug(` - ${warning}`);
424
526
  });
425
527
  }
528
+ print.debug(`\nSCAFFOLD_ID:${scaffoldId}`);
426
529
  print.header("\n📋 Next steps:");
427
530
  print.debug(" - Review the generated files");
428
531
  print.debug(" - Update imports if necessary");
@@ -436,6 +539,40 @@ scaffoldCommand.command("add <featureName>").description("Add a feature to an ex
436
539
  process.exit(1);
437
540
  }
438
541
  });
542
+ scaffoldCommand.command("generate <featureName>").description("Create a new feature scaffold configuration in a template's scaffold.yaml").option("-t, --template <name>", "Template name (optional in monolith mode)").option("--description <text>", "Feature description").option("--description-file <path>", "Read feature description from a file").option("--instruction <text>", "Detailed feature instructions").option("--instruction-file <path>", "Read detailed feature instructions from a file").option("--variables <json>", "JSON array of variable definitions: [{\"name\":\"featureName\",\"description\":\"Feature name\",\"type\":\"string\",\"required\":true}]").option("-i, --include <path>", "Template include path (repeatable)", collectOption, []).option("--pattern <glob>", "File pattern this scaffold applies to (repeatable)", collectOption, []).action(async (featureName, options) => {
543
+ try {
544
+ const templatesPath = await resolveTemplatesPath();
545
+ const isMonolith = await detectMonolithMode();
546
+ const description = await loadTextOption({
547
+ value: options.description,
548
+ filePath: options.descriptionFile,
549
+ valueFlag: "--description",
550
+ fileFlag: "--description-file",
551
+ required: true
552
+ });
553
+ const instruction = await loadTextOption({
554
+ value: options.instruction,
555
+ filePath: options.instructionFile,
556
+ valueFlag: "--instruction",
557
+ fileFlag: "--instruction-file"
558
+ });
559
+ const variables = parseJsonOption(options.variables, "--variables");
560
+ if (!Array.isArray(variables)) throw new Error("--variables must be a JSON array");
561
+ const result = await new GenerateFeatureScaffoldTool(templatesPath, isMonolith).execute({
562
+ templateName: options.template,
563
+ featureName,
564
+ description: description ?? "",
565
+ instruction,
566
+ variables,
567
+ includes: options.include ?? [],
568
+ patterns: options.pattern ?? []
569
+ });
570
+ print.info(assertToolSuccess(result));
571
+ } catch (error) {
572
+ print.error("Error generating feature scaffold:", error instanceof Error ? error.message : String(error));
573
+ process.exit(1);
574
+ }
575
+ });
439
576
  scaffoldCommand.command("info <featureName>").description("Show detailed information about a scaffold method").option("-p, --project <path>", "Project path").option("-t, --template <name>", "Template name (e.g., nextjs-15, typescript-mcp-package)").action(async (featureName, options) => {
440
577
  try {
441
578
  if (!options.project && !options.template) {
@@ -574,7 +711,7 @@ const hookCommand = new Command("hook").description("Execute scaffold hooks for
574
711
  const resolvedAdapterConfig = Object.keys(adapterConfig).length > 0 ? adapterConfig : void 0;
575
712
  if (!isHookMethod(hookMethod)) process.exit(0);
576
713
  if (agent === CLAUDE_CODE) {
577
- const hookModule = await import("./claudeCode-DNgNiLlL.mjs");
714
+ const hookModule = await import("./claudeCode-CyHppxA_.mjs");
578
715
  const claudeCallbacks = [];
579
716
  if (hookModule.UseScaffoldMethodHook) {
580
717
  const hookInstance = new hookModule.UseScaffoldMethodHook();
@@ -589,7 +726,7 @@ const hookCommand = new Command("hook").description("Execute scaffold hooks for
589
726
  if (claudeCallbacks.length === 0) process.exit(0);
590
727
  await new ClaudeCodeAdapter().executeMultiple(claudeCallbacks, resolvedAdapterConfig);
591
728
  } else if (agent === GEMINI_CLI) {
592
- const hookModule = await import("./geminiCli-CcrZEqsz.mjs");
729
+ const hookModule = await import("./geminiCli-CgajucCd.mjs");
593
730
  const geminiCallbacks = [];
594
731
  if (hookModule.UseScaffoldMethodHook) {
595
732
  const hookInstance = new hookModule.UseScaffoldMethodHook();
@@ -598,13 +735,62 @@ const hookCommand = new Command("hook").description("Execute scaffold hooks for
598
735
  }
599
736
  if (geminiCallbacks.length === 0) process.exit(0);
600
737
  await new GeminiCliAdapter().executeMultiple(geminiCallbacks, resolvedAdapterConfig);
601
- } else throw new Error(`Unsupported agent: ${agent}. Supported: ${CLAUDE_CODE}, ${GEMINI_CLI}`);
738
+ } else if (agent === CODEX) {
739
+ const hookModule = await import("./codex-7ry557jE.mjs");
740
+ const codexCallbacks = [];
741
+ if (hookModule.UseScaffoldMethodHook) {
742
+ const hookInstance = new hookModule.UseScaffoldMethodHook();
743
+ const hookFn = hookInstance[hookMethod];
744
+ if (hookFn) codexCallbacks.push(hookFn.bind(hookInstance));
745
+ }
746
+ if (codexCallbacks.length === 0) process.exit(0);
747
+ await new CodexAdapter().executeMultiple(codexCallbacks, resolvedAdapterConfig);
748
+ } else throw new Error(`Unsupported agent: ${agent}. Supported: ${CLAUDE_CODE}, ${GEMINI_CLI}, ${CODEX}`);
602
749
  } catch (error) {
603
750
  print.error(`Hook error: ${error instanceof Error ? error.message : String(error)}`);
604
751
  process.exit(1);
605
752
  }
606
753
  });
607
754
  //#endregion
755
+ //#region src/commands/template.ts
756
+ const templateCommand = new Command("template").description("Manage scaffold template authoring assets");
757
+ const fileCommand = new Command("file").description("Manage template files");
758
+ fileCommand.command("create <filePath>").description("Create or update a Liquid template file for a boilerplate or feature").option("-t, --template <name>", "Template name (optional in monolith mode)").option("--content <text>", "Template file content").option("--content-file <path>", "Read template file content from a file").option("--source-file <path>", "Copy content from an existing source file").option("--header <text>", "Header comment to prepend to the template file").option("--header-file <path>", "Read header comment from a file").action(async (filePath, options) => {
759
+ try {
760
+ if ([
761
+ options.content !== void 0,
762
+ options.contentFile !== void 0,
763
+ options.sourceFile !== void 0
764
+ ].filter(Boolean).length !== 1) throw new Error("Use exactly one of --content, --content-file, or --source-file");
765
+ const templatesPath = await resolveTemplatesPath();
766
+ const isMonolith = await detectMonolithMode();
767
+ const content = await loadTextOption({
768
+ value: options.content,
769
+ filePath: options.contentFile,
770
+ valueFlag: "--content",
771
+ fileFlag: "--content-file"
772
+ });
773
+ const header = await loadTextOption({
774
+ value: options.header,
775
+ filePath: options.headerFile,
776
+ valueFlag: "--header",
777
+ fileFlag: "--header-file"
778
+ });
779
+ const result = await new GenerateBoilerplateFileTool(templatesPath, isMonolith).execute({
780
+ templateName: options.template,
781
+ filePath,
782
+ content,
783
+ sourceFile: options.sourceFile,
784
+ header
785
+ });
786
+ print.info(assertToolSuccess(result));
787
+ } catch (error) {
788
+ print.error("Error creating template file:", error instanceof Error ? error.message : String(error));
789
+ process.exit(1);
790
+ }
791
+ });
792
+ templateCommand.addCommand(fileCommand);
793
+ //#endregion
608
794
  //#region src/cli.ts
609
795
  /**
610
796
  * Main entry point
@@ -615,6 +801,8 @@ async function main() {
615
801
  program.addCommand(mcpServeCommand);
616
802
  program.addCommand(boilerplateCommand);
617
803
  program.addCommand(scaffoldCommand);
804
+ program.addCommand(templateCommand);
805
+ program.addCommand(fileCommand$1);
618
806
  program.addCommand(hookCommand);
619
807
  await program.parseAsync(process.argv);
620
808
  }
@@ -0,0 +1,254 @@
1
+ import { t as ListScaffoldingMethodsTool } from "./ListScaffoldingMethodsTool-dHaM4QXT.mjs";
2
+ import { a as resolveApplyPatchNewFileTargets, i as resolveApplyPatchEditedPaths, n as getGlobalExcludeGlobs, r as matchesExcludeGlob, t as formatScaffoldMethodsHookMessage } from "./shared-BJjclypj.mjs";
3
+ import { ProjectFinderService, TemplatesManagerService } from "@agiflowai/aicode-utils";
4
+ import path from "node:path";
5
+ import { DECISION_ALLOW, DECISION_DENY, DECISION_SKIP, ExecutionLogService } from "@agiflowai/hooks-adapter";
6
+ //#region src/hooks/codex/useScaffoldMethod.ts
7
+ /**
8
+ * Type guard for ScaffoldMethodsResponse
9
+ */
10
+ function isScaffoldMethodsResponse(value) {
11
+ if (typeof value !== "object" || value === null) return false;
12
+ if ("methods" in value && !Array.isArray(value.methods)) return false;
13
+ if ("excludeGlobs" in value && !Array.isArray(value.excludeGlobs)) return false;
14
+ if ("nextCursor" in value && typeof value.nextCursor !== "string") return false;
15
+ return true;
16
+ }
17
+ /**
18
+ * UseScaffoldMethod Hook class for Codex CLI
19
+ *
20
+ * Provides lifecycle hooks for tool execution:
21
+ * - preToolUse: Shows available scaffolding methods before an apply_patch creates new files
22
+ * - postToolUse: Tracks scaffold completion progress after file edits
23
+ */
24
+ var UseScaffoldMethodHook = class {
25
+ /**
26
+ * PreToolUse hook for Codex CLI
27
+ * Proactively shows available scaffolding methods and guides the agent to use them
28
+ * when an apply_patch is about to create a new file.
29
+ *
30
+ * @param context - Codex CLI hook input
31
+ * @returns Hook response with scaffolding methods guidance
32
+ */
33
+ async preToolUse(context) {
34
+ try {
35
+ if (!("tool_name" in context)) return {
36
+ decision: DECISION_SKIP,
37
+ message: "Not a tool use event"
38
+ };
39
+ const command = typeof context.tool_input?.command === "string" ? context.tool_input.command : void 0;
40
+ const newFileTargets = resolveApplyPatchNewFileTargets(context.cwd, context.tool_name, command);
41
+ if (newFileTargets.length === 0) return {
42
+ decision: DECISION_SKIP,
43
+ message: "Not a new-file apply_patch operation"
44
+ };
45
+ const globalExcludeGlobs = await getGlobalExcludeGlobs(context.cwd);
46
+ const target = newFileTargets.find((p) => !matchesExcludeGlob(p, globalExcludeGlobs));
47
+ if (!target) return {
48
+ decision: DECISION_ALLOW,
49
+ message: "New file(s) match configured excludeGlobs - writing directly."
50
+ };
51
+ const executionLog = new ExecutionLogService(context.session_id);
52
+ if (await executionLog.hasExecuted({
53
+ filePath: target,
54
+ decision: DECISION_DENY
55
+ })) return {
56
+ decision: DECISION_SKIP,
57
+ message: "Scaffolding methods already provided for this file"
58
+ };
59
+ const templatesPath = await TemplatesManagerService.findTemplatesPath();
60
+ if (!templatesPath) return {
61
+ decision: DECISION_SKIP,
62
+ message: "Templates folder not found - skipping scaffold method check"
63
+ };
64
+ const tool = new ListScaffoldingMethodsTool(templatesPath, false);
65
+ const projectPath = (await new ProjectFinderService(await TemplatesManagerService.getWorkspaceRoot(context.cwd)).findProjectForFile(target))?.root || context.cwd;
66
+ const result = await tool.execute({ projectPath });
67
+ const firstContent = result.content[0];
68
+ if (firstContent?.type !== "text") return {
69
+ decision: DECISION_SKIP,
70
+ message: "⚠️ Unexpected response type from scaffolding methods tool"
71
+ };
72
+ if (result.isError) return {
73
+ decision: DECISION_SKIP,
74
+ message: `⚠️ Could not load scaffolding methods: ${firstContent.text}`
75
+ };
76
+ const resultText = firstContent.text;
77
+ if (typeof resultText !== "string") return {
78
+ decision: DECISION_SKIP,
79
+ message: "⚠️ Invalid response format from scaffolding methods tool"
80
+ };
81
+ const parsed = JSON.parse(resultText);
82
+ if (!isScaffoldMethodsResponse(parsed)) return {
83
+ decision: DECISION_SKIP,
84
+ message: "⚠️ Unexpected response shape from scaffolding methods tool"
85
+ };
86
+ const data = parsed;
87
+ if (matchesExcludeGlob(target, data.excludeGlobs)) return {
88
+ decision: DECISION_ALLOW,
89
+ message: "File matches template exclude globs - writing directly."
90
+ };
91
+ if (!data.methods || data.methods.length === 0) {
92
+ await executionLog.logExecution({
93
+ filePath: target,
94
+ operation: "list-scaffold-methods",
95
+ decision: DECISION_ALLOW
96
+ });
97
+ return {
98
+ decision: DECISION_ALLOW,
99
+ message: "No scaffolding methods are available for this project template. You should write new files directly."
100
+ };
101
+ }
102
+ const message = formatScaffoldMethodsHookMessage(data.methods);
103
+ await executionLog.logExecution({
104
+ filePath: target,
105
+ operation: "list-scaffold-methods",
106
+ decision: DECISION_DENY
107
+ });
108
+ return {
109
+ decision: DECISION_DENY,
110
+ message
111
+ };
112
+ } catch (error) {
113
+ return {
114
+ decision: DECISION_SKIP,
115
+ message: `⚠️ Hook error: ${error instanceof Error ? error.message : String(error)}`
116
+ };
117
+ }
118
+ }
119
+ /**
120
+ * PostToolUse hook for Codex CLI
121
+ * Tracks file edits after scaffold generation and reminds the agent to complete
122
+ * the implementation of all scaffold-generated files.
123
+ *
124
+ * @param context - Codex CLI hook input
125
+ * @returns Hook response with scaffold completion tracking
126
+ */
127
+ async postToolUse(context) {
128
+ try {
129
+ if (!("tool_name" in context)) return {
130
+ decision: DECISION_SKIP,
131
+ message: "Not a tool use event"
132
+ };
133
+ const executionLog = new ExecutionLogService(context.session_id);
134
+ if (context.tool_name === "use-scaffold-method" || context.tool_input?.toolName === "use-scaffold-method") return {
135
+ decision: DECISION_ALLOW,
136
+ message: "Scaffold execution logged for progress tracking"
137
+ };
138
+ const command = typeof context.tool_input?.command === "string" ? context.tool_input.command : void 0;
139
+ const editedPaths = resolveApplyPatchEditedPaths(context.tool_name, command);
140
+ if (editedPaths.length === 0) return {
141
+ decision: DECISION_SKIP,
142
+ message: "Not a file edit/write operation"
143
+ };
144
+ const lastScaffoldExecution = await getLastScaffoldExecution(executionLog);
145
+ if (!lastScaffoldExecution) return {
146
+ decision: DECISION_SKIP,
147
+ message: "No scaffold execution found"
148
+ };
149
+ const { scaffoldId, generatedFiles, featureName } = lastScaffoldExecution;
150
+ const fulfilledKey = `scaffold-fulfilled-${scaffoldId}`;
151
+ if (await executionLog.hasExecuted({
152
+ filePath: fulfilledKey,
153
+ decision: DECISION_ALLOW
154
+ })) return {
155
+ decision: DECISION_SKIP,
156
+ message: "Scaffold already fulfilled"
157
+ };
158
+ let touchedScaffoldFile = false;
159
+ for (const editedPath of editedPaths) {
160
+ const absoluteEditedPath = path.isAbsolute(editedPath) ? editedPath : path.join(context.cwd, editedPath);
161
+ const generatedMatch = generatedFiles.find((f) => f === editedPath || f === absoluteEditedPath);
162
+ if (!generatedMatch) continue;
163
+ touchedScaffoldFile = true;
164
+ const editKey = `scaffold-edit-${scaffoldId}-${generatedMatch}`;
165
+ if (!await executionLog.hasExecuted({
166
+ filePath: editKey,
167
+ decision: DECISION_ALLOW
168
+ })) await executionLog.logExecution({
169
+ filePath: editKey,
170
+ operation: "scaffold-file-edit",
171
+ decision: DECISION_ALLOW
172
+ });
173
+ }
174
+ const editedFiles = await getEditedScaffoldFiles(executionLog, scaffoldId);
175
+ const totalFiles = generatedFiles.length;
176
+ const remainingFiles = generatedFiles.filter((f) => !editedFiles.includes(f));
177
+ if (remainingFiles.length === 0) {
178
+ await executionLog.logExecution({
179
+ filePath: fulfilledKey,
180
+ operation: "scaffold-fulfilled",
181
+ decision: DECISION_ALLOW
182
+ });
183
+ return {
184
+ decision: DECISION_ALLOW,
185
+ message: `✅ All scaffold files${featureName ? ` for "${featureName}"` : ""} have been implemented! (${totalFiles}/${totalFiles} files completed)`
186
+ };
187
+ }
188
+ if (touchedScaffoldFile) {
189
+ const remainingFilesList = remainingFiles.map((f) => ` - ${f}`).join("\n");
190
+ return {
191
+ decision: DECISION_ALLOW,
192
+ message: `
193
+ ⚠️ **Scaffold Implementation Progress${featureName ? ` for "${featureName}"` : ""}: ${editedFiles.length}/${totalFiles} files completed**
194
+
195
+ **Remaining files to implement:**
196
+ ${remainingFilesList}
197
+
198
+ Don't forget to complete the implementation for all scaffolded files!
199
+ `.trim()
200
+ };
201
+ }
202
+ return {
203
+ decision: DECISION_SKIP,
204
+ message: "Edited file not part of last scaffold execution"
205
+ };
206
+ } catch (error) {
207
+ return {
208
+ decision: DECISION_SKIP,
209
+ message: `⚠️ Hook error: ${error instanceof Error ? error.message : String(error)}`
210
+ };
211
+ }
212
+ }
213
+ };
214
+ /**
215
+ * Helper function to get the last scaffold execution for a session.
216
+ * Returns null if no scaffold execution found or on error.
217
+ */
218
+ async function getLastScaffoldExecution(executionLog) {
219
+ try {
220
+ const entries = await executionLog.loadLog();
221
+ for (let i = entries.length - 1; i >= 0; i--) {
222
+ const entry = entries[i];
223
+ if (entry.operation === "scaffold" && entry.scaffoldId && entry.generatedFiles && entry.generatedFiles.length > 0) return {
224
+ scaffoldId: entry.scaffoldId,
225
+ generatedFiles: entry.generatedFiles,
226
+ featureName: entry.featureName
227
+ };
228
+ }
229
+ return null;
230
+ } catch (error) {
231
+ console.error("Error getting last scaffold execution:", error);
232
+ return null;
233
+ }
234
+ }
235
+ /**
236
+ * Helper function to get the list of edited scaffold files.
237
+ * Returns an empty array if no files found or on error.
238
+ */
239
+ async function getEditedScaffoldFiles(executionLog, scaffoldId) {
240
+ try {
241
+ const entries = await executionLog.loadLog();
242
+ const editedFiles = [];
243
+ for (const entry of entries) if (entry.operation === "scaffold-file-edit" && entry.filePath.startsWith(`scaffold-edit-${scaffoldId}-`)) {
244
+ const filePath = entry.filePath.replace(`scaffold-edit-${scaffoldId}-`, "");
245
+ editedFiles.push(filePath);
246
+ }
247
+ return editedFiles;
248
+ } catch (error) {
249
+ console.error(`Error getting edited scaffold files for ${scaffoldId}:`, error);
250
+ return [];
251
+ }
252
+ }
253
+ //#endregion
254
+ export { UseScaffoldMethodHook };