Files
1d35e90880 fix(windows): preserve a file's existing line endings on rewrite (#1958)
* fix(windows): preserve a file's existing line endings on rewrite

The parsers normalize CRLF to LF on read, but nothing restored it on
write. On a Windows checkout (core.autocrlf=true) that turned every
rewrite into a whole-file change: applying a delta that added one
requirement produced a diff of 21 insertions and 14 deletions, burying
the real change. Archiving the same spec now writes 7 insertions and 0
deletions.

- specs-apply: write an updated spec back with the convention the file
  already used; a spec that does not exist yet stays LF.
- file-system: same fix for updateFileWithMarkers, so installing shell
  completions into a CRLF .bashrc/.zshrc no longer leaves mixed endings,
  which bash reports as "$'\r': command not found".
- pack-version-check: spawn npm through cross-spawn, since execFile
  cannot resolve npm.cmd on Windows.

Adds src/utils/line-endings.ts for the detect/restore pair, plus tests
pinning the CRLF round trip through the real write paths. Also adds
regression tests for path containment under Windows case variance:
path.win32.relative already folds case, and those tests pin both halves
of the contract so a future "case-insensitive" change cannot quietly
loosen the traversal guard.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(windows): keep removeMarkerBlock on the file's own newline

Addresses the two review points and one more instance of the same bug.

`removeMarkerBlock` collapses a run of blank lines, and rebuilt the
separator as a bare '\n' regardless of the file it came from. Removing a
managed block from a CRLF CLAUDE.md or rc file therefore left a lone LF
behind - the mixed ending this PR exists to prevent. It now uses the
newline it already detects for the trailing ending.

Test fixes:

- `marker-updates.test.ts`: close `describe('line endings')` so
  `removeMarkerBlock` is no longer nested inside `updateFileWithMarkers`.
- `path-containment.test.ts`: exercise `FileSystemUtils.assertPathWithin`
  and `resolveProjectArtifactPath` instead of a private copy of the
  containment logic, which passed whatever the production guard did. The
  guard had no coverage at all; a prefix-comparison regression now fails
  the sibling case.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(windows): read the file's convention consistently, and only ENOENT as absent

Three follow-ups from CodeRabbit's pass on the superseding PR.

`writeUpdatedSpec` turned every read error into "no previous file", so an
existing but unreadable spec was treated as absent and rewritten as LF.
Only ENOENT means absent now; everything else propagates.

`removeMarkerBlock` chose CRLF whenever the content held one anywhere, so
a single stray CRLF in an otherwise-LF file pulled the whole rewrite to
CRLF. It now uses detectLineEnding, the same dominant-ending reading
matchLineEnding uses, so both write paths agree.

Added the alias-path case the containment suite was missing: a directory
link inside the root that resolves outside it. That exercises the
canonicalization half of the guard, which a lexical check cannot do - the
link's own path looks contained. Skipped where creating a directory link
needs a privilege the runner lacks.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: Travis James <travis@tribehealthsolutions.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-23 14:30:24 +00:00
..

OpenSpec Scripts

Utility scripts for OpenSpec maintenance and development.

update-flake.sh

Updates flake.nix pnpm dependency hash automatically.

When to use: After updating dependencies (pnpm install, pnpm update).

Usage:

./scripts/update-flake.sh

What it does:

  1. Reads version from package.json (dynamically used by flake.nix)
  2. Automatically determines the correct pnpm dependency hash
  3. Updates the hash in flake.nix
  4. Verifies the build succeeds

Example workflow:

# After dependency updates
pnpm install
./scripts/update-flake.sh
git add flake.nix
git commit -m "chore: update flake.nix dependency hash"

regen-parity-hashes.mjs

Recomputes the golden hashes pinned in test/core/templates/skill-templates-parity.test.ts.

When to use: After any intended workflow-template change, and after rebasing a branch that edits templates — two branches touching different templates collide on the same hash map, and hand-editing 64-character hashes during a conflict is where transcription mistakes happen.

Usage:

pnpm build && pnpm regen:parity-hashes
pnpm vitest run test/core/templates/skill-templates-parity.test.ts

What it does:

  1. Refuses to run if dist/ is missing or older than src/ — hashes come from the build, while the parity test reads src/, so regenerating against a stale build writes hashes the test then rejects
  2. Recomputes every pinned hash from the built dist/
  3. Rewrites the map in place and prints which entries moved
  4. Exits non-zero, writing nothing, if it cannot account for every pinned hash: a label with no matching export (a renamed or deleted template), or a hash line these patterns do not recognise. Both would otherwise be left stale while the run reported success, so nothing to update always means it.

Line endings round-trip unchanged, so a CRLF checkout is safe — test/** has no text eol=lf attribute, so the file arrives with CRLF on Windows.

The parity test recomputes the same hashes independently, so this script cannot silently produce a wrong value. Always run the test afterwards; it, not this script, is the authority.

The rewriting lives in parity-hash-shared.mjs so its guards can be exercised against fabricated input — see test/core/templates/parity-hash-shared.test.ts. A test that ran this script for real would rewrite the repository's own parity test file mid-suite.

pack-version-check.mjs

Validates package version consistency before publishing.