Skip to content

feat(dev): detect package manager per project dir instead of hard-coding pnpm#92

Open
kaihaase wants to merge 1 commit into
mainfrom
feat/dev-up-package-manager-detect
Open

feat(dev): detect package manager per project dir instead of hard-coding pnpm#92
kaihaase wants to merge 1 commit into
mainfrom
feat/dev-up-package-manager-detect

Conversation

@kaihaase

Copy link
Copy Markdown
Member

Summary

Every lt dev flow that spawned a package-manager call (api/app start in dev up + dev test, build + dev-runner fallbacks in the test session, worktree install on ticket start) used to hard-code pnpm via the legacy LT_PNPM_BIN env var.

In an npm-only monorepo (e.g. document-analyzer, where AGENTS.md explicitly forbids pnpm), the supervised processes died on the first call:

  1. pnpm regenerated a foreign pnpm-lock.yaml.
  2. pnpm exited non-zero with ERR_PNPM_IGNORED_BUILDS on un-approved scripts (bcrypt, sharp, esbuild).
  3. lt dev status reported api: dead + app: dead → user sees a white page behind Caddy.

Mechanics

New src/lib/dev-package-manager.ts exposes pickPackageManager(cwd) with the chain:

  1. LT_PM_BIN env (new generic override).
  2. LT_PNPM_BIN env (legacy alias — kept so older CI configs don't regress).
  3. pnpm-lock.yaml in cwd → pnpm
  4. yarn.lock in cwd → yarn
  5. package-lock.json in cwd → npm
  6. Fallback pnpm (historical default for fresh scaffolds without a lockfile).

Returns a PackageManagerCommand with bin, installArgs, runScript() and exec() so call sites emit ONE argv regardless of manager. exec() inserts -- for npm so option flags (--shard=1/2, --reporter=line) reach the binary instead of being re-parsed as npm's own.

Per-component detection — a monorepo with an npm api + pnpm app drives each component correctly.

Files changed

File Change
src/lib/dev-package-manager.ts NEW — pure resolution helper + PackageManagerCommand.
src/commands/dev/up.ts api start + app dev now per-dir resolved.
src/commands/dev/test.ts api/app test:e2e, sharded playwright call now per-dir resolved.
src/lib/dev-test-session.ts api/app build, fallback start/dev, sharded exec now per-dir.
src/lib/dev-ticket.ts pnpmInstallinstallWorktreeDeps, lockfile-aware.
src/commands/ticket/start.ts calls the renamed helper.
__tests__/dev-package-manager.test.ts NEW — 14 unit tests.

Test plan

  • npx jest __tests__/dev-package-manager.test.ts — 14 / 14 green.
  • Affected suites still pass: dev-bootstrap, dev-test-session, dev-ticket, dev-up-soft-migrate, dev-package-manager — 74 / 74 green.
  • npm run compile — TypeScript clean.
  • Live-verified end-to-end: lt dev up against an npm-only monorepo (document-analyzer) now boots api + app cleanly; previously both died on pnpm install exit 1.
  • Smoke-test against a pnpm monorepo on a clean checkout (any one of the lt fullstack starters) to confirm the historical default still applies.
  • Smoke-test the worktree-install path with lt ticket start against a yarn project (if any consumer has one).

🤖 Generated with Claude Code

…ing pnpm

Every `lt dev` flow that spawned a package-manager call (api/app start
in dev up + test, build + dev-runner fallbacks in the test session,
worktree install on ticket start) hard-coded `pnpm` via the legacy
`LT_PNPM_BIN` env var. In an npm-only monorepo (e.g. document-analyzer,
where AGENTS.md explicitly forbids pnpm) the supervised processes died
on the first call: pnpm regenerated a foreign pnpm-lock.yaml, then exited
with ERR_PNPM_IGNORED_BUILDS on un-approved scripts (bcrypt, sharp,
esbuild). `lt dev status` reported the components as dead, the user saw
a white page behind Caddy.

Mechanics:
- New `src/lib/dev-package-manager.ts` exposes `pickPackageManager(cwd)`.
  Detection chain: LT_PM_BIN env → legacy LT_PNPM_BIN env → pnpm-lock.yaml
  → yarn.lock → package-lock.json → fallback `pnpm` (preserves the
  historical default for fresh scaffolds without a lockfile yet). Returns
  a `PackageManagerCommand` with `bin`, `installArgs`, `runScript()` and
  `exec()` so call sites can emit one argv regardless of which manager
  was picked. `exec()` inserts `--` for npm so option flags
  (`--shard=1/2`, `--reporter=line`) reach the binary instead of being
  re-parsed as npm's own.
- Per-component detection — a monorepo with an npm api + pnpm app
  drives each component with the correct manager.
- Migrated all five call sites:
    1. commands/dev/up.ts            — api start + app dev
    2. commands/dev/test.ts          — api test:e2e + app test:e2e + sharded
    3. lib/dev-test-session.ts       — api/app build, ts-node/dev fallbacks,
                                       sharded playwright exec (now via pm.exec)
    4. lib/dev-ticket.ts             — worktree install (renamed pnpmInstall
                                       → installWorktreeDeps to match the
                                       new behaviour)
    5. commands/ticket/start.ts      — calls the renamed helper
- `LT_PNPM_BIN` kept as a lower-precedence override so older CI configs
  don't regress; `LT_PM_BIN` is the new generic override.

Tests:
- __tests__/dev-package-manager.test.ts — 14 unit tests covering
  lockfile precedence, both env overrides, monorepo per-component
  detection, the npm `exec --` separator, and the `runScript` /
  `installArgs` portability contracts.
- 60 existing tests across dev-bootstrap, dev-test-session, dev-ticket,
  dev-up-soft-migrate still green.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
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