diff --git a/src/template-engine/application/execute-template/steps/execute-run-step.test.ts b/src/template-engine/application/execute-template/steps/execute-run-step.test.ts index 4c84fdb..27325ca 100644 --- a/src/template-engine/application/execute-template/steps/execute-run-step.test.ts +++ b/src/template-engine/application/execute-template/steps/execute-run-step.test.ts @@ -21,7 +21,12 @@ describe("executeRunStep", () => { runCommand, ); - expect(runCommand).toHaveBeenCalledWith("npm", ["run", "build"], "."); + expect(runCommand).toHaveBeenCalledWith( + "npm", + ["run", "build"], + ".", + "all", + ); }); it("uses an empty args array when none are provided", async () => { @@ -29,6 +34,24 @@ describe("executeRunStep", () => { await executeRunStep({ type: "run", command: "echo" }, [], runCommand); - expect(runCommand).toHaveBeenCalledWith("echo", [], undefined); + expect(runCommand).toHaveBeenCalledWith("echo", [], undefined, "all"); + }); + + it("throws an error for invalid verbosity levels", async () => { + const runCommand = vi.fn().mockResolvedValue(undefined); + + await expect( + executeRunStep( + { + type: "run", + command: "echo", + verbosity: "{{verbosity}}", + }, + [{ name: "verbosity", content: "invalid" }], + runCommand, + ), + ).rejects.toThrow( + 'Invalid verbosity level "invalid" in step "undefined". Valid options are "all", "warning", or "none".', + ); }); }); diff --git a/src/template-engine/application/execute-template/steps/execute-run-step.ts b/src/template-engine/application/execute-template/steps/execute-run-step.ts index d29190d..47a7815 100644 --- a/src/template-engine/application/execute-template/steps/execute-run-step.ts +++ b/src/template-engine/application/execute-template/steps/execute-run-step.ts @@ -2,6 +2,7 @@ import type { VariableValue } from "@/template-engine/domain/variable-value"; import type { RunStep } from "@/template-engine/domain"; import { replaceVariablesInString } from "../replace-variable-in-string"; import type { RunCommandPort } from "@/template-engine/domain/ports/run-command.port"; +import { ProjgenError } from "@/shared"; export const executeRunStep = async ( step: RunStep, @@ -15,5 +16,16 @@ export const executeRunStep = async ( ? replaceVariablesInString(step.cwd, variables) : undefined; - await runCommand(command, args, cwd); + const verbosity = replaceVariablesInString( + step.verbosity ?? "all", + variables, + ); + + if (!["all", "warning", "none"].includes(verbosity)) { + throw new ProjgenError( + `Invalid verbosity level "${verbosity}" in step "${step.name}". Valid options are "all", "warning", or "none".`, + ); + } + + await runCommand(command, args, cwd, verbosity as "all" | "warning" | "none"); }; diff --git a/src/template-engine/domain/ports/run-command.port.ts b/src/template-engine/domain/ports/run-command.port.ts index 93a52bd..02e0967 100644 --- a/src/template-engine/domain/ports/run-command.port.ts +++ b/src/template-engine/domain/ports/run-command.port.ts @@ -1,3 +1,8 @@ export interface RunCommandPort { - (command: string, args: string[], cwd?: string): Promise; + ( + command: string, + args: string[], + cwd?: string, + verbosity?: "all" | "warning" | "none", + ): Promise; } diff --git a/src/template-engine/domain/schemas/steps/run-step.schema.ts b/src/template-engine/domain/schemas/steps/run-step.schema.ts index 9aad835..2292fb0 100644 --- a/src/template-engine/domain/schemas/steps/run-step.schema.ts +++ b/src/template-engine/domain/schemas/steps/run-step.schema.ts @@ -10,6 +10,7 @@ export const RunStepSchema = baseStepSchema.safeExtend({ command: z.string(), // The command to run (for example 'npm') args: z.array(z.string()).optional(), // Arguments to pass to the command (for example ['install']) cwd: z.string().optional(), // The directory to run the command in relative to the project root, if not provided it will run in the root of the project + verbosity: z.string().optional(), // The level of output to show from the command, defaults to "all" }); export type RunStep = z.infer; diff --git a/src/template-engine/infrastructure/run-command.adapter.ts b/src/template-engine/infrastructure/run-command.adapter.ts index 8be1428..39c60cf 100644 --- a/src/template-engine/infrastructure/run-command.adapter.ts +++ b/src/template-engine/infrastructure/run-command.adapter.ts @@ -5,9 +5,27 @@ export const runCommandAdapter: RunCommandPort = async ( command: string, args: string[], cwd?: string, + verbosity?: "all" | "warning" | "none", ) => { await new Promise((resolve, reject) => { - const child = spawn(command, args, { cwd, stdio: "inherit" }); + const child = spawn(command, args, { + cwd, + stdio: verbosity === "none" ? "ignore" : "pipe", + }); + + if (verbosity !== "none") { + if (verbosity === "all" && child.stdout) { + child.stdout.on("data", (chunk) => { + process.stdout.write(chunk); + }); + } + + if ((verbosity === "all" || verbosity === "warning") && child.stderr) { + child.stderr.on("data", (chunk) => { + process.stderr.write(chunk); + }); + } + } child.on("error", reject); diff --git a/template.schema.json b/template.schema.json index 0282fa1..907566a 100644 --- a/template.schema.json +++ b/template.schema.json @@ -49,7 +49,12 @@ "type": "boolean" } }, - "required": ["name", "message", "type", "required"], + "required": [ + "name", + "message", + "type", + "required" + ], "additionalProperties": false }, { @@ -72,7 +77,12 @@ "type": "boolean" } }, - "required": ["name", "message", "type", "required"], + "required": [ + "name", + "message", + "type", + "required" + ], "additionalProperties": false }, { @@ -92,7 +102,11 @@ "type": "boolean" } }, - "required": ["name", "message", "type"], + "required": [ + "name", + "message", + "type" + ], "additionalProperties": false }, { @@ -128,7 +142,13 @@ ] } }, - "required": ["name", "message", "type", "required", "options"], + "required": [ + "name", + "message", + "type", + "required", + "options" + ], "additionalProperties": false }, { @@ -164,7 +184,13 @@ ] } }, - "required": ["name", "message", "type", "required", "options"], + "required": [ + "name", + "message", + "type", + "required", + "options" + ], "additionalProperties": false } ] @@ -210,7 +236,11 @@ "$ref": "#/$defs/__schema0" } }, - "required": ["variable", "operator", "value"], + "required": [ + "variable", + "operator", + "value" + ], "additionalProperties": false } }, @@ -234,9 +264,20 @@ }, "cwd": { "type": "string" + }, + "verbosity": { + "type": "string", + "enum": [ + "all", + "warning", + "none" + ] } }, - "required": ["type", "command"], + "required": [ + "type", + "command" + ], "additionalProperties": false }, { @@ -275,7 +316,11 @@ "$ref": "#/$defs/__schema0" } }, - "required": ["variable", "operator", "value"], + "required": [ + "variable", + "operator", + "value" + ], "additionalProperties": false } }, @@ -298,7 +343,10 @@ "type": "string" } }, - "required": ["type", "path"], + "required": [ + "type", + "path" + ], "additionalProperties": false }, { @@ -337,7 +385,11 @@ "$ref": "#/$defs/__schema0" } }, - "required": ["variable", "operator", "value"], + "required": [ + "variable", + "operator", + "value" + ], "additionalProperties": false } }, @@ -373,7 +425,11 @@ "type": "string" } }, - "required": ["type", "path", "operation"], + "required": [ + "type", + "path", + "operation" + ], "additionalProperties": false }, { @@ -412,7 +468,11 @@ "$ref": "#/$defs/__schema0" } }, - "required": ["variable", "operator", "value"], + "required": [ + "variable", + "operator", + "value" + ], "additionalProperties": false } }, @@ -430,7 +490,11 @@ }, "operation": { "type": "string", - "enum": ["set", "append", "remove"] + "enum": [ + "set", + "append", + "remove" + ] }, "jsonPath": { "type": "array", @@ -442,7 +506,12 @@ "$ref": "#/$defs/__schema0" } }, - "required": ["type", "path", "operation", "jsonPath"], + "required": [ + "type", + "path", + "operation", + "jsonPath" + ], "additionalProperties": false } ]