From d3b4792e778e92466e3a9359812660b3b70ae377 Mon Sep 17 00:00:00 2001 From: Katniss Date: Fri, 5 Jun 2026 22:30:12 -0700 Subject: [PATCH] =?UTF-8?q?fix(cli):=20fix=20unreachable=20branch=20in=20r?= =?UTF-8?q?esolveInput=20=E2=80=94=20directories=20named=20*.json/yaml=20n?= =?UTF-8?q?ow=20fall=20through=20to=20'path'?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit resolveInput() had two consecutive branches classifying local paths as 'doc' when their extension matched DOC_EXTS (.json, .yaml, .yml, .toml, .md, .pdf): if (ext && DOC_EXTS.has(ext)) { // always returns here ↩ return { kind: 'doc', … }; } if (exists && statSync(abs).isFile() && ext && DOC_EXTS.has(ext)) { // UNREACHABLE return { kind: 'doc', … }; } The second branch was clearly intended to restrict 'doc' classification to actual files (excluding directories that happen to carry a doc-like extension), but the identical first predicate makes it dead code. The consequence: 'sh1pt build --from ./config.yaml' where config.yaml is a directory (a real pattern in consul-template, vault, dotnet) returns kind='doc', and downstream callers fail with EISDIR when they try to read it as a file. Fix: collapse to one correct check that returns 'doc' only when the path has a doc extension AND either does not exist yet (legitimate future- output path) or is a confirmed regular file. A directory named *.yaml now falls through to the 'path' default. --- packages/cli/src/input.ts | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/packages/cli/src/input.ts b/packages/cli/src/input.ts index c2cb559a..6998fd8f 100644 --- a/packages/cli/src/input.ts +++ b/packages/cli/src/input.ts @@ -54,12 +54,18 @@ export function resolveInput(raw: string): ResolvedInput { const abs = isAbsolute(input) ? input : resolve(process.cwd(), input); const ext = extname(abs).toLowerCase(); const exists = existsSync(abs); - if (ext && DOC_EXTS.has(ext)) { + // A path qualifies as 'doc' when it has a recognised document extension + // AND is either a regular file or doesn't exist yet (future output path). + // Previously there were two branches: the first returned unconditionally + // on extension match, making the second (which correctly checked isFile()) + // unreachable. This caused directories whose names end in a doc extension + // (e.g. './config.yaml/' used by consul-template or vault) to be + // classified as 'doc', leading to EISDIR when downstream code reads them. + const isDocLike = !!ext && DOC_EXTS.has(ext); + const isFileOrMissing = !exists || statSync(abs).isFile(); + if (isDocLike && isFileOrMissing) { return { kind: 'doc', raw, value: abs, inferredName: baseNameWithoutExt(abs), exists }; } - if (exists && statSync(abs).isFile() && ext && DOC_EXTS.has(ext)) { - return { kind: 'doc', raw, value: abs, inferredName: baseNameWithoutExt(abs), exists: true }; - } // Default: treat as a local path (may or may not exist yet). return { kind: 'path', raw, value: abs, inferredName: lastSegment(abs), exists };