Skip to content

fix(cli): make the CLI entrypoint run on Windows (normalize argv[1] separators)#3

Open
oceanlongnion wants to merge 4 commits into
aqlong:mainfrom
oceanlongnion:fix/windows-entrypoint-guard
Open

fix(cli): make the CLI entrypoint run on Windows (normalize argv[1] separators)#3
oceanlongnion wants to merge 4 commits into
aqlong:mainfrom
oceanlongnion:fix/windows-entrypoint-guard

Conversation

@oceanlongnion

@oceanlongnion oceanlongnion commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

Summary

On Windows, code2wiki <cmd> silently exits 0 without doing anything — no output, no error. --version and --help print nothing; generate / preview / list all no-op. This affects every Windows invocation path: node dist/cli/index.js …, the npm-installed code2wiki.cmd shim, and the code2wiki.ps1 shim.

Root cause

The main-entry guard at the bottom of src/cli/index.ts compares process.argv[1] against a forward-slash suffix:

if (process.argv[1]?.endsWith("cli/index.js") || process.argv[1]?.endsWith("cli/index.ts")) {
  const program = getProgram();
  await program.parseAsync(process.argv);
}

On Windows, Node sets process.argv[1] to an absolute path with backslashes for every invocation style, e.g. C:\Users\me\...\dist\cli\index.js. So endsWith("cli/index.js") is always false, parseAsync never runs, and the process exits cleanly having done nothing. On POSIX it happens to work because the separators are already /.

Verified on Windows (Node 22):

> node -e "console.log(process.argv[1])" a\b\c.js   →  C:\...\a\b\c.js   (backslashes)
> "C:\...\dist\cli\index.js".endsWith("cli/index.js")  →  false

Fix

Extract the guard into a small exported pure function isCliEntrypoint(argv1) that normalizes separators to / before the suffix check, so it fires on both Windows and POSIX. Pulling it out of the top-level if also makes the exact bug directly unit-testable. getProgram() and all command wiring are unchanged.

export function isCliEntrypoint(argv1: string | undefined): boolean {
  if (!argv1) return false;
  const normalized = argv1.replace(/\\/g, "/");
  return (
    normalized.endsWith("cli/index.js") || normalized.endsWith("cli/index.ts")
  );
}

// Only parse arguments if this is the main entry point (not imported by tests).
if (isCliEntrypoint(process.argv[1])) {
  const program = getProgram();
  await program.parseAsync(process.argv);
}

Regression test

Added src/cli/entrypoint.test.ts so this can't silently break again:

  • Unit tests pin the exact regression — a backslash argv[1] (C:\...\dist\cli\index.js) must be recognized as the entrypoint — plus POSIX paths, the .ts (tsx) variant, undefined, and non-matches (.json/.jsx, unrelated scripts). These fail under the old forward-slash-only guard.
  • Subprocess smoke tests run the real CLI as a child process via node --import tsx src/cli/index.ts and assert --version0.1.0 and --help renders the command list. This proves the guard is wired to process.argv[1] end to end; on Windows argv[1] is a backslash path, so these would fail under the old guard.

Verification

Rebuilt (npm run build) and reinstalled globally (npm install -g .) on Windows:

  • Before: code2wiki --version(empty, exit 0)
  • After: code2wiki --version0.1.0; code2wiki --help renders the full command list; code2wiki list --cwd <dir> and generate --mock / preview produce output as expected.

Test suite (npx vitest run): typecheck clean; 1408 passed, 7 skipped (the 8 new tests included). The 8 failing tests (examples.test.ts structural-snapshot hash mismatches + one README drift guard) are pre-existing on main and unrelated to this change — they stem from a Windows CRLF checkout affecting content hashes / README parsing, confirmed identical on main without this commit. This change adds no new failures.

Scope

Behavioral fix is one normalized comparison, refactored into a tested pure function, plus a new test file. No public API, config, command, or output changes. POSIX behavior is identical to before.

…on Windows

On Windows, process.argv[1] is an absolute path with backslashes (e.g.
C:\...\dist\cli\index.js) for every invocation style (direct
ode, npm
.cmd/.ps1 global shims). The guard process.argv[1].endsWith(cli/index.js)
uses a forward slash, so it never matched and the CLI silently exited 0
without parsing any command. Normalize backslashes to forward slashes before
the check so code2wiki <cmd> works on Windows as well as POSIX.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@oceanlongnion

Copy link
Copy Markdown
Contributor Author

Context for prioritization: I hit this while running code2wiki against a real production ColdFusion (CFML) codebase on Windows — the same OSS install path from the README (npm install -g ., then code2wiki generate / preview). After pulling the latest main, every command started silently no-op'ing (exit 0, no output), which took a bit to trace back to the argv[1] separator mismatch rather than an environment/PATH issue.

Worth flagging because it's a 100% failure on Windows via the documented global-install path — the CLI appears "installed and on PATH" (Get-Command code2wiki resolves fine) but does nothing, so it's an easy one for a Windows user to get stuck on. Once patched locally, the full mock pipeline (listgenerate --mockpreviewvalidate) ran clean against the CFML source, including multi-module previews. Happy to add a tiny regression test around the entry guard if you'd like one in the PR.

…point

Refactor the bottom-of-file main-entry guard into an exported pure
function isCliEntrypoint(argv1) so the Windows-path-separator bug is
directly unit-testable, and add src/cli/entrypoint.test.ts:

- Unit tests pin the exact regression: a backslash argv[1]
  (C:\...\dist\cli\index.js) must be recognized as the entrypoint,
  plus POSIX paths, the .ts (tsx) variant, undefined, and non-matches.
- Subprocess smoke tests run the real CLI via node --import tsx and
  assert --version / --help produce output, proving the guard is wired
  to process.argv[1] end to end (argv[1] is a backslash path on Windows,
  so these would fail under the old forward-slash-only guard).

No behavior change to the runtime guard. 8 new tests pass; full suite
unchanged otherwise (same 8 pre-existing, unrelated Windows-CRLF
snapshot/drift failures).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@oceanlongnion

Copy link
Copy Markdown
Contributor Author

Added the regression test I offered above (commit 81a2d50).

I extracted the main-entry guard into an exported pure function isCliEntrypoint(argv1) so the path-separator logic is directly testable, then added src/cli/entrypoint.test.ts:

  • Unit tests that pin the exact bug — a Windows backslash argv[1] (C:\...\dist\cli\index.js) must resolve to the entrypoint — alongside POSIX paths, the .ts/tsx variant, undefined, and negative cases. These go red under the old forward-slash-only guard.
  • Two subprocess smoke tests that spawn the real CLI (node --import tsx src/cli/index.ts --version / --help) and assert it produces output, so the guard stays wired to process.argv[1] end to end on Windows.

8 new tests, all green; the rest of the suite is unchanged (same pre-existing CRLF-related snapshot/drift failures on main). PR description updated to match.

oceanlongnion and others added 2 commits July 6, 2026 15:37
loadConfig read the config with Node's "utf-8" decoder, which preserves a
leading BOM (U+FEFF), then handed it to JSON.parse, which rejects it with an
opaque "Unexpected token" SyntaxError. Any editor or shell that writes
"UTF-8 with BOM" tripped this -- notably Windows PowerShell 5.1's
Set-Content -Encoding UTF8, which always prepends a BOM -- so an otherwise
valid config failed to load for list/generate/etc. Strip a single leading
BOM before parsing.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…on=length

When an Azure deployment returns a non-empty but truncated body (partial JSON
cut off at the completion budget -- common with reasoning deployments like
gpt-5-mini whose invisible chain-of-thought consumes the budget), the
empty-content guard did not fire and parseJsonObject threw the generic
"LLM did not return valid JSON" with no hint that the fix is a larger budget.
Catch that case when finish_reason=length and rethrow with guidance to raise
AZURE_OPENAI_MAX_COMPLETION_TOKENS, use a smaller file, or a non-reasoning
deployment. Malformed JSON with any other finish_reason still surfaces the
generic parse error.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@oceanlongnion

Copy link
Copy Markdown
Contributor Author

Pushed two more Windows/real-run robustness fixes surfaced while doing a real generation pass on a large ColdFusion module:

  • fix(config) (a24b712) — tolerate a leading UTF-8 BOM in config files. loadConfig decoded the file as utf-8 (which preserves the BOM) and handed it straight to JSON.parse, which rejects a leading U+FEFF with an opaque Unexpected token error. Any "UTF-8 with BOM" writer trips this — notably Windows PowerShell 5.1's Set-Content -Encoding UTF8, which always prepends a BOM — so an otherwise-valid code2wiki.config.json failed to load. Now strips a single leading BOM before parsing. +2 tests (top-level and nested config).

  • fix(llm) (486b33d) — actionable truncation error on Azure finish_reason=length. When a reasoning deployment (e.g. gpt-5-mini) runs out of completion budget mid-JSON, the body is non-empty but truncated, so the existing empty-content guard didn't fire and parseJsonObject threw the generic LLM did not return valid JSON with no hint that the fix is a bigger budget. Now, when finish_reason=length, it rethrows with guidance to raise AZURE_OPENAI_MAX_COMPLETION_TOKENS / use a smaller file / use a non-reasoning deployment. Malformed JSON with any other finish reason still surfaces the generic parse error. +2 tests (truncation → actionable error; non-length bad JSON → generic error).

Verification: typecheck + build clean; full suite 1412 passed (+4 new), same 8 pre-existing unrelated Windows-CRLF snapshot/drift failures. Also verified the BOM fix end-to-end by running the built CLI against a real BOM-encoded config (previously crashed at parse, now loads).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant